From 059d0eb41d1f6ce4e5fea8e9c3d8945045d3927e Mon Sep 17 00:00:00 2001 From: rammakablecode Date: Thu, 29 Jan 2026 03:42:47 +1100 Subject: [PATCH 1/3] Updated to use durationMinutes to reduce computation. added summary and activity trend visuals --- package-lock.json | 301 +++++++++++++++++- package.json | 1 + .../api/services/marking-session.service.ts | 2 +- src/app/doubtfire-angular.module.ts | 11 +- .../analytics-tutor-times.component.html | 70 +++- .../analytics-tutor-times.component.ts | 64 +++- .../unit-analytics-route.component.ts | 9 +- 7 files changed, 443 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index c30263893b..33c9e6ab9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@ctrl/ngx-emoji-mart": "^9.2.0", "@ngneat/hotkeys": "^4.0.0", "@ngstack/code-editor": "7.3.0", + "@swimlane/ngx-charts": "^20.5.0", "@uirouter/angular": "^13.0", "@uirouter/angular-hybrid": "^17.1.0", "@uirouter/angularjs": "^1.0.30", @@ -5471,6 +5472,39 @@ "dev": true, "license": "MIT" }, + "node_modules/@swimlane/ngx-charts": { + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@swimlane/ngx-charts/-/ngx-charts-20.5.0.tgz", + "integrity": "sha512-PNBIHdu/R3ceD7jnw1uCBVOj4k3T6IxfdW6xsDsglGkZyoWMEEq4tLoEurjLEKzmDtRv9c35kVNOXy0lkOuXeA==", + "license": "MIT", + "dependencies": { + "d3-array": "^3.1.1", + "d3-brush": "^3.0.0", + "d3-color": "^3.1.0", + "d3-ease": "^3.0.1", + "d3-format": "^3.1.0", + "d3-hierarchy": "^3.1.0", + "d3-interpolate": "^3.0.1", + "d3-sankey": "^0.12.3", + "d3-scale": "^4.0.2", + "d3-selection": "^3.0.0", + "d3-shape": "^3.2.0", + "d3-time-format": "^3.0.0", + "d3-transition": "^3.0.1", + "rfdc": "^1.3.0", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/animations": ">=12.0.0", + "@angular/cdk": ">=12.0.0", + "@angular/common": ">=12.0.0", + "@angular/core": ">=12.0.0", + "@angular/forms": ">=12.0.0", + "@angular/platform-browser": ">=12.0.0", + "@angular/platform-browser-dynamic": ">=12.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "dev": true, @@ -8788,6 +8822,263 @@ "version": "3.5.17", "license": "BSD-3-Clause" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz", + "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-time": "1 - 2" + } + }, + "node_modules/d3-time-format/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-time-format/node_modules/d3-time": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz", + "integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "2" + } + }, + "node_modules/d3-time-format/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, "node_modules/dargs": { "version": "7.0.0", "dev": true, @@ -13207,6 +13498,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/interpret": { "version": "1.1.0", "dev": true, @@ -19644,7 +19944,6 @@ }, "node_modules/rfdc": { "version": "1.3.1", - "dev": true, "license": "MIT" }, "node_modules/right-align": { diff --git a/package.json b/package.json index fb458e4b24..f7298995fb 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@ctrl/ngx-emoji-mart": "^9.2.0", "@ngneat/hotkeys": "^4.0.0", "@ngstack/code-editor": "7.3.0", + "@swimlane/ngx-charts": "^20.5.0", "@uirouter/angular": "^13.0", "@uirouter/angular-hybrid": "^17.1.0", "@uirouter/angularjs": "^1.0.30", diff --git a/src/app/api/services/marking-session.service.ts b/src/app/api/services/marking-session.service.ts index f1ce830a02..fb2d79abd3 100644 --- a/src/app/api/services/marking-session.service.ts +++ b/src/app/api/services/marking-session.service.ts @@ -33,7 +33,7 @@ export class MarkingSessionService extends CachedEntityService { ); } - public createInstanceFrom(json: object, other?: Unit): MarkingSession { + public createInstanceFrom(_json: object, other?: Unit): MarkingSession { return new MarkingSession(other); } } diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 318c601b35..4eac28aa79 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -109,7 +109,10 @@ import { } from '@angular/material/core'; import {MatDatepickerModule} from '@angular/material/datepicker'; -import {DateFnsAdapter, MAT_DATE_FNS_FORMATS} from '@angular/material-date-fns-adapter'; +import { + DateFnsAdapter, + //MAT_DATE_FNS_FORMATS (unused vars) +} from '@angular/material-date-fns-adapter'; import {enAU} from 'date-fns/locale'; import {CalendarModule, DateAdapter as CalendarDateAdapter} from 'angular-calendar'; import {adapterFactory} from 'angular-calendar/date-adapters/date-fns'; @@ -296,7 +299,9 @@ import {MarkingSessionService} from './api/services/marking-session.service'; import {PortfolioIncludedTasksComponent} from './projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component'; import {OverseerScriptEditorModalComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.component'; import {CodeEditorModule} from '@ngstack/code-editor'; -import {UploadGradesComponent} from './units/states/portfolios/upload-grades/upload-grades.component'; +//import {UploadGradesComponent} from './units/states/portfolios/upload-grades/upload-grades.component'; (unused vars) +import {CommonModule} from '@angular/common'; +import {NgxChartsModule} from '@swimlane/ngx-charts'; // See https://stackoverflow.com/questions/55721254/how-to-change-mat-datepicker-date-format-to-dd-mm-yyyy-in-simplest-way/58189036#58189036 const MY_DATE_FORMAT = { @@ -604,6 +609,8 @@ const MY_DATE_FORMAT = { MatDialogModuleNew, CalendarModule.forRoot({provide: CalendarDateAdapter, useFactory: adapterFactory}), CodeEditorModule.forRoot(), + CommonModule, + NgxChartsModule, ], }) diff --git a/src/app/units/states/analytics/directives/analytics-tutor-times.component.html b/src/app/units/states/analytics/directives/analytics-tutor-times.component.html index dbd2f4302a..d8deb54c21 100644 --- a/src/app/units/states/analytics/directives/analytics-tutor-times.component.html +++ b/src/app/units/states/analytics/directives/analytics-tutor-times.component.html @@ -31,11 +31,9 @@

Tutor Times Session Summary

- - - + + +
@@ -46,7 +44,7 @@

Tutor Times Session Summary

[(ngModel)]="tutorTimeSummaryStartDate" (dateChange)="onDateChange($event)" /> - Sessions from this day onward + Sessions from
@@ -59,7 +57,7 @@

Tutor Times Session Summary

[(ngModel)]="tutorTimeSummaryEndDate" (dateChange)="onDateChange($event)" /> - Sessions up to this day + Sessions up until @@ -72,6 +70,36 @@

Tutor Times Session Summary

>Hide sessions during tutorials
+ +
+
+

+ {{ totalMinutes / 60 | number: '1.2-2' }} hrs +

+
Total Time
+
+ +
+

+ {{ totalAssessments }} +

+
Assessements made
+
+ +
+

+ {{ avgMinPerAssessments | number: '1.0-1' }} +

+
Min per Assessment
+
+ +
+

+ {{ totalSubmissionsOpened | number: '1.0-1' }} +

+
Submissions Viewed
+
+
@@ -111,4 +139,32 @@

Tutor Times Session Summary

(beforeViewRender)="beforeViewRender($event)" /> + +
+

Marking Trend

+
+ + + +
+ No marking sessions recorded for this week. +
+
+
diff --git a/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts b/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts index 9b65234bbd..048e0cd178 100644 --- a/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts +++ b/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts @@ -48,6 +48,13 @@ export class AnalyticsTutorTimesComponent implements OnInit { events: SessionEvent[] = []; filteredEvents = []; + // hold data for new cards and chart + totalMinutes: number = 0; + totalAssessments: number = 0; + avgMinPerAssessments: number = 0; + totalSubmissionsOpened: number = 0; + weekChartData: {name: string; series: {name: string; value: number}[]}[] = []; + tutorTimeSummaryStartDate: Date; tutorTimeSummaryEndDate: Date; daysInWeek: number = 7; @@ -126,10 +133,65 @@ export class AnalyticsTutorTimesComponent implements OnInit { (e) => (this.selectedUserId === null || e.userId === this.selectedUserId) && (!this.hideSessionsDuringTutorials || !e.duringTutorial) && - e.duration >= 1, + e.duration >= 0, ); + + // recalcuate stats when filter sekection changes + this.calculateAnalytics(); + } + + calculateAnalytics() { + this.totalMinutes = 0; + this.totalAssessments = 0; + this.totalSubmissionsOpened = 0; + + const groupedMap = new Map(); + const iterDate = new Date(this.tutorTimeSummaryStartDate); + const endDate = new Date(this.tutorTimeSummaryEndDate); + + // Normalize to midnight + iterDate.setHours(0, 0, 0, 0); + endDate.setHours(0, 0, 0, 0); + + // Loop through the date range to create "buckets" + while (iterDate <= endDate) { + const label = iterDate.toLocaleDateString('en-AU', {weekday: 'short', day: 'numeric'}); + groupedMap.set(label, {time: 0, count: 0}); + iterDate.setDate(iterDate.getDate() + 1); + } + + this.filteredEvents.forEach((event) => { + const duration = event.duration; + + this.totalMinutes += duration; + this.totalAssessments += event.assessments || 0; + this.totalSubmissionsOpened += event.submissionsOpened || 0; + + const label = event.start.toLocaleDateString('en-AU', {weekday: 'short', day: 'numeric'}); + + if (groupedMap.has(label)) { + const currentVal = groupedMap.get(label)!; + currentVal.time += duration; + currentVal.count += event.assessments || 0; + } + }); + + this.avgMinPerAssessments = + this.totalAssessments > 0 ? this.totalMinutes / this.totalAssessments : 0; + + this.weekChartData = Array.from(groupedMap, ([name, data]) => ({ + name, + series: [ + {name: 'Time (hrs)', value: data.time / 60}, + {name: 'Assessments', value: data.count}, + ], + })); } + public formatDataLabel = (value: number): string => { + return value === 0 ? '' : value.toString(); + }; + onDateChange(_event: MatDatepickerInputEvent) { if (!this.tutorTimeSummaryStartDate || !this.tutorTimeSummaryEndDate) { return; diff --git a/src/app/units/states/analytics/unit-analytics-route.component.ts b/src/app/units/states/analytics/unit-analytics-route.component.ts index 4c8e068722..391d840b8f 100644 --- a/src/app/units/states/analytics/unit-analytics-route.component.ts +++ b/src/app/units/states/analytics/unit-analytics-route.component.ts @@ -1,6 +1,9 @@ -import {Component, Input, OnInit} from '@angular/core'; -import {MatDatepickerInputEvent} from '@angular/material/datepicker'; -import {CalendarEvent} from 'angular-calendar'; +import { + Component, + Input, //, OnInit (unused vars) +} from '@angular/core'; +//import {MatDatepickerInputEvent} from '@angular/material/datepicker'; (unused vars) +//import {CalendarEvent} from 'angular-calendar'; (unused vars) import {Observable} from 'rxjs'; import {SidekiqJob} from 'src/app/api/models/sidekiq-job'; import {Unit} from 'src/app/api/models/unit'; From 76a4cab112f29fdbb222c8f50af1e006a10774ae Mon Sep 17 00:00:00 2001 From: rammakablecode Date: Thu, 29 Jan 2026 21:10:00 +1100 Subject: [PATCH 2/3] Documenting feature and progess plan for handover --- docs/Tutor_session_tracking.md | 51 +++++++++++++++++++ .../analytics-tutor-times.component.ts | 2 +- 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 docs/Tutor_session_tracking.md diff --git a/docs/Tutor_session_tracking.md b/docs/Tutor_session_tracking.md new file mode 100644 index 0000000000..c35ba7794a --- /dev/null +++ b/docs/Tutor_session_tracking.md @@ -0,0 +1,51 @@ +# Analytics & Session Tracking Handover + +## 1. Overview +The Tutor Analytics dashboard visualizes marking session data to provide insights into workload and efficiency. This document outlines the architecture implemented in the 10.0.x release and identifies specific data integrity observations, testing requirements and direction for future development. + +## 2. Architecture Status (Completed Work) +The following architecture has been implemented and verified in the 10.0.x branch: + +* **Tracking Mode:** Automated / "Sticky Session" Logic. + * *Behavior:* Sessions are automatically created and extended to group user activity to continuous sessions. + * *Threshold:* A 15-minute inactivity window determines when a session closes. +* **Manual Timer:** (Mentioned in the Thoth-tec Documentation Website) Deprecated and removed from the UI to reduce tutor cognitive load. +* **Data Source:** * The `duration_minutes` field is now calculated and added on to the `MarkingSession` model in the backend (doubtfire-api). + * The frontend (doubtfire-web) dashboard consumes this pre-calculated value rather than performing date-diff logic client-side. +* **Visualization:** + * **Summary Cards:** Real-time metrics for Total Time, Tasks Assessed, Submissions Opened and Efficiency. + * **Activity Chart:** Grouped bar chart comparing "Hours Worked" and "Tasks Assessed" per day. + +## 3. Known Data Integrity Observations & Future Work + +During implementation, code review of `doubtfire-api` and `doubtfire-web` identified three areas where data capture could be refined. + +> **Note:** any changes to the logic below should be confirmed with the Product Owner, as they may alter historical data definitions or reporting metrics. + +### A. Zero-Minute Sessions (Backend) +* **Observation:** The dashboard displays valid sessions with `0 minutes` duration. +* **Root Cause:** The `SessionTracker` service uses a strict "Sticky Session" logic. + * *Scenario:* A tutor performs a single action (e.g., grading one student) at `12:00:00` and performs no further write actions. + * *Result:* `start_time` and `end_time` are identical (`12:00:00`), resulting in a 0-minute duration. +* **Impact:** This may result in the under-reporting of workload, as the reading time (cognitive load) preceding the action is not captured. +* **Suggested Improvement:** * Consider implementing a minimum duration floor (heuristic) in `SessionTracker`. + * *Example Logic:* On session creation, ensure `end_time` is at least `now + 5.minutes`. + +### B. Assessment Count Inflation (Backend) +* **Observation:** The "Tasks Assessed" metric can be higher than the actual number of unique students graded. +* **Root Cause:** `SessionTracker.record_assessment_activity` creates a `session_activity` record unconditionally every time the API endpoint is called. + * *Scenario:* A tutor clicks on a task multiple times without changing the status (e.g. re-saving 'Discuss' + status doubtfire-api/app/models/comments/discussion_comment.rb). + * *Result:* Multiple assessment activities are logged for a single logical action. +* **Suggested Improvement:** * Implement Dirty Checking or Hashing type solution in the Rails service to filter out redundant saves. + * *Example Logic:* Only call `record_assessment_activity` if there's a change in taskid. + +## 4. Future Contributions +* **Tutor Filtering:** The API can serve all tutor data to Convenors. The frontend requires a "Staff Selection" dropdown to filter the events array by user_id. + +* **Granular Task Analytics:** To visualize time spent marking per Ontrack task, future teams should expose session_activities in the MarkingSessionEntity and calculate grouped durations on the frontend. + +* **Threshhold & Date testing:** +- Ensure when running tests and asserting on session duration, call .reload on the model object. To ensure the API updates the database, but the local Ruby variable remains a stale snapshot. +- To force a session split in tests, ensure (travel 16.minutes) and ensure the previous session's end_time is properly persisted. +- Always use in_time_zone rather than to_time to avoid sub-second drift that can cause sessions to merge or split incorrectly during loops. diff --git a/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts b/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts index 048e0cd178..94a403b05f 100644 --- a/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts +++ b/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts @@ -160,7 +160,7 @@ export class AnalyticsTutorTimesComponent implements OnInit { iterDate.setDate(iterDate.getDate() + 1); } - this.filteredEvents.forEach((event) => { + this.filteredEvents.forEach((event: SessionEvent) => { const duration = event.duration; this.totalMinutes += duration; From df99b6760ff5b931630bdc8a43df19e54f8077ed Mon Sep 17 00:00:00 2001 From: rammakablecode Date: Fri, 30 Jan 2026 13:38:13 +1100 Subject: [PATCH 3/3] Cleanupp before upstream: remove docs, lockfile and unused imports --- docs/Tutor_session_tracking.md | 51 ------------------- .../unit-analytics-route.component.ts | 7 +-- 2 files changed, 1 insertion(+), 57 deletions(-) delete mode 100644 docs/Tutor_session_tracking.md diff --git a/docs/Tutor_session_tracking.md b/docs/Tutor_session_tracking.md deleted file mode 100644 index c35ba7794a..0000000000 --- a/docs/Tutor_session_tracking.md +++ /dev/null @@ -1,51 +0,0 @@ -# Analytics & Session Tracking Handover - -## 1. Overview -The Tutor Analytics dashboard visualizes marking session data to provide insights into workload and efficiency. This document outlines the architecture implemented in the 10.0.x release and identifies specific data integrity observations, testing requirements and direction for future development. - -## 2. Architecture Status (Completed Work) -The following architecture has been implemented and verified in the 10.0.x branch: - -* **Tracking Mode:** Automated / "Sticky Session" Logic. - * *Behavior:* Sessions are automatically created and extended to group user activity to continuous sessions. - * *Threshold:* A 15-minute inactivity window determines when a session closes. -* **Manual Timer:** (Mentioned in the Thoth-tec Documentation Website) Deprecated and removed from the UI to reduce tutor cognitive load. -* **Data Source:** * The `duration_minutes` field is now calculated and added on to the `MarkingSession` model in the backend (doubtfire-api). - * The frontend (doubtfire-web) dashboard consumes this pre-calculated value rather than performing date-diff logic client-side. -* **Visualization:** - * **Summary Cards:** Real-time metrics for Total Time, Tasks Assessed, Submissions Opened and Efficiency. - * **Activity Chart:** Grouped bar chart comparing "Hours Worked" and "Tasks Assessed" per day. - -## 3. Known Data Integrity Observations & Future Work - -During implementation, code review of `doubtfire-api` and `doubtfire-web` identified three areas where data capture could be refined. - -> **Note:** any changes to the logic below should be confirmed with the Product Owner, as they may alter historical data definitions or reporting metrics. - -### A. Zero-Minute Sessions (Backend) -* **Observation:** The dashboard displays valid sessions with `0 minutes` duration. -* **Root Cause:** The `SessionTracker` service uses a strict "Sticky Session" logic. - * *Scenario:* A tutor performs a single action (e.g., grading one student) at `12:00:00` and performs no further write actions. - * *Result:* `start_time` and `end_time` are identical (`12:00:00`), resulting in a 0-minute duration. -* **Impact:** This may result in the under-reporting of workload, as the reading time (cognitive load) preceding the action is not captured. -* **Suggested Improvement:** * Consider implementing a minimum duration floor (heuristic) in `SessionTracker`. - * *Example Logic:* On session creation, ensure `end_time` is at least `now + 5.minutes`. - -### B. Assessment Count Inflation (Backend) -* **Observation:** The "Tasks Assessed" metric can be higher than the actual number of unique students graded. -* **Root Cause:** `SessionTracker.record_assessment_activity` creates a `session_activity` record unconditionally every time the API endpoint is called. - * *Scenario:* A tutor clicks on a task multiple times without changing the status (e.g. re-saving 'Discuss' - status doubtfire-api/app/models/comments/discussion_comment.rb). - * *Result:* Multiple assessment activities are logged for a single logical action. -* **Suggested Improvement:** * Implement Dirty Checking or Hashing type solution in the Rails service to filter out redundant saves. - * *Example Logic:* Only call `record_assessment_activity` if there's a change in taskid. - -## 4. Future Contributions -* **Tutor Filtering:** The API can serve all tutor data to Convenors. The frontend requires a "Staff Selection" dropdown to filter the events array by user_id. - -* **Granular Task Analytics:** To visualize time spent marking per Ontrack task, future teams should expose session_activities in the MarkingSessionEntity and calculate grouped durations on the frontend. - -* **Threshhold & Date testing:** -- Ensure when running tests and asserting on session duration, call .reload on the model object. To ensure the API updates the database, but the local Ruby variable remains a stale snapshot. -- To force a session split in tests, ensure (travel 16.minutes) and ensure the previous session's end_time is properly persisted. -- Always use in_time_zone rather than to_time to avoid sub-second drift that can cause sessions to merge or split incorrectly during loops. diff --git a/src/app/units/states/analytics/unit-analytics-route.component.ts b/src/app/units/states/analytics/unit-analytics-route.component.ts index 391d840b8f..2e3eb47e3f 100644 --- a/src/app/units/states/analytics/unit-analytics-route.component.ts +++ b/src/app/units/states/analytics/unit-analytics-route.component.ts @@ -1,9 +1,4 @@ -import { - Component, - Input, //, OnInit (unused vars) -} from '@angular/core'; -//import {MatDatepickerInputEvent} from '@angular/material/datepicker'; (unused vars) -//import {CalendarEvent} from 'angular-calendar'; (unused vars) +import {Component, Input} from '@angular/core'; import {Observable} from 'rxjs'; import {SidekiqJob} from 'src/app/api/models/sidekiq-job'; import {Unit} from 'src/app/api/models/unit';