Skip to content

Commit a1d75ec

Browse files
authored
Release v2025.2.0 - Admin check-in datetime change feature (#128)
- Add ability for admins to change registration date/time during check-in - Add date-time modal component with available time slots - Enhance changeRegistrationDateTime function for admin operations - Add PROGRAM_YEAR configuration to admin app - Update firebase.json with function rewrites - Update review page UI with change date/time button Co-authored-by: Joel Meaders <joelmeaders@outlook.com>
1 parent 42e974b commit a1d75ec

File tree

15 files changed

+487
-55
lines changed

15 files changed

+487
-55
lines changed

CHANGELOG.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to a versioning scheme of `year.minor.patch`.
77

8+
## [2025.2.0] - 2025-11-15
9+
10+
### Added
11+
12+
- **Admin Check-In DateTime Change**: Admins can now change registration date/time during the check-in review process
13+
- New "Change Date/Time" button on admin check-in review page
14+
- Date/Time modal component with accordion-grouped available time slots
15+
- Real-time availability display showing remaining spots for each time slot
16+
- Confirmation dialog when changing from existing reservation
17+
- Automatic email notification sent to registrant with updated date/time
18+
- **Enhanced Firebase Functions**: Updated `changeRegistrationDateTime` function to support admin operations
19+
- Admins can change date/time for any registration (with proper permission checks)
20+
- Non-admin users can only change their own registrations
21+
- Prevents changes after check-in to maintain data integrity
22+
- **Program Year Configuration**: Added `PROGRAM_YEAR` injection token to admin application
23+
- Centralized program year management (set to 2025)
24+
- Used by date/time selection to filter available slots
25+
- **Firebase Hosting Rewrites**: Added `changeRegistrationDateTime` function to admin hosting configuration
26+
27+
### Changed
28+
29+
- **Review Page UI**: Updated check-in review page layout
30+
- Delete button now only shows when registration exists
31+
- Improved button text from "Cancel Reservation" to "Delete" for clarity
32+
- Date/time display now includes change button when reservation exists
33+
34+
### Fixed
35+
36+
- Removed duplicate batch.set call in `changeRegistrationDateTime` function
37+
838
## [2025.1.0] - 2025-11-15
939

1040
### Added

firebase.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@
108108
"source": "/callableResendRegistrationEmail",
109109
"function": "callableResendRegistrationEmail"
110110
},
111+
{
112+
"source": "/changeRegistrationDateTime",
113+
"function": "changeRegistrationDateTime"
114+
},
111115
{
112116
"source": "**",
113117
"destination": "/index.html"

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "santasworkshop",
3-
"version": "2025.1.0",
3+
"version": "2025.2.0",
44
"author": "Joel Meaders",
55
"private": true,
66
"scripts": {

santashop-admin/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@santashop/admin",
3-
"version": "2025.1.0",
3+
"version": "2025.2.0",
44
"description": "",
55
"scripts": {
66
"lint": "ng lint santashop-admin --fix --cache",

santashop-admin/src/app/pages/admin/checkin/review/review.page.html

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
<admin-header title="Check-In: Review Info" backRoute="/admin/checkin/scan">
2-
@if (allowCancelRegistration$ | async) {
2+
@if (registration$ | async; as registration) { @if (allowCancelRegistration$
3+
| async) {
34
<ion-button color="danger" (click)="cancelReservation()">
4-
Cancel Reservation
5+
Delete
56
</ion-button>
6-
}
7+
} }
78
</admin-header>
89

910
<ion-content fullscreen class="ion-padding-horizontal">
@@ -37,6 +38,15 @@ <h1>
3738
Reservation: {{ registration.dateTimeSlot?.dateTime
3839
| date:'medium' }}
3940
</ion-text>
41+
<ion-button
42+
fill="outline"
43+
size="small"
44+
color="primary"
45+
(click)="editDateTime()"
46+
color="warning"
47+
>
48+
Change Date/Time
49+
</ion-button>
4050
</p>
4151
}
4252
</ion-label>

santashop-admin/src/app/pages/admin/checkin/review/review.page.ts

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
22
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
33
import {
44
AlertController,
5+
ModalController,
56
IonContent,
67
IonList,
78
IonListHeader,
@@ -13,7 +14,7 @@ import {
1314
IonIcon,
1415
IonButton,
1516
} from '@ionic/angular/standalone';
16-
import { Child, Registration } from '@santashop/models';
17+
import { Child, DateTimeSlot, Registration } from '@santashop/models';
1718
import {
1819
catchError,
1920
firstValueFrom,
@@ -31,6 +32,7 @@ import { LookupService } from '../../../../shared/services/lookup.service';
3132
import { HeaderComponent } from '../../../../shared/components/header/header.component';
3233
import { AsyncPipe, DatePipe } from '@angular/common';
3334
import { ManageChildrenComponent } from '../../../../shared/components/manage-children/manage-children.component';
35+
import { DateTimeModalComponent } from '../../../../shared/components/date-time-modal/date-time-modal.component';
3436
import { addIcons } from 'ionicons';
3537
import { checkmarkCircle } from 'ionicons/icons';
3638
import { Functions, httpsCallable } from '@angular/fire/functions';
@@ -64,6 +66,7 @@ export class ReviewPage {
6466
private readonly checkinService = inject(CheckInService);
6567
private readonly appStateService = inject(AppStateService);
6668
private readonly alertController = inject(AlertController);
69+
private readonly modalController = inject(ModalController);
6770
private readonly functions = inject(Functions);
6871

6972
private readonly router = inject(Router);
@@ -96,6 +99,18 @@ export class ReviewPage {
9699
'undoRegistration',
97100
)(registration);
98101

102+
private readonly changeRegistrationDateTimeFn = (
103+
newDateTimeSlot: DateTimeSlot,
104+
registrationUid?: string,
105+
): Promise<HttpsCallableResult<boolean>> =>
106+
httpsCallable<
107+
{ newDateTimeSlot: DateTimeSlot; registrationUid?: string },
108+
boolean
109+
>(
110+
this.functions,
111+
'changeRegistrationDateTime',
112+
)({ newDateTimeSlot, registrationUid });
113+
99114
protected readonly setRegistrationSubscription = this.lookupRegistration$
100115
.pipe(
101116
tap((registration) => {
@@ -173,7 +188,6 @@ export class ReviewPage {
173188
registration?.children?.push(child);
174189
this.checkinContext.setRegistration(registration);
175190
}
176-
177191
public async addChild(child: Child): Promise<void> {
178192
const registration = await firstValueFrom(this.registration$);
179193
if (!registration) return;
@@ -182,10 +196,57 @@ export class ReviewPage {
182196
this.checkinContext.setRegistration(registration);
183197
}
184198

199+
public async editDateTime(): Promise<void> {
200+
const registration = await firstValueFrom(this.registration$);
201+
if (!registration?.dateTimeSlot) return;
202+
203+
const currentSlot = {
204+
id: registration.dateTimeSlot.id,
205+
dateTime: registration.dateTimeSlot.dateTime,
206+
} as DateTimeSlot;
207+
208+
const modal = await this.modalController.create({
209+
component: DateTimeModalComponent,
210+
componentProps: {
211+
currentSlot,
212+
},
213+
});
214+
215+
await modal.present();
216+
const result = await modal.onDidDismiss<DateTimeSlot | undefined>();
217+
218+
if (result.data !== undefined) {
219+
if (result.data) {
220+
try {
221+
await this.changeRegistrationDateTimeFn(
222+
result.data,
223+
registration.uid,
224+
);
225+
registration.dateTimeSlot = {
226+
dateTime: result.data.dateTime,
227+
id: result.data.id,
228+
};
229+
this.checkinContext.setRegistration(registration);
230+
this.wasEdited = true;
231+
} catch (error: unknown) {
232+
const err = error as { message?: string };
233+
const alert = await this.alertController.create({
234+
header: 'Error changing date/time',
235+
message: err.message ?? String(error),
236+
});
237+
await alert.present();
238+
}
239+
} else {
240+
delete registration.dateTimeSlot;
241+
this.checkinContext.setRegistration(registration);
242+
this.wasEdited = true;
243+
}
244+
}
245+
}
246+
185247
public async checkIn(): Promise<void> {
186248
const registration = await firstValueFrom(this.registration$);
187249
if (!registration) return;
188-
189250
try {
190251
const result: number = await this.checkinService.checkIn(
191252
registration,
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<ion-header>
2+
<ion-toolbar>
3+
<ion-title>Choose Date & Time</ion-title>
4+
<ion-button
5+
slot="end"
6+
fill="outline"
7+
color="danger"
8+
(click)="dismiss()"
9+
>
10+
Cancel
11+
</ion-button>
12+
</ion-toolbar>
13+
</ion-header>
14+
15+
<ion-content>
16+
<div class="ion-padding">
17+
@if (currentSlot) {
18+
<ion-card>
19+
<ion-card-header>
20+
<ion-card-title color="primary">
21+
Current Date/Time
22+
</ion-card-title>
23+
</ion-card-header>
24+
<ion-card-content class="ion-text-center">
25+
<ion-item lines="none">
26+
{{ currentSlot.dateTime | date: 'MMMM d' : 'MST' }},
27+
{{ currentSlot.dateTime | timeSlot: 'MST' }}
28+
</ion-item>
29+
</ion-card-content>
30+
</ion-card>
31+
}
32+
33+
<ion-list-header class="ion-padding-top">
34+
Available Dates/Times
35+
</ion-list-header>
36+
37+
<ng-container *appLet="availableDays$ | async as availableDays">
38+
@if (availableDays?.length) {
39+
<ion-accordion-group color="light">
40+
@for (day of availableDays; track day) {
41+
<ion-accordion>
42+
<ion-item slot="header" color="light">
43+
<ion-label>
44+
{{ day | date: 'EEEE, MMMM d' }}
45+
</ion-label>
46+
</ion-item>
47+
<ion-list
48+
slot="content"
49+
color="light"
50+
class="ion-no-padding"
51+
>
52+
@for (
53+
slot of availableSlotsByDay$(day) | async;
54+
track slot
55+
) {
56+
<ion-item
57+
color="light"
58+
(click)="selectDateTime(slot)"
59+
[disabled]="!slot.enabled"
60+
[detail]="true"
61+
button
62+
>
63+
<ion-label>
64+
{{
65+
slot.dateTime | timeSlot: 'MST'
66+
}}
67+
<br />
68+
<ion-text color="primary">
69+
{{ spotsRemaining(slot) }}
70+
</ion-text>
71+
</ion-label>
72+
</ion-item>
73+
}
74+
</ion-list>
75+
</ion-accordion>
76+
}
77+
</ion-accordion-group>
78+
} @else {
79+
<ion-card color="danger">
80+
<ion-card-header>
81+
<ion-note>
82+
No available time slots at this time.
83+
</ion-note>
84+
</ion-card-header>
85+
</ion-card>
86+
}
87+
</ng-container>
88+
</div>
89+
</ion-content>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
ion-card-title {
2+
font-weight: bold;
3+
}
4+
5+
ion-accordion-group {
6+
margin: 0 1rem;
7+
}
8+
9+
ion-title {
10+
font-size: 1.2rem;
11+
font-weight: bold;
12+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
2+
import { IonicModule } from '@ionic/angular';
3+
4+
import { DateTimeModalComponent } from './date-time-modal.component';
5+
import { testHelpers } from '../../../../test-helpers';
6+
7+
describe('DateTimeModalComponent', () => {
8+
let component: DateTimeModalComponent;
9+
let fixture: ComponentFixture<DateTimeModalComponent>;
10+
11+
beforeEach(waitForAsync(() => {
12+
TestBed.configureTestingModule({
13+
imports: [IonicModule.forRoot(), DateTimeModalComponent],
14+
providers: [...testHelpers],
15+
}).compileComponents();
16+
17+
fixture = TestBed.createComponent(DateTimeModalComponent);
18+
component = fixture.componentInstance;
19+
fixture.detectChanges();
20+
}));
21+
22+
it('should create', () => {
23+
expect(component).toBeTruthy();
24+
});
25+
});

0 commit comments

Comments
 (0)