diff --git a/src/app/degree-planner/actions/plan.actions.ts b/src/app/degree-planner/actions/plan.actions.ts index 9d77b310a5cda8348720f24360f36784653370cd..be3559d9e5ab023295ef540f6a5661d6bbd1508b 100644 --- a/src/app/degree-planner/actions/plan.actions.ts +++ b/src/app/degree-planner/actions/plan.actions.ts @@ -5,7 +5,9 @@ import { DegreePlannerState } from '@app/degree-planner/state'; export enum PlanActionTypes { InitialPlanLoadResponse = '[Plan] Initial Load Response', ChangeVisiblePlanRequest = '[Plan] Change Visible Request', - ChangeVisiblePlanResponse = '[Plan] Change Visible Response' + ChangeVisiblePlanResponse = '[Plan] Change Visible Response', + ChangeCourseTermRequest = '[Plan] Change Course Term Request', + ChangeCourseTermResponse = '[Plan] Change Course Term Response' } export class InitialPlanLoadResponse implements Action { @@ -24,3 +26,17 @@ export class ChangeVisiblePlanResponse implements Action { public payload: { visibleRoadmapId: number; visibleTerms: PlannedTerm[] } ) {} } + +export class ChangeCourseTermRequest implements Action { + public readonly type = PlanActionTypes.ChangeCourseTermRequest; + constructor( + public payload: {to: string, from: string, id: number} + ) {} +} + +export class ChangeCourseTermResponse implements Action { + public readonly type = PlanActionTypes.ChangeCourseTermResponse; + constructor( + public payload: {to: string, from: string, id: number} + ) {} +} diff --git a/src/app/degree-planner/effects/plan.effects.ts b/src/app/degree-planner/effects/plan.effects.ts index 4c5690ec70a4ab73091adae2156d3638300d56c8..d72480f39181964f1cc7cc92e03ab8d0e0c0d850 100644 --- a/src/app/degree-planner/effects/plan.effects.ts +++ b/src/app/degree-planner/effects/plan.effects.ts @@ -1,18 +1,22 @@ // Libraries import { Injectable } from '@angular/core'; import { ROOT_EFFECTS_INIT, Actions, Effect, ofType } from '@ngrx/effects'; -import { Observable, forkJoin, of } from 'rxjs'; -import { map, flatMap, tap } from 'rxjs/operators'; +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 + PlanActionTypes, + ChangeCourseTermResponse } from '@app/degree-planner/actions/plan.actions'; // Models @@ -22,7 +26,8 @@ import { PlannedTerm } from '@app/core/models/planned-term'; export class DegreePlanEffects { constructor( private actions$: Actions, - private api: DegreePlannerApiService + private api: DegreePlannerApiService, + private store$: Store<GlobalState>, ) {} @Effect() @@ -71,6 +76,41 @@ export class DegreePlanEffects { 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; + }) + ); + private loadTermsForPlan<T extends { visibleRoadmapId: number }>(stdin: T) { return forkJoin( this.api.getAllNotes(stdin.visibleRoadmapId), @@ -78,12 +118,12 @@ export class DegreePlanEffects { this.api.getActiveTerms() ).pipe( // Combine courses and notes by term. - map(([notes, courses, activeTerms]) => { + 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, activeTerms] + 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) @@ -101,7 +141,11 @@ export class DegreePlanEffects { }; }); - return Object.assign({}, stdin, { visibleTerms, activeTerms }); + const activeTerms = uniqueTermCodes.filter(termCode => { + return parseInt(termCode, 10) >= parseInt(currentTerms[0].termCode, 10); + }); + + return Object.assign({}, stdin, { visibleTerms }, { activeTerms }); }) ); } diff --git a/src/app/degree-planner/reducer.ts b/src/app/degree-planner/reducer.ts index d25854cad006cd6a8db1d9c8350352441ba52edc..c2c277a917f7f890f8010f0ac5021b6367dfeb87 100644 --- a/src/app/degree-planner/reducer.ts +++ b/src/app/degree-planner/reducer.ts @@ -5,7 +5,9 @@ import { import { PlanActionTypes, InitialPlanLoadResponse, - ChangeVisiblePlanResponse + ChangeVisiblePlanResponse, + ChangeCourseTermRequest, + ChangeCourseTermResponse } from '@app/degree-planner/actions/plan.actions'; import { NoteActionTypes, @@ -17,7 +19,8 @@ type SupportedActions = | InitialPlanLoadResponse | ChangeVisiblePlanResponse | WriteNoteResponse - | DeleteNoteResponse; + | DeleteNoteResponse + | ChangeCourseTermResponse; export function degreePlannerReducer( state = INITIAL_DEGREE_PLANNER_STATE, @@ -112,6 +115,37 @@ export function degreePlannerReducer( } } + case PlanActionTypes.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; + } + /** * 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. diff --git a/src/app/degree-planner/selectors.ts b/src/app/degree-planner/selectors.ts index 69738ae9754d18d9c7917ba26af6560b3b90fcc3..8ae9f2f19ed1385cd4424b85e165b66abe138878 100644 --- a/src/app/degree-planner/selectors.ts +++ b/src/app/degree-planner/selectors.ts @@ -62,6 +62,27 @@ export const getAllVisibleTermsByYear = createSelector( } ); +export const getDropZones = createSelector( + getDegreePlannerState, + (state: DegreePlannerState) => { + return [ + 'saved-courses', + ...state.activeTerms.map(termCode => { + return `term-${termCode}`; + }) + ]; + } +); + +export const isActiveTerm = (termCode: String) => createSelector( + getDegreePlannerState, + (state: DegreePlannerState) => { + console.log(termCode); + // return state.activeTerms.includes(termCode); + return true; + } +); + const getTermForCode = (termCode: string, courses: Course[], notes: Note[]) => { return { termCode, diff --git a/src/app/degree-planner/services/api.service.ts b/src/app/degree-planner/services/api.service.ts index 01cc151a8c9082f0c87ec5d5ba757dc7586e924e..287120260b70089447067bda044e86d053002377 100644 --- a/src/app/degree-planner/services/api.service.ts +++ b/src/app/degree-planner/services/api.service.ts @@ -53,6 +53,11 @@ export class DegreePlannerApiService { ); } + public updateCourseTerm(roadmapId, recordId, termCode): Observable<any> { + return this.http.put<Course>(this.config.apiPlannerUrl + '/degreePlan/' + + roadmapId + '/courses/' + recordId + '?termCode=' + termCode, HTTP_OPTIONS); + } + public createNote( planId: number, termCode: string, diff --git a/src/app/degree-planner/term-container/term-container.component.html b/src/app/degree-planner/term-container/term-container.component.html index ac4765146f20c208176bd0f454d4008f6b9444fd..19ed83c3e3fc8a2d8342124f8910c044e630ecbc 100644 --- a/src/app/degree-planner/term-container/term-container.component.html +++ b/src/app/degree-planner/term-container/term-container.component.html @@ -27,7 +27,7 @@ <div cdkDropList id="term-{{term.termCode}}" [cdkDropListData]="term.courses" - [cdkDropListConnectedTo]="this.degreePlannerDataSvc.dropZones" + [cdkDropListConnectedTo]="dropZones$ | async" class="course-list" (cdkDropListDropped)="drop($event)"> @@ -43,6 +43,10 @@ <cse-course-item [course]="course"></cse-course-item> </div> </div> + + <div class="no-courses" *ngIf="term.courses.length === 0"> + <p>No Courses Taken</p> + </div> </div> </div> <div class="add-new-wrapper"> diff --git a/src/app/degree-planner/term-container/term-container.component.scss b/src/app/degree-planner/term-container/term-container.component.scss index 7e0c9e1a85f6c182754b9b78ac66c693e6facf22..618434dfc9e38606011038f6ea20c49b61747ee6 100644 --- a/src/app/degree-planner/term-container/term-container.component.scss +++ b/src/app/degree-planner/term-container/term-container.component.scss @@ -102,4 +102,10 @@ white-space: nowrap; text-overflow: ellipsis; } +} + +.no-courses { + text-align: center; + padding: 25px 0; + font-weight: bold; } \ No newline at end of file diff --git a/src/app/degree-planner/term-container/term-container.component.ts b/src/app/degree-planner/term-container/term-container.component.ts index 95d2beaab3d7963fb7dc8d2854ddefb9a895e3f4..1f4369255420c8c20a9550fdb3dfca554df50572 100644 --- a/src/app/degree-planner/term-container/term-container.component.ts +++ b/src/app/degree-planner/term-container/term-container.component.ts @@ -1,7 +1,23 @@ -import { Component, Input } from '@angular/core'; +// Libraries +import { Component, Input, OnInit } from '@angular/core'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { MatDialog } from '@angular/material'; +// rsjx / ngrx +import { Observable } from 'rxjs'; +import { Store, select } from '@ngrx/store'; +import { DegreePlannerState } from '@app/degree-planner/state'; +import { + ChangeCourseTermRequest +} from '@app/degree-planner/actions/plan.actions'; + +// Selectors +import { + getDropZones, + isActiveTerm +} from '@app/degree-planner/selectors'; + +// Services import { DataService } from '../../core/data.service'; import { DegreePlannerDataService } from './../../core/service/degree-planner-data.service'; import { SidenavService } from './../../core/service/sidenav.service'; @@ -18,16 +34,26 @@ import { templateUrl: './term-container.component.html', styleUrls: ['./term-container.component.scss'] }) -export class TermContainerComponent { +export class TermContainerComponent implements OnInit { @Input() term: PlannedTerm; + public dropZones$: Observable<String[]>; + public isActiveTerm$: Observable<Boolean>; constructor( private dataService: DataService, public degreePlannerDataSvc: DegreePlannerDataService, public dialog: MatDialog, - private sidenavService: SidenavService + private sidenavService: SidenavService, + private store: Store<{ degreePlanner: DegreePlannerState }>, ) {} + public ngOnInit() { + this.dropZones$ = this.store.pipe(select(getDropZones)); + this.isActiveTerm$ = this.store.pipe(select(isActiveTerm(this.term.termCode))); + } + + // this.dropZones$ = this.store.pipe(select(getDropZones)); + public openAddSidenav(): void { this.sidenavService.open(); } @@ -47,54 +73,64 @@ export class TermContainerComponent { } drop(event: CdkDragDrop<string[]>) { - const { container, previousContainer, previousIndex } = event; - - // Get the course JSON - const item = event.item.data; - - console.log(event); - - const newTermCode = event.container.id.substr(5, 4); - - if ( - event.previousContainer.id === 'saved-courses' && - event.container.id !== 'saved-courses' - ) { - // If moving from favorites to term - container.data.push(item); - previousContainer.data.splice(previousIndex, 1); - - this.dataService - .addCourse( - this.degreePlannerDataSvc.getPlanId(), - event.item.data.subjectCode, - event.item.data.courseId, - newTermCode - ) - .subscribe(data => { - const yearCode = newTermCode.substring(0, 3); - const termCode = newTermCode.substring(3); - }); - - this.dataService - .removeFavoriteCourse( - event.item.data.subjectCode, - event.item.data.courseId - ) - .subscribe(); - } else if (event.previousContainer.id !== event.container.id) { - container.data.push(item); - previousContainer.data.splice(previousIndex, 1); - - // Tell the API this course updated - this.dataService - .updateCourseTerm( - this.degreePlannerDataSvc.getPlanId(), - event.item.data.id, - newTermCode - ) - .subscribe(); + const to = event.container.id.substr(5); + const { termCode: from, id } = event.item.data; + + if (from === to) { + return; } + + console.log(to); + console.log(from); + console.log(id); + this.store.dispatch(new ChangeCourseTermRequest({ to, from, id })); + + // // Get the course JSON + // const item = event.item.data; + + // console.log(event); + + // const newTermCode = event.container.id.substr(5, 4); + + // if ( + // event.previousContainer.id === 'saved-courses' && + // event.container.id !== 'saved-courses' + // ) { + // // If moving from favorites to term + // container.data.push(item); + // previousContainer.data.splice(previousIndex, 1); + + // this.dataService + // .addCourse( + // this.degreePlannerDataSvc.getPlanId(), + // event.item.data.subjectCode, + // event.item.data.courseId, + // newTermCode + // ) + // .subscribe(data => { + // const yearCode = newTermCode.substring(0, 3); + // const termCode = newTermCode.substring(3); + // }); + + // this.dataService + // .removeFavoriteCourse( + // event.item.data.subjectCode, + // event.item.data.courseId + // ) + // .subscribe(); + // } else if (event.previousContainer.id !== event.container.id) { + // container.data.push(item); + // previousContainer.data.splice(previousIndex, 1); + + // // Tell the API this course updated + // this.dataService + // .updateCourseTerm( + // this.degreePlannerDataSvc.getPlanId(), + // event.item.data.id, + // newTermCode + // ) + // .subscribe(); + // } // Do nothing on drop to same term } }