Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/doubtfire-angular.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ import { UnitStudentEnrolmentModalComponent } from './units/modals/unit-student-
imports: [
FlexLayoutModule,
BrowserModule,
BrowserAnimationsModule,
BrowserAnimationsModule,
FormsModule,
HttpClientModule,
ClipboardModule,
Expand Down
16 changes: 16 additions & 0 deletions src/app/doubtfire.states.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -433,4 +448,5 @@ export const doubtfireStates = [
ScormPlayerNormalState,
ScormPlayerReviewState,
ScormPlayerStudentReviewState,
TutorTimeDashboardState,
];
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<div class="p-4 overflow-x-auto">
<h2 class="h-title text-xl mb-4">Tutor Time Dashboard</h2>

<!-- Summary Cards -->
<div *ngIf="dashboardStats.length > 0" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<div class="summary-card text-center">
<h3 class="h-title m-0 text-2xl font-bold">{{ dashboardStats[0].sessionsTotal }}</h3>
<p class="text-muted">Total Sessions</p>
</div>
<div class="summary-card text-center">
<h3 class="h-title m-0 text-2xl font-bold">{{ dashboardStats[0].minutesTotal }} mins</h3>
<p class="text-muted">Total Time Spent</p>
</div>
<div class="summary-card text-center">
<h3 class="h-title m-0 text-2xl font-bold">{{ dashboardStats[0].tasksReviewed }}</h3>
<p class="text-muted">Tasks Reviewed</p>
</div>
<div class="summary-card text-center">
<h3 class="h-title m-0 text-2xl font-bold">
{{ (dashboardStats[0].minutesTotal / dashboardStats[0].tasksReviewed) | number:'1.0-0' }} mins
</h3>
<p class="text-muted">Avg Time per Task</p>
</div>
</div>

<!-- Chart -->
<div class="my-8">
<h3 class="h-title text-lg mb-4">Time Spent per Task</h3>
<ngx-charts-bar-vertical
[view]="view"
[scheme]="colorScheme"
[results]="taskChartData">
</ngx-charts-bar-vertical>
</div>

<!-- Task Details Table -->
<div class="mt-8">
<h3 class="h-title text-lg mb-4">Task Details</h3>
<table class="table-auto w-full border border-gray-300" *ngIf="taskSummary.length > 0">
<thead>
<tr>
<th class="p-2">Task</th>
<th class="p-2">Time Spent (mins)</th>
<th class="p-2">Assessments</th>
<th class="p-2">Avg Time/Assessment</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let t of taskSummary" class="border-b hover:bg-gray-100">
<td class="p-2">{{ t.taskLabel }}</td>
<td class="p-2">{{ t.minutesSpent }}</td>
<td class="p-2">{{ t.reviewCount }}</td>
<td class="p-2">{{ t.avgPerReview | number:'1.0-0' }}</td>
</tr>
</tbody>
</table>
</div>

<!-- Recent Sessions -->
<div class="my-8">
<h3 class="h-title text-lg mb-4">Recent Sessions</h3>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div *ngFor="let s of recentSessions" class="border p-4 rounded shadow">
<p class="font-semibold">Session #{{ s.id }}</p>
<p>{{ s.startTime | date:'short' }}</p>
<p>{{ s.durationMinutes }} mins</p>
<p>
Status:
<span [ngClass]="s.isActive ? 'text-green-600' : 'text-gray-500'">
{{ s.isActive ? 'Active' : 'Completed' }}
</span>
</p>
<button class="mt-2 px-3 py-1 bg-blue-600 text-white rounded"
(click)="openDetails(s.id)">View Details</button>
</div>
</div>
</div>

<!-- Session Details -->
<div *ngIf="showDetails && selectedSessionId" class="my-8 border p-4 rounded shadow">
<h3 class="h-title text-lg mb-4">Session Details - #{{ selectedSessionId }}</h3>
<button (click)="closeDetails()" class="mb-4 px-3 py-1 bg-gray-300 rounded">Close</button>

<!-- Activities -->
<h4 class="font-semibold mb-2">Session Activities</h4>
<div *ngFor="let ev of sessionEvents" class="border-l-4 border-blue-600 pl-2 mb-2">
<p>
<strong>{{ ev.action }}</strong>
<span *ngIf="ev.durationMinutes"> ({{ formatMinutes(ev.durationMinutes) }})</span>
<span class="ml-2 text-sm text-gray-600">{{ ev.createdAt | date:'short' }}</span>
</p>
<p *ngIf="ev.projectId">Project: {{ ev.projectId }} | Task: {{ ev.taskId }}</p>
</div>

<!-- Assessments -->
<h4 class="font-semibold mt-4 mb-2">Assessments in this Session</h4>
<table class="table-auto w-full border border-gray-300" *ngIf="sessionAssessments.length > 0">
<thead>
<tr>
<th class="p-2">Student</th>
<th class="p-2">Task</th>
<th class="p-2">Duration</th>
<th class="p-2">Start</th>
<th class="p-2">End</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let a of sessionAssessments" class="border-b hover:bg-gray-100">
<td class="p-2">{{ a.student }}</td>
<td class="p-2">{{ a.task }}</td>
<td class="p-2">{{ formatMinutes(a.durationMinutes) }}</td>
<td class="p-2">{{ a.start | date:'short' }}</td>
<td class="p-2">{{ a.end | date:'short' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -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<TutorTimeDashboardComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TutorTimeDashboardComponent]
})
.compileComponents();

fixture = TestBed.createComponent(TutorTimeDashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -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`;
}
}
Loading