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 10.93 KiB
import { UIActionTypes } from './actions/ui.actions';
import {
  DegreePlannerState,
  INITIAL_DEGREE_PLANNER_STATE,
} from '@app/degree-planner/store/state';
import {
  PlanActionTypes,
  InitialLoadSuccess,
  SwitchPlanSuccess,
  MakePlanPrimary,
  MakePlanPrimarySuccess,
  MakePlanPrimaryFailure,
  ChangePlanNameSuccess,
  ChangePlanNameFailure,
  CreatePlan,
  CreatePlanSuccess,
} from '@app/degree-planner/store/actions/plan.actions';
import {
  CourseActionTypes,
  RemoveCourseSuccess,
  MoveCourseBetweenTermsSuccess,
  AddCourseSuccess,
  RemoveSaveForLaterSuccess,
  AddSaveForLaterSuccess,
} from '@app/degree-planner/store/actions/course.actions';
import {
  NoteActionTypes,
  WriteNoteSuccess,
  DeleteNoteSuccess,
} from '@app/degree-planner/store/actions/note.actions';
import {
  AddAcademicYearActionTypes,
  AddAcademicYearRequest,
} from '@app/degree-planner/store/actions/addAcademicYear.actions';
import { ToggleAcademicYear } from '@app/degree-planner/store/actions/ui.actions';
import { SavedForLaterCourse } from '@app/core/models/saved-for-later-course';
import { DegreePlan } from '@app/core/models/degree-plan';

type SupportedActions =
  | InitialLoadSuccess
  | SwitchPlanSuccess
  | WriteNoteSuccess
  | DeleteNoteSuccess
  | MoveCourseBetweenTermsSuccess
  | RemoveCourseSuccess
  | AddCourseSuccess
  | RemoveSaveForLaterSuccess
  | AddSaveForLaterSuccess
  | AddAcademicYearRequest
  | CreatePlanSuccess
  | MakePlanPrimary
  | MakePlanPrimarySuccess
  | MakePlanPrimaryFailure
  | ChangePlanNameSuccess
  | ChangePlanNameFailure
  | ToggleAcademicYear;

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.InitialLoadSuccess: {
      return { ...action.payload };
    }

    /**
     * The `SwitchPlanSuccess` 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.SwitchPlanSuccess: {
      return { ...state, ...action.payload };
    }

    /**
     * The `AddAcademicYearRequest` action is triggered after `addAcademicYear()`
     * function runs. A new academic year container with three terms will be created.
     */
    case AddAcademicYearActionTypes.AddAcademicYearRequest: {
      const originalTerms = state.visibleTerms.map(term => {
        return parseInt(term.termCode.substr(0, 3), 10);
      });

      const newAcademicYearCode = Math.max(...originalTerms) + 1;

      const newVisibleTerms = [
        ...state.visibleTerms,
        { termCode: `${newAcademicYearCode}2`, courses: [] },
        { termCode: `${newAcademicYearCode}4`, courses: [] },
        { termCode: `${newAcademicYearCode}6`, courses: [] },
      ];
      return { ...state, visibleTerms: newVisibleTerms };
    }

    /**
     * The `ToggleAcademicYear` action toggles the expand/collapse UI state
     */
    case UIActionTypes.ToggleAcademicYear: {
      return { ...state };
    }

    /**
     * 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.WriteNoteSuccess: {
      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.DeleteNoteSuccess: {
      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.MoveCourseBetweenTermsSuccess: {
      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.AddCourseSuccess: {
      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.RemoveCourseSuccess: {
      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.RemoveSaveForLaterSuccess: {
      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.AddSaveForLaterSuccess: {
      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 };
    }

    case PlanActionTypes.CreatePlanSuccess: {
      const { newPlan } = action.payload;
      return {
        ...state,
        allDegreePlans: state.allDegreePlans.concat(newPlan),
      };
    }

    case PlanActionTypes.MakePlanPrimary: {
      // TODO add global loading state
      return state;
    }
    case PlanActionTypes.MakePlanPrimarySuccess: {
      // Get current roadmap ID
      const roadmapId = (state.visibleDegreePlan as DegreePlan).roadmapId;

      // Update the visible term object
      const newVisibleDegreePlan = {
        ...(state.visibleDegreePlan as DegreePlan),
        primary: true,
      };

      // Update all plans to only have the current one be primary
      const newPlans = state.allDegreePlans.map(plan => {
        if (plan.roadmapId !== roadmapId) {
          return { ...plan, primary: false };
        }
        return { ...plan, primary: true };
      });

      // Return the new state
      return {
        ...state,
        visibleDegreePlan: newVisibleDegreePlan,
        allDegreePlans: newPlans,
      };
    }

    case PlanActionTypes.MakePlanPrimaryFailure: {
      // TODO add error message
      return state;
    }

    case PlanActionTypes.ChangePlanNameSuccess: {
      const visibleDegreePlan = {
        ...(state.visibleDegreePlan as DegreePlan),
        name: action.payload.newName,
      };

      return {
        ...state,
        allDegreePlans: state.allDegreePlans.map(plan => {
          if (plan.roadmapId === action.payload.roadmapId) {
            return { ...plan, name: action.payload.newName };
          } else {
            return plan;
          }
        }),
        visibleDegreePlan,
      };
    }

    case PlanActionTypes.ChangePlanNameFailure: {
      const visibleDegreePlan = {
        ...(state.visibleDegreePlan as DegreePlan),
        name: action.payload.oldName,
      };

      return {
        ...state,
        allDegreePlans: state.allDegreePlans.map(plan => {
          if (plan.roadmapId === action.payload.roadmapId) {
            return { ...plan, name: action.payload.oldName };
          } else {
            return plan;
          }
        }),
        visibleDegreePlan,
      };
    }

    /**
     * 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);
};