From 7322e89a098de9eb3eb2410cd0d6dfdf8b8da09a Mon Sep 17 00:00:00 2001 From: WaelAlahamdi Date: Sat, 13 Sep 2025 19:15:44 +1000 Subject: [PATCH 1/2] feat: add Tutor Time Dashboard with sessions table, pagination, and sorting --- src/app/doubtfire.states.ts | 16 +++++ .../tutor-time-dashboard.component.css | 54 ++++++++++++++ .../tutor-time-dashboard.component.html | 36 ++++++++++ .../tutor-time-dashboard.component.spec.ts | 23 ++++++ .../tutor-time-dashboard.component.ts | 70 +++++++++++++++++++ 5 files changed, 199 insertions(+) create mode 100644 src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.css create mode 100644 src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.html create mode 100644 src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.spec.ts create mode 100644 src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.ts diff --git a/src/app/doubtfire.states.ts b/src/app/doubtfire.states.ts index 7baa910f57..650e6f1986 100644 --- a/src/app/doubtfire.states.ts +++ b/src/app/doubtfire.states.ts @@ -14,6 +14,7 @@ import {ProjectRootState} from './projects/states/project-root-state.component'; import { TaskViewerState } from './units/task-viewer/task-viewer-state.component'; import {ScormPlayerComponent} from './common/scorm-player/scorm-player.component'; import { Ng2ViewDeclaration } from '@uirouter/angular'; +import { TutorTimeDashboardComponent } from './sessions/tutor-time-dashboard/tutor-time-dashboard.component'; /* * Use this file to store any states that are sourced by angular components. @@ -189,6 +190,20 @@ const SignInState: NgHybridStateDeclaration = { }, }; +const TutorTimeDashboardState: NgHybridStateDeclaration = { + name: 'tutor-times', + url: '/tutor-times', + views: { + main: { + component: TutorTimeDashboardComponent, + }, + }, + data: { + pageTitle: 'Tutor Time Dashboard', + roleWhitelist: ['Tutor', 'Convenor', 'Admin'], + }, +}; + /** * Define the Edit Profile state. */ @@ -433,4 +448,5 @@ export const doubtfireStates = [ ScormPlayerNormalState, ScormPlayerReviewState, ScormPlayerStudentReviewState, + TutorTimeDashboardState, ]; diff --git a/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.css b/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.css new file mode 100644 index 0000000000..ad6773e92c --- /dev/null +++ b/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.css @@ -0,0 +1,54 @@ +table { + border-collapse: collapse; + width: 100%; +} + +thead tr { + background-color: #2563eb; + color: white; +} + +th { + font-weight: bold; + padding: 0.75rem; + text-align: left; + cursor: pointer; +} + +td { + padding: 0.75rem; + border-top: 1px solid #e5e7eb; +} + +tr:hover { + background-color: #f9fafb; +} + +.pagination-container { + display: flex; + justify-content: center; + align-items: center; + margin-top: 1rem; + padding: 0.5rem 1rem; + background-color: #f3f4f6; + border-radius: 0.375rem; +} + +.pagination-container button { + background-color: #e5e7eb; + padding: 0.25rem 0.75rem; + margin: 0 0.25rem; + border-radius: 0.375rem; + border: none; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.pagination-container button:hover:not(:disabled) { + background-color: #d1d5db; +} + +.pagination-container button:disabled { + opacity: 0.5; + cursor: not-allowed; +} diff --git a/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.html b/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.html new file mode 100644 index 0000000000..5f500d5df4 --- /dev/null +++ b/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.html @@ -0,0 +1,36 @@ +
+

Tutor Time Dashboard

+ + + + + + + + + + + + + + + + + + +
StartEndDurationUnit
{{ s.start }}{{ s.end }}{{ s.duration }}{{ s.unitCode }}
+ +

No sessions found.

+ +
+ + + Page {{ currentPage }} of {{ totalPages }} + + +
+ +
diff --git a/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.spec.ts b/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.spec.ts new file mode 100644 index 0000000000..c2cfec3889 --- /dev/null +++ b/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TutorTimeDashboardComponent } from './tutor-time-dashboard.component'; + +describe('TutorTimeDashboardComponent', () => { + let component: TutorTimeDashboardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TutorTimeDashboardComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(TutorTimeDashboardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.ts b/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.ts new file mode 100644 index 0000000000..dd6dca5a53 --- /dev/null +++ b/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.ts @@ -0,0 +1,70 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +interface Session { + start: string; + end: string; + duration: string; + unitCode: string; +} + +@Component({ + selector: 'f-tutor-time-dashboard', + standalone: true, + imports: [CommonModule], + templateUrl: './tutor-time-dashboard.component.html', + styleUrls: ['./tutor-time-dashboard.component.css'] +}) +export class TutorTimeDashboardComponent implements OnInit { + sessions: Session[] = []; + paginatedSessions: Session[] = []; + currentPage = 1; + pageSize = 5; + totalPages = 1; + sortField: keyof Session | '' = ''; + sortAsc = true; + + ngOnInit(): void { + // Mock data + this.sessions = [ + { start: '2025-09-15 09:00', end: '2025-09-15 11:00', duration: '2h 0m', unitCode: 'COS20007' }, + { start: '2025-09-16 14:00', end: '2025-09-16 15:30', duration: '1h 30m', unitCode: 'COS20007' }, + { start: '2025-09-17 08:00', end: '2025-09-17 09:30', duration: '1h 30m', unitCode: 'COS20007' }, + { start: '2025-09-18 10:00', end: '2025-09-18 12:00', duration: '2h 0m', unitCode: 'COS20007' }, + { start: '2025-09-19 13:00', end: '2025-09-19 14:15', duration: '1h 15m', unitCode: 'COS20007' }, + { start: '2025-09-20 09:00', end: '2025-09-20 10:00', duration: '1h 0m', unitCode: 'COS20007' } + ]; + + this.totalPages = Math.ceil(this.sessions.length / this.pageSize); + this.updatePage(); + } + + updatePage(): void { + const startIndex = (this.currentPage - 1) * this.pageSize; + this.paginatedSessions = this.sessions.slice(startIndex, startIndex + this.pageSize); + } + + goToPage(page: number): void { + if (page >= 1 && page <= this.totalPages) { + this.currentPage = page; + this.updatePage(); + } + } + + sortBy(field: keyof Session): void { + if (this.sortField === field) { + this.sortAsc = !this.sortAsc; + } else { + this.sortField = field; + this.sortAsc = true; + } + + this.sessions.sort((a, b) => { + if (a[field] < b[field]) return this.sortAsc ? -1 : 1; + if (a[field] > b[field]) return this.sortAsc ? 1 : -1; + return 0; + }); + + this.updatePage(); + } +} From 5ebe4a536e376d3403c9490099090e7f22e0261f Mon Sep 17 00:00:00 2001 From: WaelAlahamdi Date: Sun, 21 Sep 2025 07:28:56 +1000 Subject: [PATCH 2/2] feat(tutor-time-dashboard): add stats cards, charts, and session details --- src/app/doubtfire-angular.module.ts | 2 +- .../tutor-time-dashboard.component.css | 58 +++--- .../tutor-time-dashboard.component.html | 138 ++++++++++++--- .../tutor-time-dashboard.component.ts | 165 +++++++++++++----- src/main.ts | 22 ++- 5 files changed, 273 insertions(+), 112 deletions(-) diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index c714ff0e5a..c84b111aa6 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -472,7 +472,7 @@ import { UnitStudentEnrolmentModalComponent } from './units/modals/unit-student- imports: [ FlexLayoutModule, BrowserModule, - BrowserAnimationsModule, + BrowserAnimationsModule, FormsModule, HttpClientModule, ClipboardModule, diff --git a/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.css b/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.css index ad6773e92c..fae5743e9f 100644 --- a/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.css +++ b/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.css @@ -1,18 +1,35 @@ +:host { + --tt-primary: #1976d2; + --tt-text: #1f2937; +} + +.h-title { + color: var(--tt-primary); + font-weight: 600; +} + +.text-muted { + color: var(--tt-text); + opacity: 0.75; +} + +.summary-card h3 { + font-weight: 700; +} + +.summary-card p { + font-weight: 400; +} + table { border-collapse: collapse; width: 100%; } -thead tr { - background-color: #2563eb; - color: white; -} - th { font-weight: bold; padding: 0.75rem; text-align: left; - cursor: pointer; } td { @@ -23,32 +40,3 @@ td { tr:hover { background-color: #f9fafb; } - -.pagination-container { - display: flex; - justify-content: center; - align-items: center; - margin-top: 1rem; - padding: 0.5rem 1rem; - background-color: #f3f4f6; - border-radius: 0.375rem; -} - -.pagination-container button { - background-color: #e5e7eb; - padding: 0.25rem 0.75rem; - margin: 0 0.25rem; - border-radius: 0.375rem; - border: none; - cursor: pointer; - transition: background-color 0.2s ease; -} - -.pagination-container button:hover:not(:disabled) { - background-color: #d1d5db; -} - -.pagination-container button:disabled { - opacity: 0.5; - cursor: not-allowed; -} diff --git a/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.html b/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.html index 5f500d5df4..6317a4e252 100644 --- a/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.html +++ b/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.html @@ -1,36 +1,118 @@
-

Tutor Time Dashboard

+

Tutor Time Dashboard

- - - - - - - - - - - - - - - - - -
StartEndDurationUnit
{{ s.start }}{{ s.end }}{{ s.duration }}{{ s.unitCode }}
- -

No sessions found.

+ +
+
+

{{ dashboardStats[0].sessionsTotal }}

+

Total Sessions

+
+
+

{{ dashboardStats[0].minutesTotal }} mins

+

Total Time Spent

+
+
+

{{ dashboardStats[0].tasksReviewed }}

+

Tasks Reviewed

+
+
+

+ {{ (dashboardStats[0].minutesTotal / dashboardStats[0].tasksReviewed) | number:'1.0-0' }} mins +

+

Avg Time per Task

+
+
-
- + +
+

Time Spent per Task

+ + +
- Page {{ currentPage }} of {{ totalPages }} + +
+

Task Details

+ + + + + + + + + + + + + + + + + +
TaskTime Spent (mins)AssessmentsAvg Time/Assessment
{{ t.taskLabel }}{{ t.minutesSpent }}{{ t.reviewCount }}{{ t.avgPerReview | number:'1.0-0' }}
+
- + +
+

Recent Sessions

+
+
+

Session #{{ s.id }}

+

{{ s.startTime | date:'short' }}

+

{{ s.durationMinutes }} mins

+

+ Status: + + {{ s.isActive ? 'Active' : 'Completed' }} + +

+ +
+
+ +
+

Session Details - #{{ selectedSessionId }}

+ + + +

Session Activities

+
+

+ {{ ev.action }} + ({{ formatMinutes(ev.durationMinutes) }}) + {{ ev.createdAt | date:'short' }} +

+

Project: {{ ev.projectId }} | Task: {{ ev.taskId }}

+
+ + +

Assessments in this Session

+ + + + + + + + + + + + + + + + + + + +
StudentTaskDurationStartEnd
{{ a.student }}{{ a.task }}{{ formatMinutes(a.durationMinutes) }}{{ a.start | date:'short' }}{{ a.end | date:'short' }}
+
diff --git a/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.ts b/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.ts index dd6dca5a53..159f50133f 100644 --- a/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.ts +++ b/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.ts @@ -1,70 +1,149 @@ import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { NgxChartsModule } from '@swimlane/ngx-charts'; -interface Session { - start: string; - end: string; - duration: string; - unitCode: string; +interface DashboardStats { + sessionsTotal: number; + minutesTotal: number; + tasksReviewed: number; +} + +interface RecentSession { + id: number; + startTime: Date; + durationMinutes: number; + isActive: boolean; +} + +interface TaskSummary { + taskLabel: string; + minutesSpent: number; + reviewCount: number; + avgPerReview: number; +} + +interface SessionEvent { + action: string; + durationMinutes?: number; + createdAt: Date; + projectId?: string; + taskId?: string; +} + +interface SessionAssessment { + student: string; + task: string; + durationMinutes: number; + start: Date; + end: Date; } @Component({ selector: 'f-tutor-time-dashboard', standalone: true, - imports: [CommonModule], + imports: [CommonModule, NgxChartsModule], templateUrl: './tutor-time-dashboard.component.html', styleUrls: ['./tutor-time-dashboard.component.css'] }) export class TutorTimeDashboardComponent implements OnInit { - sessions: Session[] = []; - paginatedSessions: Session[] = []; - currentPage = 1; - pageSize = 5; - totalPages = 1; - sortField: keyof Session | '' = ''; - sortAsc = true; + dashboardStats: DashboardStats[] = []; + recentSessions: RecentSession[] = []; + taskSummary: TaskSummary[] = []; + sessionEvents: SessionEvent[] = []; + sessionAssessments: SessionAssessment[] = []; + + selectedSessionId?: number; + showDetails = false; + + // chart options + view: [number, number] = [600, 300]; + colorScheme = { domain: ['#1976d2', '#0288d1', '#ff9800', '#4caf50'] }; + + // chart data + taskChartData: { name: string; value: number }[] = []; ngOnInit(): void { - // Mock data - this.sessions = [ - { start: '2025-09-15 09:00', end: '2025-09-15 11:00', duration: '2h 0m', unitCode: 'COS20007' }, - { start: '2025-09-16 14:00', end: '2025-09-16 15:30', duration: '1h 30m', unitCode: 'COS20007' }, - { start: '2025-09-17 08:00', end: '2025-09-17 09:30', duration: '1h 30m', unitCode: 'COS20007' }, - { start: '2025-09-18 10:00', end: '2025-09-18 12:00', duration: '2h 0m', unitCode: 'COS20007' }, - { start: '2025-09-19 13:00', end: '2025-09-19 14:15', duration: '1h 15m', unitCode: 'COS20007' }, - { start: '2025-09-20 09:00', end: '2025-09-20 10:00', duration: '1h 0m', unitCode: 'COS20007' } + this.dashboardStats = [{ + sessionsTotal: 6, + minutesTotal: 480, + tasksReviewed: 67 + }]; + + this.recentSessions = [ + { id: 101, startTime: new Date('2025-09-15T09:00'), durationMinutes: 120, isActive: false }, + { id: 102, startTime: new Date('2025-09-16T14:00'), durationMinutes: 90, isActive: false }, + { id: 103, startTime: new Date('2025-09-17T08:00'), durationMinutes: 90, isActive: true }, + { id: 104, startTime: new Date('2025-09-18T10:00'), durationMinutes: 120, isActive: false }, + { id: 105, startTime: new Date('2025-09-19T13:00'), durationMinutes: 75, isActive: false }, + { id: 106, startTime: new Date('2025-09-20T09:00'), durationMinutes: 60, isActive: false } ]; - this.totalPages = Math.ceil(this.sessions.length / this.pageSize); - this.updatePage(); - } + this.taskSummary = [ + { taskLabel: 'Task 1', minutesSpent: 150, reviewCount: 10, avgPerReview: 15 }, + { taskLabel: 'Task 2', minutesSpent: 90, reviewCount: 5, avgPerReview: 18 }, + { taskLabel: 'Task 3', minutesSpent: 240, reviewCount: 20, avgPerReview: 12 }, + ]; - updatePage(): void { - const startIndex = (this.currentPage - 1) * this.pageSize; - this.paginatedSessions = this.sessions.slice(startIndex, startIndex + this.pageSize); + this.taskChartData = this.taskSummary.map(t => ({ + name: t.taskLabel, + value: t.minutesSpent + })); } - goToPage(page: number): void { - if (page >= 1 && page <= this.totalPages) { - this.currentPage = page; - this.updatePage(); + loadSessionEvents(sessionId: number): SessionEvent[] { + if (sessionId === 101) { + return [ + { action: 'Assessing', durationMinutes: 30, createdAt: new Date(), projectId: 'P1', taskId: 'T1' }, + { action: 'Inbox', createdAt: new Date(), projectId: 'P1', taskId: 'T2' } + ]; + } else if (sessionId === 102) { + return [ + { action: 'Assessing', durationMinutes: 45, createdAt: new Date(), projectId: 'P2', taskId: 'T3' }, + { action: 'Completed', createdAt: new Date() } + ]; } + return [ + { action: 'Completed', createdAt: new Date() } + ]; } - sortBy(field: keyof Session): void { - if (this.sortField === field) { - this.sortAsc = !this.sortAsc; - } else { - this.sortField = field; - this.sortAsc = true; + loadSessionAssessments(sessionId: number): SessionAssessment[] { + if (sessionId === 101) { + return [ + { student: 'Alice', task: 'Essay', durationMinutes: 45, start: new Date(), end: new Date() }, + { student: 'Bob', task: 'Quiz', durationMinutes: 30, start: new Date(), end: new Date() } + ]; + } else if (sessionId === 102) { + return [ + { student: 'Charlie', task: 'Presentation', durationMinutes: 60, start: new Date(), end: new Date() } + ]; } + return []; + } - this.sessions.sort((a, b) => { - if (a[field] < b[field]) return this.sortAsc ? -1 : 1; - if (a[field] > b[field]) return this.sortAsc ? 1 : -1; - return 0; - }); + openDetails(sessionId: number): void { + this.selectedSessionId = sessionId; + this.showDetails = true; + this.sessionEvents = [ + { action: 'Assessing', durationMinutes: 30, createdAt: new Date(), projectId: 'P1', taskId: 'T1' }, + { action: 'Inbox', createdAt: new Date(), projectId: 'P1', taskId: 'T2' }, + { action: 'Completed', createdAt: new Date() } + ]; + + this.sessionAssessments = [ + { student: 'Alice', task: 'Essay', durationMinutes: 45, start: new Date(), end: new Date() }, + { student: 'Bob', task: 'Quiz', durationMinutes: 30, start: new Date(), end: new Date() } + ]; + } + + closeDetails(): void { + this.showDetails = false; + this.selectedSessionId = undefined; + } - this.updatePage(); + formatMinutes(mins: number): string { + const h = Math.floor(mins / 60); + const m = mins % 60; + return `${h}h ${m}m`; } } diff --git a/src/main.ts b/src/main.ts index a03768c5fc..249bac1427 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,21 +11,32 @@ import { DoubtfireAngularModule } from './app/doubtfire-angular.module'; import { UIRouter, UrlService } from '@uirouter/core'; +// Import Browser Animations Module for ngx-charts tooltips +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + if (environment.production) { enableProdMode(); } // Using AngularJS config block, call `deferIntercept()`. // This tells UI-Router to delay the initial URL sync (until all bootstrapping is complete) -DoubtfireAngularJSModule.config(['$urlServiceProvider', ($urlService: UrlService) => $urlService.deferIntercept()]); +DoubtfireAngularJSModule.config([ + '$urlServiceProvider', + ($urlService: UrlService) => $urlService.deferIntercept() +]); // Manually bootstrap the Angular app platformBrowserDynamic() - .bootstrapModule(DoubtfireAngularModule) + .bootstrapModule(DoubtfireAngularModule, { + // Ensure BrowserAnimationsModule is available to the root module + ngZone: 'zone.js', + }) .then((platformRef) => { - // Intialize the Angular Module + // Initialize the Angular Module // get() the UIRouter instance from DI to initialize the router - const urlService: UrlService = platformRef.injector.get(UIRouter as Type).urlService; + const urlService: UrlService = platformRef.injector.get( + UIRouter as Type + ).urlService; // Instruct UIRouter to listen to URL changes function startUIRouter() { @@ -34,4 +45,5 @@ platformBrowserDynamic() } platformRef.injector.get(NgZone).run(startUIRouter); - }); + }) + .catch((err) => console.error(err));