diff --git a/src/app/home/home.component.spec.ts b/src/app/home/home.component.spec.ts index fe5fa477..df839458 100644 --- a/src/app/home/home.component.spec.ts +++ b/src/app/home/home.component.spec.ts @@ -37,6 +37,6 @@ describe('HomeComponent', () => { fakeKeyCloakService.isAllowed(); expect(component).toBeTruthy(); let cardElement = fixture.debugElement.nativeElement.getElementsByTagName('app-nav-card'); - expect(cardElement.length).toEqual(2); + expect(cardElement.length).toEqual(3); }); }); diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index 94e99e82..e28ec0c7 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -14,28 +14,26 @@ export class HomeComponent { cardHeader: 'Parks Management', cardTitle: 'Parks Management', cardText: 'Manage parks, facilities and passes.', - navigation: 'parks', + navigation: '/parks', }, ]; constructor( protected keyCloakService: KeycloakService, protected configService: ConfigService ) { - if (this.configService.config.QR_CODE_ENABLED) { - this.cardConfig.push({ - cardHeader: 'Pass Management', - cardTitle: 'Pass Management', - cardText: 'Check-in park guests via QR Code.', - navigation: 'pass-management', - }); - } + this.cardConfig.push({ + cardHeader: 'Pass Management', + cardTitle: 'Pass Management', + cardText: 'Check-in park guests via QR Code.', + navigation: '/pass-management', + }); if (keyCloakService.isAllowed('metrics')) { this.cardConfig.push({ cardHeader: 'Site Metrics', cardTitle: 'Site Metrics', cardText: 'See pass counts for various states.', - navigation: 'metrics', + navigation: '/metrics', }); } } diff --git a/src/app/parks-management/facility-details/facility-details.component.html b/src/app/parks-management/facility-details/facility-details.component.html index 0c7f7447..08f95255 100644 --- a/src/app/parks-management/facility-details/facility-details.component.html +++ b/src/app/parks-management/facility-details/facility-details.component.html @@ -115,49 +115,4 @@ - - - -
- -
-
-
- -
-
-
-
- -
-
-
-
-

Search Results

-
-
- -
-
-
-
- -
-
- + \ No newline at end of file diff --git a/src/app/parks-management/facility-details/passes-filter/passes-filter.component.spec.ts b/src/app/parks-management/facility-details/passes-filter/passes-filter.component.spec.ts deleted file mode 100644 index b318f068..00000000 --- a/src/app/parks-management/facility-details/passes-filter/passes-filter.component.spec.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { HttpClient, HttpHandler } from '@angular/common/http'; -import { Location } from '@angular/common'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { RouterTestingModule } from '@angular/router/testing'; -import { BehaviorSubject } from 'rxjs'; -import { ConfigService } from 'src/app/services/config.service'; -import { DataService } from 'src/app/services/data.service'; -import { MockData } from 'src/app/shared/utils/mock-data'; - -import { PassesFilterComponent } from './passes-filter.component'; -import { FacilityService } from 'src/app/services/facility.service'; - -describe('PassesFilterComponent', () => { - let component: PassesFilterComponent; - let fixture: ComponentFixture; - let location: Location; - - let mockFacility = MockData.mockFacility_1; - let mockFacilityKey = { pk: mockFacility.pk, sk: mockFacility.sk} - - let mockPartialPassFilter = MockData.mockPartialPassFilters_1; - - let mockSubject = new BehaviorSubject(mockFacility); - - let mockFacilityService = { - getCachedFacility: (key) => { - if (key.pk === mockFacilityKey.pk && key.sk === mockFacilityKey.sk) { - return mockFacility; - } - return null; - }, - }; - - let mockDataService = { - watchItem: () => { - return mockSubject; - }, - }; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [PassesFilterComponent], - imports: [ - FormsModule, - ReactiveFormsModule, - RouterTestingModule.withRoutes([ - { path: 'passType', component: PassesFilterComponent }, - ]), - ], - providers: [ - HttpClient, - HttpHandler, - ConfigService, - { provide: DataService, useValue: mockDataService }, - { provide: FacilityService, useValue: mockFacilityService }, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(PassesFilterComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - location = TestBed.inject(Location); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - expect(component['facility']).toBeDefined(); - expect(component.bookingTimesList).toEqual([ - { value: 'AM', display: 'AM' }, - { value: 'PM', display: 'PM' }, - ]); - }); - - it('submits filter selections', async () => { - const passServiceSpy = spyOn(component['passService'], 'fetchData'); - component.data = mockPartialPassFilter; - component.setForm(); - await fixture.isStable(); - await component.onSubmit(); - expect(location.path()).toBe('/?passStatus=reserved&passType=Trail'); - expect(passServiceSpy).toHaveBeenCalledOnceWith({ - passType: 'Trail', - park: 'MOC1', - facilityName: 'Mock Facility 1', - passStatus: 'reserved', - }); - }); -}); diff --git a/src/app/parks-management/facility-details/passes-filter/passes-filter.component.ts b/src/app/parks-management/facility-details/passes-filter/passes-filter.component.ts deleted file mode 100644 index ac78a04f..00000000 --- a/src/app/parks-management/facility-details/passes-filter/passes-filter.component.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { ChangeDetectorRef, Component } from '@angular/core'; -import { - UntypedFormBuilder, - UntypedFormControl, - UntypedFormGroup, -} from '@angular/forms'; -import { ActivatedRoute, Router } from '@angular/router'; -import { DataService } from 'src/app/services/data.service'; -import { FacilityService } from 'src/app/services/facility.service'; -import { LoadingService } from 'src/app/services/loading.service'; -import { PassService } from 'src/app/services/pass.service'; -import { ReservationService } from 'src/app/services/reservation.service'; -import { BaseFormComponent } from 'src/app/shared/components/ds-forms/base-form/base-form.component'; -import { Constants } from 'src/app/shared/utils/constants'; - -@Component({ - selector: 'app-passes-filter', - templateUrl: './passes-filter.component.html', - styleUrls: ['./passes-filter.component.scss'], -}) -export class PassesFilterComponent extends BaseFormComponent { - private facility; - - public bookingTimesList; - public statusesList = ['reserved', 'active', 'expired', 'cancelled']; - public overbookedList = [ - { value: 'all', display: 'Show all passes' }, - { value: 'show', display: 'Show overbooked only' }, - { value: 'hide', display: 'Hide overbooked' }, - ]; - - constructor( - protected formBuilder: UntypedFormBuilder, - protected router: Router, - protected dataService: DataService, - protected loadingService: LoadingService, - protected changeDetector: ChangeDetectorRef, - protected passService: PassService, - protected reservationService: ReservationService, - private route: ActivatedRoute, - private facilityService: FacilityService - ) { - super( - formBuilder, - router, - dataService, - loadingService, - changeDetector - ); - - this.subscriptions.add( - this.dataService - .watchItem(Constants.dataIds.CURRENT_FACILITY_KEY) - .subscribe((res) => { - if (res) { - this.facility = this.facilityService.getCachedFacility(res); - this.bookingTimesList = this.getBookingTimesList(); - } - }) - ); - - // push existing form data to parent subscriptions - this.subscriptions.add( - this.dataService - .watchItem(Constants.dataIds.PASS_SEARCH_PARAMS) - .subscribe((res) => { - if (res) { - this.data = res; - this.setForm(); - } - }) - ); - this.setForm(); - } - - getBookingTimesList() { - if (this.facility?.bookingTimes) { - let list: any[] = []; - for (const key of Object.keys(this.facility.bookingTimes)) { - list.push({ value: key, display: key }); - } - return list; - } - return []; - } - - setForm() { - this.form = new UntypedFormGroup({ - passType: new UntypedFormControl(this.data.passType), - date: new UntypedFormControl(this.data.date), - passStatus: new UntypedFormControl(this.data.passStatus), - firstName: new UntypedFormControl(this.data.firstName), - lastName: new UntypedFormControl(this.data.lastName), - email: new UntypedFormControl(this.data.email), - reservationNumber: new UntypedFormControl(this.data.reservationNumber), - overbooked: new UntypedFormControl(this.data.overbooked), - }); - super.setFields(); - } - - async onSubmit() { - // Save current search params - const res = await super.submit(); - const resFields = await this.passService.filterSearchParams(res.fields); - - this.updateUrl(resFields); - - let params = Object.assign(resFields, { - park: this.facility.pk.split('::')[1], - facilityName: this.facility.sk, - passType: resFields?.passType || null, - }); - this.passService.fetchData(params); - - this.reservationService.fetchData( - this.facility.pk.split('::')[1], - this.facility.sk, - resFields?.date || null, - resFields?.passType || null - ); - } - - updateUrl(resFields) { - const queryParams = { ...resFields }; - delete queryParams.park; - delete queryParams.facilityName; - if (queryParams.passStatus) { - queryParams.passStatus = queryParams.passStatus.toString(); - } - this.router.navigate(['.'], { - relativeTo: this.route, - queryParams: queryParams, - }); - } -} diff --git a/src/app/parks-management/parks-management.module.ts b/src/app/parks-management/parks-management.module.ts index f147411c..5e9f15c0 100644 --- a/src/app/parks-management/parks-management.module.ts +++ b/src/app/parks-management/parks-management.module.ts @@ -9,9 +9,6 @@ import { ParkDetailsComponent } from './park-details/park-details.component'; import { ParksManagementComponent } from './parks-management.component'; import { FacilityEditComponent } from './facility-edit/facility-edit.component'; import { FacilityDetailsComponent } from './facility-details/facility-details.component'; -import { PassesFilterFieldsComponent } from './facility-details/passes-filter/passes-filter-fields/passes-filter-fields.component'; -import { PassesFilterComponent } from './facility-details/passes-filter/passes-filter.component'; -import { PassesListComponent } from './facility-details/passes-list/passes-list.component'; import { BsModalService, ModalModule } from 'ngx-bootstrap/modal'; import { DsModalModule } from '../shared/components/modal/ds-modal.module'; import { ReactiveFormsModule } from '@angular/forms'; @@ -20,9 +17,7 @@ import { FacilityEditFormDetailsComponent } from './facility-edit/facility-edit- import { FacilityEditFormPassRequirementsComponent } from './facility-edit/facility-edit-form/facility-edit-form-pass-requirements/facility-edit-form-pass-requirements.component'; import { FacilityEditFormPublishingDetailsComponent } from './facility-edit/facility-edit-form/facility-edit-form-publishing-details/facility-edit-form-publishing-details.component'; import { FacilityEditFormComponent } from './facility-edit/facility-edit-form/facility-edit-form.component'; -import { PassesCapacityBarComponent } from './facility-details/passes-capacity-bar/passes-capacity-bar.component'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { PassesUtilityButtonsComponent } from './facility-details/passes-utility-buttons/passes-utility-buttons.component'; import { ModifiersListComponent } from './modifiers-list/modifiers-list.component'; import { ModifiersFormComponent } from './modifiers-form/modifiers-form.component'; import { ParkEditFormComponent } from './park-edit-form/park-edit-form.component'; @@ -37,15 +32,10 @@ import { TextWithIconsModule } from '../shared/components/text-with-icons/text-w ParkDetailsComponent, FacilityDetailsComponent, FacilityEditComponent, - PassesListComponent, - PassesFilterComponent, - PassesFilterFieldsComponent, FacilityEditFormComponent, FacilityEditFormPublishingDetailsComponent, FacilityEditFormDetailsComponent, FacilityEditFormPassRequirementsComponent, - PassesCapacityBarComponent, - PassesUtilityButtonsComponent, ModifiersListComponent, ModifiersFormComponent, ParkEditFormComponent, diff --git a/src/app/pass-management/pass-check-in/pass-check-in.component.html b/src/app/pass-management/pass-check-in/pass-check-in.component.html new file mode 100644 index 00000000..a9d3fc60 --- /dev/null +++ b/src/app/pass-management/pass-check-in/pass-check-in.component.html @@ -0,0 +1,50 @@ +
+
+
+
+ + +
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+
+
+ +
+
+
+ +
+ +
+
diff --git a/src/app/pass-management/pass-check-in/pass-check-in.component.scss b/src/app/pass-management/pass-check-in/pass-check-in.component.scss new file mode 100644 index 00000000..cb322f56 --- /dev/null +++ b/src/app/pass-management/pass-check-in/pass-check-in.component.scss @@ -0,0 +1,22 @@ +.btn-group { + .btn-camera { + width: 200px; + border-top-right-radius: 0; + border-top-left-radius: 5px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } + .btn-manual { + width: 200px; + border-top-right-radius: 5px; + border-top-left-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } +} + +.gutters { + max-width: 65rem; + margin-left: auto; + margin-right: auto; +} diff --git a/src/app/pass-management/pass-check-in/pass-check-in.component.spec.ts b/src/app/pass-management/pass-check-in/pass-check-in.component.spec.ts new file mode 100644 index 00000000..ceec211b --- /dev/null +++ b/src/app/pass-management/pass-check-in/pass-check-in.component.spec.ts @@ -0,0 +1,134 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { BehaviorSubject } from 'rxjs'; +import { PassCheckInComponent } from './pass-check-in.component'; +import { MockData } from 'src/app/shared/utils/mock-data'; +import { PassCheckInListComponent } from '../pass-check-in-list/pass-check-in-list.component'; +import { QrResultComponent } from '../qr-result/qr-result.component'; +import { PassService } from 'src/app/services/pass.service'; +import { DataService } from 'src/app/services/data.service'; +import { QrScannerService } from 'src/app/shared/components/qr-scanner/qr-scanner.service'; +import { LoggerService } from 'src/app/services/logger.service'; +import { ConfigService } from 'src/app/services/config.service'; +import { Constants } from 'src/app/shared/utils/constants'; + + +describe('PassCheckInComponent', () => { + let component: PassCheckInComponent; + let fixture: ComponentFixture; + + const mockUrl = `https://test.gov.bc.ca/testing?registrationNumber=${ + MockData.mockPass_1.registrationNumber + }&park=${MockData.mockPass_1.pk.split('::')[1]}`; + + const mockPassService = { + fetchData: (park, passId) => { + return [{ ...MockData.mockPass_1 }]; + }, + }; + + const mockDataService = { + setItemValue: (id, value) => {}, + watchItem: (id) => { + return new BehaviorSubject([ + { ...MockData.mockPass_1 }, + { ...MockData.mockPass_2 }, + ]); + }, + }; + + const mockQrScannerService = { + disableScanner: () => {}, + clearScannerOutput: () => {}, + watchScannerState: () => { + return new BehaviorSubject(true); + }, + watchScannerOutput: () => { + return new BehaviorSubject(null); + }, + }; + + @Component({ + selector: 'app-qr-scanner', + template: '', + }) + class MockProductEditorComponent {} + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ + PassCheckInComponent, + PassCheckInListComponent, + MockProductEditorComponent, + QrResultComponent, + ], + providers: [ + { provide: PassService, useValue: mockPassService }, + { provide: QrScannerService, useValue: mockQrScannerService }, + { provide: DataService, useValue: mockDataService }, + LoggerService, + ConfigService, + ], + imports: [HttpClientTestingModule, RouterTestingModule], + }).compileComponents(); + + fixture = TestBed.createComponent(PassCheckInComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', async () => { + expect(component).toBeTruthy(); + }); + + it('should set check in list event', async () => { + const setItemValueSpy = spyOn(mockDataService, 'setItemValue'); + const event = 'AWOOGA'; + component.passCheckInListEvent(event); + expect(setItemValueSpy).toHaveBeenCalledOnceWith( + Constants.dataIds.PASS_CHECK_IN_LIST_EVENT, + event + ); + }); + + it('should set mode', async () => { + component.setMode('manual'); + expect(component.mode).toEqual('manual'); + expect(component.passes).toEqual([]); + }); + + it('should get params from url', async () => { + const getPassSpy = spyOn(component, 'getPass'); + await component.processUrl(mockUrl); + + expect(getPassSpy).toHaveBeenCalledWith( + MockData.mockPass_1.pk.split('::')[1], + MockData.mockPass_1.registrationNumber + ); + }); + + it('should get pass', async () => { + const disableScannerSpy = spyOn(mockQrScannerService, 'disableScanner'); + await component.processUrl(mockUrl); + + expect(disableScannerSpy).toHaveBeenCalled(); + }); + + it('should clear scanner output and set empty array on destroy', async () => { + const clearScannerOutputSpy = spyOn( + mockQrScannerService, + 'clearScannerOutput' + ); + const setItemValueSpy = spyOn(mockDataService, 'setItemValue'); + + component.ngOnDestroy(); + + expect(clearScannerOutputSpy).toHaveBeenCalled(); + expect(setItemValueSpy).toHaveBeenCalledWith( + Constants.dataIds.PASS_CHECK_IN_LIST, + [] + ); + }); +}); diff --git a/src/app/pass-management/pass-check-in/pass-check-in.component.ts b/src/app/pass-management/pass-check-in/pass-check-in.component.ts new file mode 100644 index 00000000..5ed41276 --- /dev/null +++ b/src/app/pass-management/pass-check-in/pass-check-in.component.ts @@ -0,0 +1,145 @@ +import { Component, OnDestroy, ViewChild } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { DataService } from 'src/app/services/data.service'; +import { LoggerService } from 'src/app/services/logger.service'; +import { PassService } from 'src/app/services/pass.service'; +import { ToastService } from 'src/app/services/toast.service'; +import { QrScannerComponent } from 'src/app/shared/components/qr-scanner/qr-scanner.component'; +import { QrScannerService } from 'src/app/shared/components/qr-scanner/qr-scanner.service'; +import { Constants } from 'src/app/shared/utils/constants'; + +@Component({ + selector: 'app-pass-check-in', + templateUrl: './pass-check-in.component.html', + styleUrls: ['./pass-check-in.component.scss'], +}) +export class PassCheckInComponent implements OnDestroy { + @ViewChild(QrScannerComponent) qrScannerComponent: QrScannerComponent; + + private subscriptions = new Subscription(); + + public mode = 'camera'; + public scannerSwitch = false; + public passes = []; + + constructor( + private passService: PassService, + private logger: LoggerService, + private qrScannerService: QrScannerService, + private route: ActivatedRoute, + private dataService: DataService, + private router: Router, + private toastService: ToastService + ) { + if ( + this.route.snapshot.queryParamMap.get('park') && + this.route.snapshot.queryParamMap.get('registrationNumber') + ) { + this.getPass( + this.route.snapshot.queryParamMap.get('park'), + this.route.snapshot.queryParamMap.get('registrationNumber') + ); + // Clear query params after getting pass + this.router.navigate([], { queryParams: {} }); + } + + this.subscriptions.add( + this.qrScannerService.watchScannerState().subscribe((res) => { + this.scannerSwitch = res; + }) + ); + + this.subscriptions.add( + this.qrScannerService.watchScannerOutput().subscribe(async (res) => { + if (res) { + await this.processUrl(res); + } + }) + ); + + this.subscriptions.add( + this.dataService + .watchItem(Constants.dataIds.PASS_CHECK_IN_LIST) + .subscribe((res) => { + if (res) { + this.passes = res; + } + }) + ); + } + + passCheckInListEvent(event) { + this.dataService.setItemValue( + Constants.dataIds.PASS_CHECK_IN_LIST_EVENT, + event + ); + } + + setMode(modeToSet) { + this.mode = modeToSet; + if (this.mode === 'camera') { + this.qrScannerComponent.clearResult(); + this.qrScannerService.enableScanner(); + } + this.passes = []; + } + + async processUrl(url) { + this.logger.debug('QR Detected: ' + url); + try { + if (url) { + const urlParams = new URL(url); + const park = urlParams.searchParams.get('park'); + // This is not a mistake, it comes in as registrationNumber + // We set to passId + const passId = urlParams.searchParams.get('registrationNumber'); + if (park && passId) { + await this.getPass(park, passId); + } else { + throw 'Invalid QR Code.'; + } + } else { + throw 'Invalid QR Code.'; + } + } catch (error) { + this.logger.error(error); + this.toastService.addMessage( + String(error), + 'QR Service', + Constants.ToastTypes.ERROR + ); + this.qrScannerComponent.scanningState = 'scanning'; + } + } + + async getPass(park, passId) { + // TODO: Start fancy loading bar stuff + let res; + try { + res = await this.passService.fetchData({ + park: park, + passId: passId, + }); + } catch (error) { + if (error === 'Network Offline') { + // In this mode, the service layer will pop a message instead. We don't want + // multiple msgs. + return; + } else { + this.logger.error(error); + throw 'Error connecting to server. Please refresh the page.'; + } + } + if (res && res.length > 0) { + // TODO: If we got a pass successfully, make a noise + this.qrScannerService.disableScanner(); + } + } + + ngOnDestroy(): void { + this.subscriptions.unsubscribe(); + this.qrScannerService.clearScannerOutput(); + this.dataService.setItemValue(Constants.dataIds.PASS_CHECK_IN_LIST, []); + } +} diff --git a/src/app/pass-management/pass-management-home/pass-management-home.component.html b/src/app/pass-management/pass-management-home/pass-management-home.component.html new file mode 100644 index 00000000..0ecfbf81 --- /dev/null +++ b/src/app/pass-management/pass-management-home/pass-management-home.component.html @@ -0,0 +1,13 @@ +
+

Pass Management

+
+
+
+
+
+ +
+
+
+
\ No newline at end of file diff --git a/src/app/parks-management/facility-details/passes-capacity-bar/passes-capacity-bar.component.scss b/src/app/pass-management/pass-management-home/pass-management-home.component.scss similarity index 100% rename from src/app/parks-management/facility-details/passes-capacity-bar/passes-capacity-bar.component.scss rename to src/app/pass-management/pass-management-home/pass-management-home.component.scss diff --git a/src/app/pass-management/pass-management-home/pass-management-home.component.spec.ts b/src/app/pass-management/pass-management-home/pass-management-home.component.spec.ts new file mode 100644 index 00000000..ab5e6ef9 --- /dev/null +++ b/src/app/pass-management/pass-management-home/pass-management-home.component.spec.ts @@ -0,0 +1,33 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { PassManagementHomeComponent } from './pass-management-home.component'; +import { ConfigService } from 'src/app/services/config.service'; + +describe('PassManagementHomeComponent', () => { + let component: PassManagementHomeComponent; + let fixture: ComponentFixture; + + let mockConfigService = { + config: { + QR_CODE_ENABLED: true + } + } + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [PassManagementHomeComponent], + providers: [ + { provide: ConfigService, useValue: mockConfigService } + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PassManagementHomeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + expect(component.cardConfig.length).toBe(2); + }); +}); diff --git a/src/app/pass-management/pass-management-home/pass-management-home.component.ts b/src/app/pass-management/pass-management-home/pass-management-home.component.ts new file mode 100644 index 00000000..c971e7a7 --- /dev/null +++ b/src/app/pass-management/pass-management-home/pass-management-home.component.ts @@ -0,0 +1,38 @@ +import { Component, OnInit } from '@angular/core'; +import { ConfigService } from 'src/app/services/config.service'; + +@Component({ + selector: 'app-pass-management-home', + templateUrl: './pass-management-home.component.html', + styleUrls: ['./pass-management-home.component.scss'] +}) +export class PassManagementHomeComponent implements OnInit { + public cardConfig; + + constructor( + private configService: ConfigService + ) { } + + ngOnInit() { + this.cardConfig = []; + if (this.configService.config.QR_CODE_ENABLED) { + this.cardConfig.push({ + cardHeader: 'QR and Manual Check-In', + cardTitle: 'QR and Manual Check-In', + cardText: 'Scan passes or check in guests manually.', + navigation: 'check-in', + relative: true + }); + + } + this.cardConfig.push( + { + cardHeader: 'Pass Search', + cardTitle: 'Pass Search', + cardText: 'Search, export, or cancel passes', + navigation: 'search', + relative: true + }, + ) + } +} diff --git a/src/app/pass-management/pass-management-routing.module.ts b/src/app/pass-management/pass-management-routing.module.ts index d682f970..3cdb7bb2 100644 --- a/src/app/pass-management/pass-management-routing.module.ts +++ b/src/app/pass-management/pass-management-routing.module.ts @@ -1,23 +1,41 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { AuthGuard } from '../guards/auth.guard'; -import { PassManagementComponent } from './pass-management.component'; +import { PassCheckInComponent } from './pass-check-in/pass-check-in.component'; +import { PassSearchComponent } from './pass-search/pass-search.component'; +import { PassManagementHomeComponent } from './pass-management-home/pass-management-home.component'; const routes: Routes = [ { path: '', - component: PassManagementComponent, + component: PassManagementHomeComponent, canActivate: [AuthGuard], data: { breadcrumb: '', - module: 'pass-managment', + module: 'pass-management', component: '', }, }, + { + path: 'check-in', + component: PassCheckInComponent, + canActivate: [AuthGuard], + data: { + breadcrumb: 'Check-In', + } + }, + { + path: 'search', + component: PassSearchComponent, + canActivate: [AuthGuard], + data: { + breadcrumb: 'Pass Search', + } + } ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) -export class PassManagementRoutingModule {} +export class PassManagementRoutingModule { } diff --git a/src/app/pass-management/pass-management.component.html b/src/app/pass-management/pass-management.component.html index ab587431..90c6b646 100644 --- a/src/app/pass-management/pass-management.component.html +++ b/src/app/pass-management/pass-management.component.html @@ -1,52 +1 @@ -
-
-

Pass Management

- -
-
- - -
-
-
- -
-
-
-
-
- -
-
-
- -
-
-
-
- -
-
-
- -
- -
-
+ \ No newline at end of file diff --git a/src/app/pass-management/pass-management.component.spec.ts b/src/app/pass-management/pass-management.component.spec.ts index ae37a82b..cd060061 100644 --- a/src/app/pass-management/pass-management.component.spec.ts +++ b/src/app/pass-management/pass-management.component.spec.ts @@ -1,134 +1,25 @@ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { Component } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; -import { BehaviorSubject } from 'rxjs'; -import { ConfigService } from '../services/config.service'; -import { DataService } from '../services/data.service'; -import { LoggerService } from '../services/logger.service'; -import { PassService } from '../services/pass.service'; -import { QrScannerService } from '../shared/components/qr-scanner/qr-scanner.service'; -import { Constants } from '../shared/utils/constants'; -import { MockData } from '../shared/utils/mock-data'; -import { PassCheckInListComponent } from './pass-check-in-list/pass-check-in-list.component'; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { PassManagementComponent } from "./pass-management.component" -import { PassManagementComponent } from './pass-management.component'; -import { QrResultComponent } from './qr-result/qr-result.component'; describe('PassManagementComponent', () => { let component: PassManagementComponent; let fixture: ComponentFixture; - const mockUrl = `https://test.gov.bc.ca/testing?registrationNumber=${ - MockData.mockPass_1.registrationNumber - }&park=${MockData.mockPass_1.pk.split('::')[1]}`; - - const mockPassService = { - fetchData: (park, passId) => { - return [{ ...MockData.mockPass_1 }]; - }, - }; - - const mockDataService = { - setItemValue: (id, value) => {}, - watchItem: (id) => { - return new BehaviorSubject([ - { ...MockData.mockPass_1 }, - { ...MockData.mockPass_2 }, - ]); - }, - }; - - const mockQrScannerService = { - disableScanner: () => {}, - clearScannerOutput: () => {}, - watchScannerState: () => { - return new BehaviorSubject(true); - }, - watchScannerOutput: () => { - return new BehaviorSubject(null); - }, - }; - - @Component({ - selector: 'app-qr-scanner', - template: '', - }) - class MockProductEditorComponent {} - beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ - PassManagementComponent, - PassCheckInListComponent, - MockProductEditorComponent, - QrResultComponent, - ], - providers: [ - { provide: PassService, useValue: mockPassService }, - { provide: QrScannerService, useValue: mockQrScannerService }, - { provide: DataService, useValue: mockDataService }, - LoggerService, - ConfigService, - ], - imports: [HttpClientTestingModule, RouterTestingModule], - }).compileComponents(); + declarations: [PassManagementComponent] + }) + .compileComponents(); fixture = TestBed.createComponent(PassManagementComponent); component = fixture.componentInstance; fixture.detectChanges(); - }); - - it('should create', async () => { - expect(component).toBeTruthy(); - }); - - it('should set check in list event', async () => { - const setItemValueSpy = spyOn(mockDataService, 'setItemValue'); - const event = 'AWOOGA'; - component.passCheckInListEvent(event); - expect(setItemValueSpy).toHaveBeenCalledOnceWith( - Constants.dataIds.PASS_CHECK_IN_LIST_EVENT, - event - ); - }); - - it('should set mode', async () => { - component.setMode('manual'); - expect(component.mode).toEqual('manual'); - expect(component.passes).toEqual([]); - }); - - it('should get params from url', async () => { - const getPassSpy = spyOn(component, 'getPass'); - await component.processUrl(mockUrl); - - expect(getPassSpy).toHaveBeenCalledWith( - MockData.mockPass_1.pk.split('::')[1], - MockData.mockPass_1.registrationNumber - ); - }); - - it('should get pass', async () => { - const disableScannerSpy = spyOn(mockQrScannerService, 'disableScanner'); - await component.processUrl(mockUrl); - expect(disableScannerSpy).toHaveBeenCalled(); }); - it('should clear scanner output and set empty array on destroy', async () => { - const clearScannerOutputSpy = spyOn( - mockQrScannerService, - 'clearScannerOutput' - ); - const setItemValueSpy = spyOn(mockDataService, 'setItemValue'); - - component.ngOnDestroy(); - - expect(clearScannerOutputSpy).toHaveBeenCalled(); - expect(setItemValueSpy).toHaveBeenCalledWith( - Constants.dataIds.PASS_CHECK_IN_LIST, - [] - ); + it('should create', () => { + expect(component).toBeTruthy(); }); -}); + +}); \ No newline at end of file diff --git a/src/app/pass-management/pass-management.component.ts b/src/app/pass-management/pass-management.component.ts index 93b18904..2809333b 100644 --- a/src/app/pass-management/pass-management.component.ts +++ b/src/app/pass-management/pass-management.component.ts @@ -1,145 +1,9 @@ -import { Component, OnDestroy, ViewChild } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { Subscription } from 'rxjs'; -import { DataService } from '../services/data.service'; -import { LoggerService } from '../services/logger.service'; -import { PassService } from '../services/pass.service'; -import { ToastService } from '../services/toast.service'; -import { QrScannerComponent } from '../shared/components/qr-scanner/qr-scanner.component'; -import { QrScannerService } from '../shared/components/qr-scanner/qr-scanner.service'; -import { Constants } from '../shared/utils/constants'; +import { Component } from '@angular/core'; @Component({ selector: 'app-pass-management', templateUrl: './pass-management.component.html', styleUrls: ['./pass-management.component.scss'], }) -export class PassManagementComponent implements OnDestroy { - @ViewChild(QrScannerComponent) qrScannerComponent: QrScannerComponent; - - private subscriptions = new Subscription(); - - public mode = 'camera'; - public scannerSwitch = false; - public passes = []; - - constructor( - private passService: PassService, - private logger: LoggerService, - private qrScannerService: QrScannerService, - private route: ActivatedRoute, - private dataService: DataService, - private router: Router, - private toastService: ToastService - ) { - if ( - this.route.snapshot.queryParamMap.get('park') && - this.route.snapshot.queryParamMap.get('registrationNumber') - ) { - this.getPass( - this.route.snapshot.queryParamMap.get('park'), - this.route.snapshot.queryParamMap.get('registrationNumber') - ); - // Clear query params after getting pass - this.router.navigate([], { queryParams: {} }); - } - - this.subscriptions.add( - this.qrScannerService.watchScannerState().subscribe((res) => { - this.scannerSwitch = res; - }) - ); - - this.subscriptions.add( - this.qrScannerService.watchScannerOutput().subscribe(async (res) => { - if (res) { - await this.processUrl(res); - } - }) - ); - - this.subscriptions.add( - this.dataService - .watchItem(Constants.dataIds.PASS_CHECK_IN_LIST) - .subscribe((res) => { - if (res) { - this.passes = res; - } - }) - ); - } - - passCheckInListEvent(event) { - this.dataService.setItemValue( - Constants.dataIds.PASS_CHECK_IN_LIST_EVENT, - event - ); - } - - setMode(modeToSet) { - this.mode = modeToSet; - if (this.mode === 'camera') { - this.qrScannerComponent.clearResult(); - this.qrScannerService.enableScanner(); - } - this.passes = []; - } - - async processUrl(url) { - this.logger.debug('QR Detected: ' + url); - try { - if (url) { - const urlParams = new URL(url); - const park = urlParams.searchParams.get('park'); - // This is not a mistake, it comes in as registrationNumber - // We set to passId - const passId = urlParams.searchParams.get('registrationNumber'); - if (park && passId) { - await this.getPass(park, passId); - } else { - throw 'Invalid QR Code.'; - } - } else { - throw 'Invalid QR Code.'; - } - } catch (error) { - this.logger.error(error); - this.toastService.addMessage( - String(error), - 'QR Service', - Constants.ToastTypes.ERROR - ); - this.qrScannerComponent.scanningState = 'scanning'; - } - } - - async getPass(park, passId) { - // TODO: Start fancy loading bar stuff - let res; - try { - res = await this.passService.fetchData({ - park: park, - passId: passId, - }); - } catch (error) { - if (error === 'Network Offline') { - // In this mode, the service layer will pop a message instead. We don't want - // multiple msgs. - return; - } else { - this.logger.error(error); - throw 'Error connecting to server. Please refresh the page.'; - } - } - if (res && res.length > 0) { - // TODO: If we got a pass successfully, make a noise - this.qrScannerService.disableScanner(); - } - } - - ngOnDestroy(): void { - this.subscriptions.unsubscribe(); - this.qrScannerService.clearScannerOutput(); - this.dataService.setItemValue(Constants.dataIds.PASS_CHECK_IN_LIST, []); - } +export class PassManagementComponent { } diff --git a/src/app/pass-management/pass-management.module.ts b/src/app/pass-management/pass-management.module.ts index 16540da8..255d860f 100644 --- a/src/app/pass-management/pass-management.module.ts +++ b/src/app/pass-management/pass-management.module.ts @@ -4,8 +4,25 @@ import { PassManagementComponent } from './pass-management.component'; import { QrScannerModule } from '../shared/components/qr-scanner/qr-scanner.module'; import { ManualEntryComponent } from './manual-entry/manual-entry.component'; import { PassCheckInListComponent } from './pass-check-in-list/pass-check-in-list.component'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ReactiveFormsModule } from '@angular/forms'; import { QrResultComponent } from './qr-result/qr-result.component'; +import { PassCheckInComponent } from './pass-check-in/pass-check-in.component'; +import { RouterModule } from '@angular/router'; +import { PassManagementRoutingModule } from './pass-management-routing.module'; +import { PassSearchComponent } from './pass-search/pass-search.component'; +import { NavCardModule } from '../shared/components/nav-card/nav-card.module'; +import { PassManagementHomeComponent } from './pass-management-home/pass-management-home.component'; +import { PassesCapacityBarComponent } from './pass-search/passes-capacity-bar/passes-capacity-bar.component'; +import { PassesFilterComponent } from './pass-search/passes-filter/passes-filter.component'; +import { PassesListComponent } from './pass-search/passes-list/passes-list.component'; +import { PassesUtilityButtonsComponent } from './pass-search/passes-utility-buttons/passes-utility-buttons.component'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { FancyHeaderModule } from '../shared/components/fancy-header/fancy-header.module'; +import { DsModalModule } from '../shared/components/modal/ds-modal.module'; +import { DsFormsModule } from '../shared/components/ds-forms/ds-forms.module'; +import { TableModule } from '../shared/components/table/table.module'; +import { PassesFilterFieldsComponent } from './pass-search/passes-filter/passes-filter-fields/passes-filter-fields.component'; +import { BsModalService } from 'ngx-bootstrap/modal'; @NgModule({ declarations: [ @@ -13,7 +30,28 @@ import { QrResultComponent } from './qr-result/qr-result.component'; ManualEntryComponent, PassCheckInListComponent, QrResultComponent, + PassCheckInComponent, + PassSearchComponent, + PassManagementHomeComponent, + PassesCapacityBarComponent, + PassesFilterComponent, + PassesListComponent, + PassesUtilityButtonsComponent, + PassesFilterFieldsComponent ], - imports: [CommonModule, QrScannerModule, FormsModule, ReactiveFormsModule], + imports: [ + CommonModule, + QrScannerModule, + RouterModule, + ReactiveFormsModule, + PassManagementRoutingModule, + DsModalModule, + DsFormsModule, + NavCardModule, + TableModule, + NgbModule, + FancyHeaderModule + ], + providers: [BsModalService], }) -export class PassManagementModule {} +export class PassManagementModule { } diff --git a/src/app/pass-management/pass-search/pass-search.component.html b/src/app/pass-management/pass-search/pass-search.component.html new file mode 100644 index 00000000..8601508d --- /dev/null +++ b/src/app/pass-management/pass-search/pass-search.component.html @@ -0,0 +1,28 @@ +
+

Pass Search

+
+
+
+ +
+
+
+
+

Search Results

+
+
+ +
+
+
+
+
+
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/src/app/parks-management/facility-details/passes-filter/passes-filter.component.scss b/src/app/pass-management/pass-search/pass-search.component.scss similarity index 100% rename from src/app/parks-management/facility-details/passes-filter/passes-filter.component.scss rename to src/app/pass-management/pass-search/pass-search.component.scss diff --git a/src/app/pass-management/pass-search/pass-search.component.spec.ts b/src/app/pass-management/pass-search/pass-search.component.spec.ts new file mode 100644 index 00000000..97999e1f --- /dev/null +++ b/src/app/pass-management/pass-search/pass-search.component.spec.ts @@ -0,0 +1,55 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PassSearchComponent } from './pass-search.component'; +import { ConfigService } from 'src/app/services/config.service'; +import { HttpClient, HttpHandler } from '@angular/common/http'; +import { MockData } from 'src/app/shared/utils/mock-data'; + +describe('PassSearchComponent', () => { + let component: PassSearchComponent; + let fixture: ComponentFixture; + + let mockFacility = MockData.mockFacility_1; + + let mockConfigService = { + config: { + ADVANCE_BOOKING_HOUR: 8, + ADVANCE_BOOKING_LIMIT: 0 + } + } + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [PassSearchComponent], + providers: [ + HttpClient, + HttpHandler, + { provide: ConfigService, useValue: mockConfigService } + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PassSearchComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('gets opening hour text', async () => { + component.facility = mockFacility; + expect(component.bookingOpeningHourText).toEqual('7 AM'); + component.facility = null; + expect(component.bookingOpeningHourText).toEqual('8 AM'); + }); + + it('gets booking days ahead text', async () => { + component.facility = mockFacility; + expect(component.bookingDaysAheadText).toEqual('2 days'); + component.facility = null; + expect(component.bookingDaysAheadText).toEqual('Same Day'); + + }); +}); diff --git a/src/app/pass-management/pass-search/pass-search.component.ts b/src/app/pass-management/pass-search/pass-search.component.ts new file mode 100644 index 00000000..fb9b07b8 --- /dev/null +++ b/src/app/pass-management/pass-search/pass-search.component.ts @@ -0,0 +1,74 @@ +import { Component } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { ConfigService } from 'src/app/services/config.service'; +import { DataService } from 'src/app/services/data.service'; +import { FacilityService } from 'src/app/services/facility.service'; +import { Constants } from 'src/app/shared/utils/constants'; +import { Utils } from 'src/app/shared/utils/utils'; + +@Component({ + selector: 'app-pass-search', + templateUrl: './pass-search.component.html', + styleUrls: ['./pass-search.component.scss'] +}) +export class PassSearchComponent { + private subscriptions = new Subscription(); + public facility; + public Utils = new Utils(); + + constructor( + protected dataService: DataService, + protected configService: ConfigService, + protected facilityService: FacilityService + ) { + this.subscriptions.add( + dataService + .watchItem(Constants.dataIds.CURRENT_FACILITY_KEY) + .subscribe((res) => { + if (res) { + this.facility = this.facilityService.getCachedFacility(res); + } + }) + ); + } + + get bookingOpeningHourText() { + const facilityBookingOpeningHour = this.facility + ? this.facility.bookingOpeningHour + : null; + const advanceBookingHour = + facilityBookingOpeningHour || + parseInt(this.configService.config['ADVANCE_BOOKING_HOUR'], 10); + const { hour, amPm } = this.Utils.convert24hTo12hTime(advanceBookingHour); + + if (hour && amPm) { + return `${hour} ${amPm}`; + } + return ''; + } + + get bookingDaysAheadText() { + let advanceBookingDays = this.facility + ? this.facility.bookingDaysAhead + : null; + if (advanceBookingDays !== 0 && !advanceBookingDays) { + advanceBookingDays = parseInt( + this.configService.config['ADVANCE_BOOKING_LIMIT'], + 10 + ); + } + + if (advanceBookingDays === 0) { + return 'Same Day'; + } + if (advanceBookingDays === 1) { + return '1 day'; + } + + return `${advanceBookingDays} days`; + } + + ngOnDestroy(): void { + this.subscriptions.unsubscribe(); + } +} diff --git a/src/app/parks-management/facility-details/passes-capacity-bar/passes-capacity-bar.component.html b/src/app/pass-management/pass-search/passes-capacity-bar/passes-capacity-bar.component.html similarity index 100% rename from src/app/parks-management/facility-details/passes-capacity-bar/passes-capacity-bar.component.html rename to src/app/pass-management/pass-search/passes-capacity-bar/passes-capacity-bar.component.html diff --git a/src/app/parks-management/facility-details/passes-list/passes-list.component.scss b/src/app/pass-management/pass-search/passes-capacity-bar/passes-capacity-bar.component.scss similarity index 100% rename from src/app/parks-management/facility-details/passes-list/passes-list.component.scss rename to src/app/pass-management/pass-search/passes-capacity-bar/passes-capacity-bar.component.scss diff --git a/src/app/parks-management/facility-details/passes-capacity-bar/passes-capacity-bar.component.spec.ts b/src/app/pass-management/pass-search/passes-capacity-bar/passes-capacity-bar.component.spec.ts similarity index 100% rename from src/app/parks-management/facility-details/passes-capacity-bar/passes-capacity-bar.component.spec.ts rename to src/app/pass-management/pass-search/passes-capacity-bar/passes-capacity-bar.component.spec.ts diff --git a/src/app/parks-management/facility-details/passes-capacity-bar/passes-capacity-bar.component.ts b/src/app/pass-management/pass-search/passes-capacity-bar/passes-capacity-bar.component.ts similarity index 100% rename from src/app/parks-management/facility-details/passes-capacity-bar/passes-capacity-bar.component.ts rename to src/app/pass-management/pass-search/passes-capacity-bar/passes-capacity-bar.component.ts diff --git a/src/app/parks-management/facility-details/passes-filter/passes-filter-fields/passes-filter-fields.component.html b/src/app/pass-management/pass-search/passes-filter/passes-filter-fields/passes-filter-fields.component.html similarity index 80% rename from src/app/parks-management/facility-details/passes-filter/passes-filter-fields/passes-filter-fields.component.html rename to src/app/pass-management/pass-search/passes-filter/passes-filter-fields/passes-filter-fields.component.html index 921ae3f0..ba8c5027 100644 --- a/src/app/parks-management/facility-details/passes-filter/passes-filter-fields/passes-filter-fields.component.html +++ b/src/app/pass-management/pass-search/passes-filter/passes-filter-fields/passes-filter-fields.component.html @@ -1,12 +1,4 @@
-
- -
- + +
+
+ +
+
@@ -50,8 +52,15 @@ >
-
+
+ +
- + +
diff --git a/src/app/pass-management/pass-search/passes-filter/passes-filter.component.scss b/src/app/pass-management/pass-search/passes-filter/passes-filter.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/pass-management/pass-search/passes-filter/passes-filter.component.spec.ts b/src/app/pass-management/pass-search/passes-filter/passes-filter.component.spec.ts new file mode 100644 index 00000000..2c2bec47 --- /dev/null +++ b/src/app/pass-management/pass-search/passes-filter/passes-filter.component.spec.ts @@ -0,0 +1,191 @@ +import { HttpClient, HttpHandler } from '@angular/common/http'; +import { Location } from '@angular/common'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { BehaviorSubject } from 'rxjs'; +import { ConfigService } from 'src/app/services/config.service'; +import { DataService } from 'src/app/services/data.service'; +import { MockData } from 'src/app/shared/utils/mock-data'; + +import { PassesFilterComponent } from './passes-filter.component'; +import { FacilityService } from 'src/app/services/facility.service'; +import { Constants } from 'src/app/shared/utils/constants'; + +describe('PassesFilterComponent', () => { + let component: PassesFilterComponent; + let fixture: ComponentFixture; + let location: Location; + + let mockFacility = MockData.mockFacility_1; + let mockFacilityKey = { pk: mockFacility.pk, sk: mockFacility.sk } + + let mockParkAndFacility = MockData.mockParkFacility_1; + + let mockPartialPassFilter = MockData.mockPartialPassFilters_1; + let mockFullPassFilter = MockData.mockFullPassFilters_1; + + let mockSubject = new BehaviorSubject(mockFacility); + + let mockFacilityService = { + getCachedFacility: (key) => { + if (key.pk === mockFacilityKey.pk && key.sk === mockFacilityKey.sk) { + return mockFacility; + } + return null; + }, + }; + + let mockDataService = { + watchItem: (id) => { + if (id === Constants.dataIds.PARK_AND_FACILITY_LIST) { + return new BehaviorSubject(mockParkAndFacility); + } + return new BehaviorSubject(null); + }, + getItemValue: (id) => { + if (id === Constants.dataIds.PARK_AND_FACILITY_LIST) { + return mockParkAndFacility; + } + return null; + }, + setItemValue: (id, value) => { + // do nothing, see if I even care + return; + }, + + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [PassesFilterComponent], + imports: [ + FormsModule, + ReactiveFormsModule, + RouterTestingModule.withRoutes([ + { path: 'passType', component: PassesFilterComponent }, + ]), + ], + providers: [ + HttpClient, + HttpHandler, + ConfigService, + { provide: DataService, useValue: mockDataService }, + { provide: FacilityService, useValue: mockFacilityService }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(PassesFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + location = TestBed.inject(Location); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('builds picklists', async () => { + component.createParksOptions(); + expect(component.parkOptions).toEqual([ + { + value: 'MOC1', display: 'Mock Park 1' + }, + { + value: 'MOC2', display: 'Mock Park 2' + }, + ]) + component.createFacilitiesOptions('MOC1'); + expect(component.facilityOptions).toEqual([ + { + value: 'Mock Facility 1', display: 'Mock Facility 1' + }, + { + value: 'Mock Facility 2', display: 'Mock Facility 2' + } + ]) + component.createPassTypeOptions('MOC1', 'Mock Facility 1'); + expect(component.passTypeOptions).toEqual([ + { + value: 'AM', display: 'AM' + }, + { + value: 'PM', display: 'PM' + }, + ]) + }); + + it('submits filter selections', async () => { + const passServiceSpy = spyOn(component['passService'], 'fetchData'); + expect(passServiceSpy).not.toHaveBeenCalled(); + component.data = mockPartialPassFilter; + component.setForm(); + await fixture.isStable(); + await component.onSubmit(); + // overbooked = all is default + expect(location.path()).toBe('/?park=MOC1&facilityName=Mock%20Facility%201&passType=AM&overbooked=all'); + expect(passServiceSpy).toHaveBeenCalledOnceWith({ + passType: 'AM', + facilityName: 'Mock Facility 1', + park: 'MOC1', + overbooked: 'all' + }); + // full pass checklist + component.data = mockFullPassFilter; + passServiceSpy.calls.reset(); + component.setForm(); + await fixture.isStable(); + await component.onSubmit(); + expect(location.path()).toBe('/?date=2022-12-19&park=MOC1&facilityName=Mock%20Facility%201&passType=AM&passStatus=reserved&firstName=firstName&lastName=lastName&email=mock@email.ca&reservationNumber=1234567890&overbooked=all'); + expect(passServiceSpy).toHaveBeenCalledOnceWith({ + date: '2022-12-19', + park: 'MOC1', + passType: 'AM', + passStatus: 'reserved', + firstName: 'firstName', + lastName: 'lastName', + email: 'mock@email.ca', + reservationNumber: '1234567890', + overbooked: 'all', + facilityName: 'Mock Facility 1', + }) + }); + + it('updates the field', async () => { + const facilityListSpy = spyOn(component, 'createFacilitiesOptions'); + component.data = { + park: 'NEWMOC' + } + component.updateFormFields(); + expect(component.form.get('park').value).toBe('NEWMOC'); + expect(facilityListSpy).not.toHaveBeenCalled(); + }) + + it('clears the field', async () => { + const resetSpy = spyOn(component, 'reset'); + component.clearForm(); + expect(resetSpy).toHaveBeenCalled(); + expect(component.data.park).toBe(null); + }); + + it('checks if mandatory fields are non-null', async () => { + expect(component.checkFieldsForSubmission()).toBeFalse(); + component.data = { + park: 'MOC1', + facilityName: 'Mock Facility 1', + passType: 'DAY', + } + component.setForm(); + expect(component.checkFieldsForSubmission()).toBeTrue(); + }); + + it('validates the fields', async () => { + const res1 = await component.submit(); + expect(component.validateFields(res1.fields)).toBeFalse(); + component.data = mockFullPassFilter; + component.setForm(); + const res2 = await component.submit(); + expect(component.validateFields(res2.fields)).toBeTrue(); + }) + +}); diff --git a/src/app/pass-management/pass-search/passes-filter/passes-filter.component.ts b/src/app/pass-management/pass-search/passes-filter/passes-filter.component.ts new file mode 100644 index 00000000..6aee6904 --- /dev/null +++ b/src/app/pass-management/pass-search/passes-filter/passes-filter.component.ts @@ -0,0 +1,336 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validators, +} from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { DataService } from 'src/app/services/data.service'; +import { LoadingService } from 'src/app/services/loading.service'; +import { PassService } from 'src/app/services/pass.service'; +import { ReservationService } from 'src/app/services/reservation.service'; +import { BaseFormComponent } from 'src/app/shared/components/ds-forms/base-form/base-form.component'; +import { Constants } from 'src/app/shared/utils/constants'; +import { DateTime } from 'luxon'; + +@Component({ + selector: 'app-passes-filter', + templateUrl: './passes-filter.component.html', + styleUrls: ['./passes-filter.component.scss'], +}) +export class PassesFilterComponent extends BaseFormComponent { + + public parksAndFacilities; + public passTypeOptions; + public statusesList = ['reserved', 'active', 'expired', 'cancelled']; + public overbookedList = [ + { value: 'all', display: 'Show all passes' }, + { value: 'show', display: 'Show overbooked only' }, + { value: 'hide', display: 'Hide overbooked' }, + ]; + public parkOptions: any[] = []; + public facilityOptions: any[] = []; + public searchOnPageLoadFlag = false; + + constructor( + protected formBuilder: UntypedFormBuilder, + protected router: Router, + protected dataService: DataService, + protected loadingService: LoadingService, + protected changeDetector: ChangeDetectorRef, + protected passService: PassService, + protected reservationService: ReservationService, + private route: ActivatedRoute, + ) { + super( + formBuilder, + router, + dataService, + loadingService, + changeDetector + ); + + this.subscriptions.add( + this.dataService + .watchItem(Constants.dataIds.PARK_AND_FACILITY_LIST) + .subscribe((res) => { + if (res) { + this.parksAndFacilities = res; + this.createParksOptions(); + if (this.fields?.park?.value) { + this.createFacilitiesOptions(this.fields?.park?.value); + if (this.fields?.facilityName?.value) { + this.createPassTypeOptions(this.fields?.park?.value, this.fields?.facilityName?.value); + } + } + this.updateFormFields(); + } + } + ) + ) + + // If there are params in the url, fill the filters + if (Object.keys(this.route.snapshot.queryParams).length > 0) { + this.data = this.route.snapshot.queryParams; + this.searchOnPageLoadFlag = true; + } else { + // set date to today + const today = DateTime.now().setZone('America/Vancouver').toISODate(); + this.data.date = today; + } + this.parksAndFacilities = this.dataService.getItemValue(Constants.dataIds.PARK_AND_FACILITY_LIST); + if (this.parksAndFacilities) { + this.createParksOptions(); + if (this.data?.park) { + this.createFacilitiesOptions(this.data?.park); + if (this.data?.facilityName) { + this.createPassTypeOptions(this.data?.park, this.data?.facilityName); + } + } + } + this.setForm(); + } + + createParksOptions() { + this.parkOptions = []; + for (const park in this.parksAndFacilities) { + this.parkOptions.push({ value: this.parksAndFacilities[park]?.sk, display: this.parksAndFacilities[park]?.name }); + } + } + + createFacilitiesOptions(park) { + this.facilityOptions = []; + if (park) { + const facilities = this.parksAndFacilities[park]?.facilities; + for (const facility in facilities) { + this.facilityOptions.push({ value: facilities[facility]?.sk, display: facilities[facility]?.name }); + } + + } + } + + createPassTypeOptions(park, facility) { + this.passTypeOptions = []; + if (park && facility && this.parksAndFacilities) { + const bookingTimes = this.parksAndFacilities[park].facilities[facility]?.bookingTimes || null + if (bookingTimes) { + const passTypes = Object.keys(this.parksAndFacilities[park].facilities[facility].bookingTimes); + for (const type of passTypes) { + this.passTypeOptions.push({ value: type, display: type }); + } + } + } + } + + setForm() { + this.form = new UntypedFormGroup({ + date: new UntypedFormControl(this.data?.date || null), + park: new UntypedFormControl(this.data?.park || null, Validators.required), + facilityName: new UntypedFormControl(this.data?.facilityName || null, Validators.required), + passType: new UntypedFormControl(this.data?.passType || null, Validators.required), + passStatus: new UntypedFormControl(this.data?.passStatus || null), + firstName: new UntypedFormControl(this.data?.firstName || null), + lastName: new UntypedFormControl(this.data?.lastName || null), + email: new UntypedFormControl(this.data?.email || null), + reservationNumber: new UntypedFormControl(this.data?.reservationNumber || null), + overbooked: new UntypedFormControl(this.data?.overbooked || 'all'), + }); + + // update form with new values + super.updateForm(); + + // add disabled rules to the form + super.addDisabledRule( + this.fields.facilityName, + () => { + const park = this.fields?.park?.value; + if (!park) { + return true; + } + return false; + }, + this.fields.park.valueChanges + ); + super.addDisabledRule( + this.fields.passType, + () => { + if (this.fields?.facilityName?.disabled) { + return true; + } + const facility = this.fields?.facilityName?.value; + if (!facility) { + return true; + } + return false; + }, + this.fields.facilityName.valueChanges + ); + + // add subscriptions to changes in form value + super.subscribeToControlValueChanges( + this.fields.park, + () => { + this.createFacilitiesOptions(this.fields?.park?.value); + if (this.fields?.facilityName && this.facilityOptions.length > 0) { + this.fields?.facilityName?.setValue(this.facilityOptions[0].value); + } + } + ); + super.subscribeToControlValueChanges( + this.fields.facilityName, + () => { + this.createPassTypeOptions(this.fields?.park?.value, this.fields?.facilityName?.value); + if (this.fields.passType && this.passTypeOptions.length > 0) { + this.fields.passType.setValue(this.passTypeOptions[0].value); + } + } + ); + + super.subscribeToFormValueChanges( + () => { this.updateUrl() } + ); + + this.updateUrl(); + + // If there are mandatory params in the url, search for them now + if (this.searchOnPageLoadFlag && this.checkFieldsForSubmission()) { + this.onSubmit(); + } + this.searchOnPageLoadFlag = false; + } + + + updateFormFields() { + for (const field of Object.keys(this.fields)) { + this.fields[field].setValue(this.data[field], { emitEvent: false }); + } + } + + clearForm() { + this.dataService.setItemValue(Constants.dataIds.PASSES_LIST, null); + this.reset(); + this.data = { + park: null + }; + this.setForm(); + } + + // Don't allow search submission if mandatory fields are missing + checkFieldsForSubmission() { + if ( + this.fields.park.value && + this.fields.facilityName.value && + this.fields.passType.value + ) { + return true; + } + return false; + } + + validateFields(fields) { + // Check park + if (!this.checkFieldsForSubmission) { + return false; + } + if (this.parksAndFacilities) { + let hasPark = Object.keys(this.parksAndFacilities).find((park) => + park === fields.park + ) + if (!hasPark) { + return false; + } + if (fields.facilityName) { + let hasFacility = Object.keys(this.parksAndFacilities[fields.park]?.facilities).find((facility) => + facility === fields.facilityName + ); + if (!hasFacility) { + return false; + } + if (fields.passType) { + let hasPassType = Object.keys(this.parksAndFacilities[fields.park]?.facilities[fields.facilityName]?.bookingTimes).find((type) => + type === fields.passType + ); + if (!hasPassType) { + return false; + } + } + } + } else { + return false; + } + return true; + } + + async onSubmit() { + // Save current search params + const res = await super.submit(); + + // delete unused fields + for (const field in res.fields) { + if (res.fields[field] === null) { + delete res.fields[field]; + } + } + + if (this.validateFields(res.fields)) { + // update current park and facility + this.dataService.setItemValue(Constants.dataIds.CURRENT_PARK_KEY, { pk: 'park', sk: res.fields.park }); + this.dataService.setItemValue(Constants.dataIds.CURRENT_FACILITY_KEY, { pk: `facilty::${res.fields.park}`, sk: res.fields.facilityName }); + this.passService.fetchData(res.fields); + this.reservationService.fetchData( + res.fields?.park, + res.fields.facilityName, + res.fields?.date || null, + res.fields?.passType || null + ); + this.updateUrl(); + } + } + + async updateUrl() { + const res = await super.submit(); + const queryParams = { ...res.fields }; + if (queryParams.passStatus?.length) { + queryParams.passStatus = queryParams.passStatus.toString(); + } + // set current park & facility keys + // don't include a facility or passtype in the url if it doesn't belong to a park + // remove parks/facilities/passtypes that do not exist. + if (queryParams.park && this.parksAndFacilities) { + let hasPark = Object.keys(this.parksAndFacilities).find((park) => + park === queryParams.park + ) + if (!hasPark) { + delete queryParams.park; + delete queryParams.facilityName; + delete queryParams.passType; + } + if (queryParams.facilityName) { + let hasFacility = Object.keys(this.parksAndFacilities[queryParams.park]?.facilities).find((facility) => + facility === queryParams.facilityName + ); + if (!hasFacility) { + delete queryParams.facilityName; + delete queryParams.passType; + } + if (queryParams.passType) { + let hasPassType = Object.keys(this.parksAndFacilities[queryParams.park]?.facilities[queryParams.facilityName]?.bookingTimes).find((passType) => + passType === queryParams.passType + ); + if (!hasPassType) { + delete queryParams.passType; + } + } + } + } + + // set cached filter params + this.dataService.setItemValue(Constants.dataIds.PASS_SEARCH_PARAMS, queryParams); + + this.router.navigate(['.'], { + relativeTo: this.route, + queryParams: queryParams, + }); + } +} diff --git a/src/app/parks-management/facility-details/passes-list/passes-list.component.html b/src/app/pass-management/pass-search/passes-list/passes-list.component.html similarity index 100% rename from src/app/parks-management/facility-details/passes-list/passes-list.component.html rename to src/app/pass-management/pass-search/passes-list/passes-list.component.html diff --git a/src/app/pass-management/pass-search/passes-list/passes-list.component.scss b/src/app/pass-management/pass-search/passes-list/passes-list.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/parks-management/facility-details/passes-list/passes-list.component.spec.ts b/src/app/pass-management/pass-search/passes-list/passes-list.component.spec.ts similarity index 100% rename from src/app/parks-management/facility-details/passes-list/passes-list.component.spec.ts rename to src/app/pass-management/pass-search/passes-list/passes-list.component.spec.ts diff --git a/src/app/parks-management/facility-details/passes-list/passes-list.component.ts b/src/app/pass-management/pass-search/passes-list/passes-list.component.ts similarity index 100% rename from src/app/parks-management/facility-details/passes-list/passes-list.component.ts rename to src/app/pass-management/pass-search/passes-list/passes-list.component.ts diff --git a/src/app/parks-management/facility-details/passes-utility-buttons/passes-utility-buttons.component.html b/src/app/pass-management/pass-search/passes-utility-buttons/passes-utility-buttons.component.html similarity index 100% rename from src/app/parks-management/facility-details/passes-utility-buttons/passes-utility-buttons.component.html rename to src/app/pass-management/pass-search/passes-utility-buttons/passes-utility-buttons.component.html diff --git a/src/app/parks-management/facility-details/passes-utility-buttons/passes-utility-buttons.component.scss b/src/app/pass-management/pass-search/passes-utility-buttons/passes-utility-buttons.component.scss similarity index 100% rename from src/app/parks-management/facility-details/passes-utility-buttons/passes-utility-buttons.component.scss rename to src/app/pass-management/pass-search/passes-utility-buttons/passes-utility-buttons.component.scss diff --git a/src/app/parks-management/facility-details/passes-utility-buttons/passes-utility-buttons.component.spec.ts b/src/app/pass-management/pass-search/passes-utility-buttons/passes-utility-buttons.component.spec.ts similarity index 100% rename from src/app/parks-management/facility-details/passes-utility-buttons/passes-utility-buttons.component.spec.ts rename to src/app/pass-management/pass-search/passes-utility-buttons/passes-utility-buttons.component.spec.ts diff --git a/src/app/parks-management/facility-details/passes-utility-buttons/passes-utility-buttons.component.ts b/src/app/pass-management/pass-search/passes-utility-buttons/passes-utility-buttons.component.ts similarity index 100% rename from src/app/parks-management/facility-details/passes-utility-buttons/passes-utility-buttons.component.ts rename to src/app/pass-management/pass-search/passes-utility-buttons/passes-utility-buttons.component.ts diff --git a/src/app/services/facility.service.ts b/src/app/services/facility.service.ts index 7bb792ca..ae1cb2a7 100644 --- a/src/app/services/facility.service.ts +++ b/src/app/services/facility.service.ts @@ -208,6 +208,11 @@ export class FacilityService { // Get facility from cache using provided key. getCachedFacility(key) { const orcs = key?.pk.split('::')[1]; - return this.dataService.getItemValue(Constants.dataIds.PARK_AND_FACILITY_LIST)[orcs].facilities[key?.sk] || null; + let listData = this.dataService.getItemValue(Constants.dataIds.PARK_AND_FACILITY_LIST) + if (listData) { + return listData[orcs].facilities[key?.sk] || null; + } else { + return null; + } } } diff --git a/src/app/services/reservation.service.spec.ts b/src/app/services/reservation.service.spec.ts index bf1de663..cdce0fa4 100644 --- a/src/app/services/reservation.service.spec.ts +++ b/src/app/services/reservation.service.spec.ts @@ -22,6 +22,9 @@ describe('ReservationService', () => { getCurrentFacility: () => { return MockData.mockFacility_1; }, + getCachedFacility: () => { + return MockData.mockFacility_1; + }, } let mockApiService = { @@ -154,8 +157,10 @@ describe('ReservationService', () => { }); it('sets the capacity bar if reservation object does not exist', async () => { - const curFacilitySpy = spyOn(service['facilityService'], 'getCurrentFacility').and.callThrough(); + const curFacilitySpy = spyOn(service['facilityService'], 'getCachedFacility').and.callThrough(); await service.fetchData('noResObj', 'Mock Facility 1', '2022-12-29', 'AM'); + // we have to trick the subscriber + service['dataService'].watchItem(Constants.dataIds.PARK_AND_FACILITY_LIST).next('haha got you') expect(loadingSpy).toHaveBeenCalledTimes(1); expect(loggerDebugSpy).toHaveBeenCalledTimes(1); expect(apiGetSpy).toHaveBeenCalledOnceWith('reservation', { diff --git a/src/app/services/reservation.service.ts b/src/app/services/reservation.service.ts index e1135d0b..571db17e 100644 --- a/src/app/services/reservation.service.ts +++ b/src/app/services/reservation.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { firstValueFrom } from 'rxjs'; +import { Subject, firstValueFrom, takeUntil } from 'rxjs'; import { Constants } from '../shared/utils/constants'; import { ApiService } from './api.service'; import { DataService } from './data.service'; @@ -21,7 +21,7 @@ export class ReservationService { private apiService: ApiService, private loadingService: LoadingService, private facilityService: FacilityService - ) {} + ) { } async fetchData(parkSk, facilitySk, resDate = null, selectedPassType = null) { let res; @@ -50,18 +50,27 @@ export class ReservationService { this.setCapacityBar(res[0], selectedPassType); } else { // No res Object. Use facility cap - const facility = this.facilityService.getCurrentFacility(); - this.dataService.setItemValue( - Constants.dataIds.CURRENT_CAPACITY_BAR_OBJECT, - { - capPercent: 0, - reserved: 0, - capacity: facility.bookingTimes[selectedPassType].max, - modifier: 0, - overbooked: 0, - style: 'success', - } - ); + // If this is an initial load we have to make sure we wait until the facility is available. + const facilityReady = new Subject(); + this.dataService.watchItem(Constants.dataIds.PARK_AND_FACILITY_LIST) + .pipe(takeUntil(facilityReady)) + .subscribe((res) => { + if (res) { + const facility = this.facilityService.getCachedFacility({ pk: `facility::${parkSk}`, sk: facilitySk }); + this.dataService.setItemValue( + Constants.dataIds.CURRENT_CAPACITY_BAR_OBJECT, + { + capPercent: 0, + reserved: 0, + capacity: facility?.bookingTimes[selectedPassType].max, + modifier: 0, + overbooked: 0, + style: 'success', + } + ); + facilityReady.next(null); + } + }); } } else { // Set to initialized but invalid state diff --git a/src/app/shared/components/nav-card/nav-card.component.spec.ts b/src/app/shared/components/nav-card/nav-card.component.spec.ts index 1f33c933..893a94ae 100644 --- a/src/app/shared/components/nav-card/nav-card.component.spec.ts +++ b/src/app/shared/components/nav-card/nav-card.component.spec.ts @@ -25,6 +25,6 @@ describe('NavCardComponent', () => { it('navigates', async () => { const navSpy = spyOn(component['router'], 'navigate'); component.navigate('mock'); - expect(navSpy).toHaveBeenCalledOnceWith(['/mock']); + expect(navSpy).toHaveBeenCalledOnceWith(['mock']); }); }); diff --git a/src/app/shared/components/nav-card/nav-card.component.ts b/src/app/shared/components/nav-card/nav-card.component.ts index fe2b71dc..4d951ab4 100644 --- a/src/app/shared/components/nav-card/nav-card.component.ts +++ b/src/app/shared/components/nav-card/nav-card.component.ts @@ -11,12 +11,17 @@ export class NavCardComponent implements OnInit { @Input() cardTitle; @Input() cardText; @Input() navigation; + @Input() relative: boolean = false; - constructor(private router: Router, private route: ActivatedRoute) {} + constructor(private router: Router, private route: ActivatedRoute) { } - ngOnInit(): void {} + ngOnInit(): void { } navigate(nav) { - this.router.navigate(['/' + nav]); + if (this.relative) { + this.router.navigate([nav], { relativeTo: this.route }); + } else { + this.router.navigate([nav]); + } } } diff --git a/src/app/shared/utils/mock-data.ts b/src/app/shared/utils/mock-data.ts index 9b599bb1..9b6f895b 100644 --- a/src/app/shared/utils/mock-data.ts +++ b/src/app/shared/utils/mock-data.ts @@ -129,18 +129,21 @@ export class MockData { }; public static readonly mockPartialPassFilters_1 = { - passType: 'Trail', - passStatus: 'reserved', + passType: 'AM', + facilityName: 'Mock Facility 1', + park: 'MOC1' }; public static readonly mockFullPassFilters_1 = { - passType: 'Trail', + passType: 'AM', date: '2022-12-19', passStatus: 'reserved', firstName: 'firstName', lastName: 'lastName', email: 'mock@email.ca', reservationNumber: '1234567890', + park: 'MOC1', + facilityName: 'Mock Facility 1' }; public static readonly mockPass_1 = { @@ -271,132 +274,132 @@ export class MockData { } public static readonly mockMetrics1 = { - lastUpdated: '2023-03-28T10:09:37.713-07:00', - sk: '2023-01-31', - cancelled: 0, - capacities: { - AM: { - baseCapacity: 14, - capacityModifier: 0, - checkedIn: 5, - passStatuses: { - active: 5, - reserved: 3, - expired: 1, - cancelled: 4, - }, - availablePasses: 5, - overbooked: 0 - }, - PM: { - baseCapacity: 10, - capacityModifier: 0, - checkedIn: 0, - passStatuses: {}, - availablePasses: 10, - overbooked: 0 - } - }, - pk: 'metrics::MOC1::Mock Facility 1', - totalPasses: 9, - fullyBooked: false, - hourlyData: [ - { - hour: 0, - checkedIn: 0 - }, - { - hour: 1, - checkedIn: 0 - }, - { - hour: 2, - checkedIn: 0 - }, - { - hour: 3, - checkedIn: 0 - }, - { - hour: 4, - checkedIn: 0 - }, - { - hour: 5, - checkedIn: 0 - }, - { - hour: 6, - checkedIn: 0 - }, - { - hour: 7, - checkedIn: 0 - }, - { - hour: 8, - checkedIn: 0 - }, - { - hour: 9, - checkedIn: 5 - }, - { - hour: 10, - checkedIn: 0 - }, - { - hour: 11, - checkedIn: 0 - }, - { - hour: 12, - checkedIn: 0 - }, - { - hour: 13, - checkedIn: 0 - }, - { - hour: 14, - checkedIn: 0 - }, - { - hour: 15, - checkedIn: 0 - }, - { - hour: 16, - checkedIn: 0 - }, - { - hour: 17, - checkedIn: 0 - }, - { - hour: 18, - checkedIn: 0 - }, - { - hour: 19, - checkedIn: 0 - }, - { - hour: 20, - checkedIn: 0 - }, - { - hour: 21, - checkedIn: 0 - }, - { - hour: 22, - checkedIn: 0 - }, - { - hour: 23, - checkedIn: 0 - } - ] + lastUpdated: '2023-03-28T10:09:37.713-07:00', + sk: '2023-01-31', + cancelled: 0, + capacities: { + AM: { + baseCapacity: 14, + capacityModifier: 0, + checkedIn: 5, + passStatuses: { + active: 5, + reserved: 3, + expired: 1, + cancelled: 4, + }, + availablePasses: 5, + overbooked: 0 + }, + PM: { + baseCapacity: 10, + capacityModifier: 0, + checkedIn: 0, + passStatuses: {}, + availablePasses: 10, + overbooked: 0 + } + }, + pk: 'metrics::MOC1::Mock Facility 1', + totalPasses: 9, + fullyBooked: false, + hourlyData: [ + { + hour: 0, + checkedIn: 0 + }, + { + hour: 1, + checkedIn: 0 + }, + { + hour: 2, + checkedIn: 0 + }, + { + hour: 3, + checkedIn: 0 + }, + { + hour: 4, + checkedIn: 0 + }, + { + hour: 5, + checkedIn: 0 + }, + { + hour: 6, + checkedIn: 0 + }, + { + hour: 7, + checkedIn: 0 + }, + { + hour: 8, + checkedIn: 0 + }, + { + hour: 9, + checkedIn: 5 + }, + { + hour: 10, + checkedIn: 0 + }, + { + hour: 11, + checkedIn: 0 + }, + { + hour: 12, + checkedIn: 0 + }, + { + hour: 13, + checkedIn: 0 + }, + { + hour: 14, + checkedIn: 0 + }, + { + hour: 15, + checkedIn: 0 + }, + { + hour: 16, + checkedIn: 0 + }, + { + hour: 17, + checkedIn: 0 + }, + { + hour: 18, + checkedIn: 0 + }, + { + hour: 19, + checkedIn: 0 + }, + { + hour: 20, + checkedIn: 0 + }, + { + hour: 21, + checkedIn: 0 + }, + { + hour: 22, + checkedIn: 0 + }, + { + hour: 23, + checkedIn: 0 + } + ] } }