// Libraries import { Injectable } from '@angular/core'; import { ROOT_EFFECTS_INIT, Actions, Effect, ofType } from '@ngrx/effects'; import { Observable, forkJoin } from 'rxjs'; import { map, flatMap, tap, withLatestFrom, filter } from 'rxjs/operators'; import { GlobalState } from '@app/core/state'; import { Store } from '@ngrx/store'; // Services import { DegreePlannerApiService } from '@app/degree-planner/services/api.service'; import { getDegreePlannerState } from '@app/degree-planner/selectors'; // Actions import { InitialPlanLoadResponse, ChangeVisiblePlanRequest, ChangeVisiblePlanResponse, PlanActionTypes, ChangeCourseTermResponse, AddCourseResponse, RemoveCourseResponse, RemoveSavedForLaterResponse, AddSavedForLaterResponse, } from '@app/degree-planner/actions/plan.actions'; // Models import { PlannedTerm } from '@app/core/models/planned-term'; @Injectable() export class DegreePlanEffects { constructor( private actions$: Actions, private api: DegreePlannerApiService, private store$: Store<GlobalState>, ) {} @Effect() init$: Observable<InitialPlanLoadResponse> = this.actions$.pipe( ofType(ROOT_EFFECTS_INIT), // Load all plans available to the user. flatMap(() => this.api.getAllDegreePlans()), // Pick one of the plans to use as the visible plan. map(allDegreePlans => { const visibleRoadmapId = ( allDegreePlans.find(plan => plan.primary) || allDegreePlans[0] ).roadmapId; return { visibleRoadmapId, allDegreePlans }; }), // Get term data for the degree plan specified by the roadmap ID and any // courses that were 'saved for later' by the user. flatMap(stdin => { return forkJoin( this.loadTermsForPlan(stdin), this.api.getSavedForLaterCourses(), this.api.getAllSubjects(), ).pipe( map(([planDetails, savedForLater, subjects]) => { const savedForLaterCourses = savedForLater.map(course => { course.subject = subjects[course.subjectCode]; return course; }); return { ...planDetails, savedForLaterCourses, subjects }; }), ); }), // Wrap data in an Action for dispatch map(stdin => new InitialPlanLoadResponse(stdin)), ); @Effect() switch$: Observable<ChangeVisiblePlanResponse> = this.actions$.pipe( ofType<ChangeVisiblePlanRequest>(PlanActionTypes.ChangeVisiblePlanRequest), // Get the roadmap ID from the action. map(action => ({ visibleRoadmapId: action.payload.newVisibleRoadmapId })), // Get term data for the degree plan specified by the roadmap ID. flatMap(stdin => this.loadTermsForPlan(stdin)), // Wrap data in an Action for dispatch map(stdin => new ChangeVisiblePlanResponse(stdin)), ); @Effect() MoveCourseBetweenTerms$ = this.actions$.pipe( ofType<any>(PlanActionTypes.ChangeCourseTermRequest), withLatestFrom(this.store$.select(getDegreePlannerState)), filter(([_, state]) => typeof state.visibleRoadmapId === 'number'), // Get the roadmap ID from the action. tap(([action, state]) => { console.log(action); console.log(state); }), // Get term data for the degree plan specified by the roadmap ID. flatMap(([action, state]) => { // TODO error handle the API calls return this.api .updateCourseTerm( state.visibleRoadmapId, action.payload.id, action.payload.to, ) .pipe( map(response => { return { response, action, }; }), ); }), // // Wrap data in an Action for dispatch map(({ response, action }) => { if (response === 1) { return new ChangeCourseTermResponse({ id: action.payload.id, from: action.payload.from, to: action.payload.to, }); } return; }), ); @Effect() AddCourse$ = this.actions$.pipe( ofType<any>(PlanActionTypes.AddCourseRequest), withLatestFrom(this.store$.select(getDegreePlannerState)), filter(([_, state]) => typeof state.visibleRoadmapId === 'number'), // Get the roadmap ID from the action. tap(([action, state]) => { console.log(action); console.log(state); }), // Get term data for the degree plan specified by the roadmap ID. flatMap(([action, state]) => { // TODO error handle the API calls const roadmapId = state.visibleRoadmapId; const { subjectCode, termCode, courseId } = action.payload; return this.api .addCourse(roadmapId as number, subjectCode, courseId, termCode) .pipe( map(response => { return { response, action, }; }), ); }), // Wrap data in an Action for dispatch map(({ response, action }) => { // TODO add error handleing return new AddCourseResponse({ course: response }); }), ); @Effect() RemoveCourse$ = this.actions$.pipe( ofType<any>(PlanActionTypes.RemoveCourseRequest), withLatestFrom(this.store$.select(getDegreePlannerState)), filter(([_, state]) => typeof state.visibleRoadmapId === 'number'), // Get the roadmap ID from the action. // tap(([action, state]) => { // console.log(action); // console.log(state); // }), // Get term data for the degree plan specified by the roadmap ID. flatMap(([action, state]) => { // TODO error handle the API calls return this.api .removeCourse(state.visibleRoadmapId as number, action.payload.id) .pipe( map(response => { return { response, action, }; }), ); }), // Wrap data in an Action for dispatch map(({ response, action }) => { if (response === null) { const { id } = action.payload; return new RemoveCourseResponse({ id }); } return; }), ); @Effect() RemoveSavedForLater$ = this.actions$.pipe( ofType<any>(PlanActionTypes.RemoveSavedForLaterReqeust), withLatestFrom(this.store$.select(getDegreePlannerState)), filter(([_, state]) => typeof state.visibleRoadmapId === 'number'), // tap(([action, state]) => { // console.log('REMOVE SAVED FOR LATER ----------'); // console.log(action); // console.log(state); // console.log('---------------------------------'); // }), // Get term data for the degree plan specified by the roadmap ID. flatMap(([action, state]) => { // TODO error handle the API calls return this.api .removeSavedForLater( action.payload.subjectCode, action.payload.courseId, ) .pipe( map(response => { return { response, action, }; }), ); }), // // Wrap data in an Action for dispatch map(({ response, action }) => { if (response === null) { const { courseId, subjectCode } = action.payload; return new RemoveSavedForLaterResponse({ courseId, subjectCode }); // TODO Update UI and remove saved response } return; }), ); @Effect() SaveForLater$ = this.actions$.pipe( ofType<any>(PlanActionTypes.AddSavedForLaterReqeust), withLatestFrom(this.store$.select(getDegreePlannerState)), filter(([_, state]) => typeof state.visibleRoadmapId === 'number'), // Get the roadmap ID from the action. // tap(([action, state]) => { // console.log('ADD SAVED FOR LATER ----------'); // console.log(action); // console.log(state); // console.log('---------------------------------'); // }), // Get term data for the degree plan specified by the roadmap ID. flatMap(([action, state]) => { // TODO error handle the API calls return this.api .saveForLater(action.payload.subjectCode, action.payload.courseId) .pipe( map(response => { return { response, action, }; }), ); }), // // // Wrap data in an Action for dispatch map(({ response, action }) => { if (response === null) { const { subjectCode, courseId } = action.payload; return new AddSavedForLaterResponse({ subjectCode, courseId }); } // return; return; }), ); private loadTermsForPlan<T extends { visibleRoadmapId: number }>(stdin: T) { return forkJoin( this.api.getAllNotes(stdin.visibleRoadmapId), this.api.getAllCourses(stdin.visibleRoadmapId), this.api.getActiveTerms(), ).pipe( // Combine courses and notes by term. map(([notes, courses, currentTerms]) => { /** * Using the notes & courses relevant to the current degree plan and * the active terms, generate a sorted list of all unqiue term codes. */ const uniqueTermCodes = [notes, courses, currentTerms] .map((ts: { termCode: string }[]) => ts.map(t => t.termCode)) .reduce((flat, nested) => flat.concat(nested), []) .filter((termCode, index, self) => self.indexOf(termCode) === index) .sort(); /** * For each unique termCode, build a Term object that includes any * courses or notes relevant to that termCode. */ const visibleTerms: PlannedTerm[] = uniqueTermCodes.map(termCode => { return { termCode, note: notes.find(note => note.termCode === termCode), courses: courses.filter(course => course.termCode === termCode), }; }); const activeTermCodes = uniqueTermCodes.filter(termCode => { return termCode >= currentTerms[0].termCode; }); return Object.assign({}, stdin, { visibleTerms }, { activeTermCodes }); }), ); } }