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/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..fae5743e9f --- /dev/null +++ b/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.css @@ -0,0 +1,42 @@ +: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%; +} + +th { + font-weight: bold; + padding: 0.75rem; + text-align: left; +} + +td { + padding: 0.75rem; + border-top: 1px solid #e5e7eb; +} + +tr:hover { + background-color: #f9fafb; +} 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..6317a4e252 --- /dev/null +++ b/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.html @@ -0,0 +1,118 @@ +
+

Tutor Time Dashboard

+ + +
+
+

{{ 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

+ + +
+ + +
+

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.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..159f50133f --- /dev/null +++ b/src/app/sessions/tutor-time-dashboard/tutor-time-dashboard.component.ts @@ -0,0 +1,149 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { NgxChartsModule } from '@swimlane/ngx-charts'; + +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, NgxChartsModule], + templateUrl: './tutor-time-dashboard.component.html', + styleUrls: ['./tutor-time-dashboard.component.css'] +}) +export class TutorTimeDashboardComponent implements OnInit { + 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 { + 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.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 }, + ]; + + this.taskChartData = this.taskSummary.map(t => ({ + name: t.taskLabel, + value: t.minutesSpent + })); + } + + 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() } + ]; + } + + 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 []; + } + + 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; + } + + 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));