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