diff --git a/projects/v3/src/app/components/assessment/assessment.component.html b/projects/v3/src/app/components/assessment/assessment.component.html index 3b0b98f58..d5ef4737b 100644 --- a/projects/v3/src/app/components/assessment/assessment.component.html +++ b/projects/v3/src/app/components/assessment/assessment.component.html @@ -56,6 +56,24 @@ + + + + + + + + Project Brief + + + + + diff --git a/projects/v3/src/app/components/assessment/assessment.component.spec.ts b/projects/v3/src/app/components/assessment/assessment.component.spec.ts index eb1b98c2e..564ad06a0 100644 --- a/projects/v3/src/app/components/assessment/assessment.component.spec.ts +++ b/projects/v3/src/app/components/assessment/assessment.component.spec.ts @@ -17,6 +17,7 @@ import { BehaviorSubject, of, Subject } from 'rxjs'; import { MockRouter } from '@testingv3/mocked.service'; import { TestUtils } from '@testingv3/utils'; import { ApolloService } from '@v3/app/services/apollo.service'; +import { ModalController } from '@ionic/angular'; class Page { get savingMessage() { @@ -101,6 +102,7 @@ describe('AssessmentComponent', () => { let shared: SharedService; let utils: UtilsService; let apolloSpy: jasmine.SpyObj; + let modalSpy: jasmine.SpyObj; const mockQuestions = [ { @@ -236,6 +238,10 @@ describe('AssessmentComponent', () => { provide: Router, useClass: MockRouter, }, + { + provide: ModalController, + useValue: jasmine.createSpyObj('ModalController', ['create', 'dismiss']), + }, ] }).compileComponents(); @@ -256,6 +262,7 @@ describe('AssessmentComponent', () => { apolloSpy = TestBed.inject(ApolloService) as jasmine.SpyObj; shared = TestBed.inject(SharedService); utils = TestBed.inject(UtilsService); + modalSpy = TestBed.inject(ModalController) as jasmine.SpyObj; // initialise service calls /* assessmentSpy.getAssessment.and.returnValue(of({ @@ -273,6 +280,47 @@ describe('AssessmentComponent', () => { expect(component).toBeTruthy(); }); + describe('showProjectBrief()', () => { + it('should open project brief modal when review has projectBrief', async () => { + const mockProjectBrief = { + id: 'brief-1', + title: 'Test Brief', + description: 'Test Description', + }; + component.review = { + id: 1, + answers: {}, + status: 'pending review', + modified: '2024-01-01', + projectBrief: mockProjectBrief, + }; + const mockModal = { present: jasmine.createSpy('present') }; + modalSpy.create.and.returnValue(Promise.resolve(mockModal as any)); + + await component.showProjectBrief(); + + expect(modalSpy.create).toHaveBeenCalledWith({ + component: jasmine.any(Function), + componentProps: { projectBrief: mockProjectBrief }, + cssClass: 'project-brief-modal', + }); + expect(mockModal.present).toHaveBeenCalled(); + }); + + it('should not open modal when review has no projectBrief', async () => { + component.review = { + id: 1, + answers: {}, + status: 'pending review', + modified: '2024-01-01', + }; + + await component.showProjectBrief(); + + expect(modalSpy.create).not.toHaveBeenCalled(); + }); + }); + describe('ngOnChanges()', () => { it('should straightaway return when assessment not loaded', () => { expect(component.ngOnChanges({})).toBeFalsy(); diff --git a/projects/v3/src/app/components/assessment/assessment.component.ts b/projects/v3/src/app/components/assessment/assessment.component.ts index db3e6c430..c6d541701 100644 --- a/projects/v3/src/app/components/assessment/assessment.component.ts +++ b/projects/v3/src/app/components/assessment/assessment.component.ts @@ -19,6 +19,8 @@ import { Task } from '@v3/app/services/activity.service'; import { ActivityService } from '@v3/app/services/activity.service'; import { FileInput, Question, SubmitActions } from '../types/assessment'; import { FileUploadComponent } from '../file-upload/file-upload.component'; +import { ProjectBriefModalComponent, ProjectBrief } from '../project-brief-modal/project-brief-modal.component'; +import { ModalController } from '@ionic/angular'; const MIN_SCROLLING_PAGES = 8; // minimum number of pages to show pagination scrolling const MAX_QUESTIONS_PER_PAGE = 8; // maximum number of questions to display per paginated view (controls pagination granularity) @@ -149,6 +151,7 @@ export class AssessmentComponent implements OnInit, OnChanges, OnDestroy { private assessmentService: AssessmentService, private activityService: ActivityService, private cdr: ChangeDetectorRef, + private modalController: ModalController, ) { this.resubscribe$.pipe( takeUntil(this.unsubscribe$), @@ -1347,4 +1350,19 @@ Best regards`; shouldShowRequiredIndicator(question: Question): boolean { return this._isRequired(question) && (this.doAssessment || this.isPendingReview); } + + /** + * open the project brief modal for the submitter's team + */ + async showProjectBrief(): Promise { + if (!this.review?.projectBrief) { + return; + } + const modal = await this.modalController.create({ + component: ProjectBriefModalComponent, + componentProps: { projectBrief: this.review.projectBrief }, + cssClass: 'project-brief-modal', + }); + await modal.present(); + } } diff --git a/projects/v3/src/app/services/assessment.service.spec.ts b/projects/v3/src/app/services/assessment.service.spec.ts index e43b5ece2..564ae7cba 100644 --- a/projects/v3/src/app/services/assessment.service.spec.ts +++ b/projects/v3/src/app/services/assessment.service.spec.ts @@ -695,7 +695,15 @@ describe('AssessmentService', () => { submitter: { name: 'John Doe', image: 'profile.jpg', - team: { name: 'Team Alpha' } + team: { + id: 10, + name: 'Team Alpha', + projectBrief: JSON.stringify({ + id: 'brief-1', + title: 'Team Alpha Brief', + description: 'Brief description', + }), + } }, answers: [ { @@ -799,6 +807,11 @@ describe('AssessmentService', () => { expect(result.review.id).toBe(201); expect(result.review.status).toBe('done'); expect(result.review.teamName).toBe('Team Alpha'); + expect(result.review.projectBrief).toEqual({ + id: 'brief-1', + title: 'Team Alpha Brief', + description: 'Brief description', + }); // Verify review answers normalization expect(result.review.answers[1].answer).toBeNull(); diff --git a/projects/v3/src/app/services/assessment.service.ts b/projects/v3/src/app/services/assessment.service.ts index a3f67c553..ae14b23b1 100644 --- a/projects/v3/src/app/services/assessment.service.ts +++ b/projects/v3/src/app/services/assessment.service.ts @@ -12,6 +12,7 @@ import { FastFeedbackService } from './fast-feedback.service'; import { RequestService } from 'request'; import { FileInput, FileResponse } from '../components/types/assessment'; import { Choice, Question } from '@v3/components/types/assessment'; +import { ProjectBrief } from '@v3/app/components/project-brief-modal/project-brief-modal.component'; /** * @name api @@ -89,6 +90,7 @@ export interface AssessmentReview { status: string; modified: string; teamName?: string; + projectBrief?: ProjectBrief; } @Injectable({ @@ -161,7 +163,7 @@ export class AssessmentService { submitter { name image team { - name + id name projectBrief } } answers { @@ -437,6 +439,7 @@ export class AssessmentService { status: firstSubmissionReview.status, modified: firstSubmissionReview.modified, teamName: firstSubmission.submitter.team?.name, + projectBrief: this._parseProjectBrief(firstSubmission.submitter.team?.projectBrief), answers: {}, }; @@ -459,6 +462,27 @@ export class AssessmentService { return review; } + /** + * parse project brief from raw string or object + */ + private _parseProjectBrief(brief: string | object | null): ProjectBrief | null { + if (!brief) { + return null; + } + if (typeof brief === 'object') { + return brief as ProjectBrief; + } + if (typeof brief === 'string') { + try { + return JSON.parse(brief); + } catch (e) { + console.error('failed to parse project brief:', e); + return null; + } + } + return null; + } + /** * For each question that has choice (oneof & multiple), show the choice explanation in the submission if it is not empty */