diff --git a/src/app/pass-management/pass-check-in-list/pass-check-in-list.component.spec.ts b/src/app/pass-management/pass-check-in-list/pass-check-in-list.component.spec.ts index c1c5b68d..cba9bf08 100644 --- a/src/app/pass-management/pass-check-in-list/pass-check-in-list.component.spec.ts +++ b/src/app/pass-management/pass-check-in-list/pass-check-in-list.component.spec.ts @@ -94,6 +94,7 @@ describe('PassLookupComponent', () => { it('should check in pass', async () => { const checkedOutPass = { ...MockData.mockPass_1 }; + delete checkedOutPass.checkedIn checkedOutPass.passStatus = 'active'; component.passes = [{ ...MockData.mockPass_1 }]; await component.checkIn(checkedOutPass); 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 index 0ecfbf81..374cc067 100644 --- 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 @@ -4,7 +4,7 @@

Pass Management

-
+
diff --git a/src/app/pass-management/pass-management.module.ts b/src/app/pass-management/pass-management.module.ts index 255d860f..3a35f033 100644 --- a/src/app/pass-management/pass-management.module.ts +++ b/src/app/pass-management/pass-management.module.ts @@ -23,6 +23,7 @@ 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'; +import { PassAccordionComponent } from './pass-search/passes-list/pass-accordion/pass-accordion.component'; @NgModule({ declarations: [ @@ -37,7 +38,8 @@ import { BsModalService } from 'ngx-bootstrap/modal'; PassesFilterComponent, PassesListComponent, PassesUtilityButtonsComponent, - PassesFilterFieldsComponent + PassesFilterFieldsComponent, + PassAccordionComponent, ], imports: [ CommonModule, diff --git a/src/app/pass-management/pass-search/pass-search.component.html b/src/app/pass-management/pass-search/pass-search.component.html index 8601508d..57fc5ced 100644 --- a/src/app/pass-management/pass-search/pass-search.component.html +++ b/src/app/pass-management/pass-search/pass-search.component.html @@ -15,6 +15,11 @@

Search Results

+
+

{{currentPark}}: {{currentFacility}} ({{currentPassType}})

+

{{currentDate | date: 'mediumDate'}}

+
@@ -22,7 +27,7 @@

Search Results

-
+
\ No newline at end of file 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 index 97999e1f..6f2159d8 100644 --- a/src/app/pass-management/pass-search/pass-search.component.spec.ts +++ b/src/app/pass-management/pass-search/pass-search.component.spec.ts @@ -38,18 +38,4 @@ describe('PassSearchComponent', () => { 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 index fb9b07b8..2577e1eb 100644 --- a/src/app/pass-management/pass-search/pass-search.component.ts +++ b/src/app/pass-management/pass-search/pass-search.component.ts @@ -3,6 +3,7 @@ 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 { ParkService } from 'src/app/services/park.service'; import { Constants } from 'src/app/shared/utils/constants'; import { Utils } from 'src/app/shared/utils/utils'; @@ -13,61 +14,32 @@ import { Utils } from 'src/app/shared/utils/utils'; }) export class PassSearchComponent { private subscriptions = new Subscription(); - public facility; public Utils = new Utils(); + public currentFacility; + public currentPark; + public currentDate; + public currentPassType; constructor( protected dataService: DataService, protected configService: ConfigService, - protected facilityService: FacilityService + protected facilityService: FacilityService, + protected parkService: ParkService ) { this.subscriptions.add( dataService - .watchItem(Constants.dataIds.CURRENT_FACILITY_KEY) + .watchItem(Constants.dataIds.PASS_SEARCH_PARAMS) .subscribe((res) => { if (res) { - this.facility = this.facilityService.getCachedFacility(res); + this.currentFacility = res.facilityName; + this.currentPark = this.parkService.getCachedPark({pk: 'park', sk: res.park})?.name; + this.currentDate = res.date; + this.currentPassType = res.passType; } }) ); } - 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/pass-management/pass-search/passes-capacity-bar/passes-capacity-bar.component.html b/src/app/pass-management/pass-search/passes-capacity-bar/passes-capacity-bar.component.html index f8ca0a18..315333b1 100644 --- a/src/app/pass-management/pass-search/passes-capacity-bar/passes-capacity-bar.component.html +++ b/src/app/pass-management/pass-search/passes-capacity-bar/passes-capacity-bar.component.html @@ -20,15 +20,17 @@ {{ data.overbooked }} passes overbooked
-
- Modifier applied +
+ {{data?.checkInCount ? data.checkInCount : '0'}} pass{{data?.checkInCount === 1 ? '' : 'es'}} checked in
+
+ Modifier applied +
- -
+
+ + +
+ \ No newline at end of file diff --git a/src/app/pass-management/pass-search/passes-capacity-bar/passes-capacity-bar.component.scss b/src/app/pass-management/pass-search/passes-capacity-bar/passes-capacity-bar.component.scss index e69de29b..358193e8 100644 --- a/src/app/pass-management/pass-search/passes-capacity-bar/passes-capacity-bar.component.scss +++ b/src/app/pass-management/pass-search/passes-capacity-bar/passes-capacity-bar.component.scss @@ -0,0 +1,10 @@ +.capacity-bar-top { + border-bottom-left-radius: 0 !important; + border-bottom-right-radius: 0 !important; +} + +.capacity-bar-bottom { + border-top: 1px solid #ccc; + border-top-left-radius: 0 !important; + border-top-right-radius: 0 !important; +} \ No newline at end of file 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 index 6aee6904..2019ddb7 100644 --- 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 @@ -13,6 +13,7 @@ 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'; +import { Subject, takeUntil } from 'rxjs'; @Component({ selector: 'app-passes-filter', @@ -195,7 +196,14 @@ export class PassesFilterComponent extends BaseFormComponent { // If there are mandatory params in the url, search for them now if (this.searchOnPageLoadFlag && this.checkFieldsForSubmission()) { - this.onSubmit(); + // wait for parksAndFacilities to be loaded + let autofetchReady = new Subject(); + this.dataService.watchItem(Constants.dataIds.PARK_AND_FACILITY_LIST).pipe( + takeUntil(autofetchReady) + ).subscribe(() => { + this.onSubmit(); + autofetchReady.complete(); + }); } this.searchOnPageLoadFlag = false; } @@ -284,7 +292,7 @@ export class PassesFilterComponent extends BaseFormComponent { res.fields?.date || null, res.fields?.passType || null ); - this.updateUrl(); + this.dataService.setItemValue(Constants.dataIds.PASS_SEARCH_PARAMS, this.updateUrl()); } } @@ -325,12 +333,11 @@ export class PassesFilterComponent extends BaseFormComponent { } } - // set cached filter params - this.dataService.setItemValue(Constants.dataIds.PASS_SEARCH_PARAMS, queryParams); - this.router.navigate(['.'], { relativeTo: this.route, queryParams: queryParams, }); + + return queryParams; } } diff --git a/src/app/pass-management/pass-search/passes-list/pass-accordion/pass-accordion.component.html b/src/app/pass-management/pass-search/passes-list/pass-accordion/pass-accordion.component.html new file mode 100644 index 00000000..7355c043 --- /dev/null +++ b/src/app/pass-management/pass-search/passes-list/pass-accordion/pass-accordion.component.html @@ -0,0 +1,62 @@ +
+
+
+
+ {{col?.displayHeader}} +
+
+
+ +
+
+
+
+
+
+
+
+ {{col?.display(pass)}} +
+
+ + +
+
+ {{pass[col?.key]}} +
+
+
+ + +
+
+
+
+
+ {{col?.displayHeader}} +
+ {{col?.display(pass)}} +
+
+ + +
+
+ {{pass[col?.key]}} +
+
+
+
+
+
+
+
+ +
+
+
\ No newline at end of file diff --git a/src/app/pass-management/pass-search/passes-list/pass-accordion/pass-accordion.component.scss b/src/app/pass-management/pass-search/passes-list/pass-accordion/pass-accordion.component.scss new file mode 100644 index 00000000..afc44d8b --- /dev/null +++ b/src/app/pass-management/pass-search/passes-list/pass-accordion/pass-accordion.component.scss @@ -0,0 +1,3 @@ +.accordion-item { + cursor: pointer; +} \ No newline at end of file diff --git a/src/app/pass-management/pass-search/passes-list/pass-accordion/pass-accordion.component.spec.ts b/src/app/pass-management/pass-search/passes-list/pass-accordion/pass-accordion.component.spec.ts new file mode 100644 index 00000000..58a48036 --- /dev/null +++ b/src/app/pass-management/pass-search/passes-list/pass-accordion/pass-accordion.component.spec.ts @@ -0,0 +1,115 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PassAccordionComponent } from './pass-accordion.component'; + +describe('PassAccordionComponent', () => { + let component: PassAccordionComponent; + let fixture: ComponentFixture; + + let mockRowSchema = [ + { + id: 'passId', + dropdown: 0, + key: 'registrationNumber' // value of pass object associated with column + }, + { + id: 'numberOfGuests', + dropdown: 4, + key: 'numberOfGuests' + }, + { + id: 'date', + dropdown: 0, + key: 'shortPassDate' + }, + { + id: 'email', + dropdown: 3, + key: 'email' + }, + { + id: 'name', + dropdown: 5, + key: 'lastName', + }, + { + id: 'status', + dropdown: 2, + key: 'passStatus', + }, + { + id: 'checkedIn', + dropdown: 2, + key: 'checkedIn', + }, + { + id: 'isOverbooked', + dropdown: 3, + key: 'isOverbooked', + }, + { + id: 'park', + dropdown: 7, + key: 'parkName', + }, + { + id: 'facility', + dropdown: 7, + key: 'facilityName', + }, + { + id: 'passType', + dropdown: 7, + key: 'type', + }, + { + id: 'checkInTime', + dropdown: 7, + key: 'checkedInTime', + }, + ]; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ PassAccordionComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PassAccordionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('returns current screen size', async() => { + // breakpoints have added buffer to ensure breakage before actually breakpoint is reached + let widthSpy = spyOnProperty(window, 'innerWidth').and.returnValue(360); //mobile + expect(component.getCurrentScreenSize()).toEqual(0); + widthSpy.and.returnValue(600); + expect(component.getCurrentScreenSize()).toEqual(1); + widthSpy.and.returnValue(800); + expect(component.getCurrentScreenSize()).toEqual(2); + widthSpy.and.returnValue(850); + expect(component.getCurrentScreenSize()).toEqual(3); + widthSpy.and.returnValue(1050); + expect(component.getCurrentScreenSize()).toEqual(4); + widthSpy.and.returnValue(1300); + expect(component.getCurrentScreenSize()).toEqual(5); + widthSpy.and.returnValue(1500); + expect(component.getCurrentScreenSize()).toEqual(6); + }); + + it('sets accordion header and dropdown columns based on screensize', async() => { + component.rowSchema = mockRowSchema; + // header + dropdown column count should always be 12 + let widthSpy = spyOn(component, 'getCurrentScreenSize').and.returnValue(0); + expect(component.getHeaderColumns().length).toEqual(2); + expect(component.getDropdownColumns().length).toEqual(10); + widthSpy.and.returnValue(6); + expect(component.getHeaderColumns().length).toEqual(8); + expect(component.getDropdownColumns().length).toEqual(4); + }); +}); diff --git a/src/app/pass-management/pass-search/passes-list/pass-accordion/pass-accordion.component.ts b/src/app/pass-management/pass-search/passes-list/pass-accordion/pass-accordion.component.ts new file mode 100644 index 00000000..a95ef7e1 --- /dev/null +++ b/src/app/pass-management/pass-search/passes-list/pass-accordion/pass-accordion.component.ts @@ -0,0 +1,55 @@ +import { Component, Input, TemplateRef } from '@angular/core'; + +@Component({ + selector: 'app-pass-accordion', + templateUrl: './pass-accordion.component.html', + styleUrls: ['./pass-accordion.component.scss'] +}) + +export class PassAccordionComponent { + @Input() rowSchema: any[] = []; + @Input() dropDownSchema: any[] = []; + @Input() cancelTemplate: TemplateRef; + @Input() pass: any; + @Input() isHeader: boolean = false; + + getHeaderColumns() { + let columns = []; + let size = this.getCurrentScreenSize(); + if (this.rowSchema) { + columns = this.rowSchema.filter(column => column?.dropdown <= size); + } + return columns; + } + + getDropdownColumns() { + let columns = []; + let size = this.getCurrentScreenSize(); + if (this.rowSchema) { + columns = this.rowSchema.filter(column => column?.dropdown > size); + } + return columns; + } + + getCurrentScreenSize() { + // TODO: breakpoints are hard-coded. Can we load these straight from our bootstrap theme? + // Note: The breakpoints are slightly greater than the bootstrap set breakpoints to ensure no overflows + const buffer = 40; + const size = window.innerWidth; + if (size < 400 + buffer) { + return 0; + } else if (size < 576 + buffer) { + return 1; + } else if (size <= 768 + buffer) { + return 2; + } else if (size <= 992 + buffer) { + return 3; + } else if (size <= 1200 + buffer) { + return 4; + } else if (size <= 1400 + buffer) { + return 5; + } else { + return 6; + } + } +} diff --git a/src/app/pass-management/pass-search/passes-list/passes-list.component.html b/src/app/pass-management/pass-search/passes-list/passes-list.component.html index 6ddcc501..302d18b2 100644 --- a/src/app/pass-management/pass-search/passes-list/passes-list.component.html +++ b/src/app/pass-management/pass-search/passes-list/passes-list.component.html @@ -1,17 +1,103 @@ - + +
+
+

+ {{state?.label}} +

+
+
- - - - - + +
+
+ +
+
+
+ No passes to display. +
+
+ +
+
+
+ +
+
+ + +
+ +
+ + + + + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ {{pass?.passStatus}} +
+
+
+ + + +
+
+ +
+ Overbooked +
+
+
+
+ + + +
+
+
+ +
+
+ +
+
+ +
+
+
+
+ + + + + \ No newline at end of file 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 index e69de29b..6131d78f 100644 --- 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 @@ -0,0 +1,3 @@ +.filter-button { + cursor: pointer; +} \ No newline at end of file diff --git a/src/app/pass-management/pass-search/passes-list/passes-list.component.spec.ts b/src/app/pass-management/pass-search/passes-list/passes-list.component.spec.ts index e26cc2e8..68606214 100644 --- a/src/app/pass-management/pass-search/passes-list/passes-list.component.spec.ts +++ b/src/app/pass-management/pass-search/passes-list/passes-list.component.spec.ts @@ -50,6 +50,9 @@ describe('PassesListComponent', () => { mergeItemValue: (id, params) => { return new BehaviorSubject(null); }, + setItemValue: (id, params) => { + return new BehaviorSubject(null); + } }; let mockKeyCloakService = { @@ -86,64 +89,21 @@ describe('PassesListComponent', () => { expect(component.tableRows.length).toEqual(2); expect(component.lastEvaluatedKey).toBeDefined(); // seven columns if cancellation allowed. - const row = component.tableSchema.columns; - expect(row.length).toEqual(7); + const row = component.rowSchema; + expect(row.length).toEqual(12); // check row values; - expect(row[0].mapValue(mockPass1)).toEqual('1234567890'); - expect(row[1].mapValue(mockPass1)).toEqual('mock@email.ca'); - expect(row[2].mapValue(mockPass1)).toEqual(4); - expect(row[3].mapValue(mockPass1)).toEqual('2022-12-18'); - expect(row[4].mapValue(mockPass1)).toEqual('expired'); - expect(row[5].mapValue(mockPass1)).toBeNull(); - expect(row[6].mapValue(mockPass1)).toBeNull(); - expect(row[6].cellTemplate(mockPass1)).toBeDefined(); - }); - - it('displays modals when clicked', async () => { - // Information modal - const modalServiceSpy = spyOn( - component['modalService'], - 'show' - ).and.callThrough(); - const passModalConstructorSpy = spyOn(component, 'constructPassModalBody'); - const passModalSpy = spyOn(component, 'displayPassModal').and.callThrough(); - const passModalFn = component.tableSchema.rowClick(mockPass1); - passModalFn(); - expect(passModalSpy).toHaveBeenCalledTimes(1); - expect(modalServiceSpy).toHaveBeenCalledTimes(1); - expect(component.passModalRef).toBeDefined(); - expect(passModalConstructorSpy).toHaveBeenCalledTimes(1); - // Cancellation Modal - modalServiceSpy.calls.reset(); - const cancelModalSpy = spyOn( - component, - 'displayCancelModal' - ).and.callThrough(); - const cancelModalConstructorSpy = spyOn( - component, - 'constructCancelPassModal' - ); - const cancelModalTemplate = - component.tableSchema.columns[6].cellTemplate(mockPass1); - cancelModalTemplate.inputs.onClick(); - expect(cancelModalSpy).toHaveBeenCalledTimes(1); - expect(component.cancelModalRef).toBeDefined(); - expect(modalServiceSpy).toHaveBeenCalledTimes(1); - expect(cancelModalConstructorSpy).toHaveBeenCalledTimes(1); - }); - - it('creates pass modal', async () => { - // Overkill to check every field as the biz req's change often. - // Check conditional fields & modal functionality instead. - component.displayPassModal(mockPass1); - const message = component.passModal.body; - let body = document.createElement('div'); - body.innerHTML = message; - expect(body.innerText).not.toContain('Phone Number'); - expect(body.innerText).not.toContain('License Plate'); - const modalHideSpy = spyOn(component.passModalRef, 'hide'); - component.passModal.buttons[0].onClick(); - expect(modalHideSpy).toHaveBeenCalledTimes(1); + expect(mockPass1[row[0].key]).toEqual('1234567890'); // pass id + expect(mockPass1[row[1].key]).toEqual(4); // number of guests + expect(mockPass1[row[2].key]).toEqual('2022-12-18'); // date + expect(mockPass1[row[3].key]).toEqual('mock@email.ca'); // email + expect(row[4].display(mockPass1)).toEqual('FirstName LastName'); // name + expect(mockPass1[row[5].key]).toEqual('expired'); // status + expect(mockPass1[row[6].key]).toBeTrue(); // checked in + expect(mockPass1[row[7].key]).toBeFalse(); // is overbooked + expect(mockPass1[row[8].key]).toEqual('Mock Park 1'); // park + expect(mockPass1[row[9].key]).toEqual('Mock Facility 1'); // facility + expect(mockPass1[row[10].key]).toEqual('PM'); // pass type + expect(row[11].display(mockPass1)).toEqual('12/18/2022, 11:00 AM'); // checkinTime }); it('creates cancel modal', async () => { @@ -179,4 +139,45 @@ describe('PassesListComponent', () => { ); expect(fetchPassSpy).toHaveBeenCalledTimes(1); }); + + it('adapts the column widths', async () => { + let widthSpy = spyOn(component, 'setWidth'); + component.ngAfterViewInit(); + await fixture.whenStable(); + expect(widthSpy).toHaveBeenCalledTimes(1); + window.dispatchEvent(new Event('resize')); + expect(widthSpy).toHaveBeenCalledTimes(2); + }); + + it('updates the capacity bar with checked-in count', async () => { + let dataSpy = spyOn(component['dataService'], 'mergeItemValue'); + component.passes = [MockData.mockPass_1]; + component.updateCapacityBarCheckIns(); + await fixture.whenStable(); + expect(dataSpy).toHaveBeenCalledWith( + Constants.dataIds.CURRENT_CAPACITY_BAR_OBJECT, { + checkInCount: 4 + }) + }); + + it('formatsCheckedInTime', async () => { + expect(component.formatCheckedInTime({...mockPass1})).toEqual('12/18/2022, 11:00 AM'); + expect(component.formatCheckedInTime(null)).toEqual('N/A'); + }); + + it('filters pass list by checked-in status', async () => { + component.passes = [{...MockData.mockPass_1}]; + component.changeCheckInState({ value: 'checkedIn' }); + expect(component.tableRows.length).toEqual(1); + expect(component.checkedInState).toEqual('checkedIn'); + component.changeCheckInState({ value: 'notCheckedIn' }); + expect(component.tableRows.length).toEqual(0); + expect(component.checkedInState).toEqual('notCheckedIn'); + component.changeCheckInState({ value: 'all' }); + expect(component.tableRows.length).toEqual(1); + expect(component.checkedInState).toEqual('all'); + component.changeCheckInState(undefined); + expect(component.tableRows.length).toEqual(1); + expect(component.checkedInState).toEqual('all'); + }); }); diff --git a/src/app/pass-management/pass-search/passes-list/passes-list.component.ts b/src/app/pass-management/pass-search/passes-list/passes-list.component.ts index c301cea2..3fb89473 100644 --- a/src/app/pass-management/pass-search/passes-list/passes-list.component.ts +++ b/src/app/pass-management/pass-search/passes-list/passes-list.component.ts @@ -1,21 +1,22 @@ import { + ChangeDetectorRef, Component, + HostListener, OnDestroy, OnInit, TemplateRef, ViewChild, - ViewContainerRef, } from '@angular/core'; import { Subscription } from 'rxjs'; import { DataService } from 'src/app/services/data.service'; import { PassService } from 'src/app/services/pass.service'; -import { TableButtonComponent } from 'src/app/shared/components/table/table-components/table-button/table-button.component'; -import { TableIconComponent } from 'src/app/shared/components/table/table-components/table-icon/table-icon.component'; import { tableSchema } from 'src/app/shared/components/table/table.component'; import { Constants } from 'src/app/shared/utils/constants'; import { BsModalService, BsModalRef } from 'ngx-bootstrap/modal'; import { modalSchema } from 'src/app/shared/components/modal/modal.component'; import { KeycloakService } from 'src/app/services/keycloak.service'; +import { DateTime } from 'luxon'; +import { LoadingService } from 'src/app/services/loading.service'; @Component({ selector: 'app-passes-list', @@ -25,26 +26,51 @@ import { KeycloakService } from 'src/app/services/keycloak.service'; export class PassesListComponent implements OnInit, OnDestroy { private subscriptions = new Subscription(); public tableSchema: tableSchema; + public passes: any[] = []; public tableRows: any[] = []; public passModal: modalSchema; public cancelModal: modalSchema; public passModalRef: BsModalRef; public cancelModalRef: BsModalRef; public lastEvaluatedKey = null; + public rowSchema: any[]; + public dropDownSchema: any[]; + public checkedInStates: any[] = [{ value: 'all', label: 'All Passes' }, { value: 'checkedIn', label: 'Checked-in' }, { value: 'notCheckedIn', label: 'Not Checked-in' }]; + public checkedInState; + public loading; @ViewChild('passModalTemplate') passModalTemplate: TemplateRef; @ViewChild('cancelModalTemplate') cancelModalTemplate: TemplateRef; + @ViewChild('passStatusTemplate') passStatusTemplate: TemplateRef; + @ViewChild('passCheckedInTemplate') passCheckedInTemplate: TemplateRef; + @ViewChild('rowTemplate') rowTemplate: TemplateRef; + @ViewChild('passCancelButtonTemplate') passCancelButtonTemplate: TemplateRef; + @ViewChild('passIsOverbookedTemplate') passIsOverbookedTemplate: TemplateRef; + + @HostListener('window:resize', ['$event']) + onResize(event) { + this.setWidth(); + } constructor( protected dataService: DataService, protected keyCloakService: KeycloakService, protected passService: PassService, protected modalService: BsModalService, - protected vcr: ViewContainerRef + protected loadingService: LoadingService, + protected cd: ChangeDetectorRef, ) { this.subscriptions.add( dataService.watchItem(Constants.dataIds.PASSES_LIST).subscribe((res) => { - this.tableRows = res; + if (res) { + this.passes = res; + this.changeCheckInState({ value: this.checkedInState }, true); + } + }) + ); + this.subscriptions.add( + loadingService.getLoadingStatus().subscribe((res) => { + this.loading = res; }) ); this.subscriptions.add( @@ -57,7 +83,179 @@ export class PassesListComponent implements OnInit, OnDestroy { } ngOnInit(): void { - this.createTable(); + this.checkedInState = 'all' + } + + setWidth() { + let columns = [...this.rowSchema]; + // don't forget about the cancel button column + columns.push({ id: 'cancelButton' }); + // get groups of column ids + for (const col of columns) { + const columns = document.querySelectorAll(`#${col.id}`); + let maxWidth = 0; + for (let i = 0; i < columns.length; i++) { + maxWidth = Math.max(maxWidth, columns[i].scrollWidth); + } + for (let i = 0; i < columns.length; i++) { + columns[i].style.width = maxWidth + 'px'; + } + } + } + + ngAfterViewInit() { + this.rowSchema = [ + { + id: 'passId', // column identifier + displayHeader: 'Res Number', // table header display name + dropdown: 0, // when does this column collapse into the dropdown? 0-never, 1-mobile, 2 `${pass.firstName} ${pass.lastName}` + }, + { + id: 'status', + displayHeader: 'Status', + columnClasses: 'col-auto', + dropdown: 2, + grow: false, + key: 'passStatus', + template: this.passStatusTemplate + }, + { + id: 'checkedIn', + dropdown: 2, + columnClasses: 'col-auto', + grow: false, + displayHeader: 'Checked-In', + key: 'checkedIn', + template: this.passCheckedInTemplate + }, + { + id: 'isOverbooked', + dropdown: 3, + columnClasses: 'col-auto', + grow: false, + key: 'isOverbooked', + template: this.passIsOverbookedTemplate + }, + { + id: 'park', + displayHeader: 'Park', + grow: false, + dropdown: 7, + key: 'parkName', + }, + { + id: 'facility', + displayHeader: 'Facility', + grow: false, + dropdown: 7, + key: 'facilityName', + }, + { + id: 'passType', + displayHeader: 'Pass Type', + grow: false, + dropdown: 7, + key: 'type', + }, + { + id: 'checkInTime', + displayHeader: 'Check-in Time', + grow: false, + dropdown: 7, + key: 'checkedInTime', + display: (pass) => this.formatCheckedInTime(pass), + }, + ]; + this.setWidth(); + this.cd.detectChanges(); + } + + ngAfterViewChecked() { + this.setWidth(); + } + + changeCheckInState(state, forceUpdate = false) { + this.loadingService.addToFetchList(Constants.dataIds.FILTERED_PASSES_LIST); + this.tableRows = this.passes; + if (state === undefined) { + state = this.checkedInStates[0]; + this.checkedInState = state.value + } + if (state?.value !== this.checkedInState || forceUpdate === true) { + this.checkedInState = state.value; + // filter current passList + if (this.passes?.length) { + switch (state.value) { + case 'checkedIn': + this.tableRows = []; + for (const row of this.passes) { + if (row?.checkedIn) { + this.tableRows.push(row); + } + } + break; + case 'notCheckedIn': + this.tableRows = []; + for (const row of this.passes) { + if (!row?.checkedIn || row?.checkedIn === false) { + this.tableRows.push(row); + } + } + break; + case 'all': + default: + this.tableRows = this.passes; + break; + } + } + this.loadingService.removeToFetchList(Constants.dataIds.FILTERED_PASSES_LIST); + this.dataService.setItemValue(Constants.dataIds.FILTERED_PASSES_LIST, this.tableRows); + this.updateCapacityBarCheckIns(); + } + } + + formatCheckedInTime(pass) { + if (pass?.checkedInTime) { + const time = DateTime.fromISO(pass?.checkedInTime).setZone('America/Vancouver'); + return time.toLocaleString(DateTime.DATETIME_SHORT); + } + return 'N/A'; } cancelPass(pass) { @@ -88,58 +286,6 @@ export class PassesListComponent implements OnInit, OnDestroy { this.passService.fetchData(loadMoreObj); } - displayPassModal(passObj) { - const self = this; - this.passModal = { - id: 'passModal', - title: 'Pass Information', - body: this.constructPassModalBody(passObj), - buttons: [ - { - text: 'Ok', - classes: 'btn btn-primary', - onClick: function () { - self.passModalRef.hide(); - }, - }, - ], - }; - this.passModalRef = this.modalService.show(this.passModalTemplate, { - class: 'modal-lg', - }); - } - - constructPassModalBody(passObj) { - let message = `First Name:
` + passObj.firstName; - message += `

Last Name:
` + passObj.lastName; - message += `

Email:
` + passObj.email; - if (passObj.phoneNumber) { - message += - `

Phone Number:
` + passObj.phoneNumber; - } - if (passObj.facilityType === 'Parking') { - message += - `

License Plate:
` + passObj.license; - } - message += - `
Registration Number:
` + - passObj.registrationNumber; - message += - `

Facility Name:
` + passObj.facilityName; - message += `

Booking Time:
` + passObj.type; - message += `

Date:
` + passObj.shortPassDate; - message += - `

Pass Status:
` + passObj.passStatus; - if (passObj.isOverbooked === true) { - if (passObj.passStatus === 'active' || passObj.passStatus === 'reserved') { - message += `

Pass is overbooked
`; - } else { - message += `

Pass is overbooked
`; - } - } - return message; - } - displayCancelModal(passObj) { const self = this; this.cancelModal = { @@ -168,118 +314,19 @@ export class PassesListComponent implements OnInit, OnDestroy { return message; } - displayOverbookedWarning(passObj) { - if (passObj.isOverbooked === true) { - //show icon in red if pass is still actived/reserved - if ( - passObj.passStatus === 'active' || - passObj.passStatus === 'reserved' - ) { - return { - component: TableIconComponent, - inputs: { - altText: 'Overbooked', - iconClass: 'bi bi-exclamation-triangle-fill text-danger', - }, - }; - //otherwise, greyed out - } else { - return { - component: TableIconComponent, - inputs: { - altText: 'Overbooked', - iconClass: 'bi bi-exclamation-triangle-fill', - }, - }; + updateCapacityBarCheckIns() { + let checkInCount = 0; + if (this.passes) { + for (const pass of this.passes) { + if (pass.checkedIn) { + checkInCount += pass.numberOfGuests; + } } } - return { - component: TableIconComponent, - inputs: { - iconClass: '', - }, - }; + // merge the checkInCount to the current capacity bar object + this.dataService.mergeItemValue(Constants.dataIds.CURRENT_CAPACITY_BAR_OBJECT, { checkInCount: checkInCount }); } - createTable() { - - let columns = [ - { - id: 'passId', - displayHeader: 'Reg #', - columnClasses: 'ps-3 pe-3', - mapValue: (passObj) => passObj.sk, - }, - { - id: 'email', - displayHeader: 'Email', - columnClasses: 'px-3', - mapValue: (passObj) => passObj.email, - }, - { - id: 'numberOfGuests', - displayHeader: 'Guests', - columnClasses: 'px-3', - mapValue: (passObj) => passObj.numberOfGuests, - }, - { - id: 'date', - displayHeader: 'Date', - columnClasses: 'px-3', - mapValue: (passObj) => passObj.shortPassDate, - }, - { - id: 'status', - displayHeader: 'Status', - columnClasses: 'px-3', - mapValue: (passObj) => passObj.passStatus, - }, - { - id: 'overbooked', - displayHeader: '', - columnClasses: 'px-3', - mapValue: () => null, - cellTemplate: (passObj) => this.displayOverbookedWarning(passObj), - }, - ]; - - if (this.isAllowed('cancel-passes')) { - let cancelLayout = { - id: 'cancel-button', - displayHeader: 'Actions', - columnClasses: 'ps-5 pe-3', - width: '10%', - mapValue: () => null, - cellTemplate: (passObj) => { - const self = this; - return { - component: TableButtonComponent, - inputs: { - altText: 'Cancel', - buttonClass: 'btn btn-outline-danger', - iconClass: 'bi bi-x-circle-fill', - isDisabled: this.disableCancelButton(passObj), - onClick: function () { - self.displayCancelModal(passObj); - }, - }, - }; - }, - }; - columns.push(cancelLayout); - } - - this.tableSchema = { - id: 'passes-list', - rowClick: (passObj) => { - const self = this; - return function () { - self.displayPassModal(passObj); - }; - }, - columns: columns, - }; - } ngOnDestroy() { this.subscriptions.unsubscribe(); diff --git a/src/app/pass-management/pass-search/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 index 3009b20e..405b1dfd 100644 --- a/src/app/pass-management/pass-search/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 @@ -35,7 +35,7 @@ describe('PassesUtilityButtonsComponent', () => { let mockDataService = { watchItem: (id) => { - if (id === Constants.dataIds.PASSES_LIST) { + if (id === Constants.dataIds.FILTERED_PASSES_LIST) { return mockPassList; } if (id === Constants.dataIds.CURRENT_FACILITY_KEY) { diff --git a/src/app/pass-management/pass-search/passes-utility-buttons/passes-utility-buttons.component.ts b/src/app/pass-management/pass-search/passes-utility-buttons/passes-utility-buttons.component.ts index 51f5a142..03f109ce 100644 --- a/src/app/pass-management/pass-search/passes-utility-buttons/passes-utility-buttons.component.ts +++ b/src/app/pass-management/pass-search/passes-utility-buttons/passes-utility-buttons.component.ts @@ -32,7 +32,7 @@ export class PassesUtilityButtonsComponent implements OnDestroy { protected facilityService: FacilityService ) { this.subscriptions.add( - dataService.watchItem(Constants.dataIds.PASSES_LIST).subscribe((res) => { + dataService.watchItem(Constants.dataIds.FILTERED_PASSES_LIST).subscribe((res) => { this.passes = res; }) ); diff --git a/src/app/services/data.service.ts b/src/app/services/data.service.ts index b3d3e117..55e9dd08 100644 --- a/src/app/services/data.service.ts +++ b/src/app/services/data.service.ts @@ -23,7 +23,8 @@ export class DataService { // Append array data to existing dataService id appendItemValue(id, value): any[] { - if (!this.checkIfDataExists(id)) { + // We cannot concatenate an undefined object + if (!this.checkIfDataExists(id) || !this.getItemValue(id)) { this.setItemValue(id, value); return []; } else { @@ -35,7 +36,8 @@ export class DataService { // Merge object data to existing dataService id mergeItemValue(id, value, attribute = null): any { - if (!this.checkIfDataExists(id)) { + // We cannot merge to an undefined object + if (!this.checkIfDataExists(id) || !this.getItemValue(id)) { this.setItemValue(id, value); return null; } else { diff --git a/src/app/services/reservation.service.spec.ts b/src/app/services/reservation.service.spec.ts index cdce0fa4..0d9db963 100644 --- a/src/app/services/reservation.service.spec.ts +++ b/src/app/services/reservation.service.spec.ts @@ -110,6 +110,7 @@ describe('ReservationService', () => { overbooked: 0, modifier: 8, style: 'success', + checkInCount: 0, }; service.setCapacityBar(MockData.mockReservationObj_1, 'AM'); expect(setDataSpy).toHaveBeenCalledOnceWith( diff --git a/src/app/services/reservation.service.ts b/src/app/services/reservation.service.ts index 571db17e..51621710 100644 --- a/src/app/services/reservation.service.ts +++ b/src/app/services/reservation.service.ts @@ -138,6 +138,14 @@ export class ReservationService { : 0; capBarObj.style = this.calculateProgressBarColour(capBarObj.capPercent); + // get current checked-in count, if any + let currentCapBar = this.dataService.getItemValue(Constants.dataIds.CURRENT_CAPACITY_BAR_OBJECT); + if (currentCapBar?.checkInCount) { + capBarObj['checkInCount'] = currentCapBar.checkInCount; + } else { + capBarObj['checkInCount'] = 0; + } + this.dataService.setItemValue( Constants.dataIds.CURRENT_CAPACITY_BAR_OBJECT, capBarObj diff --git a/src/app/shared/utils/constants.ts b/src/app/shared/utils/constants.ts index a5090c3a..994f8141 100644 --- a/src/app/shared/utils/constants.ts +++ b/src/app/shared/utils/constants.ts @@ -6,6 +6,7 @@ export class Constants { FACILITIES_LIST: 'facilitiesList', CURRENT_FACILITY_KEY: 'currentFacilityKey', PASSES_LIST: 'passesList', + FILTERED_PASSES_LIST: 'filteredPassesList', PASS_SEARCH_PARAMS: 'passSearchParams', PASS_LAST_EVALUATED_KEY: 'passLastEvaluatedKey', CANCELLED_PASS: 'cancelledPass', diff --git a/src/app/shared/utils/mock-data.ts b/src/app/shared/utils/mock-data.ts index 9b6f895b..aba2f88a 100644 --- a/src/app/shared/utils/mock-data.ts +++ b/src/app/shared/utils/mock-data.ts @@ -166,6 +166,8 @@ export class MockData { registrationNumber: '1234567890', shortPassDate: '2022-12-18', type: 'PM', + checkedIn: true, + checkedInTime: '2022-12-18T19:00:00.000Z' }; public static readonly mockPass_2 = {