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
+
+
0"
+ >
+
+
+ Changed At |
+ Previous Grade |
+ New Grade |
+ Changed By |
+
+
+
+
+ {{ history.changed_at | date: 'medium' }} |
+ {{ history.previous_grade }} |
+ {{ history.new_grade }} |
+ {{ history.changed_by?.first_name }} {{ history.changed_by?.last_name }} |
+
+
+
+
1" class="pagination-controls mt-3">
+
+
+
+
+
+
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();
+ }
+ }
+}