diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 991ac397b..36eb879b0 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -230,6 +230,7 @@ import {FTaskSheetViewComponent} from './units/states/tasks/viewer/directives/f- import {TasksViewerComponent} from './units/states/tasks/tasks-viewer/tasks-viewer.component'; import {UnitCodeComponent} from './common/unit-code/unit-code.component'; import {GradeService} from './common/services/grade.service'; +import { TargetGradeHistoryComponent } from './projects/states/dashboard/directives/progress-dashboard/target-grade-history/target-grade-history.component'; import {ScormPlayerComponent} from './common/scorm-player/scorm-player.component'; import {ScormAdapterService} from './api/services/scorm-adapter.service'; import {ScormCommentComponent} from './tasks/task-comments-viewer/scorm-comment/scorm-comment.component'; @@ -352,6 +353,7 @@ const MY_DATE_FORMAT = { FUsersComponent, FTaskBadgeComponent, FUnitsComponent, + TargetGradeHistoryComponent, ScormPlayerComponent, ScormCommentComponent, TaskScormCardComponent, diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index e14fdbb54..a8c1643ce 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -224,6 +224,8 @@ import {FUnitsComponent} from './admin/states/f-units/f-units.component'; import {MarkedPipe} from './common/pipes/marked.pipe'; import {AlertService} from './common/services/alert.service'; import {GradeService} from './common/services/grade.service'; + +import {TargetGradeHistoryComponent} from './projects/states/dashboard/directives/progress-dashboard/target-grade-history/target-grade-history.component'; import {TaskScormCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component'; export const DoubtfireAngularJSModule = angular.module('doubtfire', [ @@ -465,6 +467,12 @@ DoubtfireAngularJSModule.directive( ); DoubtfireAngularJSModule.directive('newFUnits', downgradeComponent({component: FUnitsComponent})); +DoubtfireAngularJSModule.directive( + 'targetGradeHistory', + downgradeComponent({ + component: TargetGradeHistoryComponent + }) +); // Global configuration // If the user enters a URL that doesn't match any known URL (state), send them to `/home` diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.coffee b/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.coffee index 0a82f1ab9..4c53c74ee 100644 --- a/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.coffee +++ b/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.coffee @@ -1,19 +1,17 @@ angular.module('doubtfire.projects.states.dashboard.directives.progress-dashboard', []) -# -# Summary dashboard showing some graphs and way to change the -# current target grade -# -.directive('progressDashboard', -> +.directive 'progressDashboard', -> restrict: 'E' templateUrl: 'projects/states/dashboard/directives/progress-dashboard/progress-dashboard.tpl.html' scope: project: '=' onUpdateTargetGrade: '=' - controller: ($scope, $stateParams, newProjectService, gradeService, analyticsService, alertService) -> + controller: ($scope, $stateParams, newProjectService, gradeService, analyticsService, alertService, $http, DoubtfireConstants) -> + # Is the current user a tutor? $scope.tutor = $stateParams.tutor + # Number of tasks completed and remaining - updateTaskCompletionValues = -> + updateTaskCompletionValues = -> completedTasks = $scope.project.numberTasks("complete") $scope.numberOfTasks = completed: completedTasks @@ -25,6 +23,15 @@ angular.module('doubtfire.projects.states.dashboard.directives.progress-dashboar names: gradeService.grades values: gradeService.gradeValues + # Fetch Target Grade History + $scope.targetGradeHistory = [] + + $http.get("#{DoubtfireConstants.API_URL}/projects/#{$scope.project.id}") + .then (response) -> + $scope.targetGradeHistory = response.data.target_grade_histories + .catch (error) -> + alertService.error("Failed to load target grade history", 4000) + $scope.updateTargetGrade = (newGrade) -> $scope.project.targetGrade = newGrade newProjectService.update($scope.project).subscribe( @@ -35,10 +42,21 @@ angular.module('doubtfire.projects.states.dashboard.directives.progress-dashboar updateTaskCompletionValues() $scope.renderTaskStatusPieChart?() $scope.onUpdateTargetGrade?() - analyticsService.event("Student Project View - Progress Dashboard", "Grade Changed", $scope.grades.names[newGrade]) - alertService.success( "Updated target grade successfully", 2000) + analyticsService.event( + "Student Project View - Progress Dashboard", + "Grade Changed", + $scope.grades.names[newGrade] + ) + + # Fetch updated target grade history + $http.get("#{DoubtfireConstants.API_URL}/projects/#{$scope.project.id}") + .then (response) -> + $scope.targetGradeHistory = response.data.target_grade_histories + .catch (error) -> + alertService.error("Failed to reload target grade history", 4000) + + alertService.success("Updated target grade successfully", 2000) - (failure) -> - alertService.error( "Failed to update target grade", 4000) + , (failure) -> + alertService.error("Failed to update target grade", 4000) ) -) diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.tpl.html b/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.tpl.html index 53269a2a9..de54423c9 100644 --- a/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.tpl.html +++ b/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.tpl.html @@ -5,54 +5,68 @@

-
-
-
-

Target Grade

-
-
- -
-
-
-
-

Progress Burndown

-
- The burndown chart shows how much work remains for you to achieve your target grade. -
-
-
- -
- -
-
-
-
-
-

Task Statuses

-
- Breakdown summary of each of your task statuses. -
-
-
- - -
-
-
+
+ +
+ +
+
+

Target Grade

+
+
+ +
+
+ + +
+
+

Progress Burndown

+
+ The burndown chart shows how much work remains for you to achieve your target grade. +
+
+
+ +
+ +
+
+ + +
+ +
+
+

Task Statuses

+
+ Breakdown summary of each of your task statuses. +
+
+
+ + +
+
+ + + + + +
+
diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/target-grade-history/target-grade-history.component.html b/src/app/projects/states/dashboard/directives/progress-dashboard/target-grade-history/target-grade-history.component.html new file mode 100644 index 000000000..4aea714f7 --- /dev/null +++ b/src/app/projects/states/dashboard/directives/progress-dashboard/target-grade-history/target-grade-history.component.html @@ -0,0 +1,59 @@ +
+
+

Target Grade Change History

+
+
+
Loading history...
+
+ {{ error }} +
+
+ No grade history available +
+ + + + + + + + + + + + + + + + + +
Changed AtPrevious GradeNew GradeChanged By
{{ history.changed_at | date: 'medium' }}{{ history.previous_grade }}{{ history.new_grade }}{{ history.changed_by?.first_name }} {{ history.changed_by?.last_name }}
+
+ + + +
+
+
diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/target-grade-history/target-grade-history.component.scss b/src/app/projects/states/dashboard/directives/progress-dashboard/target-grade-history/target-grade-history.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/target-grade-history/target-grade-history.component.spec.ts b/src/app/projects/states/dashboard/directives/progress-dashboard/target-grade-history/target-grade-history.component.spec.ts new file mode 100644 index 000000000..972bab2ed --- /dev/null +++ b/src/app/projects/states/dashboard/directives/progress-dashboard/target-grade-history/target-grade-history.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TargetGradeHistoryComponent } from './target-grade-history.component'; + +describe('TargetGradeHistoryComponent', () => { + let component: TargetGradeHistoryComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TargetGradeHistoryComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(TargetGradeHistoryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/target-grade-history/target-grade-history.component.ts b/src/app/projects/states/dashboard/directives/progress-dashboard/target-grade-history/target-grade-history.component.ts new file mode 100644 index 000000000..8b7aacb85 --- /dev/null +++ b/src/app/projects/states/dashboard/directives/progress-dashboard/target-grade-history/target-grade-history.component.ts @@ -0,0 +1,160 @@ +import {Component, Input, OnInit, OnDestroy} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {catchError} from 'rxjs/operators'; +import {of, interval, Subscription} from 'rxjs'; +import {GradeService} from 'src/app/common/services/grade.service'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; + +interface User { + id: number; + email: string; + first_name: string; + last_name: string; + username: string; + nickname: string; +} + +interface TargetGradeHistory { + previous_grade: string | number; + new_grade: string | number; + changed_at: string; + changed_by: User; +} + +@Component({ + selector: 'f-target-grade-history', + templateUrl: './target-grade-history.component.html', + styleUrls: ['./target-grade-history.component.scss'], +}) +export class TargetGradeHistoryComponent implements OnInit, OnDestroy { + @Input() projectId!: number; + + targetGradeHistory: TargetGradeHistory[] = []; + paginatedGradeHistory: TargetGradeHistory[] = []; + loading = false; + error: string | null = null; + + currentPage = 1; + itemsPerPage = 10; + totalPages = 1; + private gradeCheckSubscription!: Subscription; + + constructor( + private http: HttpClient, + private gradeService: GradeService, + private constants: DoubtfireConstants, + ) {} + + ngOnInit(): void { + if (this.projectId) { + this.loadTargetGradeHistory(); + this.startGradeChangeListener(); + } + } + + ngOnDestroy(): void { + if (this.gradeCheckSubscription) { + this.gradeCheckSubscription.unsubscribe(); + } + } + + private loadTargetGradeHistory(): void { + if (!this.projectId) { + console.error('No projectId provided'); + this.error = 'Project ID is required'; + return; + } + + this.loading = true; + this.error = null; + + const url = `${this.constants.API_URL}/projects/${this.projectId}`; + + this.http + .get(url) + .pipe( + catchError((error) => { + console.error('Error fetching target grade history:', error); + this.error = 'Failed to load target grade history'; + return of({target_grade_histories: []}); + }), + ) + .subscribe({ + next: (response) => { + this.targetGradeHistory = response.target_grade_histories + .filter( + (history: TargetGradeHistory) => + history.previous_grade !== undefined && history.new_grade !== undefined, + ) + .map((history: TargetGradeHistory) => ({ + ...history, + previous_grade: this.gradeService.grades[history.previous_grade] || 'N/A', + new_grade: this.gradeService.grades[history.new_grade] || 'N/A', + })) + .sort((a, b) => new Date(b.changed_at).getTime() - new Date(a.changed_at).getTime()); // Sort newest first + + this.updatePagination(); + this.loading = false; + }, + error: () => { + this.error = 'Failed to load target grade history'; + this.loading = false; + }, + }); + } + + private startGradeChangeListener(): void { + const checkInterval = 3000; // 3 seconds + this.gradeCheckSubscription = interval(checkInterval).subscribe(() => { + this.checkForGradeChange(); + }); + } + + private checkForGradeChange(): void { + const url = `${this.constants.API_URL}/projects/${this.projectId}`; + this.http + .get(url) + .pipe( + catchError((error) => { + console.error('Error checking for grade change:', error); + return of(null); + }), + ) + .subscribe((response) => { + if (response && response.target_grade_histories) { + const latestHistory = response.target_grade_histories.map( + (history: TargetGradeHistory) => ({ + ...history, + previous_grade: this.gradeService.grades[history.previous_grade] || 'N/A', + new_grade: this.gradeService.grades[history.new_grade] || 'N/A', + }), + ); + + if (JSON.stringify(this.targetGradeHistory) !== JSON.stringify(latestHistory)) { + this.targetGradeHistory = latestHistory.sort( + (a, b) => new Date(b.changed_at).getTime() - new Date(a.changed_at).getTime(), + ); + this.updatePagination(); + } + } + }); + } + + private updatePagination(): void { + this.totalPages = Math.ceil(this.targetGradeHistory.length / this.itemsPerPage); + this.updatePaginatedGradeHistory(); + } + + private updatePaginatedGradeHistory(): void { + const startIndex = (this.currentPage - 1) * this.itemsPerPage; + const endIndex = startIndex + this.itemsPerPage; + this.paginatedGradeHistory = this.targetGradeHistory.slice(startIndex, endIndex); + } + + public goToPage(page: number): void { + if (page >= 1 && page <= this.totalPages) { + this.currentPage = page; + this.updatePaginatedGradeHistory(); + } + } +}