import { Component, Input, OnInit, OnDestroy } from '@angular/core'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { MatDialog, ErrorStateMatcher } from '@angular/material'; import { Observable, Subscription, pipe } 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'; import { TermCode } from '@app/core/models/termcode'; // Dialogs import { ConfirmDialogComponent } from '@app/shared/dialogs/confirm-dialog/confirm-dialog.component'; import { maybeQueueResolutionOfComponentResources } from '@angular/core/src/metadata/resource_loading'; import { WeekDay } from '@angular/common'; import { MediaMatcher } from '@angular/cdk/layout'; const isntUndefined = <T>(thing: T | undefined): thing is T => { return thing !== undefined; }; @Component({ selector: 'cse-term-container', templateUrl: './term-container.component.html', styleUrls: ['./term-container.component.scss'], }) export class TermContainerComponent implements OnInit, OnDestroy { @Input() termCode: TermCode; public term$: Observable<PlannedTerm>; public note$: Observable<PlannedTermNote | undefined>; public dropZoneIds$: Observable<string[]>; public termSubscription: Subscription; public activeTermHasNotOffered: boolean; // List of courses pulled for the Observable public plannedCourses: ReadonlyArray<Course>; public enrolledCourses: ReadonlyArray<Course>; public era: 'past' | 'active' | 'future'; public hasItemDraggedOver: boolean; public plannedCredits: string; public enrolledCredits: number; public visibleCredits: 'enrolled' | 'planned'; public courseNotOfferedInTerm: ReadonlyArray<Course>; public mobileView: MediaQueryList; constructor( public dialog: MatDialog, private store: Store<{ degreePlanner: DegreePlannerState }>, mediaMatcher: MediaMatcher, ) { this.mobileView = mediaMatcher.matchMedia('(max-width: 900px)'); } public ngOnInit() { this.hasItemDraggedOver = false; this.term$ = this.store.pipe( select(selectors.selectVisibleTerm, { termCode: this.termCode }), filter(isntUndefined), distinctUntilChanged(), ); this.termSubscription = this.term$.subscribe(term => { // const {plannedCourses, enrolledCourses} = term; this.plannedCourses = term.plannedCourses; this.plannedCredits = this.sumPlannedCredits(term.plannedCourses); this.enrolledCourses = term.enrolledCourses; this.enrolledCredits = this.sumEnrolledCredits(term.enrolledCourses); this.era = term.era; const activeTermEnrollmentStatus = this.plannedCourses.forEach( course => course.studentEnrollmentStatus, ); this.visibleCredits = 'planned'; if (term.era === 'past' && this.plannedCourses.length === 0) { this.visibleCredits = 'enrolled'; } }); this.note$ = this.term$.pipe( map(term => term.note), distinctUntilChanged(), ); this.dropZoneIds$ = this.store.pipe( select(selectors.selectAllVisibleYears), utils.yearsToDropZoneIds(), distinctUntilChanged(utils.compareStringArrays), ); } ngOnDestroy() { this.termSubscription.unsubscribe(); } 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)); } changeVisibleCredits(event) { switch (event.index) { case 0: this.visibleCredits = 'enrolled'; break; default: this.visibleCredits = 'planned'; } } drop(event: CdkDragDrop<TermCode>) { const newContainer = event.container.id; const previousContainer = event.previousContainer.id; const { courseId } = event.item.data as Course; const isCourseInPlannedCourses = this.plannedCourses.some( course => course.courseId === courseId, ); if (newContainer !== previousContainer && isCourseInPlannedCourses) { this.dialog .open(ConfirmDialogComponent, { data: { title: "Can't add course to term", confirmText: 'OK', dialogClass: 'alertDialog', text: `This course already exists in selected term!`, }, }) .afterClosed(); return; } 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: new TermCode(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; // FIXME: if `event.item.data` is a Course, the `id` property could be null const { id, courseId, subjectCode } = event.item.data; const newIndex = event.currentIndex; const { classNumber } = event.item.data as Course; if (classNumber !== null) { // If moving course with packages to future term this.dialog .open(ConfirmDialogComponent, { data: { title: 'Are you sure?', confirmText: 'Move course', text: `Moving this course to a future term will remove your selected section`, }, }) .afterClosed() .subscribe((result: { confirmed: true } | undefined) => { if (result !== undefined && result.confirmed === true) { this.store.dispatch( new actions.MoveCourseBetweenTerms({ to, from, id, newIndex, courseId, subjectCode, }), ); } }); } else { // Dispatch a new change request this.store.dispatch( new actions.MoveCourseBetweenTerms({ to, from, id, newIndex, courseId, subjectCode, }), ); } } 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 = event.container.data; 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 = event.container.data; 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, }), ); } } dragEnter(event, item) { this.hasItemDraggedOver = true; } dragExit(event, item) { this.hasItemDraggedOver = false; } sumEnrolledCredits(courses: ReadonlyArray<Course>): number { return courses.reduce((sum, course) => sum + course.credits, 0); } sumPlannedCredits(courses: ReadonlyArray<Course>): string { const credits = { min: 0, max: 0 }; courses.forEach(course => { credits.min = credits.min + course.creditMin; credits.max = credits.max + course.creditMax; }); return credits.min === credits.max ? credits.min.toString() : `${credits.min}-${credits.max}`; } }