Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to a versioning scheme of `year.minor.patch`.

## [2025.2.0] - 2025-11-15

### Added

- **Admin Check-In DateTime Change**: Admins can now change registration date/time during the check-in review process
- New "Change Date/Time" button on admin check-in review page
- Date/Time modal component with accordion-grouped available time slots
- Real-time availability display showing remaining spots for each time slot
- Confirmation dialog when changing from existing reservation
- Automatic email notification sent to registrant with updated date/time
- **Enhanced Firebase Functions**: Updated `changeRegistrationDateTime` function to support admin operations
- Admins can change date/time for any registration (with proper permission checks)
- Non-admin users can only change their own registrations
- Prevents changes after check-in to maintain data integrity
- **Program Year Configuration**: Added `PROGRAM_YEAR` injection token to admin application
- Centralized program year management (set to 2025)
- Used by date/time selection to filter available slots
- **Firebase Hosting Rewrites**: Added `changeRegistrationDateTime` function to admin hosting configuration

### Changed

- **Review Page UI**: Updated check-in review page layout
- Delete button now only shows when registration exists
- Improved button text from "Cancel Reservation" to "Delete" for clarity
- Date/time display now includes change button when reservation exists

### Fixed

- Removed duplicate batch.set call in `changeRegistrationDateTime` function

## [2025.1.0] - 2025-11-15

### Added
Expand Down
4 changes: 4 additions & 0 deletions firebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@
"source": "/callableResendRegistrationEmail",
"function": "callableResendRegistrationEmail"
},
{
"source": "/changeRegistrationDateTime",
"function": "changeRegistrationDateTime"
},
{
"source": "**",
"destination": "/index.html"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "santasworkshop",
"version": "2025.1.0",
"version": "2025.2.0",
"author": "Joel Meaders",
"private": true,
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion santashop-admin/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@santashop/admin",
"version": "2025.1.0",
"version": "2025.2.0",
"description": "",
"scripts": {
"lint": "ng lint santashop-admin --fix --cache",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<admin-header title="Check-In: Review Info" backRoute="/admin/checkin/scan">
@if (allowCancelRegistration$ | async) {
@if (registration$ | async; as registration) { @if (allowCancelRegistration$
| async) {
<ion-button color="danger" (click)="cancelReservation()">
Cancel Reservation
Delete
</ion-button>
}
} }
</admin-header>

<ion-content fullscreen class="ion-padding-horizontal">
Expand Down Expand Up @@ -37,6 +38,15 @@ <h1>
Reservation: {{ registration.dateTimeSlot?.dateTime
| date:'medium' }}
</ion-text>
<ion-button
fill="outline"
size="small"
color="primary"
(click)="editDateTime()"
color="warning"
>
Change Date/Time
</ion-button>
</p>
}
</ion-label>
Expand Down
67 changes: 64 additions & 3 deletions santashop-admin/src/app/pages/admin/checkin/review/review.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import {
AlertController,
ModalController,
IonContent,
IonList,
IonListHeader,
Expand All @@ -13,7 +14,7 @@ import {
IonIcon,
IonButton,
} from '@ionic/angular/standalone';
import { Child, Registration } from '@santashop/models';
import { Child, DateTimeSlot, Registration } from '@santashop/models';
import {
catchError,
firstValueFrom,
Expand All @@ -31,6 +32,7 @@ import { LookupService } from '../../../../shared/services/lookup.service';
import { HeaderComponent } from '../../../../shared/components/header/header.component';
import { AsyncPipe, DatePipe } from '@angular/common';
import { ManageChildrenComponent } from '../../../../shared/components/manage-children/manage-children.component';
import { DateTimeModalComponent } from '../../../../shared/components/date-time-modal/date-time-modal.component';
import { addIcons } from 'ionicons';
import { checkmarkCircle } from 'ionicons/icons';
import { Functions, httpsCallable } from '@angular/fire/functions';
Expand Down Expand Up @@ -64,6 +66,7 @@ export class ReviewPage {
private readonly checkinService = inject(CheckInService);
private readonly appStateService = inject(AppStateService);
private readonly alertController = inject(AlertController);
private readonly modalController = inject(ModalController);
private readonly functions = inject(Functions);

private readonly router = inject(Router);
Expand Down Expand Up @@ -96,6 +99,18 @@ export class ReviewPage {
'undoRegistration',
)(registration);

private readonly changeRegistrationDateTimeFn = (
newDateTimeSlot: DateTimeSlot,
registrationUid?: string,
): Promise<HttpsCallableResult<boolean>> =>
httpsCallable<
{ newDateTimeSlot: DateTimeSlot; registrationUid?: string },
boolean
>(
this.functions,
'changeRegistrationDateTime',
)({ newDateTimeSlot, registrationUid });

protected readonly setRegistrationSubscription = this.lookupRegistration$
.pipe(
tap((registration) => {
Expand Down Expand Up @@ -173,7 +188,6 @@ export class ReviewPage {
registration?.children?.push(child);
this.checkinContext.setRegistration(registration);
}

public async addChild(child: Child): Promise<void> {
const registration = await firstValueFrom(this.registration$);
if (!registration) return;
Expand All @@ -182,10 +196,57 @@ export class ReviewPage {
this.checkinContext.setRegistration(registration);
}

public async editDateTime(): Promise<void> {
const registration = await firstValueFrom(this.registration$);
if (!registration?.dateTimeSlot) return;

const currentSlot = {
id: registration.dateTimeSlot.id,
dateTime: registration.dateTimeSlot.dateTime,
} as DateTimeSlot;

const modal = await this.modalController.create({
component: DateTimeModalComponent,
componentProps: {
currentSlot,
},
});

await modal.present();
const result = await modal.onDidDismiss<DateTimeSlot | undefined>();

if (result.data !== undefined) {
if (result.data) {
try {
await this.changeRegistrationDateTimeFn(
result.data,
registration.uid,
);
registration.dateTimeSlot = {
dateTime: result.data.dateTime,
id: result.data.id,
};
this.checkinContext.setRegistration(registration);
this.wasEdited = true;
} catch (error: unknown) {
const err = error as { message?: string };
const alert = await this.alertController.create({
header: 'Error changing date/time',
message: err.message ?? String(error),
});
await alert.present();
}
} else {
delete registration.dateTimeSlot;
this.checkinContext.setRegistration(registration);
this.wasEdited = true;
}
}
}

public async checkIn(): Promise<void> {
const registration = await firstValueFrom(this.registration$);
if (!registration) return;

try {
const result: number = await this.checkinService.checkIn(
registration,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<ion-header>
<ion-toolbar>
<ion-title>Choose Date & Time</ion-title>
<ion-button
slot="end"
fill="outline"
color="danger"
(click)="dismiss()"
>
Cancel
</ion-button>
</ion-toolbar>
</ion-header>

<ion-content>
<div class="ion-padding">
@if (currentSlot) {
<ion-card>
<ion-card-header>
<ion-card-title color="primary">
Current Date/Time
</ion-card-title>
</ion-card-header>
<ion-card-content class="ion-text-center">
<ion-item lines="none">
{{ currentSlot.dateTime | date: 'MMMM d' : 'MST' }},
{{ currentSlot.dateTime | timeSlot: 'MST' }}
</ion-item>
</ion-card-content>
</ion-card>
}

<ion-list-header class="ion-padding-top">
Available Dates/Times
</ion-list-header>

<ng-container *appLet="availableDays$ | async as availableDays">
@if (availableDays?.length) {
<ion-accordion-group color="light">
@for (day of availableDays; track day) {
<ion-accordion>
<ion-item slot="header" color="light">
<ion-label>
{{ day | date: 'EEEE, MMMM d' }}
</ion-label>
</ion-item>
<ion-list
slot="content"
color="light"
class="ion-no-padding"
>
@for (
slot of availableSlotsByDay$(day) | async;
track slot
) {
<ion-item
color="light"
(click)="selectDateTime(slot)"
[disabled]="!slot.enabled"
[detail]="true"
button
>
<ion-label>
{{
slot.dateTime | timeSlot: 'MST'
}}
<br />
<ion-text color="primary">
{{ spotsRemaining(slot) }}
</ion-text>
</ion-label>
</ion-item>
}
</ion-list>
</ion-accordion>
}
</ion-accordion-group>
} @else {
<ion-card color="danger">
<ion-card-header>
<ion-note>
No available time slots at this time.
</ion-note>
</ion-card-header>
</ion-card>
}
</ng-container>
</div>
</ion-content>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
ion-card-title {
font-weight: bold;
}

ion-accordion-group {
margin: 0 1rem;
}

ion-title {
font-size: 1.2rem;
font-weight: bold;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';

import { DateTimeModalComponent } from './date-time-modal.component';
import { testHelpers } from '../../../../test-helpers';

describe('DateTimeModalComponent', () => {
let component: DateTimeModalComponent;
let fixture: ComponentFixture<DateTimeModalComponent>;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [IonicModule.forRoot(), DateTimeModalComponent],
providers: [...testHelpers],
}).compileComponents();

fixture = TestBed.createComponent(DateTimeModalComponent);
component = fixture.componentInstance;
fixture.detectChanges();
}));

it('should create', () => {
expect(component).toBeTruthy();
});
});
Loading