// 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/store/selectors'; // Actions import { InitialPlanLoadResponse, ChangeVisiblePlanRequest, ChangeVisiblePlanResponse, PlanActionTypes, ChangeCourseTermResponse, AddCourseResponse, RemoveCourseResponse, RemoveSavedForLaterResponse, AddSavedForLaterResponse, } from '@app/degree-planner/store/actions/plan.actions'; // Models import { DegreePlan } from '@app/core/models/degree-plan'; 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 => { return { visibleDegreePlan: allDegreePlans.find(plan => plan.primary) || allDegreePlans[0], 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(), ).pipe( map(([planDetails, savedForLaterCourses]) => { return { ...planDetails, savedForLaterCourses }; }), ); }), // 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 most recent Degree Planner state object from the store. This is // used to decide to fire either the `updateNote` API or `createNote` API. withLatestFrom(this.store$.select(getDegreePlannerState)), // Get the roadmap ID from the action and use that ID to lookup the // corresponding degree plan object. map(([action, state]) => { return { visibleDegreePlan: state.allDegreePlans.find(plan => { return plan.roadmapId === action.payload.newVisibleRoadmapId; }) as DegreePlan, }; }), // 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.visibleDegreePlan !== undefined), // 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.visibleDegreePlan as DegreePlan).roadmapId, 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]) => state.visibleDegreePlan !== undefined), // 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.visibleDegreePlan as DegreePlan).roadmapId; 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]) => state.visibleDegreePlan !== undefined), // 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.visibleDegreePlan as DegreePlan).roadmapId; return this.api.removeCourse(roadmapId, 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]) => state.visibleDegreePlan !== undefined), // 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]) => state.visibleDegreePlan !== undefined), // 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 { visibleDegreePlan: DegreePlan }>( stdin: T, ) { return forkJoin( this.api.getAllNotes(stdin.visibleDegreePlan.roadmapId), this.api.getAllTermCourses(stdin.visibleDegreePlan.roadmapId), this.api.getActiveTerms(), ).pipe( // Combine courses and notes by term. map(([notes, termCourses, 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 = termCourses.map(course => course.termCode); /** * 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: termCourses.filter(term => term.termCode === termCode)[0] .courses, }; }); const activeTermCodes = uniqueTermCodes.filter(termCode => { return termCode >= currentTerms[0].termCode; }); return Object.assign({}, stdin, { visibleTerms }, { activeTermCodes }); }), ); } }