Skip to content
Snippets Groups Projects
Forked from an inaccessible project.
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
plan.effects.ts 9.77 KiB
// 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 });
      }),
    );
  }
}