diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index d376defe49ef7b0cfa46e24a2bc6058fb08d8502..abc65acff828896f2e90e612ee557fd5a3731797 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -5,30 +5,20 @@ import { RouterModule } from '@angular/router'; import { SharedModule } from '../shared/shared.module'; import { NavigationComponent } from './navigation/navigation.component'; import { ConfigService } from './config.service'; -import { DataService } from './data.service'; -// import { ProfileService } from './profile.service' import { throwIfAlreadyLoaded } from './module-import-check'; @NgModule({ - imports: [ - CommonModule, // we use *ngFor - RouterModule, // we use router-outlet and routerLink - SharedModule - ], - exports: [ - NavigationComponent - ], - declarations: [ - NavigationComponent - ], - providers: [ - ConfigService, - DataService - ], + imports: [ + CommonModule, // we use *ngFor + RouterModule, // we use router-outlet and routerLink + SharedModule, + ], + exports: [NavigationComponent], + declarations: [NavigationComponent], + providers: [ConfigService], }) - export class CoreModule { - constructor( @Optional() @SkipSelf() parentModule: CoreModule) { - throwIfAlreadyLoaded(parentModule, 'CoreModule'); - } + constructor(@Optional() @SkipSelf() parentModule: CoreModule) { + throwIfAlreadyLoaded(parentModule, 'CoreModule'); + } } diff --git a/src/app/core/data.service.spec.ts b/src/app/core/data.service.spec.ts deleted file mode 100644 index 9b4a56abd33744ab0408d8fc9ffeaa252ad528fe..0000000000000000000000000000000000000000 --- a/src/app/core/data.service.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { TestBed, inject } from '@angular/core/testing'; -import { HttpBackend, HttpClientModule } from '@angular/common/http'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ConfigService } from './config.service'; -import { DataService } from './data.service'; - -describe('DataService', () => { - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [HttpClientModule], - providers: [ - ConfigService, - DataService, - { provide: HttpBackend, useClass: HttpClientTestingModule } - ] - }); - }); - - it('should create', inject([DataService], (service: DataService) => { - expect(service).toBeTruthy(); - })); - -}); diff --git a/src/app/core/data.service.ts b/src/app/core/data.service.ts deleted file mode 100644 index e840ed93a86aab3ead1e325aa97b3ee46a8bffec..0000000000000000000000000000000000000000 --- a/src/app/core/data.service.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { throwError, Observable, forkJoin } from 'rxjs'; -import { catchError, map } from 'rxjs/operators'; -import { ConfigService } from './config.service'; -import { Course } from './models/course'; -import { DegreePlan } from './models/degree-plan'; -import { Term } from './models/term'; -import { SavedForLaterCourse } from './models/saved-for-later-course'; -import { CourseDetails } from './models/course-details'; -import { Note } from './models/note'; - -const httpOptions = { - headers: new HttpHeaders({ - 'Content-Type': 'application/json' - }) -}; - -@Injectable() -export class DataService { - private plannerApiUrl: string; - private searchApiUrl: string; - - constructor(private http: HttpClient, private configService: ConfigService) { - this.plannerApiUrl = this.configService.apiPlannerUrl; - this.searchApiUrl = this.configService.apiSearchUrl; - } - - getAllPlanData(roadmapId: number) { - return forkJoin( - this.getDegreePlannerCourseData(roadmapId), - this.getTerms() - ); - } - - getDegreePlans(): Observable<DegreePlan[]> { - return this.http.get<DegreePlan[]>(this.plannerApiUrl + '/degreePlan') - .pipe(catchError(this.errorHandler)); - } - - getDegreePlannerCourseData(roadmapId: number): Observable<Course[]> { - return this.http.get<Course[]>(this.plannerApiUrl + '/degreePlan/' + roadmapId + '/courses') - .pipe(catchError(this.errorHandler)); - } - - getDegreePlannerCourseData2(roadmapId: number): Observable<Course> { - return this.http.get<Course>(this.plannerApiUrl + '/degreePlan/' + roadmapId + '/courses') - .pipe(catchError(this.errorHandler)); - } - - getTerms(): Observable<Term[]> { - return this.http.get<Term[]>(this.searchApiUrl + '/terms') - .pipe(catchError(this.errorHandler)); - } - - getCourseDetails(termCode: string, subjectCode: string, courseId: string): Observable<CourseDetails[]> { - return this.http.get<CourseDetails[]>(this.searchApiUrl + '/course/0000/' + subjectCode + '/' + courseId, httpOptions) - .pipe(catchError(this.errorHandler)); - } - - getSubjectsMap(): Observable<Object> { - return this.http.get(this.searchApiUrl + '/subjectsMap/0000', httpOptions) - .pipe(catchError(this.errorHandler)); - } - - getFavoriteCourses(): Observable<SavedForLaterCourse[]> { - return this.http.get<SavedForLaterCourse[]>(this.plannerApiUrl + '/favorites') - .pipe(catchError(this.errorHandler)); - } - - getAllNotes(planId: number): Observable<Note[]> { - return this.http.get<Note[]>(this.plannerApiUrl + '/degreePlan/' + planId + '/notes') - .pipe(catchError(this.errorHandler)); - } - - getNote(planId, noteId): Observable<Note[]> { - return this.http.get<Note[]>(this.plannerApiUrl + '/degreePlan/' + planId + '/notes/' + noteId) - .pipe(catchError(this.errorHandler)); - } - - updateNote(planId, note): Observable<Note[]> { - return this.http.put<Note[]>(this.plannerApiUrl + '/degreePlan/' + planId + '/notes/' + note.id, note, httpOptions) - .pipe(catchError(this.errorHandler)); - } - - createNote(planId, note): Observable<Note[]> { - return this.http.post<Note[]>(this.plannerApiUrl + '/degreePlan/' + planId + '/notes/', note, httpOptions) - .pipe(catchError(this.errorHandler)); - } - - removeNote(planId, noteId): Observable<Note[]> { - return this.http.delete<Note[]>(this.plannerApiUrl + '/degreePlan/' + planId + '/notes/' + noteId, httpOptions) - .pipe(catchError(this.errorHandler)); - } - - saveFavoriteCourse(subjectCode: string, courseId: string): Observable<SavedForLaterCourse> { - return this.http.post<SavedForLaterCourse>(this.plannerApiUrl + '/favorites/' + subjectCode + '/' + courseId, httpOptions) - .pipe(catchError(this.errorHandler)); - } - - removeFavoriteCourse(subjectCode, courseId): Observable<SavedForLaterCourse> { - return this.http.delete<SavedForLaterCourse>(this.plannerApiUrl + '/favorites/' + subjectCode + '/' + courseId, httpOptions) - .pipe(catchError(this.errorHandler)); - } - - addCourse(planId, subjectCode, courseId, termCode) { - return this.http.post(this.plannerApiUrl + '/degreePlan/' + planId + '/courses', {subjectCode, courseId, termCode }, httpOptions) - .pipe(catchError(this.errorHandler)); - } - - removeCourse(planId, recordId) { - return this.http.delete(this.plannerApiUrl + '/degreePlan/' + planId + '/courses/' + recordId, httpOptions) - .pipe(catchError(this.errorHandler)); - } - - updateCourseTerm(planId, recordId, termCode): Observable<Course> { - return this.http.put<Course>(this.plannerApiUrl + '/degreePlan/' + planId + '/courses/' + recordId + '?termCode=' + termCode, httpOptions) - .pipe(catchError(this.errorHandler)); - } - -test() { -// return this.http.delete(this.plannerApiUrl + '/degreePlan/519260/courses/259445', httpOptions) -return this.http.put(this.plannerApiUrl + '/degreePlan/519260/courses/259465?termCode=1174', httpOptions) - .pipe(catchError(this.errorHandler)); -} - - private errorHandler(error: HttpErrorResponse) { - return throwError(error || 'Server Error'); - } -} diff --git a/src/app/core/models/course.ts b/src/app/core/models/course.ts index 75a8e924f18c161a3a1f1bd2fe9cb728e421a85a..0a31f9df634cbc89d495c24119b98c0025e1ed26 100644 --- a/src/app/core/models/course.ts +++ b/src/app/core/models/course.ts @@ -1,29 +1,33 @@ -export interface Course { - id: number; - courseId: string; - termCode: string; - topicId: number; - title: string; - subjectCode: string; - catalogNumber: string; - credits: number; - creditMin: number; - creditMax: number; - grade?: any; - classNumber: string; - courseOrder: number; - honors: string; - waitlist: string; - relatedClassNumber1?: any; - relatedClassNumber2?: any; - classPermissionNumber?: any; - sessionCode?: any; - validationResults: any[]; - enrollmentResults: any[]; - pendingEnrollments: any[]; - details?: any; - classMeetings?: any; - enrollmentOptions?: any; - packageEnrollmentStatus?: any; - creditRange?: any; +interface CourseBase { + id: number; + courseId: string; + termCode: string; + topicId: number; + title: string; + subjectCode: string; + catalogNumber: string; + credits: number; + creditMin: number; + creditMax: number; + grade?: any; + classNumber: string; + courseOrder: number; + honors: string; + waitlist: string; + relatedClassNumber1?: any; + relatedClassNumber2?: any; + classPermissionNumber?: any; + sessionCode?: any; + validationResults: any[]; + enrollmentResults: any[]; + pendingEnrollments: any[]; + details?: any; + classMeetings?: any; + enrollmentOptions?: any; + packageEnrollmentStatus?: any; + creditRange?: any; +} + +export interface Course extends CourseBase { + subject: string; } diff --git a/src/app/core/models/saved-for-later-course.ts b/src/app/core/models/saved-for-later-course.ts index 14d011357de5e6fa143cfd256550cee145b1071f..a9927d1d3567959f36f594818665b7456f8f66d4 100644 --- a/src/app/core/models/saved-for-later-course.ts +++ b/src/app/core/models/saved-for-later-course.ts @@ -1,10 +1,14 @@ -export interface SavedForLaterCourse { - id: number | null; - courseId: string; - termCode: string; - topicId: number; - subjectCode: string; - title: string; - catalogNumber: string; - courseOrder: number; +interface SavedForLaterCourseBase { + id: number | null; + courseId: string; + termCode: string; + topicId: number; + subjectCode: string; + title: string; + catalogNumber: string; + courseOrder: number; +} + +export interface SavedForLaterCourse extends SavedForLaterCourseBase { + subject: string; } diff --git a/src/app/core/service/degree-planner-data.service.ts b/src/app/core/service/degree-planner-data.service.ts deleted file mode 100644 index 3f487b994849193dd3a43d8da3119f31b63ed950..0000000000000000000000000000000000000000 --- a/src/app/core/service/degree-planner-data.service.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Course } from '@app/core/models/course'; -import { Note } from './../models/note'; -import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; -import { groupBy, toArray, mergeMap, flatMap, tap, mergeAll, map, distinct, share } from 'rxjs/operators'; - -import { DataService } from '@app/core/data.service'; - -@Injectable({ - providedIn: 'root' -}) -export class DegreePlannerDataService { - degreePlannerData: Observable<any>; - coursesData$: any; - termCodes: any; - dropZones: String[]; - planId: number; - - constructor(private dataService: DataService) { } - - getDegreePlanDataById(planId: number) { - this.planId = planId; - // this.dataService.test().subscribe(() => {}); - - this.degreePlannerData = this.dataService.getDegreePlannerCourseData(planId).pipe( - map(courses => courses.sort((a, b) => Number(a.termCode) - Number(b.termCode))), - mergeAll(), - share() - ); - - this.degreePlannerData.pipe( - map(course => course.termCode), - distinct(), - toArray() - ).subscribe(termCodes => { - this.termCodes = termCodes; - this.dropZones = termCodes.map(termCode => `term-${termCode}`); - this.dropZones.push('saved-courses'); - }); - - this.coursesData$ = this.degreePlannerData.pipe( - groupBy(course => course.termCode), - mergeMap(group$ => group$.pipe(toArray())), - map(course => { - return { - termCode: course[0].termCode, - year: course[0].termCode.substring(1, 3), - courses: course - }; - }), - groupBy(terms => terms.year), - mergeMap(group$ => group$.pipe(toArray())), - toArray(), - - tap(x => console.log('new data', x)) - ); - - return this.coursesData$; - } - - getAllTerms() { - return this.termCodes; - } - - getAllNotes(): Observable<Note[]> { - return this.dataService.getAllNotes(this.planId); - } - - getPlanId(): number { - return this.planId; - } -} diff --git a/src/app/degree-planner/actions/plan.actions.ts b/src/app/degree-planner/actions/plan.actions.ts index c716a6a92d422a4271cb53521ed582293aa6e57c..c919236b6068ce904088169377995dd760b6cf52 100644 --- a/src/app/degree-planner/actions/plan.actions.ts +++ b/src/app/degree-planner/actions/plan.actions.ts @@ -1,116 +1,99 @@ import { Action } from '@ngrx/store'; + import { PlannedTerm } from '@app/core/models/planned-term'; import { DegreePlannerState } from '@app/degree-planner/state'; -import { Course} from '../../core/models/course'; +import { Course } from '@app/core/models/course'; export enum PlanActionTypes { - InitialPlanLoadResponse = '[Plan] Initial Load Response', + InitialPlanLoadResponse = '[Plan] Initial Load Response', - ChangeVisiblePlanRequest = '[Plan] Change Visible Request', - ChangeVisiblePlanResponse = '[Plan] Change Visible Response', + ChangeVisiblePlanRequest = '[Plan] Change Visible Request', + ChangeVisiblePlanResponse = '[Plan] Change Visible Response', - AddCourseRequest = '[Plan] Add Course Request', - AddCourseResponse = '[Plan] Add Course Response', + AddCourseRequest = '[Plan] Add Course Request', + AddCourseResponse = '[Plan] Add Course Response', - RemoveCourseRequest = '[Plan] Remove Course Request', - RemoveCourseResponse = '[Plan] Remove Course Response', + RemoveCourseRequest = '[Plan] Remove Course Request', + RemoveCourseResponse = '[Plan] Remove Course Response', - ChangeCourseTermRequest = '[Plan] Change Course Term Request', - ChangeCourseTermResponse = '[Plan] Change Course Term Response', + ChangeCourseTermRequest = '[Plan] Change Course Term Request', + ChangeCourseTermResponse = '[Plan] Change Course Term Response', - AddSavedForLaterReqeust = '[Plan] Add Saved For Later Request', - AddSavedForLaterResponse = '[Plan] Add Saved For Later Response', + AddSavedForLaterReqeust = '[Plan] Add Saved For Later Request', + AddSavedForLaterResponse = '[Plan] Add Saved For Later Response', - RemoveSavedForLaterReqeust = '[Plan] Remove Saved For Later Request', - RemoveSavedForLaterResponse = '[Plan] Remove Saved For Later Response', + RemoveSavedForLaterReqeust = '[Plan] Remove Saved For Later Request', + RemoveSavedForLaterResponse = '[Plan] Remove Saved For Later Response', - MoveFromSavedToTermRequest = '[Plan] Move Course From Saved to Term Request', - MoveFromSavedToTermResponse= '[Plan] Move Course From Saved to Term Response' + MoveFromSavedToTermRequest = '[Plan] Move Course From Saved to Term Request', + MoveFromSavedToTermResponse = '[Plan] Move Course From Saved to Term Response', } export class InitialPlanLoadResponse implements Action { - public readonly type = PlanActionTypes.InitialPlanLoadResponse; - constructor(public payload: DegreePlannerState) {} + public readonly type = PlanActionTypes.InitialPlanLoadResponse; + constructor(public payload: DegreePlannerState) {} } export class ChangeVisiblePlanRequest implements Action { - public readonly type = PlanActionTypes.ChangeVisiblePlanRequest; - constructor(public payload: { newVisibleRoadmapId: number }) {} + public readonly type = PlanActionTypes.ChangeVisiblePlanRequest; + constructor(public payload: { newVisibleRoadmapId: number }) {} } export class ChangeVisiblePlanResponse implements Action { - public readonly type = PlanActionTypes.ChangeVisiblePlanResponse; - constructor( - public payload: { visibleRoadmapId: number; visibleTerms: PlannedTerm[] } - ) {} + public readonly type = PlanActionTypes.ChangeVisiblePlanResponse; + constructor( + 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} - ) {} + 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} - ) {} + public readonly type = PlanActionTypes.ChangeCourseTermResponse; + constructor(public payload: { to: string; from: string; id: number }) {} } export class AddCourseRequest implements Action { - public readonly type = PlanActionTypes.AddCourseRequest; - constructor( - public payload: {subjectCode: string, courseId: string, termCode: string} - ) {} + public readonly type = PlanActionTypes.AddCourseRequest; + constructor( + public payload: { subjectCode: string; courseId: string; termCode: string }, + ) {} } export class AddCourseResponse implements Action { - public readonly type = PlanActionTypes.AddCourseResponse; - constructor( - public payload: {course: Course} - ) {} + public readonly type = PlanActionTypes.AddCourseResponse; + constructor(public payload: { course: Course }) {} } export class RemoveCourseRequest implements Action { - public readonly type = PlanActionTypes.RemoveCourseRequest; - constructor( - public payload: {id: number} - ) {} + public readonly type = PlanActionTypes.RemoveCourseRequest; + constructor(public payload: { id: number }) {} } export class RemoveCourseResponse implements Action { - public readonly type = PlanActionTypes.RemoveCourseResponse; - constructor( - public payload: {id: number} - ) {} + public readonly type = PlanActionTypes.RemoveCourseResponse; + constructor(public payload: { id: number }) {} } export class AddSavedForLaterRequest implements Action { - public readonly type = PlanActionTypes.AddSavedForLaterReqeust; - constructor( - public payload: {subjectCode: string, courseId: string} - ) {} + public readonly type = PlanActionTypes.AddSavedForLaterReqeust; + constructor(public payload: { subjectCode: string; courseId: string }) {} } export class AddSavedForLaterResponse implements Action { - public readonly type = PlanActionTypes.AddSavedForLaterResponse; - constructor( - public payload: {subjectCode: string, courseId: string} - ) {} + public readonly type = PlanActionTypes.AddSavedForLaterResponse; + constructor(public payload: { subjectCode: string; courseId: string }) {} } export class RemoveSavedForLaterRequest implements Action { - public readonly type = PlanActionTypes.RemoveSavedForLaterReqeust; - constructor( - public payload: {subjectCode: string, courseId: string} - ) {} + public readonly type = PlanActionTypes.RemoveSavedForLaterReqeust; + constructor(public payload: { subjectCode: string; courseId: string }) {} } export class RemoveSavedForLaterResponse implements Action { - public readonly type = PlanActionTypes.RemoveSavedForLaterResponse; - constructor( - public payload: {subjectCode: string, courseId: string} - ) {} + public readonly type = PlanActionTypes.RemoveSavedForLaterResponse; + constructor(public payload: { subjectCode: string; courseId: string }) {} } diff --git a/src/app/degree-planner/degree-planner.resolver.ts b/src/app/degree-planner/degree-planner.resolver.ts deleted file mode 100644 index 651a6f8d8e96b170a5c89ec00c34d3da9bb93cdb..0000000000000000000000000000000000000000 --- a/src/app/degree-planner/degree-planner.resolver.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Resolve } from '@angular/router'; -import { Observable, forkJoin } from 'rxjs'; -import { map } from 'rxjs/operators'; - -import { DataService } from './../core/data.service'; - -@Injectable({ - providedIn: 'root' -}) -export class DegreePlannerResolver implements Resolve<Observable<string>> { - constructor(private dataService: DataService) { } - - resolve(): Observable<any> { - return forkJoin( - this.dataService.getSubjectsMap(), - this.dataService.getTerms(), - this.dataService.getFavoriteCourses(), - this.dataService.getDegreePlans() - ).pipe( - map((allResponses) => { - return { - subjects: allResponses[0], - terms: allResponses[1], - favorites: allResponses[2], - degreePlans: allResponses[3] - }; - }) - ); - } -} diff --git a/src/app/degree-planner/degree-planner.routing.module.ts b/src/app/degree-planner/degree-planner.routing.module.ts index 141585deae2fa66693c5388f33837d6df8298b6e..ad9d0fba2cc10de29239d2c4d34f3ba4d30b8f06 100644 --- a/src/app/degree-planner/degree-planner.routing.module.ts +++ b/src/app/degree-planner/degree-planner.routing.module.ts @@ -1,15 +1,12 @@ -import { DegreePlannerResolver } from './degree-planner.resolver'; import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; -import { DegreePlannerComponent } from './degree-planner.component'; +import { DegreePlannerComponent } from './degree-planner.component'; -const routes: Routes = [ - { path: '', component: DegreePlannerComponent, resolve: { requiredData: DegreePlannerResolver } }, -]; +const routes: Routes = [{ path: '', component: DegreePlannerComponent }]; @NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule] + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], }) -export class DegreePlannerRoutingModule { } +export class DegreePlannerRoutingModule {} diff --git a/src/app/degree-planner/dialogs/remove-course-confirm-dialog/remove-course-confirm-dialog.component.ts b/src/app/degree-planner/dialogs/remove-course-confirm-dialog/remove-course-confirm-dialog.component.ts index 3f2fc54bc631c2bf45eb297adedcb39e011ce746..1381509c1ffcbbda32595b35e7bfdaba389d3583 100644 --- a/src/app/degree-planner/dialogs/remove-course-confirm-dialog/remove-course-confirm-dialog.component.ts +++ b/src/app/degree-planner/dialogs/remove-course-confirm-dialog/remove-course-confirm-dialog.component.ts @@ -1,52 +1,48 @@ -import { DegreePlannerDataService } from './../../../core/service/degree-planner-data.service'; -import { Component, OnInit, Input, Inject } from '@angular/core'; +import { Component, OnInit, Inject } from '@angular/core'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'; -import { DataService } from '../../../core/data.service'; -import { Course } from '../../../core/models/course'; -import { SavedForLaterCourse } from '../../../core/models/saved-for-later-course'; + +import { Course } from '@app/core/models/course'; +import { SavedForLaterCourse } from '@app/core/models/saved-for-later-course'; import { DegreePlannerState } from '@app/degree-planner/state'; import { Store } from '@ngrx/store'; -import { - RemoveCourseRequest -} from '@app/degree-planner/actions/plan.actions'; +import { RemoveCourseRequest } from '@app/degree-planner/actions/plan.actions'; @Component({ - selector: 'cse-remove-course-confirm-dialog', - templateUrl: './remove-course-confirm-dialog.component.html', - styleUrls: ['./remove-course-confirm-dialog.component.scss'] + selector: 'cse-remove-course-confirm-dialog', + templateUrl: './remove-course-confirm-dialog.component.html', + styleUrls: ['./remove-course-confirm-dialog.component.scss'], }) export class RemoveCourseConfirmDialogComponent implements OnInit { - course: Course; - savedForLater: Boolean; - courses: Course[]; - type: 'saved' | 'course' | 'search'; - favoriteCourses: SavedForLaterCourse[]; - - // tslint:disable-next-line:max-line-length - constructor(private dataService: DataService, - private dialogRef: MatDialogRef<RemoveCourseConfirmDialogComponent>, - private degreePlannerDataSvc: DegreePlannerDataService, - private store: Store<{ degreePlanner: DegreePlannerState }>, - @Inject(MAT_DIALOG_DATA) data: any) { - this.course = data.course; - this.type = data.type; - } - - ngOnInit() {} - - removeCourse() { - switch (this.type) { - case 'saved': - console.log('remove course from saved'); - break; - - case 'course': - console.log('remove course from term'); - console.log(this.course); - this.store.dispatch(new RemoveCourseRequest({ id: this.course.id })); - break; - } - } + course: Course; + savedForLater: Boolean; + courses: Course[]; + type: 'saved' | 'course' | 'search'; + favoriteCourses: SavedForLaterCourse[]; + + constructor( + private dialogRef: MatDialogRef<RemoveCourseConfirmDialogComponent>, + private store: Store<{ degreePlanner: DegreePlannerState }>, + @Inject(MAT_DIALOG_DATA) data: any, + ) { + this.course = data.course; + this.type = data.type; + } + + ngOnInit() {} + + removeCourse() { + switch (this.type) { + case 'saved': + console.log('remove course from saved'); + break; + + case 'course': + console.log('remove course from term'); + console.log(this.course); + this.store.dispatch(new RemoveCourseRequest({ id: this.course.id })); + break; + } + } } diff --git a/src/app/degree-planner/effects/plan.effects.ts b/src/app/degree-planner/effects/plan.effects.ts index 6775d84593af94ead48c344da54a0beec34ccbd7..23def492259c653f64ec3580a231b8390b7a69f6 100644 --- a/src/app/degree-planner/effects/plan.effects.ts +++ b/src/app/degree-planner/effects/plan.effects.ts @@ -8,19 +8,22 @@ import { Store } from '@ngrx/store'; // Services import { DegreePlannerApiService } from '@app/degree-planner/services/api.service'; -import { getDegreePlannerState, getVisibleRoadmapId } from '@app/degree-planner/selectors'; +import { + getDegreePlannerState, + getVisibleRoadmapId, +} from '@app/degree-planner/selectors'; // Actions import { - InitialPlanLoadResponse, - ChangeVisiblePlanRequest, - ChangeVisiblePlanResponse, - PlanActionTypes, - ChangeCourseTermResponse, - AddCourseResponse, - RemoveCourseResponse, - RemoveSavedForLaterResponse, - AddSavedForLaterResponse + InitialPlanLoadResponse, + ChangeVisiblePlanRequest, + ChangeVisiblePlanResponse, + PlanActionTypes, + ChangeCourseTermResponse, + AddCourseResponse, + RemoveCourseResponse, + RemoveSavedForLaterResponse, + AddSavedForLaterResponse, } from '@app/degree-planner/actions/plan.actions'; // Models @@ -28,281 +31,308 @@ 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() - ).pipe( - map(([planDetails, savedForLaterCourses]) => { - return { ...planDetails, savedForLaterCourses }; - }) - ); - }), - - // 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 }); - }) - ); - } + 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 }); + }), + ); + } } diff --git a/src/app/degree-planner/favorites-container/favorites-container.component.html b/src/app/degree-planner/favorites-container/favorites-container.component.html index ffb45b305b51fab5f9c8b75f0b139649a5b1ceab..48f82c41048c1a41c275aca43f9f0490b1f3d93a 100644 --- a/src/app/degree-planner/favorites-container/favorites-container.component.html +++ b/src/app/degree-planner/favorites-container/favorites-container.component.html @@ -1,30 +1,14 @@ <div class="term-container"> - <div - id="saved-courses" - class="course-list" - cdkDropList - [cdkDropListConnectedTo]="dropZones$ | async" - class="course-list" - (cdkDropListDropped)="drop($event)" - > - <div - class="course-wrapper" - *ngFor="let course of courses$ | async" - cdkDrag - [cdkDragData]="course" - > - <div class="course-wrapper-inner"> - <cse-course-item - [course]="course" - type="saved" - [savedForLater]="true" - class="course-favorite" - ></cse-course-item> - </div> - </div> + <div id="saved-courses" class="course-list" cdkDropList [cdkDropListConnectedTo]="dropZones$ | async" class="course-list" + (cdkDropListDropped)="drop($event)"> + <div class="course-wrapper" *ngFor="let course of courses$ | async" cdkDrag [cdkDragData]="course"> + <div class="course-wrapper-inner"> + <cse-course-item [course]="course" type="saved" class="course-favorite"></cse-course-item> + </div> + </div> - <div *ngIf="(courses$ | async).length === 0" class="no-courses text-center semi-bold"> - <p class="no-courses text-center semi-bold">No courses saved for later</p> - </div> - </div> -</div> + <div *ngIf="(courses$ | async).length === 0" class="no-courses text-center semi-bold"> + <p class="no-courses text-center semi-bold">No courses saved for later</p> + </div> + </div> +</div> \ No newline at end of file diff --git a/src/app/degree-planner/reducer.ts b/src/app/degree-planner/reducer.ts index efd8643fbd7d11ec144cdbae5a588595390803ba..2ab6474ed37a0a7d479ff2c2172a79f8362477f9 100644 --- a/src/app/degree-planner/reducer.ts +++ b/src/app/degree-planner/reducer.ts @@ -1,230 +1,230 @@ import { - DegreePlannerState, - INITIAL_DEGREE_PLANNER_STATE + DegreePlannerState, + INITIAL_DEGREE_PLANNER_STATE, } from '@app/degree-planner/state'; import { - PlanActionTypes, - InitialPlanLoadResponse, - ChangeVisiblePlanResponse, - RemoveCourseResponse, - ChangeCourseTermResponse, - AddCourseResponse, - RemoveSavedForLaterResponse, - AddSavedForLaterResponse + PlanActionTypes, + InitialPlanLoadResponse, + ChangeVisiblePlanResponse, + RemoveCourseResponse, + ChangeCourseTermResponse, + AddCourseResponse, + RemoveSavedForLaterResponse, + AddSavedForLaterResponse, } from '@app/degree-planner/actions/plan.actions'; import { - NoteActionTypes, - WriteNoteResponse, - DeleteNoteResponse + NoteActionTypes, + WriteNoteResponse, + DeleteNoteResponse, } from '@app/degree-planner/actions/note.actions'; type SupportedActions = - | InitialPlanLoadResponse - | ChangeVisiblePlanResponse - | WriteNoteResponse - | DeleteNoteResponse - | ChangeCourseTermResponse - | RemoveCourseResponse - | AddCourseResponse - | RemoveSavedForLaterResponse - | AddSavedForLaterResponse; + | InitialPlanLoadResponse + | ChangeVisiblePlanResponse + | WriteNoteResponse + | DeleteNoteResponse + | ChangeCourseTermResponse + | RemoveCourseResponse + | AddCourseResponse + | RemoveSavedForLaterResponse + | AddSavedForLaterResponse; export function degreePlannerReducer( - state = INITIAL_DEGREE_PLANNER_STATE, - action: SupportedActions + 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 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; - } - - case PlanActionTypes.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 PlanActionTypes.RemoveCourseResponse: { - const { 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 PlanActionTypes.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 PlanActionTypes.AddSavedForLaterResponse: { - const { courseId, subjectCode } = action.payload; - - // // Create new saved for later array - const newSavedForLater = [ - ...state.savedForLaterCourses, - // TODO Update this when the API is fixed, the API should be sending a fav course as a response - { - id: null, - courseId, - subjectCode, - topicId: 0, - courseOrder: 0, - catalogNumber: '', - title: '', - termCode: '0000' - } - ]; - - 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; - } + 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 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; + } + + case PlanActionTypes.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 PlanActionTypes.RemoveCourseResponse: { + const { 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 PlanActionTypes.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 PlanActionTypes.AddSavedForLaterResponse: { + const { courseId, subjectCode } = action.payload; + + // // Create new saved for later array + const newSavedForLater = [ + ...state.savedForLaterCourses, + // TODO Update this when the API is fixed, the API should be sending a fav course as a response + { + id: null, + courseId, + subjectCode, + topicId: 0, + courseOrder: 0, + catalogNumber: '', + title: '', + termCode: '0000', + }, + ]; + // @ts-ignore + 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); + return things.some(thing => thing.termCode === termCode); }; diff --git a/src/app/degree-planner/selectors.ts b/src/app/degree-planner/selectors.ts index 2c7618e027ab1182c8f12c18940b1c0f61883ddc..bcfae8558bc54e9f3a0e390018ca00842cc3f321 100644 --- a/src/app/degree-planner/selectors.ts +++ b/src/app/degree-planner/selectors.ts @@ -9,83 +9,93 @@ import { Course } from '@app/core/models/course'; import { DegreePlannerState } from './state'; export const getDegreePlannerState = ({ degreePlanner }: GlobalState) => { - return degreePlanner; + return degreePlanner; }; export const getSavedForLaterCourses = createSelector( - getDegreePlannerState, - state => state.savedForLaterCourses + getDegreePlannerState, + state => state.savedForLaterCourses, ); export const getAllDegreePlans = createSelector( - getDegreePlannerState, - state => state.allDegreePlans + getDegreePlannerState, + state => state.allDegreePlans, ); export const getVisibleRoadmapId = createSelector( - getDegreePlannerState, - state => state.visibleRoadmapId + getDegreePlannerState, + state => state.visibleRoadmapId, +); + +export const getAllSubjects = createSelector( + getDegreePlannerState, + state => state.subjects, ); export const getAllVisibleTerms = createSelector( - getDegreePlannerState, - state => state.visibleTerms + getDegreePlannerState, + state => state.visibleTerms, ); export const getAllVisibleTermsByYear = createSelector( - getDegreePlannerState, - state => { - const unqiueYears = state.visibleTerms - .map(term => term.termCode.slice(0, 3)) - .filter(year => year.match(/^\d{3}/)) - .filter((year, index, self) => self.indexOf(year) === index) - .sort(); + getDegreePlannerState, + state => { + const unqiueYears = state.visibleTerms + .map(term => term.termCode.slice(0, 3)) + .filter(year => year.match(/^\d{3}/)) + .filter((year, index, self) => self.indexOf(year) === index) + .sort(); - const allNotes = state.visibleTerms - .map(term => term.note) - .filter((note): note is Note => note !== undefined); + const allNotes = state.visibleTerms + .map(term => term.note) + .filter((note): note is Note => note !== undefined); - const allCourses = state.visibleTerms - .map(term => term.courses) - .reduce((flat, nested) => flat.concat(nested), []); + const allCourses = state.visibleTerms + .map(term => term.courses) + .reduce((flat, nested) => flat.concat(nested), []) + .map(course => { + course.subject = state.subjects[course.subjectCode]; + return course; + }); - return unqiueYears.map<Year>(year => { - const century = year[0] === '0' ? 0 : 1; - const twoDigitYearCode = parseInt(year.substr(1, 2), 10); - return { - century, - twoDigitYearCode, - fall: getTermForCode(`${year}2`, allCourses, allNotes), - spring: getTermForCode(`${year}4`, allCourses, allNotes), - summer: getTermForCode(`${year}6`, allCourses, allNotes) - }; - }); - } + return unqiueYears.map<Year>(year => { + const century = year[0] === '0' ? 0 : 1; + const twoDigitYearCode = parseInt(year.substr(1, 2), 10); + return { + century, + twoDigitYearCode, + fall: getTermForCode(`${year}2`, allCourses, allNotes), + spring: getTermForCode(`${year}4`, allCourses, allNotes), + summer: getTermForCode(`${year}6`, allCourses, allNotes), + }; + }); + }, ); export const getDropZones = createSelector( - getDegreePlannerState, - (state: DegreePlannerState) => { - return [ - 'saved-courses', - ...state.activeTermCodes.map(termCode => { - return `term-${termCode}`; - }) - ]; - } + getDegreePlannerState, + (state: DegreePlannerState) => { + return [ + 'saved-courses', + ...state.activeTermCodes.map(termCode => { + return `term-${termCode}`; + }), + ]; + }, ); -export const isActiveTerm = (termCode: string) => createSelector( - getDegreePlannerState, - (state: DegreePlannerState) => { - return state.activeTermCodes.includes(termCode); - } -); +export const isActiveTerm = (termCode: string) => + createSelector( + getDegreePlannerState, + (state: DegreePlannerState) => { + return state.activeTermCodes.includes(termCode); + }, + ); const getTermForCode = (termCode: string, courses: Course[], notes: Note[]) => { - return { - termCode, - note: notes.find(note => note.termCode === termCode), - courses: courses.filter(course => course.termCode === termCode) - }; + return { + termCode, + note: notes.find(note => note.termCode === termCode), + courses: courses.filter(course => course.termCode === termCode), + }; }; diff --git a/src/app/degree-planner/services/api.service.ts b/src/app/degree-planner/services/api.service.ts index d71d7d6c636259f2723be83e12b8ea4f543a699a..99cbfa796385eb3179e8e6e087ea86d46955b72c 100644 --- a/src/app/degree-planner/services/api.service.ts +++ b/src/app/degree-planner/services/api.service.ts @@ -16,126 +16,165 @@ import { DegreePlan } from '@app/core/models/degree-plan'; import { SavedForLaterCourse } from '@app/core/models/saved-for-later-course'; const HTTP_OPTIONS = { - headers: new HttpHeaders({ - 'Content-Type': 'application/json' - }) + headers: new HttpHeaders({ + 'Content-Type': 'application/json', + }), }; @Injectable({ providedIn: 'root' }) export class DegreePlannerApiService { - constructor(private http: HttpClient, private config: ConfigService) {} - - public getSavedForLaterCourses(): Observable<SavedForLaterCourse[]> { - return this.http.get<SavedForLaterCourse[]>( - `${this.config.apiPlannerUrl}/favorites` - ); - } - - public getAllDegreePlans(): Observable<DegreePlan[]> { - return this.http.get<DegreePlan[]>(this.degreePlanEndpoint()); - } - - public getPlan(roadmapId: number): Observable<DegreePlan> { - return this.http.get<DegreePlan>(this.degreePlanEndpoint(roadmapId)); - } - - public getActiveTerms(): Observable<Term[]> { - return this.http.get<Term[]>(this.searchEndpoint('terms')); - } - - public getAllNotes(roadmapId: number): Observable<Note[]> { - return this.http.get<Note[]>(this.degreePlanEndpoint(roadmapId, 'notes')); - } - - public getAllCourses(roadmapId: number): Observable<Course[]> { - return this.http.get<Course[]>( - this.degreePlanEndpoint(roadmapId, 'courses') - ); - } - - public updateCourseTerm(roadmapId, recordId, termCode): Observable<any> { - return this.http.put<Course>(this.config.apiPlannerUrl + '/degreePlan/' + - roadmapId + '/courses/' + recordId + '?termCode=' + termCode, HTTP_OPTIONS); - } - - public addCourse(planId: number, subjectCode: string, courseId: string, termCode: string): Observable<Course> { - return this.http.post<Course>( - this.config.apiPlannerUrl + '/degreePlan/' + planId + '/courses', {subjectCode, courseId, termCode }, HTTP_OPTIONS); - } - - public removeCourse(planId: number, recordId: string) { - return this.http.delete(this.config.apiPlannerUrl + '/degreePlan/' + planId + '/courses/' + recordId, HTTP_OPTIONS); - } - - public saveForLater(subjectCode: string, courseId: string): Observable<SavedForLaterCourse> { - return this.http.post<SavedForLaterCourse>(this.config.apiPlannerUrl + '/favorites/' + subjectCode + '/' + courseId, HTTP_OPTIONS); - } - - public removeSavedForLater(subjectCode: string, courseId: string): Observable<SavedForLaterCourse> { - return this.http.delete<SavedForLaterCourse>(this.config.apiPlannerUrl + '/favorites/' + subjectCode + '/' + courseId, HTTP_OPTIONS); - } - - public createNote( - planId: number, - termCode: string, - noteText: string - ): Observable<Note> { - const payload = { - termCode: termCode, - note: noteText - }; - - return this.http.post<Note>( - this.degreePlanEndpoint(planId, 'notes'), - payload, - HTTP_OPTIONS - ); - } - - public updateNote( - planId: number, - termCode: string, - noteText: string, - noteId: number - ): Observable<Note> { - const payload = { - termCode: termCode, - note: noteText, - id: noteId - }; - - return this.http - .put<null>( - this.degreePlanEndpoint(planId, 'notes', noteId), - payload, - HTTP_OPTIONS - ) - .pipe(map(() => payload)); - } - - public deleteNote(planId: number, noteId: number): Observable<null> { - return this.http.delete<null>( - this.degreePlanEndpoint(planId, 'notes', noteId), - HTTP_OPTIONS - ); - } - - /** - * Helper function for building API endpoint URLs - */ - private degreePlanEndpoint(...parts: any[]): string { - return ['degreePlan'] - .concat(parts.map(part => part.toString())) - .reduce((soFar, next) => { - return Location.joinWithSlash(soFar, next); - }, this.config.apiPlannerUrl); - } - - private searchEndpoint(...parts: any[]): string { - return parts - .map(part => part.toString()) - .reduce((soFar, next) => { - return Location.joinWithSlash(soFar, next); - }, this.config.apiSearchUrl); - } + constructor(private http: HttpClient, private config: ConfigService) {} + + public getSavedForLaterCourses(): Observable<SavedForLaterCourse[]> { + return this.http.get<SavedForLaterCourse[]>( + `${this.config.apiPlannerUrl}/favorites`, + ); + } + + public getAllDegreePlans(): Observable<DegreePlan[]> { + return this.http.get<DegreePlan[]>(this.degreePlanEndpoint()); + } + + public getPlan(roadmapId: number): Observable<DegreePlan> { + return this.http.get<DegreePlan>(this.degreePlanEndpoint(roadmapId)); + } + + public getAllSubjects(): Observable<Object> { + return this.http.get<Object>(this.searchEndpoint('subjectsMap/0000')); + } + + public getActiveTerms(): Observable<Term[]> { + return this.http.get<Term[]>(this.searchEndpoint('terms')); + } + + public getAllNotes(roadmapId: number): Observable<Note[]> { + return this.http.get<Note[]>(this.degreePlanEndpoint(roadmapId, 'notes')); + } + + public getAllCourses(roadmapId: number): Observable<Course[]> { + return this.http.get<Course[]>( + this.degreePlanEndpoint(roadmapId, 'courses'), + ); + } + + public updateCourseTerm(roadmapId, recordId, termCode): Observable<any> { + return this.http.put<Course>( + this.config.apiPlannerUrl + + '/degreePlan/' + + roadmapId + + '/courses/' + + recordId + + '?termCode=' + + termCode, + HTTP_OPTIONS, + ); + } + + public addCourse( + planId: number, + subjectCode: string, + courseId: string, + termCode: string, + ): Observable<Course> { + return this.http.post<Course>( + this.config.apiPlannerUrl + '/degreePlan/' + planId + '/courses', + { subjectCode, courseId, termCode }, + HTTP_OPTIONS, + ); + } + + public removeCourse(planId: number, recordId: string) { + return this.http.delete( + this.config.apiPlannerUrl + + '/degreePlan/' + + planId + + '/courses/' + + recordId, + HTTP_OPTIONS, + ); + } + + public saveForLater( + subjectCode: string, + courseId: string, + ): Observable<SavedForLaterCourse> { + return this.http.post<SavedForLaterCourse>( + this.config.apiPlannerUrl + '/favorites/' + subjectCode + '/' + courseId, + HTTP_OPTIONS, + ); + } + + public removeSavedForLater( + subjectCode: string, + courseId: string, + ): Observable<SavedForLaterCourse> { + return this.http.delete<SavedForLaterCourse>( + this.config.apiPlannerUrl + '/favorites/' + subjectCode + '/' + courseId, + HTTP_OPTIONS, + ); + } + + public createNote( + planId: number, + termCode: string, + noteText: string, + ): Observable<Note> { + const payload = { + termCode: termCode, + note: noteText, + }; + + return this.http.post<Note>( + this.degreePlanEndpoint(planId, 'notes'), + payload, + HTTP_OPTIONS, + ); + } + + public updateNote( + planId: number, + termCode: string, + noteText: string, + noteId: number, + ): Observable<Note> { + const payload = { + termCode: termCode, + note: noteText, + id: noteId, + }; + + return this.http + .put<null>( + this.degreePlanEndpoint(planId, 'notes', noteId), + payload, + HTTP_OPTIONS, + ) + .pipe(map(() => payload)); + } + + public deleteNote(planId: number, noteId: number): Observable<null> { + return this.http.delete<null>( + this.degreePlanEndpoint(planId, 'notes', noteId), + HTTP_OPTIONS, + ); + } + + /** + * Helper function for building API endpoint URLs + */ + private degreePlanEndpoint(...parts: any[]): string { + return ['degreePlan'] + .concat(parts.map(part => part.toString())) + .reduce((soFar, next) => { + return Location.joinWithSlash(soFar, next); + }, this.config.apiPlannerUrl); + } + + private searchEndpoint(...parts: any[]): string { + return parts + .map(part => part.toString()) + .reduce((soFar, next) => { + return Location.joinWithSlash(soFar, next); + }, this.config.apiSearchUrl); + } } diff --git a/src/app/degree-planner/shared/course-item/course-item.component.html b/src/app/degree-planner/shared/course-item/course-item.component.html index 86abfef917a375768a6fe01fc9504f6bdd217032..5874c903fd786129b38f50307dfbe7626cc45d65 100644 --- a/src/app/degree-planner/shared/course-item/course-item.component.html +++ b/src/app/degree-planner/shared/course-item/course-item.component.html @@ -1,41 +1,42 @@ <div class="course-item {{status}} {{disabled ? 'disabled' : ''}}"> - <div fxLayout="row" fxLayoutAlign="space-between start"> - <div fxLayout="column" fxLayoutAlign="space-between start" fxFlex="80" (click)="openCourseDetailsDialog(course)"> - <div fxLayout="row" fxLayoutAlign="start center"> - <div class="icon-number-wrapper"> - <p class="course-number"> - {{ subjects[course.subjectCode] }} {{course.catalogNumber}} - </p> - <div [ngSwitch]="status"> - <i *ngSwitchCase="'complete'" class="material-icons complete-icon">check_circle</i> - <i *ngSwitchCase="'in-progress'" class="material-icons in-progress-icon">check_circle</i> - <i *ngSwitchCase="'waitlist'" class="material-icons problem-icon">report_problem</i> - <i *ngSwitchCase="'incomplete'" class="material-icons error-icon">error</i> - </div> - </div> - </div> - <div fxLayout="row" fxLayoutAlign="start center"> - <p class="course-title">{{course.title}}</p> - </div> - </div> + <div fxLayout="row" fxLayoutAlign="space-between start"> + <div fxLayout="column" fxLayoutAlign="space-between start" fxFlex="80" (click)="openCourseDetailsDialog(course)"> + <div fxLayout="row" fxLayoutAlign="start center"> + <div class="icon-number-wrapper"> + <p class="course-number"> + {{ course.subject }} {{course.catalogNumber}} + </p> + <div [ngSwitch]="status"> + <i *ngSwitchCase="'complete'" class="material-icons complete-icon">check_circle</i> + <i *ngSwitchCase="'in-progress'" class="material-icons in-progress-icon">check_circle</i> + <i *ngSwitchCase="'waitlist'" class="material-icons problem-icon">report_problem</i> + <i *ngSwitchCase="'incomplete'" class="material-icons error-icon">error</i> + </div> + </div> + </div> + <div fxLayout="row" fxLayoutAlign="start center"> + <p class="course-title">{{course.title}}</p> + </div> + </div> - <div fxLayout="column" fxLayoutAlign="space-between end" fxFlex="20"> - <div fxLayout="row" fxLayoutAlign="end center"> - <mat-icon [matMenuTriggerFor]="courseMenu" aria-label="Course menu" matTooltip="Course Menu" matTooltipPosition="right" class="material-icons">more_horiz</mat-icon> - <mat-menu #courseMenu="matMenu" class="course-item-menu"> - <button mat-menu-item (click)="openCourseDetailsDialog(course)">Course Details</button> - <button mat-menu-item [matMenuTriggerFor]="academicYearsGroup">Move</button> - <mat-menu #academicYearsGroup="matMenu" class="course-item-submenu"> - <!-- <button mat-menu-item (click)="moveToFavorites(course)" *ngIf="!savedForLater" class="favorites-list">Saved for later</button> + <div fxLayout="column" fxLayoutAlign="space-between end" fxFlex="20"> + <div fxLayout="row" fxLayoutAlign="end center"> + <mat-icon [matMenuTriggerFor]="courseMenu" aria-label="Course menu" matTooltip="Course Menu" matTooltipPosition="right" + class="material-icons">more_horiz</mat-icon> + <mat-menu #courseMenu="matMenu" class="course-item-menu"> + <button mat-menu-item (click)="openCourseDetailsDialog(course)">Course Details</button> + <button mat-menu-item [matMenuTriggerFor]="academicYearsGroup">Move</button> + <mat-menu #academicYearsGroup="matMenu" class="course-item-submenu"> + <!-- <button mat-menu-item (click)="moveToFavorites(course)" *ngIf="!savedForLater" class="favorites-list">Saved for later</button> <button mat-menu-item *ngFor="let termCode of termCodes" (click)="savedForLater ? addToTerm(termCode) : switchTerm(termCode)">{{ termCode | getTermDescription }}</button> --> - </mat-menu> - <button *ngIf="!disabled" mat-menu-item (click)="openRemoveConfirmationDialog()">Remove</button> - </mat-menu> - </div> - <div fxLayout="row" fxLayoutAlign="end center"> - <p class="course-credits">{{ course.credits !== undefined ? course.credits : "--" }} Cr</p> - </div> - </div> - </div> -</div> + </mat-menu> + <button *ngIf="!disabled" mat-menu-item (click)="openRemoveConfirmationDialog()">Remove</button> + </mat-menu> + </div> + <div fxLayout="row" fxLayoutAlign="end center"> + <p class="course-credits">{{ course.credits !== undefined ? course.credits : "--" }} Cr</p> + </div> + </div> + </div> +</div> \ No newline at end of file diff --git a/src/app/degree-planner/shared/course-item/course-item.component.ts b/src/app/degree-planner/shared/course-item/course-item.component.ts index a9c076a641d86c4e7cab0a502eb3e06289563e9f..cb84c36a3e3ff9c1f4c68a1c5a8e910c80896981 100644 --- a/src/app/degree-planner/shared/course-item/course-item.component.ts +++ b/src/app/degree-planner/shared/course-item/course-item.component.ts @@ -1,67 +1,42 @@ -import { DegreePlannerDataService } from './../../../core/service/degree-planner-data.service'; -import { ActivatedRoute } from '@angular/router'; -import { Component, Input, ChangeDetectorRef, OnInit } from '@angular/core'; -import { Course } from '../../../core/models/course'; -import { Subject } from '@app/core/models/course-details'; -import { Term } from '../../../core/models/term'; -import { DegreePlan } from '../../../core/models/degree-plan'; -import { SavedForLaterCourse } from '../../../core/models/saved-for-later-course'; -import { DataService } from '../../../core/data.service'; -import { CourseDetailsDialogComponent } from '../../dialogs/course-details-dialog/course-details-dialog.component'; -// tslint:disable-next-line:max-line-length -import { RemoveCourseConfirmDialogComponent } from '../../dialogs/remove-course-confirm-dialog/remove-course-confirm-dialog.component'; +import { Component, Input, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material'; +import { Course } from '@app/core/models/course'; + +// tslint:disable-next-line:max-line-length +import { CourseDetailsDialogComponent } from '@app/degree-planner/dialogs/course-details-dialog/course-details-dialog.component'; +// tslint:disable-next-line:max-line-length +import { RemoveCourseConfirmDialogComponent } from '@app/degree-planner/dialogs/remove-course-confirm-dialog/remove-course-confirm-dialog.component'; + @Component({ - selector: 'cse-course-item', - templateUrl: './course-item.component.html', - styleUrls: ['./course-item.component.scss'] + selector: 'cse-course-item', + templateUrl: './course-item.component.html', + styleUrls: ['./course-item.component.scss'], }) - export class CourseItemComponent implements OnInit { - termsInAcademicYear: []; - // favoriteCourse: FavoriteCourse; - degreePlans: DegreePlan[]; - degreePlanCourses: any[]; - selectedDegreePlan: number; - termCodes: any; - subjects: Object; - - @Input() course: Course; - @Input() courses: Course[]; - @Input() status: string; - @Input() disabled: string; - @Input() type: 'saved' | 'course' | 'search'; - @Input() savedForLater: Boolean; - @Input() favoriteCourses: SavedForLaterCourse[]; + dataService; // TODO refactor to use api service + status; // TODO make this work k thx plz!? + @Input() course: Course; + @Input() disabled: string; + @Input() type: 'saved' | 'course' | 'search'; - constructor( - private dataService: DataService, - public dialog: MatDialog, - private route: ActivatedRoute, - private degreePlannerDataSvc: DegreePlannerDataService, - private cdRef: ChangeDetectorRef) { - this.degreePlans = route.snapshot.data.requiredData.degreePlans; - this.selectedDegreePlan = this.degreePlans[0].roadmapId; - this.subjects = route.snapshot.data.requiredData.subjects; - this.termCodes = this.degreePlannerDataSvc.getAllTerms(); - } + constructor(public dialog: MatDialog) {} - ngOnInit() { - } + ngOnInit() {} - openCourseDetailsDialog(course) { - this.dataService.getCourseDetails(course.termCode, course.subjectCode, course.courseId) - .subscribe(courseDetails => { - const dialogRef = this.dialog.open(CourseDetailsDialogComponent, { - data: { courseDetails: courseDetails } - }); - }); - } + openCourseDetailsDialog(course) { + this.dataService + .getCourseDetails(course.termCode, course.subjectCode, course.courseId) + .subscribe(courseDetails => { + const dialogRef = this.dialog.open(CourseDetailsDialogComponent, { + data: { courseDetails: courseDetails }, + }); + }); + } - openRemoveConfirmationDialog() { - const dialogRef = this.dialog.open(RemoveCourseConfirmDialogComponent, { - data: { course: this.course, type: this.type } - }); - } + openRemoveConfirmationDialog() { + const dialogRef = this.dialog.open(RemoveCourseConfirmDialogComponent, { + data: { course: this.course, type: this.type }, + }); + } } diff --git a/src/app/degree-planner/state.ts b/src/app/degree-planner/state.ts index 86b2c4f07a4ac2905a23b24fcf4d705fdda10bfe..5c49b3528ee8fa75d41173b89d6188de0e405d41 100644 --- a/src/app/degree-planner/state.ts +++ b/src/app/degree-planner/state.ts @@ -5,17 +5,19 @@ import { DegreePlan } from '@app/core/models/degree-plan'; import { SavedForLaterCourse } from '@app/core/models/saved-for-later-course'; export interface DegreePlannerState { - visibleRoadmapId?: number; - visibleTerms: PlannedTerm[]; - savedForLaterCourses: SavedForLaterCourse[]; - activeTermCodes: string[]; - allDegreePlans: DegreePlan[]; + visibleRoadmapId?: number; + visibleTerms: PlannedTerm[]; + savedForLaterCourses: SavedForLaterCourse[]; + activeTermCodes: string[]; + allDegreePlans: DegreePlan[]; + subjects: Object; } export const INITIAL_DEGREE_PLANNER_STATE: DegreePlannerState = { - visibleRoadmapId: undefined, - visibleTerms: [], - savedForLaterCourses: [], - activeTermCodes: [], - allDegreePlans: [] + visibleRoadmapId: undefined, + visibleTerms: [], + savedForLaterCourses: [], + activeTermCodes: [], + allDegreePlans: [], + subjects: Object }; 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 42936dfb65a72c1ac2a23fe627ae19bc4f57f67d..c7ae8e37abb40e3b414bab8330f6453a9c7ef479 100644 --- a/src/app/degree-planner/term-container/term-container.component.ts +++ b/src/app/degree-planner/term-container/term-container.component.ts @@ -1,5 +1,10 @@ // Libraries -import { Component, Input, OnInit } from '@angular/core'; +import { + Component, + Input, + OnInit, + ChangeDetectionStrategy, +} from '@angular/core'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { MatDialog } from '@angular/material'; @@ -8,100 +13,97 @@ import { Observable } from 'rxjs'; import { Store, select } from '@ngrx/store'; import { DegreePlannerState } from '@app/degree-planner/state'; import { - ChangeCourseTermRequest, - AddCourseRequest, - RemoveSavedForLaterRequest + ChangeCourseTermRequest, + AddCourseRequest, + RemoveSavedForLaterRequest, } from '@app/degree-planner/actions/plan.actions'; // Selectors -import { - getDropZones, - isActiveTerm -} from '@app/degree-planner/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'; // Models import { PlannedTerm } from '@app/core/models/planned-term'; import { - NotesDialogComponent, - NotesDialogData + NotesDialogComponent, + NotesDialogData, } from '../dialogs/notes-dialog/notes-dialog.component'; @Component({ - selector: 'cse-term-container', - templateUrl: './term-container.component.html', - styleUrls: ['./term-container.component.scss'] + selector: 'cse-term-container', + templateUrl: './term-container.component.html', + styleUrls: ['./term-container.component.scss'], }) 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 store: Store<{ degreePlanner: DegreePlannerState }>, - ) {} - - public ngOnInit() { - this.dropZones$ = this.store.pipe(select(getDropZones)); - this.isActiveTerm$ = this.store.pipe(select(isActiveTerm(this.term.termCode))); - } - - public openAddSidenav(): void { - this.sidenavService.open(); - } - - public openNotesDialog(): void { - const termCode = this.term.termCode; - const data: NotesDialogData = this.term.note - ? { termCode, hasExistingNote: true, initialText: this.term.note.note } - : { termCode, hasExistingNote: false }; - this.dialog.open<any, NotesDialogData>(NotesDialogComponent, { data }); - } - - public getTotalCredits(): number { - return this.term.courses.reduce((sum, course) => { - return sum + course.credits; - }, 0); - } - - drop(event: CdkDragDrop<string[]>) { - const newContainer = event.container.id; - const previousContainer = event.previousContainer.id; - - if (newContainer === previousContainer) { - // If the user dropped a course into the same container do nothing - return; - - } else if (previousContainer.indexOf('term-') === 0) { - // If moving from term to term - - // Get the pervious and new term code, and the record ID - const to = newContainer.substr(5); - const { termCode: from, id } = event.item.data; - - // Dispatch a new change request - this.store.dispatch(new ChangeCourseTermRequest({ to, from, id })); - - } else if (previousContainer === 'saved-courses') { - // If moving from saved courses to term - - // Get the term code from the new term dropzone's ID - const termCode = newContainer.substr(5); - - // Pull the course data from the moved item - const { subjectCode, courseId} = event.item.data; - - // Dispatch the add event - this.store.dispatch(new AddCourseRequest({ subjectCode, courseId, termCode })); - this.store.dispatch(new RemoveSavedForLaterRequest({ subjectCode, courseId })); - } - } + @Input() term: PlannedTerm; + public dropZones$: Observable<String[]>; + public isActiveTerm$: Observable<Boolean>; + + constructor( + public dialog: MatDialog, + 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)), + ); + } + + public openAddSidenav(): void { + this.sidenavService.open(); + } + + public openNotesDialog(): void { + const termCode = this.term.termCode; + const data: NotesDialogData = this.term.note + ? { termCode, hasExistingNote: true, initialText: this.term.note.note } + : { termCode, hasExistingNote: false }; + this.dialog.open<any, NotesDialogData>(NotesDialogComponent, { data }); + } + + public getTotalCredits(): number { + return this.term.courses.reduce((sum, course) => { + return sum + course.credits; + }, 0); + } + + drop(event: CdkDragDrop<string[]>) { + const newContainer = event.container.id; + const previousContainer = event.previousContainer.id; + + if (newContainer === previousContainer) { + // If the user dropped a course into the same container do nothing + return; + } else if (previousContainer.indexOf('term-') === 0) { + // If moving from term to term + + // Get the pervious and new term code, and the record ID + const to = newContainer.substr(5); + const { termCode: from, id } = event.item.data; + + // Dispatch a new change request + this.store.dispatch(new ChangeCourseTermRequest({ to, from, id })); + } else if (previousContainer === 'saved-courses') { + // If moving from saved courses to term + + // Get the term code from the new term dropzone's ID + const termCode = newContainer.substr(5); + + // Pull the course data from the moved item + const { subjectCode, courseId } = event.item.data; + + // Dispatch the add event + this.store.dispatch( + new AddCourseRequest({ subjectCode, courseId, termCode }), + ); + this.store.dispatch( + new RemoveSavedForLaterRequest({ subjectCode, courseId }), + ); + } + } } diff --git a/src/app/shared/pipes/academic-year-state.pipe.ts b/src/app/shared/pipes/academic-year-state.pipe.ts index ae43459468405ab2be6372baba35e4d8d2569ced..142b1c9b9dfe384e624d62917b135b3336193a9d 100644 --- a/src/app/shared/pipes/academic-year-state.pipe.ts +++ b/src/app/shared/pipes/academic-year-state.pipe.ts @@ -1,30 +1,35 @@ -import { ActivatedRoute } from '@angular/router'; import { Pipe, PipeTransform } from '@angular/core'; +import { GlobalState } from '@app/core/state'; +import { State, select } from '@ngrx/store'; + +import { getAllVisibleTerms } from './../../degree-planner/selectors'; +import { PlannedTerm } from './../../core/models/planned-term'; @Pipe({ - name: 'academicYearState' + name: 'academicYearState', }) - export class AcademicYearStatePipe implements PipeTransform { - terms = [] as string[]; - constructor(private route: ActivatedRoute) { - this.terms = route.snapshot.data.requiredData.terms.map(t => t.termCode); - } + terms: PlannedTerm[]; + constructor(private store: State<GlobalState>) { + this.store + .pipe(select(getAllVisibleTerms)) + .subscribe(terms => (this.terms = terms)); + } - transform(year: string): string { - const termCode = this.terms[0]; - let century = 2000; - if (termCode.substr(0, 1) === '0') { - century = 1900; - } - const YYYY = century + Number(year); + transform(year: string): string { + const termCode = this.terms[0].termCode; + let century = 2000; + if (termCode.substr(0, 1) === '0') { + century = 1900; + } + const YYYY = century + Number(year); - if (year < termCode.substr(1, 2)) { - return 'Past: ' + (YYYY - 1) + '-' + YYYY; - } else if (year > termCode.substr(1, 2)) { - return 'Future: ' + (YYYY - 1) + '-' + YYYY; - } else { - return 'Current: ' + (YYYY - 1) + '-' + YYYY; - } - } + if (year < termCode.substr(1, 2)) { + return 'Past: ' + (YYYY - 1) + '-' + YYYY; + } else if (year > termCode.substr(1, 2)) { + return 'Future: ' + (YYYY - 1) + '-' + YYYY; + } else { + return 'Current: ' + (YYYY - 1) + '-' + YYYY; + } + } }