import { Component, Input, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material'; import { Store, select } from '@ngrx/store'; import { Observable } from 'rxjs'; import * as utils from '@app/degree-planner/shared/utils'; import { AddCourse, AddSaveForLater, MoveCourseBetweenTerms, RemoveSaveForLater, RemoveCourse, } from '@app/degree-planner/store/actions/course.actions'; import { GlobalState } from '@app/core/state'; import { Course } from '@app/core/models/course'; import * as selectors from '@app/degree-planner/store/selectors'; import { DegreePlannerApiService } from '@app/degree-planner/services/api.service'; import { ConfirmDialogComponent } from '@app/shared/dialogs/confirm-dialog/confirm-dialog.component'; import { CourseDetailsDialogComponent } from '@app/degree-planner/dialogs/course-details-dialog/course-details-dialog.component'; import { distinctUntilChanged, filter } from 'rxjs/operators'; import { TermCode } from '@app/core/models/termcode'; import { PlannedTerm, PlannedTermEra } from '@app/core/models/planned-term'; const isntUndefined = <T>(thing: T | undefined): thing is T => { return thing !== undefined; }; @Component({ selector: 'cse-course-item', templateUrl: './course-item.component.html', styleUrls: ['./course-item.component.scss'], }) export class CourseItemComponent implements OnInit { @Input() course: Course; @Input() isCurrentTerm: boolean; @Input() isPastTerm: boolean; @Input() disabled: boolean; @Input() type: 'saved' | 'course' | 'search'; @Input() era?: unknown; visibleTerms: any; activeTerm: any; public status: | 'InProgress' | 'Waitlisted' | 'Incomplete' | 'NotOfferedInTerm' | 'NoLongerOffered' | 'Normal'; public visibleTermCodes$: Observable<string[]>; public droppableTermCodes$: Observable<string[]>; public term$: Observable<PlannedTerm>; public plannedCourses: ReadonlyArray<Course>; public toActiveTerm: boolean; public isStruckthrough = false; constructor( private api: DegreePlannerApiService, public dialog: MatDialog, private store: Store<GlobalState>, ) {} ngOnInit() { const isActive = this.era === 'active'; const isPast = this.era === 'past'; const isNotOffered = this.course.studentEnrollmentStatus === 'NOTOFFERED'; const isNoLongerOffered = this.course.studentEnrollmentStatus === 'DOESNOTEXIST'; this.isStruckthrough = (isPast || isActive) && isNoLongerOffered; const isIncomplete = isPast && this.course.grade === null; const isWaitlisted = this.course.studentEnrollmentStatus === 'Waitlisted'; const isInProgress = isActive && this.course.studentEnrollmentStatus === 'Enrolled'; if (isIncomplete) { this.status = 'Incomplete'; } else if (isWaitlisted) { this.status = 'Waitlisted'; } else if (isInProgress) { this.status = 'InProgress'; } else if (isNotOffered) { this.status = 'NotOfferedInTerm'; } else { this.status = 'Normal'; } } onMenuOpen() { this.droppableTermCodes$ = this.store.pipe( select(selectors.selectAllVisibleYears), utils.yearsToDroppableTermCodes(), distinctUntilChanged(utils.compareStringArrays), ); } moveToSavedForLater(course) { this.store.dispatch( new AddSaveForLater({ courseId: course.courseId, subjectCode: course.subjectCode, title: course.title, catalogNumber: course.catalogNumber, newIndex: 0, }), ); } /** * * Handle moving a course to different terms based on course type * */ onMove(termCode: string) { const toTermCode = new TermCode(termCode); this.term$ = this.store.pipe( select(selectors.selectVisibleTerm, { termCode: toTermCode }), filter(isntUndefined), distinctUntilChanged(), ); this.term$.subscribe(term => { this.plannedCourses = term.plannedCourses; this.toActiveTerm = term.era === 'active'; }); const isCourseInPlannedCourses = this.plannedCourses.some( course => course.courseId === this.course.courseId, ); const courseNotOfferedInTerm = this.plannedCourses.some( course => course.studentEnrollmentStatus === 'DOESNOTEXIST' || course.studentEnrollmentStatus === 'NOTOFFERED', ); if (isCourseInPlannedCourses) { this.dialog .open(ConfirmDialogComponent, { data: { title: "Can't add course", confirmText: 'OK', dialogClass: 'alertDialog', text: `This course already exists in selected term`, }, }) .afterClosed(); return; } if (this.toActiveTerm && courseNotOfferedInTerm) { const notOfferedCourse = this.course.subject + ' ' + this.course.catalogNumber; this.dialog .open(ConfirmDialogComponent, { data: { title: "Can't add course to term", confirmText: 'OK', dialogClass: 'alertDialog', text: `${notOfferedCourse} is not offered in ${ toTermCode.description }`, }, }) .afterClosed(); return; } switch (this.type) { case 'course': { const id = this.course.id as number; const { subjectCode, courseId } = this.course; const from = new TermCode(this.course.termCode); this.store.dispatch( new MoveCourseBetweenTerms({ to: toTermCode, from, id, newIndex: 0, courseId, subjectCode, }), ); break; } case 'saved': { const { subjectCode, courseId } = this.course; this.addToTerm(toTermCode); this.store.dispatch(new RemoveSaveForLater({ subjectCode, courseId })); break; } case 'search': { this.addToTerm(toTermCode); break; } } } /** * * Handle saving a course for later (This is not possible if a course is already saved) * */ onSaveForLater() { const { courseId, subjectCode, title, catalogNumber, termCode, } = this.course; // Dispatch a save for later event this.store.dispatch( new AddSaveForLater({ courseId: courseId, subjectCode: subjectCode, title: title, catalogNumber: catalogNumber, newIndex: 0, }), ); // If course is in a term, we need to remove it if (this.type === 'course') { this.store.dispatch( new RemoveCourse({ fromTermCode: new TermCode(termCode), recordId: this.course.id as number, }), ); } } /** * * Handle removing a course (This is not possible for type 'search') * */ onRemove() { const dialogOptions = { title: 'Remove Course?', text: '', confirmText: 'Remove Course', confirmColor: 'accent', }; switch (this.type) { case 'saved': dialogOptions.text = `This will remove "${ this.course.title }" from your saved courses.`; break; default: dialogOptions.text = `This will remove "${ this.course.title }" from your degree plan and your cart.`; } this.dialog .open(ConfirmDialogComponent, { data: dialogOptions }) .afterClosed() .subscribe((result: { confirmed: boolean }) => { // If the user confirmed the removal, remove course if (result && result.confirmed) { console.log(this.type); console.log({ type: this.type, fromTermCode: this.course.termCode, recordId: this.course.id, }); switch (this.type) { case 'course': this.store.dispatch( new RemoveCourse({ fromTermCode: new TermCode(this.course.termCode), recordId: this.course.id as number, }), ); break; case 'saved': const { subjectCode, courseId } = this.course; this.store.dispatch( new RemoveSaveForLater({ subjectCode, courseId }), ); break; } } }); } addToTerm(toTermCode: TermCode) { this.store.dispatch( new AddCourse({ courseId: this.course.courseId, termCode: toTermCode, subjectCode: this.course.subjectCode, title: this.course.title, catalogNumber: this.course.catalogNumber, }), ); } openCourseDetailsDialog(course) { this.api .getCourseDetails(course.subjectCode, course.courseId) .subscribe(courseDetails => { const dialogRef = this.dialog.open(CourseDetailsDialogComponent, { maxWidth: '800px', width: '80%', data: { courseDetails: courseDetails, courseType: this.type }, }); }); } }