import { Component, Input, OnInit } from '@angular/core'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { MatDialog, ErrorStateMatcher } from '@angular/material'; import { Observable } from 'rxjs'; import { filter, map, distinctUntilChanged } from 'rxjs/operators'; import { Store, select } from '@ngrx/store'; import { DegreePlannerState } from '@app/degree-planner/store/state'; import { ToggleCourseSearch, OpenCourseSearch, } from '@app/degree-planner/store/actions/ui.actions'; // Models import * as actions from '@app/degree-planner/store/actions/course.actions'; import * as selectors from '@app/degree-planner/store/selectors'; import { PlannedTerm, PlannedTermNote } from '@app/core/models/planned-term'; import { Note } from '@app/core/models/note'; import { Course } from '@app/core/models/course'; import { NotesDialogComponent, NotesDialogData, } from '@app/degree-planner/dialogs/notes-dialog/notes-dialog.component'; import * as utils from '@app/degree-planner/shared/utils'; import { findSafariExecutable } from 'selenium-webdriver/safari'; const isntUndefined = <T>(thing: T | undefined): thing is T => { return thing !== undefined; }; const sumCredits = (courses: Course[]): number => { return courses.reduce((sum, course) => { if (course.credits == undefined) { return sum + course.credits; } return 0; }, 0); }; const sumCreditsInCart = (courses: Course[]): number => { return courses .filter(course => course.id && course.creditRange) .reduce((sum, course) => { return sum + parseInt(course.creditRange); }, 0); }; @Component({ selector: 'cse-term-container', templateUrl: './term-container.component.html', styleUrls: ['./term-container.component.scss'], }) export class TermContainerComponent implements OnInit { @Input() yearCode: string; @Input() termName: 'fall' | 'spring' | 'summer'; public termCode: string; public term$: Observable<PlannedTerm>; public note$: Observable<PlannedTermNote | undefined>; public courses$: Observable<Course[]>; public credits$: Observable<number>; public cartCredits$: Observable<number>; public isPastTerm$: Observable<boolean>; public dropZoneIds$: Observable<string[]>; public isCurrentTerm$: Observable<boolean>; constructor( public dialog: MatDialog, private store: Store<{ degreePlanner: DegreePlannerState }>, ) {} public ngOnInit() { const termOffset = { fall: 2, spring: 4, summer: 6 }; this.termCode = `${this.yearCode}${termOffset[this.termName]}`; this.term$ = this.store.pipe( select(selectors.selectVisibleTerm, { termCode: this.termCode }), filter(isntUndefined), distinctUntilChanged(), ); this.note$ = this.term$.pipe( map(term => term.note), distinctUntilChanged(), ); this.courses$ = this.term$.pipe( map(term => term.courses), distinctUntilChanged(), ); this.credits$ = this.courses$.pipe( map(sumCredits), distinctUntilChanged(), ); this.cartCredits$ = this.courses$.pipe( map(sumCreditsInCart), distinctUntilChanged(), ); this.isPastTerm$ = this.term$.pipe( map(term => term.era === 'past'), distinctUntilChanged(), ); this.dropZoneIds$ = this.store.pipe( select(selectors.selectAllVisibleYears), utils.yearsToDropZoneIds(), distinctUntilChanged(utils.compareStringArrays), ); this.isPastTerm$ = this.store.pipe( select(selectors.isPastTerm(this.termCode)), ); this.isCurrentTerm$ = this.store.pipe( select(selectors.isCurrentTerm(this.termCode)), ); } openNotesDialog(note?: PlannedTermNote) { if (note === undefined || note.isLoaded) { const termCode = this.termCode; const data: NotesDialogData = note ? { termCode, hasExistingNote: true, initialText: note.text, noteId: note.id, } : { termCode, hasExistingNote: false }; this.dialog.open(NotesDialogComponent, { data }); } } openCourseSearch() { this.store.dispatch(new OpenCourseSearch(this.termCode)); } drop(event: CdkDragDrop<string>) { const newContainer = event.container.id; const previousContainer = event.previousContainer.id; if (newContainer === previousContainer) { const newIndex = event.currentIndex; const { id: recordId, termCode } = event.item.data as Course; if (recordId !== null) { const action = new actions.MoveCourseInsideTerm({ termCode, recordId, newIndex, }); this.store.dispatch(action); } } else if (previousContainer.indexOf('term-') === 0) { // If moving from term to term // Get the pervious and new term code, and the record ID const from = event.previousContainer.data; const to = event.container.data; const { id } = event.item.data; const newIndex = event.currentIndex; // Dispatch a new change request this.store.dispatch( new actions.MoveCourseBetweenTerms({ to, from, id, newIndex }), ); } 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); const newIndex = event.currentIndex; // Pull the course data from the moved item const { subjectCode, courseId, title, catalogNumber } = event.item.data; this.store.dispatch( new actions.AddCourse({ courseId, termCode, subjectCode, title, catalogNumber, newIndex, }), ); this.store.dispatch( new actions.RemoveSaveForLater({ subjectCode, courseId }), ); } else if ( previousContainer === 'queried-courses-list' && newContainer.indexOf('term-') === 0 ) { const termCode = newContainer.substr(5); const newIndex = event.currentIndex; this.store.dispatch( new actions.AddCourse({ courseId: event.item.data.courseId, termCode, subjectCode: event.item.data.subjectCode, title: event.item.data.title, catalogNumber: event.item.data.catalogNumber, newIndex, }), ); } } }