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
+
+
+
0" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
+
+
{{ 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
+
0">
+
+
+ | Task |
+ Time Spent (mins) |
+ Assessments |
+ Avg 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
+
0">
+
+
+ | Student |
+ Task |
+ Duration |
+ Start |
+ End |
+
+
+
+
+ | {{ 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));