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.
reducer.ts 7.25 KiB
import {
  DegreePlannerState,
  INITIAL_DEGREE_PLANNER_STATE,
} from '@app/degree-planner/store/state';
import {
  PlanActionTypes,
  InitialPlanLoadResponse,
  ChangeVisiblePlanResponse,
} from '@app/degree-planner/store/actions/plan.actions';
import {
  CourseActionTypes,
  RemoveCourseResponse,
  ChangeCourseTermResponse,
  AddCourseResponse,
  RemoveSavedForLaterResponse,
  AddSavedForLaterResponse,
} from '@app/degree-planner/store/actions/course.actions';
import {
  NoteActionTypes,
  WriteNoteResponse,
  DeleteNoteResponse,
} from '@app/degree-planner/store/actions/note.actions';
import { SavedForLaterCourse } from '@app/core/models/saved-for-later-course';

type SupportedActions =
  | InitialPlanLoadResponse
  | ChangeVisiblePlanResponse
  | WriteNoteResponse
  | DeleteNoteResponse
  | ChangeCourseTermResponse
  | RemoveCourseResponse
  | AddCourseResponse
  | RemoveSavedForLaterResponse
  | AddSavedForLaterResponse;

export function degreePlannerReducer(
  state = INITIAL_DEGREE_PLANNER_STATE,
  action: SupportedActions,
): DegreePlannerState {
  switch (action.type) {
    /**
     * The `InitialPlanLoadResponse` action is triggered on initial Degree
     * Planner app load. It downloads a list of the user's degree plans and
     * picks the primary plan from that list to load as the first visible plan.
     */
    case PlanActionTypes.InitialPlanLoadResponse: {
      return { ...action.payload };
    }

    /**
     * The `ChangeVisiblePlanResponse` action is triggered whenever the UI needs
     * to switch which degree plan is being shown and load the data associated
     * with that degree plan. The reducer extracts that downloaded data from the
     * action payload and builds a new state using that data.
     */
    case PlanActionTypes.ChangeVisiblePlanResponse: {
      return { ...state, ...action.payload };
    }

    /**
     * The `WriteNoteResponse` action is dispatched by the `Note.write$` effect
     * upon a successful response from the `updateNote` or `createNote` API
     * endpoints. The reducer in this case either:
     *
     *  - Replaces a note on a term that already had a note.
     *  - *OR* adds a note to a term that didn't previously have a note.
     *  - *OR* adds a new term with the given note if no term exists with the note's termCode.
     */
    case NoteActionTypes.WriteNoteResponse: {
      const updatedNote = action.payload.updatedNote;
      const updatedTermCode = updatedNote.termCode;
      const originalTerms = state.visibleTerms;
      if (termCodeExists(updatedTermCode, originalTerms)) {
        /**
         * If a term with the given `termCode` *DOES exist* in the state,
         * replace just that term with the new data inside the action.
         */
        const newVisibleTerms = originalTerms.map(term => {
          if (term.termCode === updatedTermCode) {
            return { ...term, note: updatedNote };
          } else {
            return term;
          }
        });

        return { ...state, visibleTerms: newVisibleTerms };
      } else {
        /**
         * If a term with the given `termCode` *DOES NOT exist*
         * in the state, add it to the end of the term list.
         */
        const newVisibleTerms = originalTerms.concat({
          termCode: updatedTermCode,
          note: updatedNote,
          courses: [],
        });

        return { ...state, visibleTerms: newVisibleTerms };
      }
    }

    /**
     * The `DeleteNoteResponse` action is dispatched after the `deleteNote` API
     * has been called and it is okay to remote the note with the given
     * termCode from the degree planner state.
     */
    case NoteActionTypes.DeleteNoteResponse: {
      const deletedTermCode = action.payload.termCode;
      const originalTerms = state.visibleTerms;
      if (termCodeExists(deletedTermCode, originalTerms)) {
        /**
         * If a term with the given `termCode` *DOES EXIST* in the state,
         * remove that term's note.
         */
        const newVisibleTerms = originalTerms.map(term => {
          if (term.termCode === deletedTermCode) {
            return { ...term, note: undefined };
          } else {
            return term;
          }
        });

        return { ...state, visibleTerms: newVisibleTerms };
      } else {
        return state;
      }
    }

    case CourseActionTypes.ChangeCourseTermResponse: {
      const { to, from, id } = action.payload;
      const t = state.visibleTerms.find(term => term.termCode === from);

      if (t) {
        const course = t.courses.find(c => c.id === id);

        if (course) {
          course.termCode = to;

          // Create new visibleTerms array
          const newVisibleTerms = state.visibleTerms.map(term => {
            if (term.termCode === from) {
              // Remove the course from the previous term
              term.courses = term.courses.filter(c => c.id !== id);
            } else if (term.termCode === to) {
              // Add the new course to this term
              term.courses = [...term.courses, course];
            }
            return term;
          });

          return { ...state, visibleTerms: newVisibleTerms };
        }

        return state;
      }
      return state;
    }

    case CourseActionTypes.AddCourseResponse: {
      const { course } = action.payload;

      const newVisibleTerms = state.visibleTerms.map(term => {
        if (term.termCode === course.termCode) {
          term.courses.push(course);
        }
        return term;
      });

      return { ...state, visibleTerms: newVisibleTerms };

      // return {...state, visibleTerms: newVisibleTerms};
      // return state;
    }

    case CourseActionTypes.RemoveCourseResponse: {
      const { recordId: id } = action.payload;

      // Create new visibleTerms array
      const newVisibleTerms = state.visibleTerms.map(term => {
        term.courses = term.courses.filter(course => {
          return course.id !== id;
        });
        return term;
      });

      return { ...state, visibleTerms: newVisibleTerms };
    }

    case CourseActionTypes.RemoveSavedForLaterResponse: {
      const { courseId, subjectCode } = action.payload;

      // // Create new saved for later array
      const newSavedForLater = state.savedForLaterCourses.filter(
        course =>
          course.subjectCode !== subjectCode && course.courseId !== courseId,
      );

      return { ...state, savedForLaterCourses: newSavedForLater };
    }

    case CourseActionTypes.AddSavedForLaterResponse: {
      const newSavedForLater: SavedForLaterCourse[] = [
        ...state.savedForLaterCourses,
        {
          id: null,
          courseId: action.payload.courseId,
          termCode: '0000',
          topicId: 0,
          subjectCode: action.payload.subjectCode,
          title: action.payload.title,
          catalogNumber: action.payload.catalogNumber,
          courseOrder: 0,
          subject: state.subjects[action.payload.subjectCode],
        },
      ];
      return { ...state, savedForLaterCourses: newSavedForLater };
    }

    /**
     * It's okay if the action didn't match any of the cases above. If that's
     * the case, just return the existing state object.
     */
    default:
      return state;
  }
}

const termCodeExists = (termCode: string, things: { termCode: string }[]) => {
  return things.some(thing => thing.termCode === termCode);
};