Skip to content

Commit 52ed6bd

Browse files
committed
feat(visitor-kiosk): add guest catering endpoints
1 parent 74dd7b2 commit 52ed6bd

3 files changed

Lines changed: 78 additions & 115 deletions

File tree

apps/visitor-kiosk/src/app/checkin/checkin-preferences.component.ts

Lines changed: 25 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,20 @@ import { MatRippleModule } from '@angular/material/core';
2222
import { MatFormFieldModule } from '@angular/material/form-field';
2323
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
2424
import { MatSelectModule } from '@angular/material/select';
25-
import { saveBooking, updateBooking } from '@placeos/bookings';
2625
import {
2726
AsyncHandler,
28-
Booking,
2927
CateringItem,
30-
CateringOrder,
3128
i18n,
32-
LinkedCalendarEvent,
3329
log,
3430
nextValueFrom,
3531
notifyError,
3632
notifySuccess,
3733
OrganisationService,
3834
parseJWT,
39-
SettingsService,
35+
settingSignal,
4036
} from '@placeos/common';
4137
import { IconComponent, TranslatePipe } from '@placeos/components';
42-
import { showEventMetadata, updateEventMetadata } from '@placeos/events';
38+
import { getGuestCateringItem, setGuestCateringItem } from '@placeos/users';
4339
import { CheckinStateService } from './checkin-state.service';
4440
import { parseTokenFromUrl } from './token-from-url';
4541

@@ -50,7 +46,7 @@ import { parseTokenFromUrl } from './token-from-url';
5046
<div
5147
class="bg-base-100 relative flex w-xl flex-col items-center overflow-hidden rounded-sm p-4 shadow-sm"
5248
>
53-
@let has_beverage = !!(event | async)?.extension_data.beverage;
49+
@let has_beverage = !!existing_beverage();
5450
<h3 class="mb-2 w-full text-xl">
5551
{{ 'APP.VISITOR_KIOSK.BEVERAGE_MSG' | translate }}
5652
</h3>
@@ -144,15 +140,19 @@ export class CheckinPreferencesComponent
144140
private _route = inject(ActivatedRoute);
145141
private _router = inject(Router);
146142
private _checkin = inject(CheckinStateService);
147-
private _settings = inject(SettingsService);
148143
private _org = inject(OrganisationService);
149144
private _last_jwt = '';
150145

151146
public loading = signal(false);
152147
public type = signal<'save' | 'menu'>('menu');
148+
public existing_beverage = signal<CateringItem>(null);
153149
public beverage: CateringItem;
154150
public readonly event = this._checkin.event;
155151
public readonly bld_id = new BehaviorSubject('');
152+
public readonly allow_standalone = settingSignal(
153+
'standalone_visitor_location',
154+
'',
155+
);
156156

157157
public readonly menu = this.bld_id.pipe(
158158
filter((_) => !!_),
@@ -233,16 +233,25 @@ export class CheckinPreferencesComponent
233233
this.timeout(
234234
'event',
235235
() => {
236-
this.event.pipe(first()).subscribe((event) => {
236+
this.event.pipe(first()).subscribe(async (event) => {
237237
if (!event) return this.next();
238-
if (!event.linked_event) {
238+
if (!event.linked_event && !this.allow_standalone()) {
239239
log(
240240
'CHECKIN',
241241
'Visitor booking does not support catering.',
242242
undefined,
243243
'info',
244244
);
245245
}
246+
const existing = await lastValueFrom(
247+
getGuestCateringItem(event.asset_id, event.id).pipe(
248+
catchError(() => of(null)),
249+
),
250+
);
251+
if (existing) {
252+
this.existing_beverage.set(existing);
253+
this.beverage = existing;
254+
}
246255
});
247256
},
248257
1000,
@@ -273,54 +282,14 @@ export class CheckinPreferencesComponent
273282
this.loading.set(true);
274283
const booking = await nextValueFrom(this._checkin.event);
275284
if (!booking) return notifyError(i18n('APP.VISITOR_KIOSK.LOAD_ERROR'));
285+
const email = booking.asset_id;
286+
const catering_item = new CateringItem({
287+
...this.beverage,
288+
quantity: 1,
289+
});
276290
await lastValueFrom(
277-
updateBooking(booking.id, {
278-
...booking.toJSON(),
279-
extension_data: {
280-
...booking.extension_data,
281-
beverage: this.beverage,
282-
},
283-
}),
291+
setGuestCateringItem(email, catering_item, booking.id),
284292
);
285-
if (booking.linked_event) {
286-
const event = booking.linked_event;
287-
const metadata = await lastValueFrom(
288-
showEventMetadata(event.event_id, event.system_id),
289-
);
290-
const order_list = metadata.catering || [];
291-
let order =
292-
order_list.find((_) => _.caterer == this.beverage.caterer) ||
293-
new CateringOrder({ caterer: this.beverage.caterer });
294-
order = await this._createCateringOrder(booking, order, event);
295-
await lastValueFrom(
296-
updateEventMetadata(
297-
event.event_id,
298-
event.system_id,
299-
{
300-
...metadata,
301-
catering: [
302-
...(metadata.catering?.filter(
303-
(_) => _.id !== order.id,
304-
) || []),
305-
order,
306-
],
307-
},
308-
{ ical_uid: event.ical_uid },
309-
),
310-
);
311-
} else {
312-
const standalone_location = this._settings.get(
313-
'app.standalone_visitor_location',
314-
);
315-
this._createCateringOrder(
316-
booking,
317-
booking.linked_bookings[0]
318-
? booking.linked_bookings[0].extension_data.details
319-
: undefined,
320-
undefined,
321-
standalone_location,
322-
);
323-
}
324293
notifySuccess(i18n('APP.VISITOR_KIOSK.BEVERAGE_SUCCESS'));
325294
this.loading.set(false);
326295
this.next();
@@ -334,58 +303,4 @@ export class CheckinPreferencesComponent
334303
this._checkin.setError(message?.statusText || message);
335304
this._router.navigate(['/checkin', 'error']);
336305
}
337-
338-
private async _createCateringOrder(
339-
parent: Booking,
340-
old_order: CateringOrder = new CateringOrder(),
341-
event?: LinkedCalendarEvent,
342-
location?: string,
343-
) {
344-
const existing_item = old_order.items.find(
345-
(_) => _.custom_id === this.beverage.custom_id,
346-
);
347-
if (existing_item) (existing_item as any).quantity += 1;
348-
const order = new CateringOrder({
349-
...old_order,
350-
caterer: this.beverage.caterer,
351-
items: existing_item
352-
? [...old_order.items]
353-
: [
354-
...old_order.items,
355-
new CateringItem({
356-
...this.beverage,
357-
quantity: 1,
358-
}),
359-
],
360-
});
361-
const booking = new Booking({
362-
type: 'catering-order',
363-
booking_type: 'catering-order',
364-
date: parent.date,
365-
duration: parent.duration,
366-
description: parent.title,
367-
user_id: parent.user_id,
368-
user_email: parent.user_email,
369-
booked_by_email: parent.asset_id,
370-
asset_id: order.id,
371-
title: `Catering order for ${parent.user_name}`,
372-
attendees: [],
373-
approved: true,
374-
extension_data: {
375-
parent_id: parent.id,
376-
details: order,
377-
location: location || parent.location,
378-
},
379-
parent_id: parent.id,
380-
zones: parent.zones,
381-
location: location || parent.location,
382-
});
383-
const query: Record<string, any> = { booking_id: booking.id };
384-
if (event) {
385-
query.event_id = event.id;
386-
query.ical_uid = event.ical_uid;
387-
}
388-
await lastValueFrom(saveBooking(booking, query));
389-
return order;
390-
}
391306
}

apps/visitor-kiosk/src/tests/checkin/checkin-preference.component.spec.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
22
import { MatFormFieldModule } from '@angular/material/form-field';
33
import { MatSelectModule } from '@angular/material/select';
44
import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/jest';
5-
import { CateringStateService } from '@placeos/catering';
65
import { IconComponent } from '@placeos/components';
76
import { MockComponent, MockModule, MockProvider } from 'ng-mocks';
87
import { of } from 'rxjs';
98

109
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
11-
import { SettingsService } from '@placeos/common';
1210
import { CheckinPreferencesComponent } from '../../app/checkin/checkin-preferences.component';
1311
import { CheckinStateService } from '../../app/checkin/checkin-state.service';
1412
import { parseTokenFromUrl } from '../../app/checkin/token-from-url';
@@ -23,8 +21,6 @@ describe('CheckinPreferencesComponent', () => {
2321
event: of({}),
2422
guest: of({}),
2523
} as any),
26-
MockProvider(CateringStateService, { menu: of([]) }),
27-
MockProvider(SettingsService, { get: jest.fn() }),
2824
],
2925
imports: [
3026
MatFormFieldModule,

libs/users/src/lib/guests.fn.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import { del, get, patch } from '@placeos/ts-client';
22
import { Observable } from 'rxjs';
33
import { map } from 'rxjs/operators';
44

5-
import { CalendarEvent, GuestUser, toQueryString } from '@placeos/common';
5+
import {
6+
CalendarEvent,
7+
CateringItem,
8+
GuestUser,
9+
toQueryString,
10+
} from '@placeos/common';
611

712
const GUEST_ENDPOINT = '/api/staff/v1/guests';
813

@@ -79,3 +84,50 @@ export function listGuestMeetings(id: string) {
7984
map((list) => list.map((item) => new CalendarEvent(item))),
8085
);
8186
}
87+
88+
/**
89+
* Get the catering item requested for the specified guest
90+
* @param email Email address of the guest
91+
* @param booking_id Optional ID of the related visitor booking to get from
92+
*/
93+
export function getGuestCateringItem(email: string, booking_id = '') {
94+
const path = `${GUEST_ENDPOINT}/${encodeURIComponent(email)}/catering`;
95+
const query = booking_id
96+
? `?booking_id=${encodeURIComponent(booking_id)}`
97+
: '';
98+
return get(`${path}${query}`).pipe(
99+
map((item) => (item ? new CateringItem(item) : null)),
100+
);
101+
}
102+
103+
/**
104+
* Set the catering item requested for the specified guest
105+
* @param email Email address of the guest
106+
* @param booking_id Optional ID of the related visitor booking to set on
107+
*/
108+
export function setGuestCateringItem(
109+
email: string,
110+
catering_item: CateringItem,
111+
booking_id = '',
112+
) {
113+
const path = `${GUEST_ENDPOINT}/${encodeURIComponent(email)}/catering`;
114+
const query = booking_id
115+
? `?booking_id=${encodeURIComponent(booking_id)}`
116+
: '';
117+
return patch(`${path}${query}`, catering_item).pipe(
118+
map((item) => (item ? new CateringItem(item) : null)),
119+
);
120+
}
121+
122+
/**
123+
* Clears any set catering for the specified guest
124+
* @param email Email address of the guest
125+
* @param booking_id Optional ID of the related visitor booking to clear
126+
*/
127+
export function clearGuestCateringItem(email: string, booking_id = '') {
128+
const path = `${GUEST_ENDPOINT}/${encodeURIComponent(email)}/catering`;
129+
const query = booking_id
130+
? `?booking_id=${encodeURIComponent(booking_id)}`
131+
: '';
132+
return del(`${path}${query}`);
133+
}

0 commit comments

Comments
 (0)