From bfd4239408accebf8d4d4ef0e3a206241be7f3c1 Mon Sep 17 00:00:00 2001 From: ievavold <ievavold@wisc.edu> Date: Fri, 28 Jun 2019 10:35:55 -0500 Subject: [PATCH] split stepper component into two dialogs, add form validation logic --- src/app/app.routing.module.ts | 2 + .../dars/dars-view/dars-view.component.html | 4 +- src/app/dars/dars-view/dars-view.component.ts | 25 +- src/app/dars/dars.module.ts | 11 +- .../new-audit-options.component.html | 133 ---------- .../new-audit-options.component.scss | 39 --- .../new-audit-options.component.spec.ts | 25 -- .../new-audit-options.component.ts | 178 -------------- .../new-degree-audit-dialog.component.html | 120 +++++++++ .../new-degree-audit-dialog.component.scss | 46 ++++ .../new-degree-audit-dialog.component.ts | 193 +++++++++++++++ .../new-what-if-audit-dialog.component.html | 154 ++++++++++++ .../new-what-if-audit-dialog.component.scss | 46 ++++ .../new-what-if-audit-dialog.component.ts | 228 ++++++++++++++++++ src/app/dars/services/api.service.ts | 9 +- 15 files changed, 821 insertions(+), 392 deletions(-) delete mode 100644 src/app/dars/new-audit-options/new-audit-options.component.html delete mode 100644 src/app/dars/new-audit-options/new-audit-options.component.scss delete mode 100644 src/app/dars/new-audit-options/new-audit-options.component.spec.ts delete mode 100644 src/app/dars/new-audit-options/new-audit-options.component.ts create mode 100644 src/app/dars/new-degree-audit-dialog/new-degree-audit-dialog.component.html create mode 100644 src/app/dars/new-degree-audit-dialog/new-degree-audit-dialog.component.scss create mode 100644 src/app/dars/new-degree-audit-dialog/new-degree-audit-dialog.component.ts create mode 100644 src/app/dars/new-what-if-audit-dialog/new-what-if-audit-dialog.component.html create mode 100644 src/app/dars/new-what-if-audit-dialog/new-what-if-audit-dialog.component.scss create mode 100644 src/app/dars/new-what-if-audit-dialog/new-what-if-audit-dialog.component.ts diff --git a/src/app/app.routing.module.ts b/src/app/app.routing.module.ts index 4711141..fb09a39 100644 --- a/src/app/app.routing.module.ts +++ b/src/app/app.routing.module.ts @@ -7,10 +7,12 @@ import { DARSViewComponent } from './dars/dars-view/dars-view.component'; const routes: Routes = [ { path: 'dars', + resolve: { constants: ConstantsService }, component: DARSViewComponent, }, { path: 'dars-beta', + resolve: { constants: ConstantsService }, component: DARSViewComponent, }, { diff --git a/src/app/dars/dars-view/dars-view.component.html b/src/app/dars/dars-view/dars-view.component.html index 7aa7173..f3bb66c 100644 --- a/src/app/dars/dars-view/dars-view.component.html +++ b/src/app/dars/dars-view/dars-view.component.html @@ -45,7 +45,7 @@ aria-label="Run new degree audit" color="primary" [disabled]="(metadataStatus$ | async) != 'Loaded'" - (click)="openNewAuditOptionsDialog('degree')"> + (click)="openDegreeAuditDialog()"> Run new degree audit </button> </div> @@ -77,7 +77,7 @@ aria-label="Run new degree audit" [disabled]="(metadataStatus$ | async) != 'Loaded'" color="primary" - (click)="openNewAuditOptionsDialog('whatif')"> + (click)="openWhatIfAuditDialog()"> Run new ‘what if’ audit </button> </div> diff --git a/src/app/dars/dars-view/dars-view.component.ts b/src/app/dars/dars-view/dars-view.component.ts index ffd480a..7240c02 100644 --- a/src/app/dars/dars-view/dars-view.component.ts +++ b/src/app/dars/dars-view/dars-view.component.ts @@ -2,7 +2,6 @@ import { MediaMatcher } from '@angular/cdk/layout'; import { Component, OnInit } from '@angular/core'; import { AuditMetadata } from '../models/audit-metadata'; import { MatDialog } from '@angular/material'; -import { NewAuditOptionsComponent } from '../new-audit-options/new-audit-options.component'; import { DARSState } from '../store/state'; import { Store } from '@ngrx/store'; import { GlobalState } from '@app/core/state'; @@ -12,6 +11,14 @@ import * as darsActions from '../store/actions'; import { Audit } from '../models/audit/audit'; import { DarsApiService } from '../services/api.service'; import { Alert } from '@app/core/models/alert'; +import { + NewDegreeAuditDialogComponent, + NewDegreeAuditFields, +} from '../new-degree-audit-dialog/new-degree-audit-dialog.component'; +import { + NewWhatIfAuditDialogComponent, + NewWhatIfAuditFields, +} from '../new-what-if-audit-dialog/new-what-if-audit-dialog.component'; @Component({ selector: 'cse-dars-view', @@ -51,10 +58,18 @@ export class DARSViewComponent implements OnInit { this.store.dispatch(new darsActions.DismissAlert({ key })); } - public openNewAuditOptionsDialog(selectedAuditType) { - this.dialog.open(NewAuditOptionsComponent, { - data: { selectedAuditType: selectedAuditType }, - }); + public openDegreeAuditDialog() { + this.dialog + .open<any, any, NewDegreeAuditFields>(NewDegreeAuditDialogComponent) + .afterClosed() + .subscribe(event => console.log(event)); + } + + public openWhatIfAuditDialog() { + this.dialog + .open<any, any, NewWhatIfAuditFields>(NewWhatIfAuditDialogComponent) + .afterClosed() + .subscribe(event => console.log(event)); } public openAudit(metadata: AuditMetadata) { diff --git a/src/app/dars/dars.module.ts b/src/app/dars/dars.module.ts index 38963c7..0f7ab8e 100644 --- a/src/app/dars/dars.module.ts +++ b/src/app/dars/dars.module.ts @@ -5,12 +5,13 @@ import { SharedModule } from '@app/shared/shared.module'; import { DARSEffects } from './store/effects'; import { DarsAuditComponent } from './audit/audit.component'; import { DarsMetadataTableComponent } from './metadata-table/metadata-table.component'; -import { NewAuditOptionsComponent } from './new-audit-options/new-audit-options.component'; import { MatStepperModule } from '@angular/material'; import { AlertContainerComponent } from '../shared/components/alert-container/alert-container.component'; import { StoreModule } from '@ngrx/store'; import { darsReducer } from './store/reducer'; import { AuditLinePipe } from './pipes/audit-line.pipe'; +import { NewDegreeAuditDialogComponent } from './new-degree-audit-dialog/new-degree-audit-dialog.component'; +import { NewWhatIfAuditDialogComponent } from './new-what-if-audit-dialog/new-what-if-audit-dialog.component'; @NgModule({ imports: [ @@ -22,12 +23,16 @@ import { AuditLinePipe } from './pipes/audit-line.pipe'; exports: [MatStepperModule], declarations: [ AuditLinePipe, - NewAuditOptionsComponent, + NewDegreeAuditDialogComponent, + NewWhatIfAuditDialogComponent, DARSViewComponent, DarsAuditComponent, DarsMetadataTableComponent, ], - entryComponents: [NewAuditOptionsComponent], + entryComponents: [ + NewDegreeAuditDialogComponent, + NewWhatIfAuditDialogComponent, + ], schemas: [CUSTOM_ELEMENTS_SCHEMA], }) export class DARSModule {} diff --git a/src/app/dars/new-audit-options/new-audit-options.component.html b/src/app/dars/new-audit-options/new-audit-options.component.html deleted file mode 100644 index 9eaf3cf..0000000 --- a/src/app/dars/new-audit-options/new-audit-options.component.html +++ /dev/null @@ -1,133 +0,0 @@ -<mat-toolbar color="primary" class="dialog-toolbar"> - <h2 class="dialog-toolbar-title"> - Request Degree Audit - </h2> - <button - mat-button - mat-dialog-close - class="close-btn" - aria-label="Close audit dialog" - cdkFocusRegionEnd> - <i - class="material-icons" - alt="Close audit dialog" - matTooltip="Close audit dialog" - matTooltipPosition="above" - >clear</i> - </button> - </mat-toolbar> - - <mat-dialog-content id="new-audit-options-stepper" class="mat-typography dialog-with-toolbar"> - - <div fxLayout="column" fxLayoutAlign="none" fxLayoutGap="10px"> - <form [formGroup]="newAuditForm" id="newAuditForm"> - <mat-vertical-stepper linear #stepper formArrayName="formArray"> - <mat-step class="audit-step" formGroupName="0" [stepControl]="formArray?.get([0])"> - <ng-template matStepLabel>Choose audit type</ng-template> - <mat-radio-group [(ngModel)]="selectedAuditType" class="audit-radio-group" formControlName="auditType"> - <mat-radio-button [checked]="selectedAuditType === 'declaredMajorAudit'" class="audit-radio-button" name="declared-audit-type" id="declared-audit-type" value="declaredMajorAudit" aria-label="Select run a degree audit with declared major"> - <p>Run a degree audit with declared major <br /> - <span class="sub-line">See the progress towards your current program of study.</span></p> - </mat-radio-button> - - <mat-radio-button [checked]="selectedAuditType === 'whatIfAudit'" class="audit-radio-button" name="what-if-audit-type" id="what-if-audit-type" value="whatIfAudit" aria-label="Select run a what-if degree audit"> - <p>Run a what-if degree audit <br /> - <span class="sub-line">See the progress towards a new program of study and degree plans.</span></p> - </mat-radio-button> - </mat-radio-group> - <div class="step-buttons-group"> - <button mat-button matStepperNext mat-stroked-button color="primary">Next</button> - </div> - </mat-step> - - <mat-step class="audit-step" formGroupName="1" [stepControl]="formArray?.get([1])"> - <ng-template matStepLabel>Select program of study</ng-template> - <mat-radio-group *ngIf="newAuditForm.value.formArray[0].auditType === 'declaredMajorAudit'" class="audit-radio-group" formControlName="darsDegreeProgram"> - <mat-radio-button *ngFor="let program of studentDegreeProgram; let i = index" [value]="i" [checked]="i === 0" class="audit-radio-button" name="darsDegreeProgramCode" id="{{ program.darsDegreeProgramCode }}" aria-label="{{ program.darsDegreeProgramCode }}"> - {{ program.sisAcademicPlanDescription }} - </mat-radio-button> - </mat-radio-group> - - <!-- If what-if audit was selected in #1 --> - <mat-form-field *ngIf="newAuditForm.value.formArray[0].auditType === 'whatIfAudit'" class="audit-program"> - <mat-select formControlName="darsInstitution" placeholder="School, College or Population" aria-label="Select School, College or Population" [disableOptionCentering]="true"> - <mat-option value="{{ programOfStudy }}">{{ programOfStudy }}</mat-option> - </mat-select> - </mat-form-field> - - <!-- Show after School, College or Population was selected --> - <mat-form-field *ngIf="(newAuditForm.value.formArray[0].auditType === 'whatIfAudit') && (newAuditForm.value.formArray[1].darsInstitution)" class="audit-program"> - <mat-select formControlName="darsInstitutionProgram" placeholder="Academic Plan Program" aria-label="Academic Plan Program" [disableOptionCentering]="true"> - <mat-option class="mat-option-long" *ngFor="let program of DARSprograms" value="{{ program.darsDegreeProgramDescription }}">{{ program.darsDegreeProgramDescription | titlecase }}</mat-option> - </mat-select> - </mat-form-field> - - <div class="step-buttons-group"> - <button mat-button matStepperPrevious color="primary">Back</button> - <button mat-button matStepperNext mat-stroked-button color="primary">Next</button> - </div> - </mat-step> - - <mat-step class="audit-step" formGroupName="2" [stepControl]="formArray?.get([2])"> - <ng-template matStepLabel>Choose audit settings</ng-template> - <mat-form-field class="audit-settings-option"> - <mat-label>Honors Degree Options</mat-label> - <mat-select formControlName="honors" placeholder="Honors Degree Option" aria-label="Honors Degree Options" [disableOptionCentering]="true"> - <mat-option *ngFor="let honor of honorsOptions" value="{{ honor.darsHonorsOptionDescription }}">{{ honor.darsHonorsOptionDescription }}</mat-option> - </mat-select> - </mat-form-field> - <mat-form-field class="audit-settings-option"> - <mat-label>Include Courses From</mat-label> - <mat-select formControlName="includeCoursesFrom" placeholder="Include Courses From" aria-label="Include Courses From" [disableOptionCentering]="true"> - <mat-option *ngFor="let term of termsToInclude" [value]='term' id="{{ term.id }}">{{ term.name }}</mat-option> - </mat-select> - </mat-form-field> - - <p style="margin-top: 12px;"><strong>Run against which degree plan?</strong></p> - <mat-form-field class="audit-settings-option"> - <mat-label>Degree Plan</mat-label> - <mat-select formControlName="runAgainst" placeholder="Degree Plan" aria-label="Degree Plan" [disableOptionCentering]="true"> - <mat-option *ngFor="let degreePlan of degreePlans" value="{{ degreePlan.roadmapId }}">{{ degreePlan.name }}</mat-option> - </mat-select> - </mat-form-field> - <div class="step-buttons-group"> - <button mat-button matStepperPrevious color="primary">Back</button> - <button mat-button matStepperNext mat-stroked-button color="primary" (click)="getCoursesList(newAuditForm.value.formArray[2].runAgainst)">Next</button> - </div> - </mat-step> - - <mat-step class="audit-step" formGroupName="3" [stepControl]="formArray?.get([3])"> - <ng-template matStepLabel>Select credits</ng-template> - <p>To provide more accurate audit, specify the number of credits you planto make in the following course(s):</p> - - <p *ngIf="!hasVariableCredits && isLoaded"><strong>No variable credits found.The audit is ready to be run.</strong></p> - <div class="course-items-loading"> - <mat-progress-spinner *ngIf="!isLoaded" - mode="indeterminate" - diameter="24"> - </mat-progress-spinner> - </div> - - <div *ngFor="let course of courses"> - <div *ngIf="hasVariableCredits" fxLayout="row" fxFlex="100" style="margin-top: 18px;"> - <div fxLayoutAlign="start center" fxFlex="80"> - <span>{{ course.termCode | getTermDescription }}: {{ course.subjectDescription }} {{ course.catalogNumber }} <br /> {{ course.title }}</span> - </div> - <div fxLayoutAlign="end center" fxFlex="20"> - <mat-form-field id="audit-credits-selector"> - <mat-select formControlName="credits" placeholder="" aria-label="Number of credits" [disableOptionCentering]="true"> - <mat-option class="mat-option-center" *ngFor="let credit of course.creditsRange" value="{{ credit }}">{{ credit }}</mat-option> - </mat-select> - </mat-form-field> - </div> - </div> - </div> - <div class="step-buttons-group"> - <button mat-button matStepperPrevious color="primary">Back</button> - <button mat-button mat-raised-button color="primary" (click)="runDARSAudit();">Run Audit</button> - </div> - </mat-step> - </mat-vertical-stepper> - </form> - </div> - </mat-dialog-content> diff --git a/src/app/dars/new-audit-options/new-audit-options.component.scss b/src/app/dars/new-audit-options/new-audit-options.component.scss deleted file mode 100644 index 8b1624d..0000000 --- a/src/app/dars/new-audit-options/new-audit-options.component.scss +++ /dev/null @@ -1,39 +0,0 @@ -.mat-form-field { - display: flex !important; -} - -.audit-radio-button { - margin-bottom: 1em; - .mat-radio-label { - white-space: normal !important; - } - p { - margin-bottom: 0px; - margin-left: 12px; - font-weight: 500; - .sub-line { - color: #6e655f; - font-weight: normal; - } - } -} - -.audit-radio-group { - display: flex; - flex-direction: column; - margin: 15px 0; -} - -.step-buttons-group { - margin-top: 2em; - button { - text-transform: uppercase; - } -} - -.course-items-loading { - mat-progress-spinner { - position: relative; - top: 8px; - } -} diff --git a/src/app/dars/new-audit-options/new-audit-options.component.spec.ts b/src/app/dars/new-audit-options/new-audit-options.component.spec.ts deleted file mode 100644 index c910771..0000000 --- a/src/app/dars/new-audit-options/new-audit-options.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { NewAuditOptionsComponent } from './new-audit-options.component'; - -describe('NewAuditOptionsComponent', () => { - let component: NewAuditOptionsComponent; - let fixture: ComponentFixture<NewAuditOptionsComponent>; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ NewAuditOptionsComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(NewAuditOptionsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/dars/new-audit-options/new-audit-options.component.ts b/src/app/dars/new-audit-options/new-audit-options.component.ts deleted file mode 100644 index 61cf8be..0000000 --- a/src/app/dars/new-audit-options/new-audit-options.component.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { Component, OnInit, Inject } from '@angular/core'; -import { MatDialogRef, MatSnackBar } from '@angular/material'; -import { DarsApiService } from '../services/api.service'; -import { DegreeProgram, DegreePrograms } from '../models/degree-program'; -import { SingleAuditRequest } from '../models/single-audit-request'; -import { StudentDegreeProgram } from '../models/student-degree-program'; -import { Observable } from 'rxjs'; -import { Store, select } from '@ngrx/store'; -import { GlobalState } from '@app/core/state'; -import * as selectors from '../store/selectors'; -import * as darsActions from '../store/actions'; -import { DARSState } from '../store/state'; -import { MAT_DIALOG_DATA } from '@angular/material'; -import { - FormBuilder, - FormGroup, - FormControl, - AbstractControl, -} from '@angular/forms'; -import { HonorsOption } from '../models/honors-option'; -import { DegreePlan } from '@app/core/models/degree-plan'; -import { CourseBase } from '@app/core/models/course'; - -@Component({ - selector: 'cse-new-audit-options', - templateUrl: './new-audit-options.component.html', - styleUrls: ['./new-audit-options.component.scss'], -}) -export class NewAuditOptionsComponent implements OnInit { - public newAuditForm: FormGroup; - public degreeProgram: DegreeProgram; - public degreePrograms: DegreePrograms; - public programOfStudy: string; - public studentDegreeProgram: StudentDegreeProgram[]; - public DARSprograms: DegreeProgram[] = []; - public honorsOptions: HonorsOption[] = []; - public courses: { termCode: string; courses: CourseBase[] }[]; - public degreePlans$: Observable<DARSState['degreePlans']>; - public singleAuditRequest$: Observable<SingleAuditRequest>; - public degreePlans: DegreePlan[]; - public primaryPlan: DegreePlan[]; - public primaryPlanId: string; - public hasVariableCredits: boolean; - public creditsRange: (CourseBase & { creditRange: number[] })[]; - public isLoaded: boolean; - public selectedAuditType: string; - public termsToInclude: { name: string; id: string; value: string }[]; - get formArray(): AbstractControl | null { - return this.newAuditForm.get('formArray'); - } - - constructor( - private fb: FormBuilder, - private dialogRef: MatDialogRef<NewAuditOptionsComponent>, - private snackBar: MatSnackBar, - private api: DarsApiService, - private store: Store<GlobalState>, - @Inject(MAT_DIALOG_DATA) - data: { - selectedAuditType: string; - }, - ) { - this.selectedAuditType = 'declaredMajorAudit'; - this.termsToInclude = [ - { - name: 'Previous, current, future, and planned terms', - id: 'future-whatif', - value: 'future', - }, - { - name: 'Previous, current, future', - id: 'future-degree', - value: 'future', - }, - { name: 'Previous, current', id: 'current', value: 'current' }, - { name: 'Previous', id: 'previous', value: 'previous' }, - ]; - } - - ngOnInit() { - // Get Degree plans - this.degreePlans$ = this.store.pipe(select(selectors.degreePlans)); - this.degreePlans$.subscribe(plans => { - this.degreePlans = plans; - this.primaryPlan = plans.filter(function(plan) { - return plan.primary === true; - }); - this.primaryPlanId = this.primaryPlan[0].roadmapId.toString(); - }); - // Get student degree programs - this.api.getStudentDegreePrograms().subscribe(studentDegreeProgram => { - this.studentDegreeProgram = studentDegreeProgram; - }); - - this.api.getStaticData().subscribe(degreePrograms => { - this.degreePrograms = degreePrograms; - - for (const institution of Object.values(degreePrograms)) { - this.programOfStudy = institution.darsInstitutionCodeDescription; - this.honorsOptions.push(...institution.honorsOptions); - this.DARSprograms.push(...institution.programs); - } - }); - this.newAuditForm = this.fb.group({ - formArray: this.fb.array([ - this.fb.group({ - auditType: new FormControl(''), - }), - this.fb.group({ - darsDegreeProgram: new FormControl(''), - darsInstitution: new FormControl(''), - darsInstitutionProgram: new FormControl(''), - }), - this.fb.group({ - honors: new FormControl('Keep current status'), - includeCoursesFrom: new FormControl( - this.selectedAuditType === 'declaredMajorAudit' - ? this.termsToInclude[1] - : this.termsToInclude[0], - ), - runAgainst: new FormControl(this.primaryPlanId), - }), - this.fb.group({ - credits: new FormControl(''), - }), - ]), - }); - } - - public getCoursesList(roadmapId) { - this.isLoaded = false; - - this.api.getAllCourses(roadmapId).subscribe(courses => { - this.courses = courses; - this.creditsRange = []; - this.isLoaded = true; - this.hasVariableCredits = false; - - courses.forEach(term => { - term.courses.forEach(course => { - const { creditMin: min, creditMax: max } = course; - if (min !== undefined && max !== undefined && min < max) { - this.hasVariableCredits = true; - const creditRange: number[] = []; - for (let i = min; i <= max; i++) { - creditRange.push(i); - } - this.creditsRange.push({ ...course, creditRange }); - } - }); - }); - }); - } - - public runDARSAudit() { - const { - auditType, - programOfStudy, - academicProgram, - honors, - includeCoursesFrom, - runAgainst, - credits, - } = this.newAuditForm.value; - - this.store.dispatch( - new darsActions.StartSendingAudit({ - darsInstitutionCode: programOfStudy, - darsDegreeProgramCode: academicProgram, - }), - ); - - this.dialogRef.close(); - this.snackBar.open( - 'Audit in progress. You will be notified when the audit is ready to be viewed.', - ); - } -} diff --git a/src/app/dars/new-degree-audit-dialog/new-degree-audit-dialog.component.html b/src/app/dars/new-degree-audit-dialog/new-degree-audit-dialog.component.html new file mode 100644 index 0000000..cf2d695 --- /dev/null +++ b/src/app/dars/new-degree-audit-dialog/new-degree-audit-dialog.component.html @@ -0,0 +1,120 @@ +<mat-toolbar class="dialog-toolbar" color="primary"> + <h2 class="dialog-toolbar-title">Run degree audit</h2> + <button + class="close-btn" + mat-button + mat-dialog-close + cdkFocusRegionEnd + aria-label="Close audit dialog" + matTooltip="Close audit dialog" + matTooltipPosition="above"> + <i class="material-icons">clear</i> + </button> +</mat-toolbar> + +<mat-dialog-content dialog-with-toolbar> + <mat-vertical-stepper linear> + <ng-template matStepperIcon="edit" let-index="index"> + {{ index + 1 }} + </ng-template> + + <mat-step [stepControl]="chosenProgram" label="Select program of study"> + <!-- + User picks one of the majors or certificates that they're already enrolled in + --> + <ng-container *ngIf="degreePrograms$ | async as degreePrograms; else loading"> + <mat-radio-group [formControl]="chosenProgram"> + <mat-radio-button *ngFor="let dp of degreePrograms" [value]="dp"> + {{ dp.sisAcademicPlanDescription }} + </mat-radio-button> + </mat-radio-group> + </ng-container> + + <div class="step-actions"> + <button mat-stroked-button matStepperNext [disabled]="!chosenProgram.valid">Next</button> + </div> + </mat-step> + + <mat-step [stepControl]="chosenAuditSettings" label="Choose audit settings"> + <form [formGroup]="chosenAuditSettings"> + <ng-container *ngIf="honorsOptions$ | async as honorsOptions; else loading"> + <!-- + User picks one of the honors options provided by the institution + associated with the program they selected in step 1 + --> + <mat-form-field> + <mat-label>Honors Degree Options</mat-label> + <mat-select formControlName="honorsOptions"> + <mat-option + *ngFor="let op of honorsOptions" + value="{{ op.darsHonorsOptionCode }}"> + {{ op.darsHonorsOptionDescription }} + </mat-option> + </mat-select> + </mat-form-field> + + <!-- + User picks whether to include courses from the past, current, or planned future + --> + <mat-form-field> + <mat-label>Include Courses From</mat-label> + <mat-select formControlName="includeCoursesFrom"> + <mat-option value="future">Previous, current, and future terms</mat-option> + <mat-option value="current">Previous and current terms</mat-option> + <mat-option value="previous">Previous terms</mat-option> + </mat-select> + </mat-form-field> + </ng-container> + + <div class="step-actions"> + <button mat-button matStepperPrevious>Back</button> + <button mat-stroked-button matStepperNext [disabled]="!chosenAuditSettings.valid">Next</button> + </div> + </form> + </mat-step> + + <mat-step [stepControl]="chosenCreditSettings" label="Select credits"> + <p>To provide a more accurate audit, specify the number of credits you plant to take in the following courses:</p> + + <form [formGroup]="chosenCreditSettings"> + <ng-container *ngIf="variableCreditCourses$ | async as variableCreditCourses; else loading"> + <ng-container *ngIf="variableCreditCourses.length > 0; else noVariableCreditCourses"> + <div class="credit-selector" *ngFor="let course of variableCreditCourses; let i = index" [formGroupName]="i"> + <label> + {{ course.termCode | getTermDescription }}: {{ course | courseDescription }} + <br> + <small>{{ course.title }}</small> + </label> + <mat-select formControlName="credits"> + <mat-option + *ngFor="let credit of course.range" + [value]="credit"> + {{ credit }} + </mat-option> + </mat-select> + </div> + </ng-container> + + <ng-template #noVariableCreditCourses> + <p><strong>No variable credits found. The audit is ready to be run.</strong></p> + </ng-template> + </ng-container> + + <div class="step-actions"> + <button mat-button matStepperPrevious>Back</button> + <button + mat-raised-button + color="primary" + [disabled]="!chosenCreditSettings.valid" + (click)="submitAudit()"> + Request audit + </button> + </div> + </form> + </mat-step> + </mat-vertical-stepper> +</mat-dialog-content> + +<ng-template #loading> + <mat-spinner diameter="20"></mat-spinner> +</ng-template> diff --git a/src/app/dars/new-degree-audit-dialog/new-degree-audit-dialog.component.scss b/src/app/dars/new-degree-audit-dialog/new-degree-audit-dialog.component.scss new file mode 100644 index 0000000..22af959 --- /dev/null +++ b/src/app/dars/new-degree-audit-dialog/new-degree-audit-dialog.component.scss @@ -0,0 +1,46 @@ +.mat-dialog-content { + max-width: 512px; +} + +.mat-radio-group { + display: flex; + flex-direction: column; +} + +.mat-radio-button { + margin: 5px; +} + +.mat-form-field { + display: block; + + & + & { + margin-top: 15px; + } +} + +.step-actions { + margin-top: 15px; + display: flex; + justify-content: flex-end; +} + +.credit-selector { + display: flex; + flex-direction: row; + + & + & { + margin-top: 15px; + } + + label { + display: block; + flex-grow: 1; + } + + mat-select { + width: 48px; + display: block; + flex-grow: 0; + } +} diff --git a/src/app/dars/new-degree-audit-dialog/new-degree-audit-dialog.component.ts b/src/app/dars/new-degree-audit-dialog/new-degree-audit-dialog.component.ts new file mode 100644 index 0000000..e835031 --- /dev/null +++ b/src/app/dars/new-degree-audit-dialog/new-degree-audit-dialog.component.ts @@ -0,0 +1,193 @@ +import { Component, OnInit } from '@angular/core'; +import { DarsApiService } from '../services/api.service'; +import { DegreePrograms } from '../models/degree-program'; +import { StudentDegreeProgram } from '../models/student-degree-program'; +import { Observable, combineLatest } from 'rxjs'; +import { + FormBuilder, + FormGroup, + Validators, + FormControl, + FormArray, +} from '@angular/forms'; +import { HonorsOption } from '../models/honors-option'; +import { map, share, filter, flatMap } from 'rxjs/operators'; +import { CourseBase } from '@app/core/models/course'; +import { Store } from '@ngrx/store'; +import { GlobalState } from '@app/core/state'; +import { degreePlans } from '../store/selectors'; +import { DegreePlan } from '@app/core/models/degree-plan'; +import { MatDialogRef } from '@angular/material'; + +const inclusiveRange = (from: number, to: number) => { + const range: number[] = []; + for (let i = from; i <= to; i++) { + range.push(i); + } + return range; +}; + +export interface NewDegreeAuditFields { + darsInstitutionCode: string; + darsDegreeProgramCode: string; + darsHonorsOptionCode: string; + includeCoursesFrom: string; + fixedCredits: { + termCode: string; + subjectCode: string; + courseId: string; + credits: number; + }[]; +} + +@Component({ + selector: 'cse-new-degree-audit-dialog', + templateUrl: './new-degree-audit-dialog.component.html', + styleUrls: ['./new-degree-audit-dialog.component.scss'], +}) +export class NewDegreeAuditDialogComponent implements OnInit { + // Form-builder objects + public chosenProgram: FormControl; + public chosenAuditSettings: FormGroup; + public chosenCreditSettings: FormArray; + + // API observables + public degreePrograms$: Observable<StudentDegreeProgram[]>; + public institutions$: Observable<DegreePrograms>; + public honorsOptions$: Observable<HonorsOption[]>; + public variableCreditCourses$: Observable< + (CourseBase & { range: number[] })[] + >; + + constructor( + private fb: FormBuilder, + private api: DarsApiService, + private store: Store<GlobalState>, + private dialogRef: MatDialogRef<NewDegreeAuditDialogComponent>, + ) { + this.chosenProgram = fb.control('', Validators.required); + + this.chosenAuditSettings = fb.group({ + honorsOptions: fb.control('', Validators.required), + includeCoursesFrom: fb.control('future', Validators.required), + }); + + this.chosenCreditSettings = fb.array([]); + } + + public ngOnInit() { + this.degreePrograms$ = this.api.getStudentDegreePrograms().pipe(share()); + this.institutions$ = this.api.getStaticData().pipe(share()); + + this.honorsOptions$ = combineLatest([ + this.institutions$, + this.chosenProgram.valueChanges.pipe( + filter(value => value.hasOwnProperty('darsInstitutionCode')), + map(value => value.darsInstitutionCode as string), + ), + ]).pipe( + map(([institutions, darsInstitutionCode]) => { + return institutions.hasOwnProperty(darsInstitutionCode) + ? institutions[darsInstitutionCode].honorsOptions + : []; + }), + ); + + this.variableCreditCourses$ = this.store.select(degreePlans).pipe( + map(plans => plans.find(plan => plan.primary)), + filter((plan): plan is DegreePlan => plan !== undefined), + flatMap(plan => this.api.getAllCourses(plan.roadmapId)), + map(courses => { + return courses.filter(course => { + return ( + !!course.creditMin && + !!course.creditMax && + course.creditMax > course.creditMin + ); + }); + }), + map(courses => + courses.map(course => ({ + ...course, + range: inclusiveRange( + course.creditMin as number, + course.creditMax as number, + ), + })), + ), + share(), + ); + + this.variableCreditCourses$.subscribe(courses => { + while (this.chosenCreditSettings.length !== 0) { + this.chosenCreditSettings.removeAt(0); + } + + courses.forEach(course => { + this.chosenCreditSettings.push( + this.fb.group({ + course, + credits: this.fb.control('', Validators.required), + }), + ); + }); + }); + } + + public darsInstitutionCode<T>(fallback: T): string | T { + if (typeof this.chosenProgram.value === 'object') { + return this.chosenProgram.value.darsInstitutionCode; + } else { + return fallback; + } + } + + public darsDegreeProgramCode<T>(fallback: T): string | T { + if (typeof this.chosenProgram.value === 'object') { + return this.chosenProgram.value.darsDegreeProgramCode; + } else { + return fallback; + } + } + + public darsHonorsOptionCode<T>(fallback: T): string | T { + const control = this.chosenAuditSettings.get('honorsOptions'); + if (control !== null) { + return control.value.toString(); + } else { + return fallback; + } + } + + public includeCoursesFrom<T>(fallback: T): string | T { + const control = this.chosenAuditSettings.get('includeCoursesFrom'); + if (control !== null) { + return control.value.toString(); + } else { + return fallback; + } + } + + public fixedCredits(): NewDegreeAuditFields['fixedCredits'] { + return (this.chosenCreditSettings.value as any[]).map( + (group: { course: CourseBase; credits: number }) => { + return { + termCode: group.course.termCode || '', + subjectCode: group.course.subjectCode || '', + courseId: group.course.courseId || '', + credits: group.credits, + }; + }, + ); + } + + public submitAudit() { + this.dialogRef.close({ + darsInstitutionCode: this.darsInstitutionCode(''), + darsDegreeProgramCode: this.darsDegreeProgramCode(''), + darsHonorsOptionCode: this.darsHonorsOptionCode(''), + includeCoursesFrom: this.includeCoursesFrom(''), + fixedCredits: this.fixedCredits(), + }); + } +} diff --git a/src/app/dars/new-what-if-audit-dialog/new-what-if-audit-dialog.component.html b/src/app/dars/new-what-if-audit-dialog/new-what-if-audit-dialog.component.html new file mode 100644 index 0000000..0cedef4 --- /dev/null +++ b/src/app/dars/new-what-if-audit-dialog/new-what-if-audit-dialog.component.html @@ -0,0 +1,154 @@ +<mat-toolbar class="dialog-toolbar" color="primary"> + <h2 class="dialog-toolbar-title">Run ‘what-if’ audit</h2> + <button + class="close-btn" + mat-button + mat-dialog-close + cdkFocusRegionEnd + aria-label="Close audit dialog" + matTooltip="Close audit dialog" + matTooltipPosition="above"> + <i class="material-icons">clear</i> + </button> +</mat-toolbar> + +<mat-dialog-content dialog-with-toolbar> + <mat-vertical-stepper linear> + <ng-template matStepperIcon="edit" let-index="index"> + {{ index + 1 }} + </ng-template> + + <mat-step [stepControl]="chosenProgram" label="Select program of study"> + <form [formGroup]="chosenProgram"> + <ng-container *ngIf="institutions$ | async as institutions; else loading"> + <!-- + User picks one of the institutions to narrow the number of choices in the next step + --> + <mat-form-field> + <mat-label>School, College, or Population</mat-label> + <mat-select formControlName="institution"> + <mat-option + *ngFor="let pair of institutions | keyvalue" + [value]="pair.key"> + {{ pair.value.darsInstitutionCodeDescription }} + </mat-option> + </mat-select> + </mat-form-field> + + <!-- + User picks one of the insitution's programs + --> + <mat-form-field> + <mat-label>Academic Plan / Program</mat-label> + <mat-select formControlName="planOrProgram"> + <mat-option + *ngFor="let p of (programOrPlanOptions$ | async)" + value="{{ p.darsDegreeProgramCode }}"> + {{ p.darsDegreeProgramDescription }} + </mat-option> + </mat-select> + </mat-form-field> + </ng-container> + + <ng-container *ngIf="degreePlans$ | async as degreePlans; else loading"> + <mat-form-field> + <mat-label>Degree Plan</mat-label> + <mat-select formControlName="degreePlan"> + <mat-option + *ngFor="let plan of degreePlans" + [value]="plan"> + {{ plan.name }} + </mat-option> + </mat-select> + </mat-form-field> + </ng-container> + </form> + + <div class="step-actions"> + <button mat-stroked-button matStepperNext [disabled]="!chosenProgram.valid">Next</button> + </div> + </mat-step> + + <mat-step [stepControl]="chosenAuditSettings" label="Choose audit settings"> + <form [formGroup]="chosenAuditSettings"> + <ng-container *ngIf="honorsOptions$ | async as honorsOptions; else loading"> + <!-- + User picks one of the honors options provided by the institution + associated with the program they selected in step 1 + --> + <mat-form-field> + <mat-label>Honors Degree Options</mat-label> + <mat-select formControlName="honorsOptions"> + <mat-option + *ngFor="let op of honorsOptions" + value="{{ op.darsHonorsOptionCode }}"> + {{ op.darsHonorsOptionDescription }} + </mat-option> + </mat-select> + </mat-form-field> + + <!-- + User picks whether to include courses from the past, current, or planned future + --> + <mat-form-field> + <mat-label>Include Courses From</mat-label> + <mat-select formControlName="includeCoursesFrom"> + <mat-option value="future">Previous, current, and future terms</mat-option> + <mat-option value="current">Previous and current terms</mat-option> + <mat-option value="previous">Previous terms</mat-option> + </mat-select> + </mat-form-field> + </ng-container> + + <div class="step-actions"> + <button mat-button matStepperPrevious>Back</button> + <button mat-stroked-button matStepperNext [disabled]="!chosenAuditSettings.valid">Next</button> + </div> + </form> + </mat-step> + + <mat-step [stepControl]="chosenCreditSettings" label="Select credits"> + <p>To provide a more accurate audit, specify the number of credits you plant to take in the following courses:</p> + + <form [formGroup]="chosenCreditSettings"> + <ng-container *ngIf="variableCreditCourses$ | async as variableCreditCourses; else loading"> + <ng-container *ngIf="variableCreditCourses.length > 0; else noVariableCreditCourses"> + <div class="credit-selector" *ngFor="let course of variableCreditCourses; let i = index" [formGroupName]="i"> + <label> + {{ course.termCode | getTermDescription }}: {{ course | courseDescription }} + <br> + <small>{{ course.title }}</small> + </label> + <mat-select formControlName="credits"> + <mat-option + *ngFor="let credit of course.range" + [value]="credit"> + {{ credit }} + </mat-option> + </mat-select> + </div> + </ng-container> + + <ng-template #noVariableCreditCourses> + <p><strong>No variable credits found. The audit is ready to be run.</strong></p> + </ng-template> + </ng-container> + + <div class="step-actions"> + <button mat-button matStepperPrevious>Back</button> + <button + mat-raised-button + color="primary" + [disabled]="!chosenCreditSettings.valid" + (click)="submitAudit()"> + Request audit + </button> + </div> + </form> + </mat-step> + </mat-vertical-stepper> +</mat-dialog-content> + +<ng-template #loading> + <mat-spinner diameter="20"></mat-spinner> +</ng-template> diff --git a/src/app/dars/new-what-if-audit-dialog/new-what-if-audit-dialog.component.scss b/src/app/dars/new-what-if-audit-dialog/new-what-if-audit-dialog.component.scss new file mode 100644 index 0000000..22af959 --- /dev/null +++ b/src/app/dars/new-what-if-audit-dialog/new-what-if-audit-dialog.component.scss @@ -0,0 +1,46 @@ +.mat-dialog-content { + max-width: 512px; +} + +.mat-radio-group { + display: flex; + flex-direction: column; +} + +.mat-radio-button { + margin: 5px; +} + +.mat-form-field { + display: block; + + & + & { + margin-top: 15px; + } +} + +.step-actions { + margin-top: 15px; + display: flex; + justify-content: flex-end; +} + +.credit-selector { + display: flex; + flex-direction: row; + + & + & { + margin-top: 15px; + } + + label { + display: block; + flex-grow: 1; + } + + mat-select { + width: 48px; + display: block; + flex-grow: 0; + } +} diff --git a/src/app/dars/new-what-if-audit-dialog/new-what-if-audit-dialog.component.ts b/src/app/dars/new-what-if-audit-dialog/new-what-if-audit-dialog.component.ts new file mode 100644 index 0000000..823300c --- /dev/null +++ b/src/app/dars/new-what-if-audit-dialog/new-what-if-audit-dialog.component.ts @@ -0,0 +1,228 @@ +import { Component, OnInit } from '@angular/core'; +import { DarsApiService } from '../services/api.service'; +import { DegreePrograms, DegreeProgram } from '../models/degree-program'; +import { Observable, combineLatest } from 'rxjs'; +import { + FormBuilder, + FormGroup, + Validators, + FormArray, + FormControl, +} from '@angular/forms'; +import { HonorsOption } from '../models/honors-option'; +import { map, share, filter, flatMap, withLatestFrom } from 'rxjs/operators'; +import { CourseBase } from '@app/core/models/course'; +import { Store } from '@ngrx/store'; +import { GlobalState } from '@app/core/state'; +import { degreePlans } from '../store/selectors'; +import { DegreePlan } from '@app/core/models/degree-plan'; +import { MatDialogRef } from '@angular/material'; + +const inclusiveRange = (from: number, to: number) => { + const range: number[] = []; + for (let i = from; i <= to; i++) { + range.push(i); + } + return range; +}; + +export interface NewWhatIfAuditFields { + darsInstitutionCode: string; + darsDegreeProgramCode: string; + roadmapId: number; + darsHonorsOptionCode: string; + includeCoursesFrom: string; + fixedCredits: { + termCode: string; + subjectCode: string; + courseId: string; + credits: number; + }[]; +} + +@Component({ + selector: 'cse-new-what-if-audit-dialog', + templateUrl: './new-what-if-audit-dialog.component.html', + styleUrls: ['./new-what-if-audit-dialog.component.scss'], +}) +export class NewWhatIfAuditDialogComponent implements OnInit { + // Form-builder objects + public chosenProgram: FormGroup; + public chosenAuditSettings: FormGroup; + public chosenCreditSettings: FormArray; + + // API observables + public institutions$: Observable<DegreePrograms>; + public programOrPlanOptions$: Observable<DegreeProgram[]>; + public degreePlans$: Observable<DegreePlan[]>; + public honorsOptions$: Observable<HonorsOption[]>; + public variableCreditCourses$: Observable< + (CourseBase & { range: number[] })[] + >; + + constructor( + private fb: FormBuilder, + private api: DarsApiService, + private store: Store<GlobalState>, + private dialogRef: MatDialogRef< + NewWhatIfAuditDialogComponent, + NewWhatIfAuditFields + >, + ) { + this.chosenProgram = fb.group({ + institution: fb.control('', Validators.required), + planOrProgram: fb.control( + { value: '', disabled: true }, + Validators.required, + ), + degreePlan: fb.control('', Validators.required), + }); + + this.chosenAuditSettings = fb.group({ + honorsOptions: fb.control('', Validators.required), + includeCoursesFrom: fb.control('future', Validators.required), + }); + + this.chosenCreditSettings = fb.array([]); + } + + public ngOnInit() { + this.institutions$ = this.api.getStaticData().pipe(share()); + + this.programOrPlanOptions$ = combineLatest([ + this.institutions$, + (this.chosenProgram.get('institution') as FormControl).valueChanges, + ]).pipe( + map(([institutions, darsInstitutionCode]) => { + this.chosenProgram.controls.planOrProgram.enable(); + if (institutions.hasOwnProperty(darsInstitutionCode)) { + return institutions[darsInstitutionCode].programs; + } else { + return []; + } + }), + ); + + this.degreePlans$ = this.store.select(degreePlans); + + this.honorsOptions$ = combineLatest([ + this.institutions$, + (this.chosenProgram.get('institution') as FormControl).valueChanges, + ]).pipe( + map(([institutions, darsInstitutionCode]) => { + return institutions.hasOwnProperty(darsInstitutionCode) + ? institutions[darsInstitutionCode].honorsOptions + : []; + }), + ); + + this.variableCreditCourses$ = (this.chosenProgram.get( + 'degreePlan', + ) as FormControl).valueChanges.pipe( + flatMap(plan => this.api.getAllCourses(plan.roadmapId)), + map(courses => { + return courses.filter(course => { + return ( + !!course.creditMin && + !!course.creditMax && + course.creditMax > course.creditMin + ); + }); + }), + map(courses => + courses.map(course => ({ + ...course, + range: inclusiveRange( + course.creditMin as number, + course.creditMax as number, + ), + })), + ), + share(), + ); + + this.variableCreditCourses$.subscribe(courses => { + while (this.chosenCreditSettings.length !== 0) { + this.chosenCreditSettings.removeAt(0); + } + + courses.forEach(course => { + this.chosenCreditSettings.push( + this.fb.group({ + course, + credits: this.fb.control('', Validators.required), + }), + ); + }); + }); + } + + public darsInstitutionCode<T>(fallback: T): string | T { + const control = this.chosenProgram.get('institution'); + if (control !== null) { + return control.value.toString(); + } else { + return fallback; + } + } + + public darsDegreeProgramCode<T>(fallback: T): string | T { + const control = this.chosenProgram.get('planOrProgram'); + if (control !== null) { + return control.value.toString(); + } else { + return fallback; + } + } + + public roadmapId<T>(fallback: T): number | T { + const control = this.chosenProgram.get('degreePlan'); + if (control !== null) { + return control.value.roadmapId; + } else { + return fallback; + } + } + + public darsHonorsOptionCode<T>(fallback: T): string | T { + const control = this.chosenAuditSettings.get('honorsOptions'); + if (control !== null) { + return control.value.toString(); + } else { + return fallback; + } + } + + public includeCoursesFrom<T>(fallback: T): string | T { + const control = this.chosenAuditSettings.get('includeCoursesFrom'); + if (control !== null) { + return control.value.toString(); + } else { + return fallback; + } + } + + public fixedCredits(): NewWhatIfAuditFields['fixedCredits'] { + return (this.chosenCreditSettings.value as any[]).map( + (group: { course: CourseBase; credits: number }) => { + return { + termCode: group.course.termCode || '', + subjectCode: group.course.subjectCode || '', + courseId: group.course.courseId || '', + credits: group.credits, + }; + }, + ); + } + + public submitAudit() { + this.dialogRef.close({ + darsInstitutionCode: this.darsInstitutionCode(''), + darsDegreeProgramCode: this.darsDegreeProgramCode(''), + roadmapId: this.roadmapId(-1), + darsHonorsOptionCode: this.darsHonorsOptionCode(''), + includeCoursesFrom: this.includeCoursesFrom(''), + fixedCredits: this.fixedCredits(), + }); + } +} diff --git a/src/app/dars/services/api.service.ts b/src/app/dars/services/api.service.ts index ca614b6..ae64371 100644 --- a/src/app/dars/services/api.service.ts +++ b/src/app/dars/services/api.service.ts @@ -7,11 +7,8 @@ import { StudentDegreeProgram } from '../models/student-degree-program'; import { environment } from './../../../environments/environment'; import { Audit } from '../models/audit/audit'; import { CourseBase } from '@app/core/models/course'; -import { delay } from 'rxjs/operators'; const auditResponse: Audit = require('../../../assets/mock-data/audit-response.json'); const degreeProgramsResponse: DegreePrograms = require('../../../assets/mock-data/degreeprograms-response.json'); -const auditMetadata: AuditMetadata[] = require('../../../assets/mock-data/auditmetadata.json'); -const studentplans: StudentDegreeProgram[] = require('../../../assets/mock-data/studentplans.json'); const HTTP_OPTIONS = { headers: new HttpHeaders({ @@ -42,10 +39,8 @@ export class DarsApiService { * * Get courses */ - public getAllCourses( - roadmapId: number, - ): Observable<{ termCode: string; courses: CourseBase[] }[]> { - return this.http.get<{ termCode: string; courses: CourseBase[] }[]>( + public getAllCourses(roadmapId: number): Observable<CourseBase[]> { + return this.http.get<CourseBase[]>( `${environment.apiPlannerUrl}/degreePlan/${roadmapId}/courses`, HTTP_OPTIONS, ); -- GitLab