Skip to content

Commit cb6f413

Browse files
committed
Merge branch 'develop' into custom/HIO687AS
# Conflicts: # apps/concierge/src/app/parking/parking-state.service.ts
2 parents 9ef99d7 + 02205d3 commit cb6f413

21 files changed

Lines changed: 286 additions & 44 deletions

File tree

apps/concierge/src/app/desks/desks-state.service.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
rejectBooking,
1414
removeBooking,
1515
saveBooking,
16+
updateBooking,
1617
} from '@placeos/bookings';
1718
import {
1819
AsyncHandler,
@@ -37,7 +38,14 @@ import {
3738
showMetadata,
3839
updateMetadata,
3940
} from '@placeos/ts-client';
40-
import { addHours, endOfDay, getUnixTime, set, startOfDay } from 'date-fns';
41+
import {
42+
addHours,
43+
endOfDay,
44+
getUnixTime,
45+
set,
46+
startOfDay,
47+
subDays,
48+
} from 'date-fns';
4149
import { combineLatest, lastValueFrom, of, Subject } from 'rxjs';
4250
import {
4351
catchError,
@@ -525,19 +533,32 @@ export class DesksStateService extends AsyncHandler {
525533
}
526534

527535
private async _clearAssignedBooking(desk: Desk) {
536+
const today = Date.now();
528537
const booking_list = await lastValueFrom(
529538
queryBookings({
530-
period_start: getUnixTime(startOfDay(Date.now())),
531-
period_end: getUnixTime(endOfDay(Date.now())),
539+
period_start: getUnixTime(startOfDay(today)),
540+
period_end: getUnixTime(endOfDay(today)),
532541
type: 'desk',
533542
email: desk.assigned_to,
534543
include_checked_out: true,
535544
}),
536545
);
537546
const filtered = booking_list.filter((_) => _.asset_id === desk.id);
538-
await Promise.all(
539-
filtered.map((_) => lastValueFrom(removeBooking(_.id))),
540-
);
547+
for (const booking of filtered) {
548+
const is_recurring =
549+
booking.recurrence_type && booking.recurrence_type !== 'none';
550+
if (is_recurring && booking.instance) {
551+
// Set recurrence_end to end of yesterday to preserve past instances
552+
const yesterday_end = getUnixTime(endOfDay(subDays(today, 1)));
553+
await lastValueFrom(
554+
updateBooking(booking.id, {
555+
recurrence_end: yesterday_end,
556+
}),
557+
);
558+
} else {
559+
await lastValueFrom(removeBooking(booking.id));
560+
}
561+
}
541562
}
542563

543564
private _getActiveZones(zones: string[] = []): string[] {

apps/concierge/src/app/parking/parking-state.service.ts

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
rejectBookingInstance,
2424
removeBooking,
2525
saveBooking,
26+
updateBooking,
2627
} from '@placeos/bookings';
2728
import {
2829
AsyncHandler,
@@ -48,8 +49,9 @@ import {
4849
set,
4950
startOfDay,
5051
startOfWeek,
52+
subDays,
5153
} from 'date-fns';
52-
import { BehaviorSubject, combineLatest, of } from 'rxjs';
54+
import { BehaviorSubject, combineLatest, lastValueFrom, of } from 'rxjs';
5355
import {
5456
debounceTime,
5557
filter,
@@ -182,7 +184,9 @@ export class ParkingStateService extends AsyncHandler {
182184
: startOfDay(options.date);
183185
const period_end =
184186
options.period === 'week'
185-
? endOfWeek(options.date, { weekStartsOn: this._week_start })
187+
? endOfWeek(options.date, {
188+
weekStartsOn: this._week_start,
189+
})
186190
: endOfDay(options.date);
187191
this._loading.next([...this._loading.getValue(), '[BOOKINGS]']);
188192
return queryBookings({
@@ -577,16 +581,33 @@ export class ParkingStateService extends AsyncHandler {
577581
if (result) this._change.next(Date.now());
578582
}
579583

580-
private async _clearAssignedBooking(space: ParkingSpace) {
581-
const booking_list = await queryBookings({
582-
period_start: getUnixTime(startOfDay(Date.now())),
583-
period_end: getUnixTime(endOfDay(Date.now())),
584-
type: 'parking',
585-
email: space.assigned_to,
586-
include_checked_out: true,
587-
}).toPromise();
588-
const filtered = booking_list.filter((_) => _.asset_id === space.id);
589-
await Promise.all(filtered.map((_) => removeBooking(_.id).toPromise()));
584+
private async _clearAssignedBooking(resource: ParkingSpace) {
585+
const today = Date.now();
586+
const booking_list = await lastValueFrom(
587+
queryBookings({
588+
period_start: getUnixTime(startOfDay(today)),
589+
period_end: getUnixTime(endOfDay(today)),
590+
type: 'parking',
591+
email: resource.assigned_to,
592+
include_checked_out: true,
593+
}),
594+
);
595+
const filtered = booking_list.filter((_) => _.asset_id === resource.id);
596+
for (const booking of filtered) {
597+
const is_recurring =
598+
booking.recurrence_type && booking.recurrence_type !== 'none';
599+
if (is_recurring && booking.instance) {
600+
// Set recurrence_end to end of yesterday to preserve past instances
601+
const yesterday_end = getUnixTime(endOfDay(subDays(today, 1)));
602+
await lastValueFrom(
603+
updateBooking(booking.id, {
604+
recurrence_end: yesterday_end,
605+
}),
606+
);
607+
} else {
608+
await lastValueFrom(removeBooking(booking.id));
609+
}
610+
}
590611
}
591612

592613
private get _week_start(): 0 | 1 | 2 | 3 | 4 | 5 | 6 {

libs/bookings/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,5 @@ export * from './lib/parking-select-modal/parking-select-modal.component';
1515
export * from './lib/parking-space-list-field.component';
1616
export * from './lib/parking.service';
1717

18-
export * from './lib/recurring-clash-modal.component';
1918
export * from './lib/visitor-invite-form.component';
2019
export * from './lib/visitor-invite-success.component';

libs/bookings/src/lib/booking-form.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { queryParkingSpacesForZones } from '@placeos/assets';
55
import {
66
AsyncHandler,
77
Booking,
8+
BookingClash,
89
BookingRuleset,
910
BookingType,
1011
currentUser,
@@ -38,6 +39,7 @@ import {
3839
getUnixTime,
3940
startOfDay,
4041
} from 'date-fns';
42+
import { openRecurringClashModal } from 'libs/components/src/lib/recurring-clash-modal.component';
4143
import {
4244
BehaviorSubject,
4345
combineLatest,
@@ -69,14 +71,12 @@ import {
6971
} from './booking.utilities';
7072
import {
7173
bookedResourceList,
72-
BookingClash,
7374
findBookingClashes,
7475
queryBookings,
7576
removeBooking,
7677
saveBooking,
7778
} from './bookings.fn';
7879
import { DeskQuestionsModalComponent } from './desk-questions-modal.component';
79-
import { openRecurringClashModal } from './recurring-clash-modal.component';
8080

8181
import { AssetStateService } from 'libs/assets/src/lib/asset-state.service';
8282
import { validateAssetRequestsForResource } from 'libs/assets/src/lib/assets.fn';

libs/bookings/src/lib/bookings.fn.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { catchError, map, switchMap } from 'rxjs/operators';
55
import {
66
BookableResource,
77
Booking,
8+
BookingClash,
89
BookingType,
910
CalendarEvent,
1011
flatten,
@@ -81,12 +82,6 @@ export interface BookingClashQueryOptions {
8182
include_clash_time?: boolean;
8283
}
8384

84-
export interface BookingClash {
85-
asset_id: string;
86-
booking_start: number;
87-
booking_end: number;
88-
}
89-
9085
/**
9186
* List resources that clash within the given parameters
9287
* @param q Parameters to pass to the API request
@@ -366,7 +361,7 @@ export function queryBookingGuests(id: string) {
366361
export function checkinBookingGuest(
367362
id: string,
368363
guest_id: string,
369-
state: boolean = true,
364+
state = true,
370365
) {
371366
return post(
372367
`${BOOKINGS_ENDPOINT}/${encodeURIComponent(

libs/common/src/lib/types/booking.class.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ export interface BookingComplete extends Booking {
3535
guests?: User[];
3636
}
3737

38+
export interface BookingClash {
39+
asset_id: string;
40+
booking_start: number;
41+
booking_end: number;
42+
}
43+
3844
export enum RecurrenceDays {
3945
SUNDAY = 1 << 0,
4046
MONDAY = 1 << 1,
@@ -154,7 +160,7 @@ export class Booking {
154160
public readonly linked_parent_booking?: LinkedBooking;
155161

156162
public readonly process_state: string;
157-
/** Unix epoch for the start time of the reccurence instance in seconds */
163+
/** Unix epoch for the start time of the reccurence instance in seconds. Only set when instance of a recurring series */
158164
public readonly instance?: number;
159165
/** Type of recurrence instance */
160166
public readonly recurrence_type: 'none' | 'daily' | 'weekly' | 'monthly';

libs/bookings/src/lib/recurring-clash-modal.component.ts renamed to libs/components/src/lib/recurring-clash-modal.component.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@ import {
77
MatDialogModule,
88
MatDialogRef,
99
} from '@angular/material/dialog';
10-
import { DialogEvent } from '@placeos/common';
10+
import { BookingClash, DialogEvent } from '@placeos/common';
1111
import { IconComponent } from 'libs/components/src/lib/icon.component';
1212
import { TranslatePipe } from 'libs/components/src/lib/translate.pipe';
1313
import { first } from 'rxjs/operators';
14-
import { BookingClash } from './bookings.fn';
1514

1615
export interface RecurringClashModalData {
1716
clashes: BookingClash[];

libs/events/src/lib/event-form.service.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
BehaviorSubject,
2222
combineLatest,
2323
forkJoin,
24+
lastValueFrom,
2425
merge,
2526
Observable,
2627
of,
@@ -53,10 +54,13 @@ import {
5354
queryResourceAvailability,
5455
saveBooking,
5556
} from 'libs/bookings/src/lib/bookings.fn';
57+
import { openRecurringClashModal } from 'libs/bookings/src/lib/recurring-clash-modal.component';
5658
import { SpacePipe } from 'libs/events/src/lib/space.pipe';
5759
import { requestSpacesForZone } from 'libs/events/src/lib/space.utilities';
5860
import { PaymentsService } from 'libs/payments/src/lib/payments.service';
5961
import {
62+
EventClash,
63+
findEventClashes,
6064
querySpaceAvailability,
6165
removeEvent,
6266
saveEvent,
@@ -642,6 +646,19 @@ export class OldEventFormService extends AsyncHandler {
642646
throw _;
643647
});
644648
}
649+
// Check for clashing events in recurring series
650+
if (value.recurring && spaces.length) {
651+
await this._checkRecurringClashes(
652+
new CalendarEvent({
653+
...value,
654+
resources: spaces,
655+
}),
656+
).catch((_) => {
657+
this._loading.next('');
658+
reject(_);
659+
throw _;
660+
});
661+
}
645662
spaces = form.get('resources')?.value || [];
646663
const is_owner =
647664
host === currentUser()?.email ||
@@ -919,6 +936,62 @@ export class OldEventFormService extends AsyncHandler {
919936
return true;
920937
}
921938

939+
/**
940+
* Check for clashing events in a recurring event series
941+
* @param event The calendar event to check for clashes
942+
* @returns true if no clashes or user confirmed to continue
943+
* @throws Error if first instance clashes or clashes not allowed
944+
*/
945+
private async _checkRecurringClashes(
946+
event: CalendarEvent,
947+
): Promise<boolean> {
948+
if (!event.recurring) {
949+
return true;
950+
}
951+
952+
const clashes = (await lastValueFrom(
953+
findEventClashes(event, { include_clash_time: true }),
954+
)) as EventClash[];
955+
956+
if (!clashes?.length) {
957+
return true;
958+
}
959+
960+
const sorted_clashes = [...clashes].sort(
961+
(a, b) => a.booking_start - b.booking_start,
962+
);
963+
964+
const event_start_unix = Math.floor(event.date / 1000);
965+
const first_clash = sorted_clashes[0];
966+
const is_first_instance_clash =
967+
first_clash.booking_start === event_start_unix;
968+
969+
if (is_first_instance_clash) {
970+
throw i18n('CALENDAR_EVENT.FIRST_INSTANCE_CLASH');
971+
}
972+
973+
const allow_clashes =
974+
this._settings.get('app.events.allow_recurring_instance_clashes') ??
975+
true;
976+
977+
if (!allow_clashes) {
978+
throw i18n('CALENDAR_EVENT.RECURRING_CLASHES_NOT_ALLOWED', {
979+
count: clashes.length,
980+
});
981+
}
982+
983+
const result = await openRecurringClashModal(
984+
{ clashes: sorted_clashes },
985+
this._dialog,
986+
);
987+
988+
if (result?.reason !== 'done') {
989+
throw 'User cancelled';
990+
}
991+
992+
return true;
993+
}
994+
922995
private _updateVisitorList(attendees: User[]) {
923996
const visitors = attendees.filter((user) => user.is_external);
924997
if (!visitors?.length) return;

libs/events/src/lib/events.fn.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { CalendarEvent, GuestUser, toQueryString } from '@placeos/common';
1+
import {
2+
BookingClash,
3+
CalendarEvent,
4+
GuestUser,
5+
toQueryString,
6+
} from '@placeos/common';
27
import { del, get, patch, post, put, query } from '@placeos/ts-client';
38
import { addMinutes, getUnixTime } from 'date-fns';
49
import { Observable, combineLatest, of } from 'rxjs';
@@ -402,3 +407,32 @@ export function querySpaceAvailability(
402407
}),
403408
);
404409
}
410+
411+
export interface EventClashQueryOptions {
412+
// Requires multple assets in the event to use
413+
return_available?: boolean;
414+
// Added the time that the clashes occur with each returned asset
415+
include_clash_time?: boolean;
416+
}
417+
418+
/**
419+
* List resources that clash within the given parameters
420+
* @param q Parameters to pass to the API request
421+
*/
422+
export function findEventClashes(
423+
event: CalendarEvent,
424+
q: EventClashQueryOptions = {},
425+
): Observable<string[] | BookingClash[]> {
426+
const query = toQueryString({ ...q, limit: 10000 });
427+
return post(
428+
`${EVENTS_ENDPOINT}/clashing-assets${query ? '?' + query : ''}`,
429+
event.toJSON(),
430+
).pipe(
431+
map((list) =>
432+
q.include_clash_time
433+
? (list as BookingClash[])
434+
: (list as string[]),
435+
),
436+
catchError((_) => of([])),
437+
);
438+
}

0 commit comments

Comments
 (0)