Skip to content

Commit 51e01f6

Browse files
committed
[Web] Add student-stats report
1 parent 79dced5 commit 51e01f6

File tree

8 files changed

+157
-2
lines changed

8 files changed

+157
-2
lines changed

attendance-web/src/app/app-routing.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { MeetingEventsComponent } from './components/meeting-events/meeting-even
1919
import { MeetingsReportComponent } from './components/reports/meetings-report/meetings-report.component';
2020
import { ErrorComponent } from './components/error/error.component';
2121
import { MeetingAttendanceReportComponent } from './components/reports/meeting-attendance-report/meeting-attendance-report.component';
22+
import { StudentStatsComponent } from './components/reports/student-stats/student-stats.component';
2223

2324
const routes: Routes = [
2425
{ path: 'students', component: StudentsComponent,
@@ -48,6 +49,7 @@ const routes: Routes = [
4849
{ path: 'meetings', component: MeetingsReportComponent },
4950
{ path: 'meetings/attendance', component: MeetingAttendanceReportComponent },
5051
{ path: 'meetings/attendance/:date', component: MeetingAttendanceReportComponent },
52+
{ path: 'student-stats', component: StudentStatsComponent },
5153
{ path: 'event-log', component: EventLogComponent },
5254
{ path: 'export', component: CsvExportComponent }
5355
]

attendance-web/src/app/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import { ErrorComponent } from './components/error/error.component';
5555
import { MeetingAttendanceReportComponent } from './components/reports/meeting-attendance-report/meeting-attendance-report.component';
5656
import { CropImageComponent } from './components/crop-image/crop-image.component';
5757
import { DatePickerComponent } from './components/reuse/date-picker/date-picker.component';
58+
import { StudentStatsComponent } from './components/reports/student-stats/student-stats.component';
5859

5960

6061
@NgModule({ declarations: [
@@ -73,6 +74,7 @@ import { DatePickerComponent } from './components/reuse/date-picker/date-picker.
7374
SearchBoxComponent,
7475
ShowStudentComponent,
7576
SpinnerComponent,
77+
StudentStatsComponent,
7678
ReportsComponent,
7779
CsvExportComponent,
7880
EventLogComponent,

attendance-web/src/app/components/reports/reports.component.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,19 @@ export class ReportsComponent implements OnInit {
1212
path: './meetings',
1313
name: 'Meetings'
1414
},
15+
{
16+
path: './student-stats',
17+
name: 'Students'
18+
},
1519
{
1620
path: './event-log',
1721
name: 'Event Log'
1822
},
1923
{
2024
path: './export',
2125
name: 'Export CSV',
22-
}
26+
},
27+
2328
]
2429
constructor() { }
2530

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<div id="datePicker">
2+
<app-date-picker [initialDuration]="{months: 6}" (dateRangeSelection)="updateDateSelection($event)"></app-date-picker>
3+
</div>
4+
5+
<ng-container *ngIf="state|async as state">
6+
<ng-container *ngIf="state == stateType.LOADED">
7+
<mat-card *ngIf="report|async as report">
8+
<mat-card-content>
9+
<table mat-table [dataSource]="report.report">
10+
<ng-container matColumnDef="student-name">
11+
<th mat-header-cell *matHeaderCellDef>Student Name</th>
12+
<td mat-cell *matCellDef="let element">{{element.student.name}}</td>
13+
</ng-container>
14+
<ng-container matColumnDef="checkin-count">
15+
<th mat-header-cell *matHeaderCellDef>Meetings Attended <br> (out of {{report.meeting_count}}) </th>
16+
<td mat-cell *matCellDef="let element">{{element.stats.checkin_count}}</td>
17+
</ng-container>
18+
<ng-container matColumnDef="missed-checkout-count">
19+
<th mat-header-cell *matHeaderCellDef>Missed Checkouts</th>
20+
<td mat-cell *matCellDef="let element">{{element.stats.missed_checkout_count}}</td>
21+
</ng-container>
22+
<ng-container matColumnDef="meeting-time">
23+
<th mat-header-cell *matHeaderCellDef>Time Spent at Meetings</th>
24+
<td mat-cell *matCellDef="let element">{{formatTimeDiff(element.stats.meeting_time)}}</td>
25+
</ng-container>
26+
27+
<tr mat-header-row *matHeaderRowDef="studentStatsColumns"></tr>
28+
<tr mat-row *matRowDef="let row; columns: studentStatsColumns;"></tr>
29+
</table>
30+
</mat-card-content>
31+
</mat-card>
32+
</ng-container>
33+
<ng-container *ngIf="state == stateType.LOADING">
34+
<app-spinner></app-spinner>
35+
</ng-container>
36+
</ng-container>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#datePicker {
2+
margin-top: 20px;
3+
margin-bottom: 20px;
4+
}
5+
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { Component, OnDestroy, OnInit } from '@angular/core';
2+
import { BehaviorSubject, combineLatest, ReplaySubject, Subject, Subscription, switchMap, tap } from 'rxjs';
3+
import { StudentStats } from 'src/app/models/report-models';
4+
import { Student } from 'src/app/models/student.model';
5+
import { ReportsService } from 'src/app/services/reports.service';
6+
import { StudentsService } from 'src/app/services/students.service';
7+
import { SelectedDateRange } from 'src/app/components/reuse/date-picker/date-picker.component';
8+
9+
interface StudentStatsReport {
10+
meeting_count: number,
11+
report: {
12+
student: Student,
13+
stats: StudentStats
14+
}[]
15+
};
16+
17+
enum PageState {
18+
LOADING = 1,
19+
LOADED
20+
}
21+
22+
@Component({
23+
selector: 'app-student-stats',
24+
templateUrl: './student-stats.component.html',
25+
styleUrl: './student-stats.component.scss',
26+
standalone: false
27+
})
28+
export class StudentStatsComponent implements OnInit, OnDestroy {
29+
studentStatsColumns = ["student-name", "checkin-count", "missed-checkout-count", "meeting-time"];
30+
31+
dateRangeSelection = new Subject<SelectedDateRange>();
32+
33+
report = new ReplaySubject<StudentStatsReport>(1);
34+
reportSub: Subscription|null = null;
35+
36+
state = new BehaviorSubject<PageState>(PageState.LOADING);
37+
stateType = PageState;
38+
39+
constructor(
40+
private studentsService: StudentsService,
41+
private reportsService: ReportsService
42+
) {}
43+
44+
ngOnInit(): void {
45+
this.reportSub = combineLatest({
46+
meetings: this.dateRangeSelection.pipe(
47+
switchMap(range => this.reportsService.getMeetingList(range))
48+
),
49+
stats: this.dateRangeSelection.pipe(
50+
switchMap(range => this.reportsService.getStudentStats(range))
51+
),
52+
students: this.studentsService.getStudentMap(false)
53+
}).subscribe(({meetings, stats, students}) => {
54+
55+
this.report.next({
56+
meeting_count: meetings.length,
57+
report: stats.map(stats => ({
58+
student: students.get(stats.student_id)!!,
59+
stats: stats
60+
})).filter(it => !!it.student)
61+
});
62+
this.state.next(PageState.LOADED);
63+
});
64+
}
65+
66+
ngOnDestroy(): void {
67+
this.reportSub?.unsubscribe();
68+
}
69+
70+
updateDateSelection(selection: SelectedDateRange) {
71+
this.state.next(PageState.LOADING);
72+
this.dateRangeSelection.next(selection);
73+
}
74+
75+
formatTimeDiff(diffSeconds: number) {
76+
const hours = Math.floor(diffSeconds / (60 * 60));
77+
const minutes = Math.floor((diffSeconds % (60 * 60)) / 60);
78+
const seconds = diffSeconds % 60;
79+
return hours.toString()
80+
+ ":" + minutes.toString().padStart(2, '0')
81+
+ ":" + seconds.toString().padStart(2, '0');
82+
}
83+
}

attendance-web/src/app/models/report-models.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,10 @@ export interface MeetingAttendance {
1010
meeting_date: DateTime,
1111
attendance_sessions: AttendanceSession[]
1212
}
13+
14+
export interface StudentStats {
15+
student_id: number,
16+
checkin_count: number,
17+
missed_checkout_count: number,
18+
meeting_time: number
19+
}

attendance-web/src/app/services/reports.service.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
33
import { DateTime } from 'luxon';
44
import { Observable } from 'rxjs';
55
import { environment } from 'src/environments/environment';
6-
import { MeetingAttendance, MeetingStudentCount } from '../models/report-models';
6+
import { MeetingAttendance, MeetingStudentCount, StudentStats } from '../models/report-models';
77

88
@Injectable({
99
providedIn: 'root'
@@ -44,4 +44,19 @@ export class ReportsService {
4444

4545
return this.httpClient.get<MeetingAttendance>(environment.apiRoot + '/reports/meeting-attendance', {params});
4646
}
47+
48+
getStudentStats(options?: {
49+
since?: DateTime,
50+
until?: DateTime
51+
}) {
52+
let params = new HttpParams();
53+
if(options?.since) {
54+
params = params.set('since', Math.floor(options.since.toMillis() / 1000));
55+
}
56+
if(options?.until) {
57+
params = params.set('until', Math.floor(options.until.toMillis() / 1000));
58+
}
59+
60+
return this.httpClient.get<Array<StudentStats>>(environment.apiRoot + '/reports/student-stats', {params});
61+
}
4762
}

0 commit comments

Comments
 (0)