diff --git a/staff/assets/maps/Zurich-L5.svg b/staff/assets/maps/Zurich-L5.svg
new file mode 100644
index 000000000..fb270c890
--- /dev/null
+++ b/staff/assets/maps/Zurich-L5.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/staff/assets/maps/frankfurt/level_37.svg b/staff/assets/maps/frankfurt/level_37.svg
index 235479fa8..20518619f 100644
--- a/staff/assets/maps/frankfurt/level_37.svg
+++ b/staff/assets/maps/frankfurt/level_37.svg
@@ -87,7 +87,8 @@
C439.4,1043.5,439.6,1043.1,439.7,1042.7z M436,1040.9h3.8c0-0.5-0.2-0.9-0.4-1.3c-0.3-0.4-0.9-0.7-1.4-0.7s-1,0.2-1.3,0.5
C436.2,1039.8,436,1040.4,436,1040.9L436,1040.9z"/>
+ c-0.3-0.2-0.5-0.2-0.8-0.2c-0.2,0-0.5,0.1-0.7,0.2c-0.2,0.2-0.4,0.4-0.4,0.6c-0.1,0.4-0.2,0.9-0.2,1.3v3.5h-1.1L442.3,1044.9
+ L442.3,1044.9z"/>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -1433,9 +1194,9 @@
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -2739,6 +2155,14 @@
+
+
+
+
+
+
+
+
@@ -2846,1033 +2270,1940 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/staff/assets/maps/frankfurt/level_7.svg b/staff/assets/maps/frankfurt/level_7.svg
index 20518619f..78f2b85a2 100644
--- a/staff/assets/maps/frankfurt/level_7.svg
+++ b/staff/assets/maps/frankfurt/level_7.svg
@@ -4403,5 +4403,6 @@
+
diff --git a/staff/assets/maps/houston/level_22.svg b/staff/assets/maps/houston/level_22.svg
index bd882050d..86fe52bbc 100644
--- a/staff/assets/maps/houston/level_22.svg
+++ b/staff/assets/maps/houston/level_22.svg
@@ -3372,6 +3372,7 @@
+
+
hide', animate('200ms ease-in'))\n]);\n","import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\n\n@NgModule({\n imports: [CommonModule],\n})\nexport class AnimationsModule {}\n","export * from './lib/base.module';\nexport * from './lib/types.utilities';\nexport * from './lib/general.utilities';\nexport * from './lib/base.class';\nexport * from './lib/base.directive';\nexport * from './lib/date.utils'\nexport * from './lib/date.spec-helpers'\nexport * from './lib/replace.pipe'\nexport * from './lib/cdk-drop-list-scroll-container.directive'\nexport * from './lib/image.directive';\n","import { Subscription, BehaviorSubject } from \"rxjs\";\n\nexport class BaseClass {\n /** Store for named timers */\n protected _timers: { [name: string]: number } = {};\n /** Store for named intervals */\n protected _intervals: { [name: string]: number } = {};\n /** Store for named subscription unsub callbacks */\n protected _subscriptions: { [name: string]: (Subscription | (() => void)) } = {};\n /** Subject which stores the initialised state of the object */\n protected readonly _initialised = new BehaviorSubject(false);\n\n /** Observable of the initialised state of the object */\n public get initialised(): BehaviorSubject {\n return this._initialised;\n }\n /** Whether the object has been initialised */\n public get is_initialised(): boolean {\n return this._initialised.getValue();\n }\n\n protected destroy() {\n for (const key in this._timers) {\n if (this._timers.hasOwnProperty(key)) {\n this.clearTimeout(key);\n }\n }\n for (const key in this._intervals) {\n if (this._intervals.hasOwnProperty(key)) {\n this.clearInterval(key);\n }\n }\n for (const key in this._subscriptions) {\n if (this._subscriptions.hasOwnProperty(key)) {\n this.unsub(key);\n }\n }\n }\n\n /**\n * Creates a named timer\n * @param name Name of the timer\n * @param fn Callback function for the timer\n * @param delay Callback delay\n */\n protected timeout(name: string, fn: () => void, delay: number = 300) {\n if (name && fn && fn instanceof Function) {\n this.clearTimeout(name);\n this._timers[name] = setTimeout(() => {\n fn();\n this._timers[name] = null;\n }, delay);\n } else {\n throw new Error(\n name ? 'Cannot create named timeout without a name' : 'Cannot create a timeout without a callback'\n );\n }\n }\n\n /**\n * Clears the named timer\n * @param name Timer name\n */\n protected clearTimeout(name: string) {\n if (this._timers[name]) {\n clearTimeout(this._timers[name]);\n this._timers[name] = null;\n }\n }\n\n /**\n * Creates a named interval\n * @param name Name of the interval\n * @param fn Callback function for the interval\n * @param delay Callback delay\n */\n protected interval(name: string, fn: () => void, delay: number = 300) {\n if (name && fn && fn instanceof Function) {\n this.clearInterval(name);\n this._intervals[name] = setInterval(() => fn(), delay);\n } else {\n throw new Error(\n name ? 'Cannot create named interval without a name' : 'Cannot create a interval without a callback'\n );\n }\n }\n\n /**\n * Clears the named interval\n * @param name Timer name\n */\n protected clearInterval(name: string) {\n if (this._intervals[name]) {\n clearInterval(this._intervals[name]);\n this._intervals[name] = null;\n }\n }\n\n /**\n * Store named subscription\n * @param name Name of the subscription\n * @param unsub Unsubscribe callback or Subscription object\n */\n protected subscription(name: string, unsub: Subscription | (() => void)) {\n this.unsub(name);\n this._subscriptions[name] = unsub\n }\n\n /**\n * Call unsubscribe callback with the given name\n * @param name\n */\n protected unsub(name: string) {\n if (this._subscriptions && this._subscriptions[name]) {\n this._subscriptions[name] instanceof Subscription\n ? (this._subscriptions[name] as Subscription).unsubscribe()\n : (this._subscriptions[name] as any)();\n this._subscriptions[name] = null;\n }\n }\n}\n","\nimport { Directive, OnDestroy } from '@angular/core';\nimport { BaseClass } from './base.class';\n\n@Directive({\n selector: 'a-very-basic-component-base-that-should-not-be-used'\n})\nexport class BaseDirective extends BaseClass implements OnDestroy {\n public ngOnDestroy(): void {\n this.destroy();\n }\n}","import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { NumbersOnlyDirective } from './numbers-only.directive';\n\n@NgModule({\n imports: [CommonModule,],\n declarations: [\n NumbersOnlyDirective\n ],\n exports: [\n NumbersOnlyDirective\n ]\n})\nexport class BaseModule {\n}\n","import {\n Directive,\n Input,\n Renderer2,\n SimpleChanges,\n OnChanges,\n ContentChildren,\n QueryList,\n AfterContentInit\n} from '@angular/core';\nimport { CdkDropList, CdkDrag } from '@angular/cdk/drag-drop';\nimport { BaseDirective } from './base.directive';\n\nexport enum ScrollDirection {\n NONE,\n X,\n Y,\n BOTH\n}\n\n@Directive({\n selector: '[cdkDropList][scrollContainer]'\n})\nexport class CdkDropListScrollContainer extends BaseDirective\n implements OnChanges, AfterContentInit {\n /** Direction of scroll to determine updating the position of the drop list */\n @Input() direction: ScrollDirection = ScrollDirection.X;\n /** Name of the scroll container for the list */\n @Input() scrollContainer: string;\n /** Scroll container element */\n public element: HTMLElement;\n /** Last scroll position */\n public last_scroll: { x: number; y: number } = { x: 0, y: 0 };\n\n /** Draggable children elements */\n @ContentChildren(CdkDrag) private items: QueryList;\n\n constructor(private _cdkDropList: CdkDropList, private _renderer: Renderer2) {\n super();\n }\n\n public ngOnChanges(changes: SimpleChanges): void {\n if (changes.scrollContainer && this.scrollContainer) {\n this.element = this._cdkDropList.element.nativeElement.closest(\n this.scrollContainer\n ) as HTMLElement;\n }\n }\n\n public ngAfterContentInit(): void {\n this.subscription(\n 'drag_items',\n this.items.changes.subscribe((items: QueryList) => {\n const list = items.toArray();\n list.forEach((i, index) => {\n this.subscription(\n `list-item-${index}`,\n this._renderer.listen(i.element.nativeElement, 'mousedown', () => {\n this.subscription(\n 'item-dragged',\n this._renderer.listen('window', 'mouseup', () => this.onDrop())\n );\n this.onDrag();\n })\n );\n this.subscription(\n `list-item-touch-${index}`,\n this._renderer.listen(i.element.nativeElement, 'touchstart', () => {\n this.subscription(\n 'item-dragged',\n this._renderer.listen('window', 'touchend', () => this.onDrop())\n );\n this.onDrag();\n })\n );\n });\n })\n );\n }\n\n /** Start listing for scroll events on the container */\n public onDrag() {\n if (this.element) {\n this.subscription(\n 'scroll',\n this._renderer.listen(this.element, 'scroll', () => this.updateListPosition())\n );\n }\n }\n\n /** Stop listening for scroll events on the container */\n public onDrop() {\n this.unsub('scroll');\n }\n\n /**\n * Forcefully update the position data of the drop list\n */\n private updateListPosition() {\n this.timeout(\n 'update_positions',\n () => {\n const scroll = { x: this.element.scrollLeft, y: this.element.scrollTop };\n if (\n ((this.direction === ScrollDirection.BOTH ||\n this.direction === ScrollDirection.Y) &&\n scroll.y !== this.last_scroll.y) ||\n ((this.direction === ScrollDirection.BOTH ||\n this.direction === ScrollDirection.X) &&\n scroll.x !== this.last_scroll.x)\n ) {\n (this._cdkDropList._dropListRef as any)._cacheOwnPosition();\n (this._cdkDropList._dropListRef as any)._siblings.forEach(i =>\n i.isReceiving() ? i._cacheOwnPosition() : null\n );\n }\n this.last_scroll = scroll;\n },\n 50\n );\n }\n}\n","import MockDate from 'mockdate';\n\n/**\n * August 13, 2020 at 7:22:12 UTC\n */\nconst initialTime = 1597346532 * 1000;\n\nexport const mockDate = (timeOverride = initialTime) => MockDate.set(new Date(timeOverride));\n\nexport const resetDate = () => MockDate.reset();\n","import { DateNow, DateTZ } from '@mckinsey-converge/date-tz'\nimport { DateTime } from 'luxon';\nimport * as dayjs from 'dayjs';\nimport {\n dayJsHoursMinutes,\n dayJsTimeFormatString,\n} from './general.utilities';\n\n/**\n * Allows you to split up durations into a group.\n */\nexport interface DurationGroup {\n /**\n * Step amount to generate between start and max.\n */\n step: number;\n /**\n * Where to start in minutes.\n */\n start: number;\n /**\n * Where to end in minutes.\n */\n max: number;\n}\n\n/**\n * Find the multiple of `stepMinute` which is closest to the 'minutes' property of the given date.\n * @param date - Any Lexon date.\n * @param stepMinute - The number of minutes between one timeslot and the next. Integer value\n * 1 to 59, inclusive.\n */\n// R--- depreciate this one\nexport const closestToTimeSlot = (date: DateTime, stepMinute: number, start: number = 0) : DateTime => {\n return date.set({ millisecond: 0, second: 0, minute: Math.ceil(date.minute / stepMinute) * stepMinute })\n .plus({ minutes: start % 60 }) // apply offset as well if it starts at 15.\n};\n\nexport const closestToTimeSlotTz = (dateTz: DateTZ, stepMinute: number, start: number = 0) : DateTZ => {\n return dateTz.setValue({ millisecond: 0, second: 0, minute: Math.ceil(dateTz.minutes / stepMinute) * stepMinute })\n .addValue({ minutes: start % 60 }) // apply offset as well if it starts at 15.\n};\n\n/**\n * Convert duration to human readable string\n * @param duration Duration in minutes\n * @param short Whether to use short form of duration words e.g. hours as hrs, or minutes as mins\n */\nexport function durationHumanized(duration: number, short: boolean = false): string {\n if (!duration || duration < 0) {\n return '';\n }\n const h = Math.floor(duration / 60);\n let d = `${h >= 1 ? h + (short ? ' hr' : ' hour' + (h === 1 ? '' : 's')) : ''}`;\n if (duration % 60 !== 0) {\n if (d) {\n d += short ? ' ' : ', ';\n }\n const m = duration % 60;\n d += `${m >= 1 ? m + (short ? ' min' : ' minute' + (m === 1 ? '' : 's')) : ''}`;\n }\n return d;\n}\n\nexport const weekDayMonthYearFormat = (date: DateTZ): string => date.formatDate('ccc dd MMM yyyy');\n\n/**\n * Tries to extract best-fit input\n * @return undefined if not valid. otherwise if good\n */\nexport const extractDateFromInput = (input: string,\n checkHour,\n startDate: dayjs.Dayjs): dayjs.Dayjs | undefined => {\n // check if input has am/pm or normal 24 hour time.\n // let date = dayjs(input, 'HH:mm');\n // let date = dayjs(input, 'h:mma');\n let date = dayjs(input, dayJsTimeFormatString());\n if (!date.isValid()) {\n // patch if time is in format xx:x to assume you meant xx:x0\n let cleanedInput = input;\n const times = input.split(':');\n if(!input){\n return undefined\n }\n if (times.length === 2) {\n if (times[1].length === 1) {\n cleanedInput = `${times[0]}:${times[1]}0`;\n }\n }\n // may be other input, lets try next value (without am/pm)\n date = dayjs(cleanedInput, dayJsHoursMinutes());\n // might be just purely an hour\n if (!date.isValid() && checkHour) {\n date = dayjs(input, 'H');\n }\n }\n // We expect input times to be in the future. If now is in the afternoon, this\n // simple block will ensure the returned date is also in the afternoon.\n //\n // NB: dayjs parses times as morning by default.\n if (date.isValid()) {\n // Move to the selected start date\n date = date.month(startDate.month()).date(startDate.date()).year(startDate.year());\n if (startDate.hour() > date.hour() && date.date() === startDate.date()) {\n date = date.set('hour', date.hour() + 12);\n }\n }\n\n return date.isValid() ? date : undefined;\n};\n\n\n/**\n * 1. Calculates the date from input via {@link extractDateFromInput}.\n * 2. Finds the nearest time slot that it can be via {@link closestToTimeSlot}.\n * 3. Then returns the time-format string the input expects so autocomplete can suggest\n * closest match.\n * @param step The step between time slots.\n * @param input The input text.\n */\nexport const nearestStepToInput = (\n step: number,\n input: string,\n currentStartDate: number,\n start: number = 0\n): string => {\n let date = extractDateFromInput(input, false, dayjs(currentStartDate));\n if (date) {\n const luxonDate = DateTime.fromMillis(date.valueOf());\n const closestDate = closestToTimeSlot(luxonDate, step, start);\n return closestDate.toFormat(dayJsHoursMinutes());\n } else {\n return input;\n }\n};\n/**\n * Similiar to {@link nearestStepToInput} instead:\n * 1. Calculates the date from input via {@link extractDateFromInput}.\n * 2. Finds which duration group is closest to the currentStartDate. If not found\n * return vanilla text.\n * 3. Finds the nearest time slot that it can be via {@link closestToTimeSlot}.\n * 4. Then returns the time-format string the input expects so autocomplete can suggest\n * closest match.\n */\nexport const nearestDurationToInput = (durationGroups: DurationGroup[],\n currentStartDate: number,\n value: string): string => {\n\n let date = extractDateFromInput(value, false, dayjs(currentStartDate));\n\n if (!date) {\n // Check if the input is a duration \n const duration = parseInt(value);\n if (typeof (duration) === 'number' && value.indexOf(':') === -1 && duration > 12) {\n date = dayjs(currentStartDate).add(duration, 'm');\n }\n }\n\n if (date) {\n const minutes = date.diff(currentStartDate, 'minute');\n const closestDurationGroup = durationGroups.find(d => {\n return (minutes <= d.max);\n });\n if (closestDurationGroup) {\n const offset = [closestDurationGroup].reduce((previousValue, currentValue) => {\n return previousValue + currentValue.start;\n }, 0);\n const luxonDate = DateTime.fromMillis(date.valueOf());\n const closestDate = closestToTimeSlot(luxonDate, closestDurationGroup.step, 0);\n return closestDate.toFormat(dayJsHoursMinutes());\n }\n }\n\n return value;\n};\n\nexport const resetSecondsOnTimestamp = (timestamp: number) => {\n return DateTime.fromMillis(timestamp).set({ second: 0, millisecond: 0 }).toMillis();\n}\n\nexport const formatDateWithSuffix = (date: string): string => {\n const dateObj = new Date(date);\n const day = dateObj.getDate();\n const month = dateObj.toLocaleString(\"default\", { month: \"short\" });\n const year = dateObj.getFullYear();\n return `${day}${nthNumber(day)} ${month} ${year}`; //this.event.date_string;\n}\n\nconst nthNumber = (number) => {\n return number > 0\n ? [\"th\", \"st\", \"nd\", \"rd\"][\n (number > 3 && number < 21) || number % 10 > 3 ? 0 : number % 10\n ]\n : \"\";\n};\n\nexport const getListOfDateFormat = () => {\n return [\t\n 'MMMM dd y', \t//March 07 2023\n 'MMM dd, y',\t//Mar 07 2023\n 'MMMM dd',\t\t//March 07\n 'MMM dd', \t\t//Mar 07\n 'MM dd', \t\t//03 07\n\n 'MM-dd-yy',\t\t//03-07-23\n 'MM-dd-yyyy',\t//03-07-2023\n 'MMMM-dd-yyyy', //March-07-2023\n 'MMMM-dd-yy',\t//March-07-23\n\n 'MMMM-dd',\t\t//March-07\n 'MMM-dd',\t\t//Mar-07\n \n 'MMM-dd-y',\t\t//Mar-07-2023\n\n 'MM/dd',\t\t//03/07\n 'MM/dd/yy',\t\t//03/07/23\n 'MM/dd/yyyy',\t//03/07/2023\n \n 'dd/MM',\t\t//07/03\n 'dd/MM/yy',\t\t//07/03/23\n 'dd/MM/yyyy',\t//07/03/2023\n \n 'dd-MMMM',\t\t//07-March\n 'dd MMMM',\t\t//07 March\n 'dd-MMM',\t\t//07-Mar\n 'dd MMM',\t\t//07 Mar\n\n 'dd-MM-y',\t\t//07-03-23\n 'dd MMMM yy',\t//07 March 23\n \n 'dd/MMM/y',\t //07/Mar/2023\n 'dd/MMMM/y', //07/March/2023\n 'dd/MMM/yy', //07/Mar/23\n 'dd/MMMM/yy', //07/March/23\n \n\n 'dd-MMMM-yy',\t//07-March-23\n 'dd-MMM-yy',\t//07-Mar-23\n 'dd-MM-yy',\t\t//07-03-23\n 'MMMM-dd-yy',\t//March-07-23\n 'MMM-dd-yy',\t//Mar-07-23\n\n 'MMMM dd yy',\t//March 07 23\n 'MMM dd, yy',\t//Mar 07 23\n 'MM dd, yy',\t//03 07 23\n\n 'dd MMMM y',\t//07 March 2023\n 'dd MMM, y',\t//07 Mar 2023\n 'dd MM, yy',\t//07 03, 23\n 'dd-MMM-y',\t//07-Mar-2023\n\n 'h:mm a', // 3:30 pm\n 'h:mma' // 3:30 pm\n\n ];\n}","import { HashMap, Point } from './types.utilities';\n\nimport * as dayjs from 'dayjs';\nimport * as utc from 'dayjs/plugin/utc';\nimport * as timezone from 'dayjs/plugin/timezone';\nimport * as isToday from 'dayjs/plugin/isToday';\nimport * as weekday from 'dayjs/plugin/weekday';\nimport * as customParseFormat from 'dayjs/plugin/customParseFormat';\nimport { DateNow, DateTZ } from '@mckinsey-converge/date-tz';\n\n// TODO: this is not the best place to import this timezone addition\ndayjs.extend(utc);\ndayjs.extend(timezone);\ndayjs.extend(isToday);\ndayjs.extend(weekday);\ndayjs.extend(customParseFormat);\n\n/** Available console output streams. */\nexport type ConsoleStream = 'debug' | 'warn' | 'log' | 'error' | 'info';\n\n/**\n * Log data to the browser console\n * @param type Type of message\n * @param msg Message body\n * @param args array of argments to log to the console\n * @param stream Stream to emit the console on. 'debug', 'log', 'warn' or 'error'\n * @param force Whether to force message to be emitted when debug is disabled\n */\nexport function log(\n type: string,\n msg: string,\n args?: any,\n stream: ConsoleStream = 'debug',\n force: boolean = false,\n app_name: string = 'STAFF'\n) {\n if ((window as any).debug || force) {\n const colors: string[] = [\n 'color: #E91E63',\n 'color: #3F51B5',\n 'color: default',\n ];\n if (args) {\n console[stream](\n `%c[${app_name}]%c[${type}] %c${msg}`,\n ...colors,\n args\n );\n } else {\n console[stream](`%c[${app_name}]%c[${type}] %c${msg}`, ...colors);\n }\n }\n}\n\n/**\n * Get item from the nested object\n * @param keys List of sub-keys to search for\n * @param map Object to search\n */\n export function getItemWithKeys(keys: string[], map: HashMap) {\n const key = keys[0];\n if (map && key in map) {\n return keys.length > 1\n ? getItemWithKeys(keys.slice(1), map[key] || {})\n : map[key];\n }\n return null;\n}\n\n/* istanbul ignore next */\n/**\n * Checks whether the platform is a mobile device.\n */\nexport function isMobileDevice(): boolean {\n const r = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;\n return !!navigator.userAgent.match(r);\n}\n\n/* istanbul ignore next */\n/**\n * Checks whether the browser is Mobile Safari.\n */\nexport function isMobileSafari(): boolean {\n const agent = navigator.userAgent;\n return !!(\n agent.match(/iPhone|iPad|iPod/) &&\n agent.match(/AppleWebKit/) &&\n !agent.match('CriOS')\n );\n}\n\n/* istanbul ignore next */\n/**\n * Checks whether the browser is Android Chrome.\n */\nexport function isAndroidChrome(): boolean {\n const agent = navigator.userAgent;\n return !!(agent.match(/Android/) && agent.match(/Chrome/));\n}\n\n/**\n * Generate string representation of a number with zeros padding the length\n * @param value Number to pad with zeros\n * @param length Length of the resulting string\n */\nexport function padZero(value: number, length: number): string {\n let str = value.toString();\n while (str.length < length) {\n str = '0' + str;\n }\n return str;\n}\n\n/**\n * Remove duplicates from the given array\n * @param array List of items to remove duplicates from\n * @param key Key on array objects to compare for uniqueness\n */\nexport function unique(array: T[], key: keyof T | undefined = undefined) {\n return array.filter(\n (el, pos, arr) =>\n el &&\n arr.indexOf(\n key\n ? arr.find((i) => i && i[key] === el[key])\n : arr.find((i) => i === el)\n ) === pos\n );\n}\n\n/**\n * Convert duration to human readable string\n * @param duration Duration in minutes\n * @param short Whether to use short form of duration words e.g. hours as h\n */\nexport function humaniseDuration(durationInput: number, size: string = 'long') {\n const duration = Math.floor(durationInput);\n if (!duration || duration < 0) {\n return '';\n }\n let singular = false;\n let format = { hours: ' hour', minutes: ' minute' };\n switch (size) {\n case 'medium':\n format = { hours: 'hr', minutes: 'min' };\n break;\n case 'short':\n format = { hours: 'h', minutes: 'm' };\n singular = true;\n break;\n }\n const h = Math.floor(duration / 60);\n let d = `${\n h >= 1\n ? h +\n (singular ? format.hours : format.hours + (h === 1 ? '' : 's'))\n : ''\n }`;\n if (duration % 60 !== 0) {\n if (d) {\n d += singular ? ' ' : ', ';\n }\n const m = duration % 60;\n d += `${\n m >= 1\n ? m +\n (singular\n ? format.minutes\n : format.minutes + (m === 1 ? '' : 's'))\n : ''\n }`;\n }\n return d;\n}\n\n/**\n * Get a filtered list of items\n * @param filter Value to filter on\n * @param items List of results to filter\n * @param fields Fields to check for matches on each item\n */\nexport function filterList(\n filter: string,\n items?: T[],\n fields: string[] = ['id']\n): T[] {\n let results: any[];\n // Tokenise filter string\n const filters = (filter || '').toLowerCase().split(' ');\n const list = {};\n for (const f of filters) {\n /* istanbul ignore else */\n if (f) {\n /* istanbul ignore else */\n if (!list[f]) {\n list[f] = 0;\n }\n list[f]++;\n }\n }\n // Group similar tokens\n const parts = [];\n for (const f in list) {\n /* istanbul ignore else */\n if (list.hasOwnProperty(f)) {\n parts.push({ word: f, count: list[f], regex: new RegExp(f, 'gi') });\n }\n }\n parts.sort(\n (a, b) => b.word.length - a.word.length || a.word.localeCompare(b.word)\n );\n const item_list = JSON.parse(JSON.stringify(items || []));\n /* istanbul ignore else */\n if (filter) {\n results = item_list.filter((item) => {\n let match_count = 0;\n item.match_index = 65535;\n item.match = '';\n const field_list = {};\n // Initialise field match variables\n for (const f of fields) {\n field_list[f] = {\n value: (item[f] || '').toLowerCase(),\n index: 65536,\n matched: 0,\n };\n }\n // Search for matches with the tokenised filter string\n for (const i of parts) {\n /* istanbul ignore else */\n if (i.word) {\n // Check fields for matches\n for (const f of fields) {\n const field = field_list[f];\n const index = field.value.indexOf(i.word);\n field.index = index < field.index ? index : field.index;\n field.matches = (\n field.value.match(i.regex) || []\n ).length;\n field.value = field.value.replace(i.regex, ' ');\n }\n // Update token match count\n for (const f of fields) {\n const field = field_list[f];\n /* istanbul ignore else */\n if (field.matches >= i.count) {\n match_count++;\n // Update field matches\n let changed = 0;\n const tokens = (\n item[`match_${f}`] ||\n item[f] ||\n ''\n ).split(' ');\n for (const k of tokens) {\n /* istanbul ignore else */\n if (changed >= i.count) {\n break;\n }\n /* istanbul ignore else */\n if (\n k.toLowerCase().indexOf(i.word) >= 0 &&\n k.indexOf('`') < 0\n ) {\n tokens[tokens.indexOf(k)] = k.replace(\n i.regex,\n '`$&`'\n );\n changed++;\n }\n }\n item[`match_${f}`] = tokens.join(' ');\n break;\n }\n }\n }\n }\n // Get field with the most relevent match\n for (const f of fields) {\n const field = field_list[f];\n /* istanbul ignore else */\n if (field.index < item.match_index && field.index >= 0) {\n item.match_index = field.index;\n item.match = f;\n }\n }\n return (\n item.match_index >= 0 &&\n item.match &&\n match_count >= parts.length\n );\n });\n } else {\n results = item_list;\n }\n // Sort by order of relevence then name\n results.sort((a, b) => {\n const diff = a.match_index - b.match_index;\n return diff === 0 ? a.name.localeCompare(b.name) : diff;\n });\n return results;\n}\n\n/**\n * Convert a match string from `filterList` to renderable HTML\n * @param str Match string to change\n */\nexport function matchToHighlight(str: string): string {\n /* istanbul ignore else */\n if (str) {\n str = str.replace(\n /\\`[a-zA-Z0-9\\@\\.\\_]*\\`/g,\n '$&'\n );\n str = str.replace(/\\`/g, '');\n }\n return str;\n}\n\n/**\n * Convert time string to ms from UTC epoch for today\n * @param time Time string in the format `HH:mm`\n */\nexport function timeToDate(time: string): number {\n const parts = time.split(':');\n const date = DateNow(new Date())\n .setValue({\n hour: +parts[0],\n minute: +parts[1],\n })\n .startOfValue('minute');\n return date.ms;\n}\n\n/**\n * Generate a random number\n * @param ceil Biggest value to generate not inclusive\n * @param floor Smallest value to generate. Defaults to 0\n */\nexport function randomInt(ceil: number, floor: number = 0) {\n return Math.floor(Math.random() * (ceil - floor)) + floor;\n}\n\n/**\n * Get time format string for locale\n * @param isLowerCaseAmPm - if true, we use lowercase for am/pm.\n */\nexport function timeFormatString(): string {\n return is24HourTime() ? 'HH:mm' : 'h:mma';\n}\n\nexport const hoursMinutes = (): string => (is24HourTime() ? 'HH:mm' : 'h:mma');\n\nexport function dayJsTimeFormatString(): string {\n return is24HourTime() ? 'HH:mm' : 'h:mma';\n}\n\nexport const dayJsHoursMinutes = (): string =>\n is24HourTime() ? 'HH:mm' : 'h:mm';\n\n/**\n * Converts a timeZone name into a date object.\n * @param timeZoneName - name of timezone\n */\nexport const timezoneNameToDate = (\n timeZoneName: string | undefined\n): DateTZ | undefined => {\n let zone: DateTZ | undefined;\n if (timeZoneName) {\n try {\n zone = DateNow(new Date()).toZone(timeZoneName);\n } catch (e) {\n // invalid timezone. Suppress logs here.\n // console.error(e);\n }\n }\n return zone;\n};\n\nexport const timezoneDisplay = (date: DateTZ) =>\n !!date ? `${date.formatDate('ZZZZ')}` : '';\nexport const gmtOffsetDisplay = (date: DateTZ) =>\n !!date ? `(GMT${date.formatDate('ZZ')})` : '';\n\n/**\n * Returns a full time with timezone.\n * @param date\n */\nexport function timeWithZoneFormatString(date: DateTZ): string {\n return `${date\n .formatDate(timeFormatString())\n .toLocaleLowerCase()} ${timezoneDisplay(date)}`;\n}\n\nexport function timeWithGmtOffsetFormatString(date: DateTZ): string {\n return `${date\n .formatDate(timeFormatString())\n .toLocaleLowerCase()} ${gmtOffsetDisplay(date)}`;\n}\n\n/**\n * Returns a full start end range.\n *\n * Start and end both use lowercase am/pm markers (c.f. timeFormatString).\n * @param startDateTz start time DateTZ object\n * @param endDateTz end time DateTZ object\n */\nexport function startEndTimeFormatString(\n startDateTz: DateTZ,\n endDateTz: DateTZ\n): string {\n return `${startDateTz\n .formatDate(timeFormatString())\n .toLocaleLowerCase()}-${endDateTz\n .formatDate(timeFormatString())\n .toLocaleLowerCase()}`;\n}\n\n/**\n * Returns a full start end range with timezone.\n *\n * Start and end both use lowercase am/pm markers (c.f. timeFormatString).\n * @param startDateTz start time DateTZ object\n * @param endDateTz end time DateTZ object\n */\nexport function startEndTimeWithZoneFormatString(\n startDateTz: DateTZ,\n endDateTz: DateTZ\n): string {\n return `${startDateTz\n .formatDate(timeFormatString())\n .toLocaleLowerCase()}-${endDateTz\n .formatDate(timeFormatString())\n .toLocaleLowerCase()} ${gmtOffsetDisplay(startDateTz)}`;\n}\n\n/**\n * Returns a full date with weekday day month year.\n * @param date\n * @param comma optional boolean to display a comma after the month\n */\nexport function dateLocalFormatString(date: DateTZ, comma?: boolean): string {\n if (comma) {\n return date.formatLocalDate('cccc d MMMM, yyyy');\n }\n return date.formatLocalDate('cccc d MMMM yyyy');\n}\n\nexport function dateBuildingFormatString(\n date: DateTZ,\n comma?: boolean\n): string {\n if (comma) {\n return date.formatDate('cccc d MMMM, yyyy');\n }\n return date.formatDate('cccc d MMMM yyyy');\n}\n\nexport function shorterLocalDateFormatString(\n date: DateTZ,\n comma?: boolean\n): string {\n if (comma) {\n return date.formatLocalDate('ccc d MMM, yyyy');\n }\n return date.formatLocalDate('ccc d MMM yyyy');\n}\n\nexport function shorterBuildingDateFormatString(\n date: DateTZ,\n comma?: boolean\n): string {\n if (comma) {\n return date.formatDate('ccc d MMM, yyyy');\n }\n return date.formatDate('ccc d MMM yyyy');\n}\n\n/** Whether locale string is displayed in 24 hour time */\nexport function is24HourTime(): boolean {\n const date = new Date();\n const localeString = date\n .toLocaleTimeString(\n document.querySelector('html').getAttribute('lang') ||\n navigator.language\n )\n .toLowerCase();\n return localeString.indexOf('am') < 0 && localeString.indexOf('pm') < 0;\n}\n\n/* istanbul ignore next */\n/**\n * Downloads a file to the users computer with the given filename and contents\n * @param filename Name of the file to download\n * @param contents Contents of the file to download\n */\nexport function downloadFile(filename: string, contents: string) {\n const element = document.createElement('a');\n element.setAttribute(\n 'href',\n 'data:text/plain;charset=utf-8,' +\n encodeURIComponent('\\uFEFF' + contents)\n );\n element.setAttribute('download', filename);\n\n element.style.display = 'none';\n document.body.appendChild(element);\n\n element.click();\n\n document.body.removeChild(element);\n}\n\n/**\n * Parse raw CSV data into a JSON object\n * @param csv CSV data to parse\n */\nexport function csvToJson(csv: string) {\n const lines = csv.split('\\n');\n let fields = lines.splice(0, 1)[0].split(',');\n fields = fields.map((v) => v.replace('\\r', ''));\n const list: any[] = [];\n for (const line of lines) {\n let parts = line.split(',');\n parts = parts.map((v) => v.replace('\\r', ''));\n /* istanbul ignore else */\n if (parts.length >= fields.length) {\n const item: any = {};\n for (let i = 0; i <= parts.length; i++) {\n let part = null;\n part = parts[i];\n /* istanbul ignore else */\n if (part !== undefined) {\n item[(fields[i] || '').split(' ').join('_').toLowerCase()] =\n part;\n }\n }\n list.push(item);\n }\n }\n\n return list;\n}\n\n/**\n * Convert javascript array to CSV string\n * @param json Javascript array to convert\n */\nexport function jsonToCsv(json: HashMap[]) {\n /* istanbul ignore else */\n if (json instanceof Array && json.length > 0) {\n const keys = Object.keys(json[0]);\n const valid_keys = keys.filter((key) => json[0].hasOwnProperty(key));\n return `${valid_keys.join(',')}\\n${json\n .map((item) =>\n valid_keys\n .map((key) => {\n return typeof item[key] !== 'boolean'\n ? `\"${item[key] || ''}\"`\n : item[key];\n })\n .join(',')\n )\n .join('\\n')}`;\n }\n return '';\n}\n\n/* istanbul ignore next */\n/**\n * detect IE\n * returns version of IE or false, if browser is not Internet Explorer\n */\nexport function detectIE() {\n var ua = window.navigator.userAgent;\n\n var msie = ua.indexOf('MSIE ');\n if (msie > 0) {\n // IE 10 or older => return version number\n return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10);\n }\n\n var trident = ua.indexOf('Trident/');\n if (trident > 0) {\n // IE 11 => return version number\n var rv = ua.indexOf('rv:');\n return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10);\n }\n\n var edge = ua.indexOf('Edge/');\n if (edge > 0) {\n // Edge (IE 12+) => return version number\n return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10);\n }\n\n // other browser\n return false;\n}\n\n/**\n * Grab point details from mouse or touch event\n * @param event Event to grab details from\n */\nexport function eventToPoint(event: MouseEvent | TouchEvent): Point {\n if (!event) {\n return { x: -1, y: -1 };\n }\n if (event instanceof MouseEvent) {\n return { x: event.clientX, y: event.clientY };\n } else {\n return event.touches && event.touches.length > 0\n ? { x: event.touches[0].clientX, y: event.touches[0].clientY }\n : { x: -1, y: -1 };\n }\n}\n\n/* istanbul ignore next */\n/**\n * Flatten nested array\n * @param an_array Array to flatten\n */\nexport function flatten(an_array: T[]) {\n const stack = [...an_array];\n const res = [];\n while (stack.length) {\n // pop value from stack\n const next = stack.pop();\n if (Array.isArray(next)) {\n // push back array items, won't modify the original input\n stack.push(...next);\n } else {\n res.push(next);\n }\n }\n // reverse to restore input order\n return res.reverse();\n}\n\nconst seed = xmur3('PlaceOS');\nconst rand = sfc32(0x9e3779b9, 0x243f6a88, 0xb7e15162, seed());\n\nexport function predictableRandomInt(ceil: number = 100, floor: number = 0) {\n return Math.floor(rand() * (ceil - floor)) + floor;\n}\n\n// https://stackoverflow.com/questions/521295/seeding-the-random-number-generator-in-javascript\nfunction xmur3(str) {\n for (var i = 0, h = 1779033703 ^ str.length; i < str.length; i++)\n (h = Math.imul(h ^ str.charCodeAt(i), 3432918353)),\n (h = (h << 13) | (h >>> 19));\n return function () {\n h = Math.imul(h ^ (h >>> 16), 2246822507);\n h = Math.imul(h ^ (h >>> 13), 3266489909);\n return (h ^= h >>> 16) >>> 0;\n };\n}\n\nfunction sfc32(a, b, c, d) {\n return function () {\n a >>>= 0;\n b >>>= 0;\n c >>>= 0;\n d >>>= 0;\n var t = (a + b) | 0;\n a = b ^ (b >>> 9);\n b = (c + (c << 3)) | 0;\n c = (c << 21) | (c >>> 11);\n d = (d + 1) | 0;\n t = (t + d) | 0;\n c = (c + t) | 0;\n return (t >>> 0) / 4294967296;\n };\n}\n\nexport function isLessThanBreakpoint(breakpoint: number): boolean {\n if (window) {\n return window.innerWidth < breakpoint;\n }\n return false;\n}\n\nexport function toTitleCase(str: string): string {\n return typeof str === 'string'\n ? str\n .toLowerCase()\n .split(' ')\n .map(function (word) {\n return word.replace(word[0], word[0].toUpperCase());\n })\n .join(' ')\n : '';\n}\n","\nimport { Directive, OnDestroy } from '@angular/core';\nimport { BaseClass } from './base.class';\nimport { SpaceImageObject } from '@mckinsey-converge/base';\nimport { BehaviorSubject } from 'rxjs';\n\n@Directive({\n selector: 'a-very-basic-component-image-that-should-not-be-used'\n})\nexport class ImageDirective extends BaseClass implements OnDestroy {\n public foundImages?: SpaceImageObject[];\n public loadImages?: BehaviorSubject\n public ngOnDestroy(): void {\n this.destroy();\n }\n}","import {\n Directive,\n ElementRef,\n HostListener\n} from '@angular/core';\n\n@Directive({\n selector: 'input[type=number], input[numbersOnly]'\n})\nexport class NumbersOnlyDirective {\n\n constructor(private _el: ElementRef) {\n }\n\n @HostListener('input', ['$event']) onInputChange(event) {\n const initalValue = this._el.nativeElement.value;\n this._el.nativeElement.value = initalValue.replace(/[^0-9]*/g, '');\n if (initalValue !== this._el.nativeElement.value) {\n event.stopPropagation();\n }\n }\n}\n","import { Pipe, PipeTransform } from '@angular/core';\n\n@Pipe({\n name: 'replace'\n})\nexport class ReplacePipe implements PipeTransform {\n\n transform(value: string, from: string = '_', to: string = ' '): string {\n return (value|| '').split(from).join(to);\n }\n}\n","export * from './lib/bookings.module';\nexport * from './lib/bookings.actions'\nexport * from './lib/bookings.reducer'\nexport * from './lib/bookings.types'\nexport * from './lib/bookings.utils'\n","\nimport { IBookingQueryOptions, PaginatedBooking } from '@mckinsey-converge/data-common';\nimport { \n createLoadingAction\n} from '../../../loading/src/lib/loading.actions';\n\nimport {\n MyBookingsState,\n MyBookingsStoreState,\n} from './bookings.types';\nimport {\n createAction,\n props\n} from '@ngrx/store';\n\nexport const bookingStateSelector = (state: MyBookingsStoreState) => state.bookings;\n\nexport const loadBookingsWithQuery = createAction('[Bookings] Load Bookings with query', props());\n\nexport const loadHomepageBookingsResults = createLoadingAction('HomepageBookings', 'homepage');\n\nexport const loadUpcomingBookingsResults = createLoadingAction('UpcomingBookings', 'upcoming');\n\nexport const loadPastBookingsResults = createLoadingAction('PastBookings', 'past');\n\nexport const loadCancelledBookingsResults = createLoadingAction('CancelledBookings', 'cancelled');\n\nexport const loadBookingByIdResults = createLoadingAction('BookingById', 'bookingById');\n\nexport const clearBookingByIdResults = createAction('[BookingById] Clear Booking Data');\n","import { Injectable } from '@angular/core';\nimport {\n Actions,\n Effect,\n ofType\n} from '@ngrx/effects';\nimport {\n IBookingQueryOptions,\n BookingsPaginatedService\n} from '../../../data-common/src/lib/bookings';\nimport {\n createLoadingEffect,\n LoadingAction\n} from '@mckinsey-converge/loading';\nimport {\n loadHomepageBookingsResults,\n loadPastBookingsResults,\n loadUpcomingBookingsResults,\n loadCancelledBookingsResults,\n loadBookingsWithQuery,\n loadBookingByIdResults\n} from './bookings.actions';\nimport { mergeMap } from 'rxjs/operators';\nimport {\n\n} from './bookings.utils';\nimport { DateTZ } from '@mckinsey-converge/date-tz';\n\n@Injectable()\nexport class BookingsEffects {\n constructor(private actions: Actions,\n private bookingService: BookingsPaginatedService) {\n }\n\n @Effect()\n public afterBookingsRequestResultsLoadOthersEffect = this.actions.pipe(\n ofType(loadBookingsWithQuery),\n mergeMap((action: LoadingAction) => {\n const date = new DateTZ({date: action.from})\n const yourBookingsRequest = {\n email: action.email,\n pagination: true,\n limit: null,\n offset: action.offset,\n show_cancelled: false,\n sort: 'start_epoch asc',\n filters: {\n end_epoch: `>=${date.subtractValue({ minutes: 5 }).seconds}`,\n },\n include_rooms: true\n };\n const upcomingRequest = {\n email: action.email,\n pagination: true,\n limit: null,\n offset: action.offset,\n show_cancelled: false,\n sort: 'start_epoch asc',\n filters: {\n end_epoch: `>=${date.seconds}`,\n },\n include_rooms: true\n };\n const pastRequest = {\n email: action.email,\n pagination: true,\n limit: null,\n offset: action.offset,\n show_cancelled: false,\n sort: 'start_epoch desc',\n filters: {\n start_epoch: `>${date.subtractValue({ hours: 48 }).seconds}`,\n end_epoch: `<${date.seconds}`,\n },\n include_rooms: true\n };\n const cancelledRequest = {\n email: action.email,\n pagination: true,\n limit: null,\n offset: action.offset,\n show_cancelled: true,\n sort: 'start_epoch desc',\n filters: {\n start_epoch: `>${date.subtractValue({ hours: 48 }).seconds}`,\n },\n include_rooms: true\n };\n const byIdRequest = {\n email: action.email,\n id: action.id,\n pagination: true,\n limit: 1,\n include_rooms: true\n };\n\n const effects = [];\n switch(action.target) {\n case 'upcoming':\n effects.push(loadUpcomingBookingsResults.request(upcomingRequest));\n break;\n case 'past':\n effects.push(loadPastBookingsResults.request(pastRequest));\n break;\n case 'cancelled':\n effects.push(loadCancelledBookingsResults.request(cancelledRequest));\n break;\n case 'bookingById':\n effects.push(loadBookingByIdResults.request(byIdRequest));\n break\n case 'homepage':\n effects.push(loadHomepageBookingsResults.request(yourBookingsRequest));\n // effects.push(loadPastBookingsResults.request(pastRequest));\n break\n default:\n }\n return effects;\n })\n );\n\n @Effect()\n public loadHomepageBookingsResultsEffect = createLoadingEffect(this.actions, loadHomepageBookingsResults,\n (action) => this.bookingService.userBookings(action, `homepage-${action.filters?.start_epoch || ''}${action.filters?.end_epoch || ''}`));\n\n @Effect()\n public loadUpcomingBookingsResultsEffect = createLoadingEffect(this.actions, loadUpcomingBookingsResults,\n (action) => this.bookingService.userBookings(action, `upcoming-${action.filters?.start_epoch || ''}${action.filters?.end_epoch || ''}`));\n \n @Effect()\n public loadPastBookingsResultsEffect = createLoadingEffect(this.actions, loadPastBookingsResults,\n (action) =>\n this.bookingService.userBookings(action, `past-${action.filters?.start_epoch || ''}${action.filters?.end_epoch || ''}`));\n\n @Effect()\n public loadCancelledBookingsResultsEffect = createLoadingEffect(this.actions, loadCancelledBookingsResults,\n (action) =>\n this.bookingService.userBookings(action, `cancelled-${action.filters?.start_epoch || ''}${action.filters?.end_epoch || ''}`));\n\n @Effect()\n public loadBookingByIdResultsEffect = createLoadingEffect(this.actions, loadBookingByIdResults,\n (action) => \n this.bookingService.userBookings(action, `bookingById-${action.id}`));\n\n}\n","import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { EffectsModule } from '@ngrx/effects';\nimport { StoreModule } from '@ngrx/store';\nimport { bookingsReducer } from './bookings.reducer';\nimport { BookingsEffects } from './bookings.effects';\n\n@NgModule({\n imports: [\n CommonModule,\n EffectsModule.forFeature([BookingsEffects]),\n StoreModule.forFeature('bookings', bookingsReducer)\n ]\n})\nexport class BookingsModule {\n}\n","import { LoadingModel } from '@mckinsey-converge/loading';\nimport {\n createReducer,\n on\n} from '@ngrx/store';\nimport {\n loadHomepageBookingsResults,\n loadUpcomingBookingsResults,\n loadPastBookingsResults,\n loadCancelledBookingsResults,\n loadBookingByIdResults,\n clearBookingByIdResults,\n} from './bookings.actions';\nimport { MyBookingsState } from './bookings.types';\n \nimport { \n BookingFormState,\n clearBookingFormData,\n clearRoomFilters,\n openBookingSurvey,\n storeBookingFormData,\n storeRoomFilters\n} from '@mckinsey-converge/data-common';\n\n\nexport const initialBookingsState: MyBookingsState = {\n query: LoadingModel.empty(),\n homepage: LoadingModel.empty(),\n upcoming: LoadingModel.empty(),\n past: LoadingModel.empty(),\n cancelled: LoadingModel.empty(),\n bookingById: LoadingModel.empty(),\n};\n\nexport const bookingsReducer = createReducer(initialBookingsState,\n on(clearBookingByIdResults, (state: MyBookingsState) => ({\n ...state,\n bookingById: LoadingModel.empty(),\n }) as MyBookingsState),\n ...loadHomepageBookingsResults.ons,\n ...loadUpcomingBookingsResults.ons,\n ...loadPastBookingsResults.ons,\n ...loadCancelledBookingsResults.ons,\n ...loadBookingByIdResults.ons,\n );\n\n\n\n export const initialBookingFormState: BookingFormState = {\n activeFormFilters: []\n };\n \n export const bookingFormReducer = createReducer(initialBookingFormState,\n on(storeBookingFormData, (state: BookingFormState, action) => {\n return {\n ...state,\n activeForm: {\n ...state.activeForm,\n ...action.payload\n }\n } as BookingFormState;\n }),\n on(clearBookingFormData, state => ({\n ...state,\n activeForm: undefined\n }) as BookingFormState),\n on(openBookingSurvey, state => ({\n ...state,\n bookingCompleted: new Date()\n }) as BookingFormState),\n on(storeRoomFilters, (state: BookingFormState, action) => {\n return {\n ...state,\n activeFormFilters: action.payload\n } as BookingFormState;\n }),\n on(clearRoomFilters, (state: BookingFormState) => ({\n ...state,\n activeFormFilters: initialBookingFormState.activeFormFilters\n }) as BookingFormState)\n );\n \n","import { Booking } from '@mckinsey-converge/data-common';\nimport { SelectOption } from '@mckinsey-converge/ui';\n\nexport const mapBookingToSelectOption = (booking?: Booking): SelectOption => booking ? ({\n value: booking.id,\n display: booking.name\n}) : undefined;\n","export * from './lib/buildings.module';\nexport * from './lib/buildings.actions'\nexport * from './lib/buildings.types'\nexport * from './lib/buildings.reducer'\n","import { \n createLoadingAction,\n} from '../../../loading/src/lib/loading.actions';\nimport {\n Building,\n BuildingCity,\n} from '../../../data-common/src/lib/organisation/building.class';\nimport {\n BuildingLevel,\n} from '../../../data-common/src/lib/organisation/level.class'\nimport {\n Organisation,\n} from '../../../data-common/src/lib/organisation/organisation.class'\nimport { createSelector } from '@ngrx/store';\nimport {\n BuildingState,\n BuildingStoreState\n} from './buildings.types';\n\nexport const loadBuildings = createLoadingAction('Buildings', 'data');\n\nexport const buildingStateSelector = (state: BuildingStoreState) => state.buildings;\n\nexport const selectLoadBuildingsSuccess = createSelector(buildingStateSelector, loadBuildings.selectors.optionalSuccess);\n\n/**\n * Groups buildings by city\n */\nexport const selectBuildingOptionsGroupedByCity = createSelector(loadBuildings.selectors.optionalSuccess, (success) => {\n if (success) {\n const mapped = new Map();\n success.forEach(s => {\n const found = Array.from(mapped.keys()).find(c => c.name === s.city);\n let list = found ? mapped.get(found) : undefined;\n if (!list) {\n list = [];\n mapped.set({\n name: s.city,\n timezone: s.timezone\n }, list);\n }\n list.push(s);\n });\n return mapped;\n }\n return new Map();\n});\n\nexport const selectLevelByZoneId = createSelector(selectLoadBuildingsSuccess,\n (buildings, param: string) => {\n if (buildings) {\n return buildings.map(b => b.levels.find((l: BuildingLevel) => l.id === param))\n ?.[0] ?? undefined;\n }\n return undefined;\n });\n","import { Injectable } from '@angular/core';\nimport {\n Actions,\n Effect,\n ofType\n} from '@ngrx/effects';\nimport {\n Organisation,\n OrganisationService\n} from '../../../data-common/src/lib/organisation';\nimport { loadBuildings } from './buildings.actions';\nimport {\n createLoadingEffect,\n LoadingAction,\n Payload\n} from '@mckinsey-converge/loading';\nimport { loadOrganisations } from '../../../organisation/src/lib/organisation.actions';\nimport { map } from 'rxjs/operators';\n\n\n@Injectable()\nexport class BuildingsEffects {\n\n constructor(private actions: Actions,\n private organizationService: OrganisationService) {\n\n }\n\n @Effect()\n public loadBuildings = createLoadingEffect(this.actions, loadBuildings,\n (action) => this.organizationService.loadBuildingsWithOrg(action));\n\n @Effect()\n public loadBuildingsAfterOrg = this.actions.pipe(\n ofType(loadOrganisations.success),\n map((action: LoadingAction>) => loadBuildings.request(action.payload)));\n}\n","import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { EffectsModule } from '@ngrx/effects';\nimport { BuildingsEffects } from './buildings.effects';\nimport { StoreModule } from '@ngrx/store';\nimport { buildingsReducer } from './buildings.reducer';\n\n\n@NgModule({\n declarations: [],\n imports: [\n CommonModule,\n EffectsModule.forFeature([\n BuildingsEffects\n ]),\n StoreModule.forFeature('buildings', buildingsReducer)\n ]\n})\nexport class BuildingsModule {\n}\n","import { createReducer } from '@ngrx/store';\nimport { LoadingModel } from '@mckinsey-converge/loading';\nimport { loadBuildings } from './buildings.actions';\nimport { BuildingState } from './buildings.types';\n\nexport const initialBuildingState: BuildingState = {\n data: LoadingModel.empty()\n};\n\nexport const buildingsReducer = createReducer(initialBuildingState,\n ...loadBuildings.ons\n);\n","export * from './lib/data-common.module';\nexport * from './lib/booking';\nexport * from './lib/bookings';\nexport * from './lib/catering';\nexport * from './lib/location';\nexport * from './lib/organisation';\nexport * from './lib/spaces';\nexport * from './lib/users';\nexport * from './lib/app.service';\nexport * from './lib/hotkeys.service';\nexport * from './lib/base.service';\nexport * from './lib/base-api.class';\nexport * from './lib/reports'\nexport * from './lib/service-manager.class';\nexport * from './lib/settings.service';\nexport * from './lib/spec-helpers';\nexport * from './lib/settings.interfaces';\nexport * from './lib/validation.utilities';\nexport * from './lib/status.interfaces';\nexport * from './lib/recurrence/recurrence.utils';\nexport * from './lib/close-modal-dialog-service';\nexport * from './lib/validation.utilities';\nexport * from './lib/collapse-accordion-service';","import { HashMap } from '@mckinsey-converge/base';\n\n/**\n * Convert map into a query string\n * @param map Key value pairs to convert\n */\nexport function toQueryString(map: HashMap) {\n let str = '';\n if (map) {\n for (const key in map) {\n if (map.hasOwnProperty(key) && map[key] !== undefined && map[key] !== null) {\n str += `${(str ? '&' : '')}${key}=${map[key]}`;\n }\n }\n }\n return str;\n}\n","import {\n ApplicationRef,\n Injectable,\n NgZone,\n} from '@angular/core';\nimport { Title } from '@angular/platform-browser';\nimport { MatSnackBar } from '@angular/material/snack-bar';\nimport { take, first } from 'rxjs/operators';\n\nimport { ComposerService } from '@placeos/composer';\nimport { PlaceOSOptions } from '@placeos/ts-client';\nimport { HeapIoService } from '@acaprojects/ngx-heap-io';\n\nimport {\n BehaviorSubject,\n Observable,\n Subject\n} from 'rxjs';\n\nimport {\n ApplicationLoadingState,\n BaseClass,\n ConsoleStream,\n log\n} from '@mckinsey-converge/base';\nimport { SettingsService } from './settings.service';\n\nimport { HotkeysService } from './hotkeys.service';\nimport {\n ApplicationIcon,\n ComposerSettings\n} from './settings.interfaces';\nimport { EnvironmentService } from '../../../environment/src/lib/environment.service'; // '@mckinsey-converge/environment';\nimport {\n Store\n} from '@ngrx/store';\nimport { selectCurrentUser } from '../../../user/src/lib/user.actions';\nimport { UserStoreState } from '../../../user/src/lib/user.types';\nimport { Booking } from './bookings';\nimport { filter } from 'rxjs/operators';\nimport { SnackBarService } from '../../../ui/src/lib/custom-snackbar-component/custom-snackbar-component.service';\n\ndeclare global {\n interface Window {\n application: ApplicationService;\n mock: {\n enabled: boolean;\n backend: any;\n };\n }\n}\n\n@Injectable({\n providedIn: 'root'\n})\nexport class ApplicationService extends BaseClass {\n /** Map of state variables for Service */\n protected _subjects: {\n [key: string]: BehaviorSubject | Subject;\n } = {};\n /** Map of observables for state variables */\n protected _observers: { [key: string]: Observable } = {};\n _kioskFormFilter: boolean;\n\n constructor(\n public store: Store,\n public analytics: HeapIoService,\n private _app_ref: ApplicationRef,\n private _zone: NgZone,\n private _title: Title,\n private _settings: SettingsService,\n private _hotkeys: HotkeysService,\n private _composer: ComposerService,\n private _snackbar: MatSnackBar,\n private _environment: EnvironmentService,\n private snack: SnackBarService\n\n ) {\n super();\n this.set('system', null);\n this.set('title', 'Home');\n this.set('loading', {});\n this.set('CONCIERGE.day_view.viewing', null);\n this.set('undo', new BehaviorSubject<{\n action: 'series' | 'booking',\n booking: Booking\n } | null>(null));\n\n this.set('APP.breakdown', false);\n\n this._app_ref.isStable.pipe(first(_ => _)).subscribe(() => {\n this._zone.run(() => {\n this.log('APP', `Application has stablised.`);\n this.waitForSettings();\n });\n });\n }\n\n public set kioskFormFilter(isIt: boolean) {\n this._kioskFormFilter = isIt;\n }\n\n public get kioskFormFilter():boolean {\n return this._kioskFormFilter \n }\n\n /** Analytics service */\n public get Analytics() {\n return {};\n }\n\n /** Hotkeys service */\n public get Hotkeys() {\n return this._hotkeys;\n }\n\n /**\n * Get a setting from the settings service\n * @param key Name of the setting. i.e. nested items can be grabbed using `.` to seperate key names\n */\n public setting(key: string): any {\n return this._settings.get(key);\n }\n\n /**\n * Title of the page\n */\n public set title(value: string) {\n const title_suffix = this.setting('app.title');\n this.set('title', value);\n this._title.setTitle(`${value ? value + ' | ' : ''}${title_suffix}`);\n }\n\n /**\n * Title of the page\n */\n public get title(): string {\n return this._title.getTitle();\n }\n\n /** Root API Endpoint */\n public get endpoint() {\n return `/api/staff/`;\n }\n\n /** Root API Endpoint for engine */\n public get engine_endpoint() {\n return this._composer.auth.api_endpoint + '/';\n }\n\n /** Whether settings has been loaded */\n public get has_settings(): boolean {\n return this._settings.is_initialised;\n }\n\n /**\n * Create notification popup\n * @param type CSS Class to add to the notification\n * @param message Message to display on the notificaiton\n * @param action Display text for the callback action\n * @param on_action Callback of action on the notification\n * @param icon Icon to render to the left of the notification message\n */\n public notify(\n type: string,\n message: string,\n action: string = 'OK',\n on_action?: () => void,\n icon: ApplicationIcon = {\n type: 'icon',\n class: 'material-icons',\n content: 'info'\n }\n ): void {\n this.openSnack(message, type);\n // const snackbar_ref = this._snackbar.open(message, action, {\n // panelClass: [type],\n // duration: 5000\n // });\n // this.subscription(\n // 'snackbar_close',\n // snackbar_ref.afterDismissed().subscribe(() => {\n // this.unsub('snackbar_close');\n // this.unsub('notify');\n // })\n // );\n // if (action) {\n // on_action = on_action || (() => snackbar_ref.dismiss());\n // this.subscription(\n // 'notify',\n // snackbar_ref.onAction().subscribe(() => on_action())\n // );\n // }\n }\n\n public openSnack(message, type) {\n this.snack.openSnackBar(message, type, 5000);\n }\n \n /**\n * Create success notification popup\n * @param msg Message to display on the notificaiton\n * @param action Display text for the callback action\n * @param on_action Callback of action on the notification\n */\n public notifySuccess(msg: string, action?: string, on_action?: () => void): void {\n const icon: ApplicationIcon = {\n type: 'icon',\n class: 'material-icons',\n content: 'done'\n };\n this.notify('success', msg, action, on_action, icon);\n }\n\n /**\n * Create error notification popup\n * @param msg Message to display on the notificaiton\n * @param action Display text for the callback action\n * @param on_action Callback of action on the notification\n */\n public notifyError(msg: string, action?: string, on_action?: () => void): void {\n const icon: ApplicationIcon = {\n type: 'icon',\n class: 'material-icons',\n content: 'error'\n };\n this.notify('error', msg, action, on_action, icon);\n }\n\n /**\n * Create warning notification popup\n * @param msg Message to display on the notificaiton\n * @param action Display text for the callback action\n * @param on_action Callback of action on the notification\n */\n public notifyWarn(msg: string, action?: string, on_action?: () => void): void {\n const icon: ApplicationIcon = {\n type: 'icon',\n class: 'material-icons',\n content: 'warning'\n };\n this.notify('warn', msg, action, on_action, icon);\n }\n\n /**\n * Create info notification popup\n * @param msg Message to display on the notificaiton\n * @param action Display text for the callback action\n * @param on_action Callback of action on the notification\n */\n public notifyInfo(msg: string, action?: string, on_action?: () => void): void {\n this.notify('info', msg, action, on_action);\n }\n\n /**\n * Log data to the browser console\n * @param type Type of message\n * @param msg Message body\n * @param args array of argments to log to the console\n * @param stream Stream to emit the console on. 'debug', 'log', 'warn' or 'error'\n * @param force Whether to force message to be emitted when debug is disabled\n */\n public log(\n type: string,\n msg: string,\n args?: any,\n stream: ConsoleStream = 'debug',\n force: boolean = false\n ): void {\n log(type, msg, args, stream, force);\n }\n\n /**\n * Get the current value of the named property\n * @param name Property name\n */\n public get(name: string): U {\n return this._subjects[name] && this._subjects[name] instanceof BehaviorSubject\n ? (this._subjects[name] as BehaviorSubject).getValue()\n : null;\n }\n\n /**\n * Listen to value change of the named property\n * @param name Property name\n * @param next Callback for value changes\n */\n public listen(name: string): Observable {\n if (!this._observers[name]) {\n this.set(name, null);\n }\n return this._observers[name];\n }\n\n /**\n * Update the value of the named property\n * @param name Property name\n * @param value New value\n */\n public set(name: string, value: U): void {\n if (!this._subjects[name]) {\n this._subjects[name] = new BehaviorSubject(value);\n this._observers[name] = this._subjects[name].asObservable();\n } else {\n this._subjects[name].next(value);\n }\n }\n\n /** Wait for settings to be initialised before setting up the application */\n private waitForSettings() {\n // Wait until the settings have loaded before initialising\n this._settings.initialised.pipe(first(_ => _)).subscribe(() => this.init());\n }\n\n /**\n * Initialise application services\n */\n private init(): void {\n this.setupComposer();\n this.subscription('currentUser', this.store.select(selectCurrentUser)\n .pipe( filter(user => user !== undefined) )\n .pipe(take(1))\n .subscribe((user: any) => {\n // Once we know we have the user loaded.\n this.setupAnalytics(user?.fmno);\n })\n );\n this._composer.initialised.pipe(first(_ => _)).subscribe(() => {\n this._initialised.next(true);\n });\n // Add service to window if in debug mode\n if (window.debug) {\n window.application = this;\n }\n }\n\n /**\n * Initialise the composer library comms\n */\n private setupComposer(): void {\n this.log('SYSTEM', 'Setup up composer...');\n const loading: ApplicationLoadingState = this.get('loading');\n loading.composer = {\n message: 'Initialising service connection',\n state: 'loading'\n };\n this.set('loading', loading);\n // Get application settings\n const settings: ComposerSettings = this._settings.get('composer') || {};\n const protocol = settings.protocol || location.protocol;\n const host = settings.domain || location.hostname;\n const port = settings.port || location.port;\n const url = settings.use_domain ? `${protocol}//${host}:${port}` : location.origin;\n const route = host.includes('localhost') && port === '4200' ? '' : settings.route || '';\n const mock =\n this._settings.get('mock') ||\n location.href.includes('mock=true') ||\n localStorage.getItem('mock') === 'true';\n // Generate configuration object\n const config: PlaceOSOptions = {\n scope: 'public',\n host: `${host}:${port}`,\n auth_uri: `${url}/auth/oauth/authorize`,\n token_uri: `${url}/auth/token`,\n redirect_uri: `${location.origin}${route}/oauth-resp.html`,\n handle_login: !settings.local_login,\n mock\n };\n this._composer.setup(config);\n loading.composer = {\n message: 'Initialising service connection',\n state: 'complete'\n };\n this.set('loading', loading);\n }\n\n private setupAnalytics(fmno: string) {\n this.log('HEAP', `Loading heap ${this._environment?.heap_io_id}`);\n // Default Heap app ID via the default setting\n const heapIo = this._settings.get('app.heap_io');\n if (this._environment?.heap_io_id) {\n // Apply Heap app ID via the environmental setting\n heapIo.app_id = this._environment.heap_io_id;\n }\n if (heapIo) {\n this.analytics.load(heapIo);\n if (fmno) {\n this.analytics.identify(fmno);\n }\n } else {\n this.log('HEAP', `Heap could not be found for the current frontend ${this._settings.frontend}`);\n }\n }\n\n}\n","import { Subject } from 'rxjs';\n\nimport {\n BaseClass,\n HashMap\n} from '@mckinsey-converge/base';\nimport {\n ServiceLike,\n ServiceManager\n} from './service-manager.class';\n\nexport type ApiEventType = 'value_change' | 'item_saved' | 'reset' | 'other';\n\nexport interface ApiEvent {\n type: ApiEventType;\n metadata: T;\n}\n\n\nexport class BaseDataClass extends BaseClass {\n /** Subject for emitting events on the object */\n protected readonly event_subject = new Subject();\n /** Observable for events on this object */\n public readonly events = this.event_subject.asObservable();\n /** Unique Identifier of the object */\n public readonly id: string;\n /** Human readable name of the object */\n public readonly name: string;\n /** Email address associated with the object */\n public readonly email: string;\n /** Map of local property names to server ones */\n protected _server_names: HashMap = {};\n\n /** Service for managing model on the server */\n protected get _service(): ServiceLike {\n return ServiceManager.serviceFor(BaseDataClass);\n }\n\n constructor(raw_data: HashMap) {\n super();\n this.id = raw_data.id || raw_data.zone_id || raw_data.email || '';\n this.name = raw_data.name || '';\n this.email = `${raw_data.email || ''}`.toLowerCase();\n }\n\n /**\n * Save pending changes to server\n */\n public save(): Promise {\n if (this._service) {\n const form = this.toJSON();\n return new Promise((resolve, reject) => {\n const promise = this.id\n ? this._service.update(this.id, form)\n : this._service.add(form);\n promise.then(\n (d) => {\n this.event_subject.next({ type: 'item_saved', metadata: d });\n resolve(d);\n },\n (_) => reject(_)\n );\n });\n } else {\n Promise.reject('No service to process request');\n }\n }\n\n /**\n * Delete this item from the server\n */\n public delete(): Promise {\n if (this.id) {\n return this._service.delete(this.id);\n }\n }\n\n /**\n * Run task for this item on the service\n * @param task_name Name of the task\n * @param parameters Parameters to pass to the task request\n */\n public runTask(task_name: string, parameters: HashMap): Promise {\n if (this.id) {\n return this._service.task(this.id, task_name, parameters);\n }\n }\n\n /**\n * Convert object into plain object\n */\n public toJSON(this: BaseDataClass): HashMap {\n const obj: any = { ...this };\n // Remove local private members\n delete obj._service;\n delete obj._changes;\n delete obj.event_subject;\n delete obj.events;\n // Remove parent private members\n delete obj._timers;\n delete obj._intervals;\n delete obj._subscriptions;\n delete obj._server_names;\n delete obj._initialised;\n // Convert remaining members to be public\n const keys = Object.keys(obj);\n for (const key of keys) {\n if (key[0] === '_') {\n const new_key = this._server_names[key.substring(1)] || key.substring(1);\n obj[new_key] = obj[key];\n delete obj[key];\n } else if (obj[key] === undefined) {\n delete obj[key];\n }\n }\n return obj;\n }\n\n /**\n * Make a copy of this object\n */\n public clone(): BaseDataClass {\n return new BaseDataClass(this);\n }\n\n /**\n * Make a copy of this object without identification data\n */\n public duplicate(): BaseDataClass {\n return new BaseDataClass({ ...this, id: null, email: null });\n }\n}\n","import { ComposerService } from '@placeos/composer';\nimport { BehaviorSubject, Observable, of, Subject, Subscriber } from 'rxjs';\n\nimport { BaseDataClass } from './base-api.class';\nimport { BaseClass, HashMap } from '@mckinsey-converge/base';\nimport { ApplicationService } from './app.service';\nimport { toQueryString } from './api.utilities';\nimport { SettingsService } from './settings.service';\nimport { catchError, map } from 'rxjs/operators';\nimport { HttpClient, HttpHeaders } from '@angular/common/http';\n\nexport interface IEngineResponse {\n results: HashMap[];\n total: number;\n}\n\nexport class BaseAPIService extends BaseClass {\n /** Application service */\n public parent: ApplicationService;\n /** Display name of the service */\n protected _name: string;\n /** API Route of the service */\n protected _api_route: string;\n /** Map of state variables for Service */\n protected _subjects: { [key: string]: BehaviorSubject | Subject } = {};\n /** Map of observables for state variables */\n protected _observers: { [key: string]: Observable } = {};\n /** Map of poll subscribers for API endpoints */\n protected _subscribers: { [key: string]: Subscriber } = {};\n /** Map of promises for Service */\n protected _promises: { [key: string]: Promise } = {};\n /** Comparison function for service items */\n protected _compare: (a: T, b: T) => boolean = (a, b) =>\n a === b || (a as any).id === (b as any).id;\n /** Default filter function for list method */\n protected _list_filter: (a: T) => boolean = (a) => !!a;\n\n /** Http Client */\n protected get http() {\n return this._composer.http;\n }\n constructor(protected _composer: ComposerService,\n protected settingsService: SettingsService) {\n super();\n this._name = 'Base';\n this._api_route = 'base';\n this.set('list', []);\n }\n\n /**\n * Injects concierge into form_data.\n */\n private injectConcierge(form_data: HashMap) {\n // we only send it over when concierge, since the BE may check for presence rather than\n // if its true or not.\n if (this.settingsService.concierge) {\n return { ...form_data, concierge: true }\n }\n return form_data;\n }\n\n /**\n * Initailise service\n */\n public init() {\n this.load().then(\n (_) => this._initialised.next(true),\n (err) => this.timeout('init', () => this.init(), 1000)\n );\n }\n\n /**\n * Get API route for the service\n * @param engine Whether endpoint is using the application API or engine API\n */\n public route(engine: boolean = false) {\n const endpoint = engine\n ? this._composer.auth.api_endpoint\n : '/api/staff';\n return `${endpoint}/${this._api_route}`;\n }\n\n /** API Route of the service */\n public get api_route() {\n return this._api_route;\n }\n\n /**\n * Get the current value of the named property\n * @param name Property name\n */\n public get(name: string): U {\n if (!this._observers[name]) {\n this.set(name, null);\n }\n return (this._subjects[name] as BehaviorSubject).getValue();\n }\n\n /**\n * Listen to value change of the named property\n * @param name Property name\n * @param next Callback for value changes\n */\n public listen(name: string): Observable {\n if (!this._observers[name]) {\n this.set(name, null);\n }\n return this._observers[name];\n }\n\n /**\n * Update the value of the named property\n * @param name Property name\n * @param value New value\n */\n protected set(name: string, value: U): void {\n if (!this._subjects[name]) {\n this._subjects[name] = new BehaviorSubject(value);\n this._observers[name] = this._subjects[name].asObservable();\n } else {\n this._subjects[name].next(value);\n }\n }\n\n /**\n * Get list of loaded items\n * @param predicate Function for filtering the list\n */\n public filter(predicate: (a: T) => boolean = this._list_filter): T[] {\n const list: T[] = this.get('list');\n return list.filter(predicate);\n }\n\n /**\n * Get item with the given id from the loaded items\n * @param id ID of the item\n */\n public find(id: string): T {\n const list = this.get('list');\n return list.find((i) => i.id === id || (i.email?.toLowerCase() === id?.toLowerCase()));\n }\n\n /**\n * Query the index of the API route associated with this service\n * @param query_params Map of query paramaters to add to the request URL\n */\n public query(query_params: HashMap = {}): Promise {\n let engine = false;\n let cache = 1000;\n /* istanbul ignore else */\n if (query_params) {\n engine = !!query_params.engine;\n delete query_params.engine;\n cache = typeof query_params.cache !== 'boolean' ? query_params.cache || 1000 : 1000;\n typeof query_params.cache !== 'boolean' && delete query_params.cache;\n }\n let query = toQueryString(query_params);\n const key = `query|${query}`;\n if (!this._promises[key]) {\n // Bring back once implemented in the API\n // if (this.settingsService.concierge) {\n // query = query + '&check_access=true';\n // }\n this._promises[key] = new Promise((resolve, reject) => {\n const url = `${this.route(engine)}${query ? '?' + query : ''}`;\n let result: T[] | HashMap[] = [];\n this.http.get(url).subscribe(\n (d: IEngineResponse | HashMap[]) => {\n result =\n d && d instanceof Array\n ? d.map((i) => this.process(i))\n : d && !(d instanceof Array) && d.results\n ? (d.results as HashMap[])\n : d && !(d instanceof Array) && !d.results \n ? [d]\n : [];\n },\n (e) => {\n reject(e);\n this._promises[key] = null;\n },\n () => {\n resolve(result);\n this.timeout(key, () => (this._promises[key] = null), cache);\n }\n );\n });\n }\n return this._promises[key];\n }\n\n /**\n * query function version -2 - returns observable instead of promise...\n * Query the index of the API route associated with this service\n * @param query_params Map of query paramaters to add to the request URL\n */\n public queryObsr(query_params: HashMap = {}):Observable{\n let engine = false;\n let cache = 1000;\n /* istanbul ignore else */\n if (query_params) {\n engine = !!query_params.engine;\n delete query_params.engine;\n }\n let query = toQueryString(query_params);\n const key = `query|${query}`;\n const url = `${this.route(engine)}${query ? '?' + query : ''}`;\n return this.http.get(url).pipe(\n map((d: IEngineResponse | HashMap[]) => this.processApiResult(d) ),\n catchError((error: any, result?: T) => {\n console.log(error);\n return of(result as T);\n })\n );;\n }\n\n\n /**\n * \n * @param d \n * @returns \n */\n processApiResult(d: IEngineResponse | HashMap[]): IEngineResponse | HashMap[]{\n let result: IEngineResponse | HashMap[] =\n d && d instanceof Array\n ? d.map((i) => this.process(i))\n : d && !(d instanceof Array) && d.results\n ? (d.results as HashMap[])\n : d && !(d instanceof Array) && !d.results \n ? [d]\n : [];\n return result;\n }\n\n /**\n * query function version -2 - returns observable instead of promise...\n * Query the index of the API route associated with this service\n * @param query_params Map of query paramaters to add to the request URL\n */\n public queryRoomsForQR(query_params: HashMap = {}):Observable{\n let engine = false;\n let cache = 1000;\n /* istanbul ignore else */\n if (query_params) {\n engine = !!query_params.engine;\n delete query_params.engine;\n }\n let query = toQueryString(query_params);\n const key = `query|${query}`;\n const url = `${this.route(engine)}${query ? '?' + query : ''}`;\n return this.http.get(url);\n }\n \n /**\n * Query the API route for a sepecific item\n * @param id ID of the item\n * @param query_params Map of query paramaters to add to the request URL\n */\n public show(id: string, query_params: HashMap = {}): Promise {\n let engine = false;\n /* istanbul ignore else */\n if (query_params) {\n engine = !!query_params.engine;\n delete query_params.engine;\n }\n const query = toQueryString(query_params);\n const key = `show|${id}|${query}`;\n /* istanbul ignore else */\n if (!this._promises[key]) {\n this._promises[key] = new Promise((resolve, reject) => {\n const url = `${this.route(engine)}/${id}${query ? '?' + query : ''}`;\n let result: T = null;\n this.http.get(url).subscribe(\n (d) => (result = this.process(d)),\n (e) => {\n reject(e);\n this._promises.new_item = null;\n },\n () => {\n resolve(result);\n this.timeout(key, () => (this._promises[key] = null), 1000);\n }\n );\n });\n }\n return this._promises[key];\n }\n\n /**\n * Make post request for a new item to the service\n * @param form_data Data to post to the server\n * @param query_params Map of query paramaters to add to the request URL\n */\n public add(form_data: HashMap, query_params: HashMap = {}): Promise {\n /* istanbul ignore else */\n if (!this._promises.new_item) {\n this._promises.new_item = new Promise((resolve, reject) => {\n const query = toQueryString(query_params);\n const url = `${this.route(query_params.engine)}${query ? '?' + query : ''}`;\n let result: T = null;\n this.http.post(url, this.injectConcierge(form_data)).subscribe(\n (d) => (result = this.process(d)),\n (e) => {\n reject(e);\n this.analyticsEvent(`create-${this._name.toLowerCase()}-failed`);\n this._promises.new_item = null;\n },\n () => {\n resolve(result);\n this.set('list', this.updateList(this.get('list'), [result]));\n this.analyticsEvent(`create-${this._name.toLowerCase()}-success`);\n this._promises.new_item = null;\n }\n );\n });\n }\n return this._promises.new_item;\n }\n\n /**\n * Perform API task for the given item ID\n * @param id ID of the item\n * @param task_name Name of the task\n * @param form_data Map of data to pass to the API\n * @param method Verb to use for request\n */\n public \n task(\n id: string,\n task_name: string,\n form_data: HashMap = {},\n method: 'post' | 'get' = 'post'\n ): Promise {\n const query = toQueryString(this.injectConcierge(form_data));\n const key = `task|${id}|${task_name}|${query}`;\n /* istanbul ignore else */\n if (!this._promises[key]) {\n this._promises[key] = new Promise((resolve, reject) => {\n const post_data = { ...form_data, id, _task: task_name };\n const url = `${this.route(false)}/${id}/${task_name}`;\n let result: any;\n const request =\n method === 'post'\n ? this.http.post(url, post_data)\n : this.http.get(`${url}${query ? '?' + query : ''}`);\n request.subscribe(\n (d) => (result = d),\n (e) => {\n reject(e);\n this.analyticsEvent(\n `${this._name.toLowerCase()}-task-${task_name}-failed`,\n id\n );\n delete this._promises[key];\n },\n () => {\n resolve(result as U);\n this.analyticsEvent(\n `${this._name.toLowerCase()}-task-${task_name}-success`,\n id\n );\n this.timeout(key, () => delete this._promises[key], 1000);\n }\n );\n });\n }\n return this._promises[key];\n }\n\n\n /**\n * V-2\n * Perform API task for the given item ID\n * @param id ID of the item\n * @param task_name Name of the task\n * @param form_data Map of data to pass to the API\n * @param method Verb to use for request\n */\n public taskObsr( \n id: string, \n task_name: string, \n form_data: HashMap = {}, \n method: 'post' | 'get' = 'post' ):Observable{\n const query = toQueryString(this.injectConcierge(form_data));\n let engine = false;\n let cache = 1000;\n const post_data = { ...form_data, id, _task: task_name };\n const url = `${this.route(false)}/${id}/${task_name}`;\n let result: any;\n\n const request = method === 'post'\n ? this.http.post(url, post_data)\n : this.http.get(`${url}${query ? '?' + query : ''}`);\n \n return request;\n }\n\n\n /**\n * Make put request for changes to the item with the given id\n * @param id ID of the item being updated\n * @param form_data New values for the item\n * @param query_params Map of query paramaters to add to the request URL\n */\n public update(id: string, form_data: HashMap, query_params: HashMap = {}): Promise {\n const key = `update|${id}`;\n /* istanbul ignore else */\n if (!this._promises[key]) {\n this._promises[key] = new Promise((resolve, reject) => {\n const query = toQueryString(this.injectConcierge(query_params));\n const url = `${this.route(query_params.engine)}/${id}${query ? '?' + query : ''}`;\n let result: T = null;\n this.http.put(url, this.injectConcierge(form_data)).subscribe(\n (d) => (result = this.process(d)),\n (e) => {\n reject(e);\n this.analyticsEvent(`update-${this._name.toLowerCase()}-failed`, id);\n this._promises[key] = null;\n },\n () => {\n resolve(result);\n this.set(\n 'list',\n this.updateList(this.removeItem(this.get('list'), { id } as any), [\n result\n ])\n );\n this.analyticsEvent(`update-${this._name.toLowerCase()}-success`, id);\n this._promises[key] = null;\n }\n );\n });\n }\n return this._promises[key];\n }\n\n\n /**\n * update function version -2 - returns observable instead of promise...\n * @param id url id with respect to update api\n * @param form_data data to be update \n * @param should_inject_concierge common value to be set to add concierge: true to the query url and body\n * @param query_params Map of query paramaters to add to the request URL\n */\n public updateObsr(id: string, form_data: HashMap, should_inject_concierge: Boolean = false, query_params: HashMap = {}):Observable{\n const query = should_inject_concierge ? toQueryString(this.injectConcierge(query_params)) : false;\n const url = `${this.route(query_params.engine)}/${id}${query ? '?' + query : ''}`;\n const body = should_inject_concierge ? this.injectConcierge(form_data) : form_data;\n \n return this.http.put(url, body)\n .pipe(\n map((d: IEngineResponse | HashMap[]) => this.processApiResult(d) \n ),\n catchError((error: any, result?: T) => {\n console.log(error);\n this.analyticsEvent(`update-${this._name.toLowerCase()}-failed`, id);\n return of(error as T);\n })\n );\n\n }\n\n /**\n * update function version -2 - returns observable instead of promise...\n * @param form_data data to be update \n * @param should_inject_concierge common value to be set to add concierge: true to the query url and body\n * @param query_params Map of query paramaters to add to the request URL\n */\n public uploadSpacePhotos(apiSubRoute: string, form_data: HashMap, should_inject_concierge: Boolean = false, query_params: HashMap = {}):Observable{\n const query = should_inject_concierge ? toQueryString(this.injectConcierge(query_params)) : false;\n const url = `${this.route(query_params.engine)}/${apiSubRoute}${query ? '?' + query : ''}`;\n const body = should_inject_concierge ? this.injectConcierge(form_data) : form_data;\n\n const token = sessionStorage.length ? JSON.parse(sessionStorage.getItem('OAUTH.params')).access_token : '';\n\n const headers = new HttpHeaders({\n 'Authorization': `Bearer ${token}`,\n });\n \n const requestOptions = { headers: headers };\n\n // return this.httpClient.post(url, body, {\n // ...requestOptions \n // });\n\n return this.http.post(url, body);\n\n }\n\n /**\n * Make delete request for the given item\n * @param id ID of item\n */\n public delete(id: string, q: HashMap = {}): Promise {\n const key = `delete|${id}`;\n /* istanbul ignore else */\n if (!this._promises[key]) {\n this._promises[key] = new Promise((resolve, reject) => {\n const query = toQueryString(q);\n const url = `${this.route()}/${id}${query ? '?' + query : ''}`;\n this.http.delete(url).subscribe(\n (_) => null,\n (e) => {\n reject(e);\n this._promises[key] = null;\n },\n () => {\n this.set('list', this.removeItem(this.get('list'), { id } as any));\n this._promises[key] = null;\n resolve();\n }\n );\n });\n }\n return this._promises[key];\n }\n\n /**\n * Load initial data for the service\n */\n protected async load(): Promise {\n }\n\n /**\n * Post analytics event for this service\n * @param action Name of the action to post\n */\n protected analyticsEvent(action: string, label?: string) {\n // if (this.parent && this.parent.Analytics) {\n // this.parent.Analytics.track(this._name, { desc: `${this.parent.name.toLowerCase()}-${action}`, label });\n // }\n }\n\n /**\n * Convert raw API data into a valid API Object\n * @param raw_item Raw API data\n */\n protected process(raw_item: HashMap): T {\n return raw_item as T;\n }\n\n /**\n * Update recorded list of items\n * @param old_list Old list of items\n * @param list List of updated items\n * @param compareFn Function to compare items to remove duplicates\n */\n public updateList(\n old_list: T[],\n list: T[],\n compareFn: (a: T, b: T) => boolean = this._compare\n ): T[] {\n /* istanbul ignore else */\n if (!list || list.length === 0) {\n return old_list;\n }\n const new_list: T[] = [];\n const mixed_list = [...list, ...old_list];\n /* istanbul ignore else */\n if (!compareFn) {\n compareFn = this._compare;\n }\n for (const item of mixed_list) {\n const found = new_list.find((i) => compareFn(i, item));\n /* istanbul ignore else */\n if (!found) {\n new_list.push(item);\n }\n }\n return new_list;\n }\n\n /**\n * Remove the given item from the given list\n * @param list List of items\n * @param item Item to remove\n * @param compareFn Function to compare items\n */\n protected removeItem(list: T[], item: T, compareFn?: (a: T, b: T) => boolean) {\n const new_list = [];\n /* istanbul ignore else */\n if (!compareFn) {\n compareFn = this._compare;\n }\n list.forEach((i) => (compareFn(item, i) ? null : new_list.push(i)));\n return new_list;\n }\n}\n","import { SelectOption } from '../../../../ui/src/lib/options/select-option';\nimport { Building } from '../organisation/building.class';\nimport { Booking } from '../bookings/booking.class';\nimport { User } from '../users';\nimport { Space } from '../spaces';\nimport { SpaceBookingRuleOptions } from '../spaces/space.class';\nimport {\n BookingFormData,\n} from './booking.form.data';\nimport { statusFromBookings } from '../bookings/booking.utilities';\nimport { rulesForSpace } from '../bookings/space.utilities';\nimport { SpaceStatus } from '../bookings/space.types';\nimport { SpaceFeatures } from '../spaces/space.class';\nimport { HashMap } from 'libs/base/src/lib/types.utilities';\n\nexport const spaceExtraFeatureToDisplayName = (feature: SpaceFeatures) => {\n switch (feature) {\n case SpaceFeatures.VideoConference:\n return 'Video conference (VC)';\n case SpaceFeatures.ConferencePhone:\n return 'Conference phone';\n case SpaceFeatures.WirelessContentSharing:\n return 'Wireless content sharing';\n case SpaceFeatures.FlipChart:\n return 'Flip chart';\n case SpaceFeatures.Glassboard:\n return 'Glassboard';\n case SpaceFeatures.ElectronicWhiteboard:\n return 'Whiteboard';\n case SpaceFeatures.ConferenceRoom:\n return 'Conference';\n case SpaceFeatures.TeamRoom:\n return 'Team';\n case SpaceFeatures.PartnerOffice:\n return 'Partner';\n case SpaceFeatures.PhoneBooth:\n return 'Phone booth';\n case SpaceFeatures.NaturalLight:\n return 'Room with window only (natural light)';\n case SpaceFeatures.BoardRoom:\n return 'Boardroom room style';\n case SpaceFeatures.CocktailRoom:\n return 'Cocktail room style';\n case SpaceFeatures.TheatreRoom:\n return 'Theatre room style';\n case SpaceFeatures.WorkshopRoom:\n return 'Workshop room style';\n case SpaceFeatures.UShapeRoom:\n return 'U Shape room style';\n case SpaceFeatures.Miscellaneous:\n return 'Miscellaneous room style'\n case SpaceFeatures.Catering:\n return 'Rooms with catering';\n // TODO: add any special display handling here.\n default:\n return feature;\n }\n};\n\n\nexport const instantBookOption: SelectOption = {\n value: 'instant-book',\n display: 'Listing you can book without waiting for a host approval',\n shortDisplay: 'Instant book'\n};\nexport const instantBookOptions: SelectOption[] = [\n instantBookOption\n];\n\nexport const mapSpaceFeaturesToSelectOption = (f: SpaceFeatures): SelectOption => {\n const display = spaceExtraFeatureToDisplayName(f);\n return ({\n value: f,\n display,\n // Useful for long text, like that of the instant-book\n // option.\n //\n // We do not expect this option object to be modified\n // at any further point before it appears on screen -\n // although that may change in future releases.\n shortDisplay: display\n });\n};\n\nexport const roomEquipmentRequiredOptions: SelectOption[] = [\n SpaceFeatures.VideoConference,\n SpaceFeatures.ConferencePhone,\n SpaceFeatures.WirelessContentSharing,\n SpaceFeatures.FlipChart,\n SpaceFeatures.Glassboard,\n SpaceFeatures.ElectronicWhiteboard\n].map(mapSpaceFeaturesToSelectOption);\n\nexport const internalRoomTypeRequiredOptions: SelectOption[] = [\n SpaceFeatures.PartnerOffice,\n SpaceFeatures.ConferenceRoom,\n SpaceFeatures.TeamRoom,\n SpaceFeatures.PhoneBooth\n].map(mapSpaceFeaturesToSelectOption);\n\nexport const externalRoomTypeRequiredOptions: SelectOption[] = [\n SpaceFeatures.ConferenceRoom,\n SpaceFeatures.TeamRoom\n].map(mapSpaceFeaturesToSelectOption);\n\nexport const roomDetailsRequired: SelectOption[] = [\n SpaceFeatures.NaturalLight,\n SpaceFeatures.BoardRoom,\n SpaceFeatures.CocktailRoom,\n SpaceFeatures.TheatreRoom,\n SpaceFeatures.WorkshopRoom,\n SpaceFeatures.UShapeRoom,\n SpaceFeatures.Miscellaneous\n].map(mapSpaceFeaturesToSelectOption);\n\nexport const roomCateringAvailable: SelectOption[] = [\n SpaceFeatures.Catering,\n].map(mapSpaceFeaturesToSelectOption);\n\n/**\n * Defines the reason by which spaces are filtered out / missing.\n */\nexport enum SpaceFilterReason {\n /**\n * Spaces were shown and not all filtered out.\n */\n None = 'None',\n\n /**\n * If room filters are applied to the set of rooms,\n * and rooms are available if not applied by filtering.\n */\n Filters = 'Filters',\n\n /**\n * We check office rules first to determine if its been filtered.\n */\n OfficeRules = 'OfficeRules',\n\n /**\n * Fallback as the end case if no results are found.\n */\n DateTime = 'DateTime',\n\n /**\n * In case there are no results for a buiding but there are other office buidings in the same city.\n */\n AlternateBuildingsFound = 'AlternateBuildingsFound',\n\n /**\n * If room is non-bookable in the system\n */\n NonBookable = 'NonBookable'\n}\n\nexport interface FilteredSpaces {\n spaces: Space[] | undefined;\n reason: SpaceFilterReason;\n statusMap: HashMap;\n specificReason?: SpaceFilterReason;\n}\n\n/**\n * Filter spaces by filters locally. TBD in the future for real API pagination.\n *\n * @param formFilters - the set of filters to apply on the list of spaces.\n * @param spaces - the list of spaces to filter\n * @param activeForm - the landing page set of filters.\n * @param buildings - the list of buildings loaded, used to retrieve building-specific booking rules.\n * @param currentUser - the current user\n */\nexport const filterSpacesByAppliedFilters = (\n formFilters: SelectOption[],\n spaces: Space[] | undefined, // the resultshere are raw JSON Spaces, not an array of the Space class\n activeForm: BookingFormData,\n buildings: Building[] = [],\n currentUser: User | undefined,\n isStaffMap: boolean = false): FilteredSpaces => {\n const instantBook: boolean = formFilters.some(f => f.value === instantBookOption.value);\n const onlyCatering: boolean = formFilters.some(f => f.value === SpaceFeatures.Catering);\n // Drop incompatible spaces. If no filters were given, just return the array unchanged.\n const featureFilters = formFilters.filter(filter => ![instantBookOption.value, SpaceFeatures.Catering].includes(filter.value));\n const formFiltersEmpty = featureFilters.length === 0;\n\n // R--- refactor activeForm? Only after forms can handle dateTz\n // Blocked by date time input refactor\n const options: SpaceBookingRuleOptions = {\n duration: activeForm.duration,\n host: currentUser,\n dateTz: activeForm.dateTz,\n };\n let rulesCount = 0;\n let ruleReason = '';\n let excludedByFiltersCount = 0;\n const statusMap: HashMap = {};\n // console.group();\n const filteredSpaces = spaces?.map(space => space instanceof Space ? space : new Space(space)).filter(raw_space => {\n const space = new Space(raw_space); // Construct the full fledged space\n const building = buildings.find(b => space.zones.includes(b.id));\n const rules = rulesForSpace({\n time: options?.dateTz?.ms,\n duration: options.duration,\n user: options.host,\n rules: building?.booking_rules,\n space\n });\n ruleReason = rules.reason;\n let valid = !rules.hide;\n if (!valid) {\n rulesCount++;\n return false;\n }\n\n\n // Check for all the form filters ( except 'catering' )\n let hasAllFeatures: boolean;\n const internalRoomTypeFilters = [\n 'conference',\n 'meeting_room',\n 'partner',\n 'phone_booth',\n 'team_room',\n ];\n\n if (featureFilters.length === 0) {\n hasAllFeatures = true;\n } else if(featureFilters.every(f => internalRoomTypeFilters.includes(f.value))) { // If all featureFilters(selected filters) are present in internalRoomTypeFilters, make the filtering inclusive(return on first true) \n hasAllFeatures = featureFilters.some(f => space.featuresArray.includes(f.value));\n } else { // If any featureFilters(selected filters) are not present in internalRoomTypeFilters, make the filtering exclusive(return on first false)\n hasAllFeatures = featureFilters.every(f => space.featuresArray.includes(f.value));\n }\n\n const canBook = instantBook ? space.bookable : true;\n const matchesType = activeForm?.bookingType?.value === 'allRooms'\n ? true\n : space?.internal_or_external?.length\n ? space.internal_or_external === activeForm?.bookingType?.value\n : true;\n valid = valid && matchesType && (formFiltersEmpty || hasAllFeatures) && canBook;\n let hasCatering = building?.has_catering;\n if (space?.has_catering === false) {\n hasCatering = false;\n }\n // need the room catering to override.\n if (onlyCatering && !hasCatering) {\n valid = false;\n // console.log('Excluded by Catering');\n }\n\n // count this space if the only reason it can't be shown is because one of the filters excluded it\n if (!hasAllFeatures) {\n valid = false;\n // console.log('Excluded by Status');\n }\n\n const status = statusFromBookings(\n space.bookings.length ? space.bookings.map(b => new Booking(b)) : space.settings.bookings.map(b => new Booking(b)),\n space.bookable && !rules.hide,\n !rules.auto_approve,\n activeForm.dateTz,\n space\n );\n\n // only compute room availability here right now if instant book option is chosen and room still valid.\n if (instantBook && valid) {\n valid = status.status === SpaceStatus.Available;\n if (!valid) console.group('Excluded by Status');\n \n }\n\n\n /** \n * If there are rooms that are recurring but not available at all during the queried times, we filter them out from the results displayed on the page.\n * The \"Available\" status indicates whether a room can be booked during the queried times.\n * The \"isStaffMap\" flag- This helps to indicate that all occurrences of this room are booked during the queried times and are currently in use.\n * \"isStaffMap\" flag allows including such rooms in the list, which will be displayed in red on the map.\n */\n if(activeForm.is_recurrent && !space.availableOccurrences && !isStaffMap) {\n valid = false;\n }\n /** */\n\n // cache space status\n if (valid) {\n if(activeForm.is_recurrent && space.availableOccurrences && space.availableOccurrences<= space.totalOccurrences) {\n // space is available and requested for recurrence booking and few occurences are available to book then let the space select/bookable from map with limited availability\n statusMap[space.id] = !rules.auto_approve ? SpaceStatus.Requestable : SpaceStatus.Available;\n }else\n {\n statusMap[space.id] = status.status;\n }\n \n } else {\n excludedByFiltersCount++;\n }\n // console.log('Valid:', valid);\n return valid;\n });\n\n const buildingsInSameCity = buildings.filter(\n (_) =>activeForm?.location!==undefined && activeForm?.location?.length && _.city === activeForm?.location[0]?.meta?.building?.city\n ) || [];\n const alternateBuildings = buildingsInSameCity.filter(building =>\n !activeForm.location.some(locationItem =>\n locationItem.meta?.building?.id === building.id\n )\n );\n\n // console.groupEnd();\n let reason, specificReason = SpaceFilterReason.None;\n // console.log('Spaces:', filteredSpaces?.length, excludedByFiltersCount, rulesCount, ruleReason)\n if (filteredSpaces?.length === 0) {\n if (excludedByFiltersCount > 0 && (!formFiltersEmpty || onlyCatering)) {\n reason = SpaceFilterReason.Filters;\n } else if (rulesCount > 0) {\n reason = SpaceFilterReason.OfficeRules;\n } else if(alternateBuildings){\n reason = SpaceFilterReason.AlternateBuildingsFound;\n }\n else {\n reason = SpaceFilterReason.DateTime;\n }\n\n if (ruleReason === SpaceFilterReason.OfficeRules) {\n specificReason = SpaceFilterReason.OfficeRules;\n }\n }\n\n return { spaces: filteredSpaces, reason, statusMap, specificReason };\n};\n","import { BookingFormData } from './booking.form.data';\nimport {\n createAction,\n props\n} from '@ngrx/store';\nimport { Payload } from '../../../../loading/src/lib/loading.actions';;\nimport { SelectOption } from '../../../../ui/src/lib/options/select-option';\n\n\nexport const storeBookingFormData = createAction('[BookingForm] Store Form Data',\n props>>());\n\nexport const clearBookingFormData = createAction('[BookingForm] Clear Form Data');\n\nexport const storeRoomFilters = createAction('[BookingForm] Store Room Filters',\n props[]>>());\n\nexport const clearRoomFilters = createAction('[BookingForm] Clear Room Filters');\n\nexport const openBookingSurvey = createAction('[BookingSurvey] Open Survey Modal');\n","import { SelectOption } from '../../../../ui/src/lib/options/select-option';\nimport { RoomQueryOptions } from '../../../../rooms/src/lib/rooms.types';\nimport { User } from '../users/user.class';\nimport { DateTZ } from '@mckinsey-converge/date-tz';\nimport { DaysOfWeek, RecurrencePeriod } from '../recurrence/recurrence.utils';\nimport { Building } from '../organisation';\n\nexport const DEFAULT_BOOKING_DURATION = 30;\n\nexport const internalBookingTypeOption = {\n value: 'internal',\n display: 'Internal',\n};\n\nexport const allBookingTypeOption = {\n value: 'allRooms',\n display: 'All Rooms',\n};\n\nexport const bookingTypeOptions: SelectOption[] = [\n internalBookingTypeOption,\n {\n value: 'external',\n display: 'External',\n }\n];\n\nexport const defaultRoomSizeOption = {\n value: '2',\n display: '3-9 People',\n};\n\nexport const roomSizeOptions: SelectOption[] = [\n {\n value: '1',\n display: '1-2 People',\n },\n defaultRoomSizeOption,\n {\n value: '3',\n display: '10+ People',\n }\n];\n\nexport const roomSizeOptionsKiosk: SelectOption[] = [\n ...roomSizeOptions,\n {\n value: '4',\n display: 'All Rooms',\n },\n];\n\nexport const roomCapacityToValue = (roomSize: string): number => {\n switch (roomSize) {\n case '1':\n return 1;\n case '2':\n return 3;\n case '3':\n return 10;\n case '4':\n return null;\n }\n};\n\nexport const roomMaxCapacityToValue = (roomSize: string): number => {\n switch (roomSize) {\n case '1':\n return 2;\n case '2':\n return 9;\n default:\n return null;\n }\n};\n\nexport interface BookingFormData {\n // R--- off the rails, define these \"any's\"\n id?: string;\n location: SelectOption[];\n dateTz: DateTZ;\n /**\n * Duration, in minutes.\n */\n duration: number;\n bookingType: SelectOption;\n roomSize: SelectOption;\n selectedRoom?: any;\n organiser?: SelectOption;\n title?: string;\n attendees?: any[];\n code?: string;\n notes?: string;\n head_count?: number;\n creator?: User;\n company?: any[];\n //Recurrence fields\n recurrence_period?: RecurrencePeriod;\n recurrence_interval?: number;\n recurrence_endTz?: DateTZ;\n recurrence_count?: number;\n recurrence_starts?: Array;\n recurrence_exceptions?: Array; //SHOULD BE DATETZ\n recurrence_days?: Array;\n timezone?: string;\n offset?: number;\n buildings?: Building[];\n is_recurrent?: boolean;\n is_multiroom?: boolean;\n merged?: boolean;\n action?: string; // helping to set staff app recurring action flag for edit/clone in the active form\n ignore?: string; // helping while editing - recurring series rooms search api call\n bookable?: boolean; // set while editing - recurring series rooms search api call\n occurrence_edits?: string[];\n opt_out?:boolean;\n nextBusinessDay?: boolean; // set property if user room selection from next business day section on result page\n outlook_opt_out?:boolean;\n expanded_section_ids?: string[]; // set property if user room selection from next business day section on result page\n resultLoaded?: number; // set property if user room selection from any section on result page after clicking load more button\n}\n\n/**\n * Converts form data into API query parameters.\n */\nexport const roomFormDataToQuery = (\n data: BookingFormData\n): RoomQueryOptions => {\n // remove all buildings chip from query\n const filteredLocationIds = data?.location\n .filter((l) => l.groupChild)\n .map((l) => l.value)\n .join(',');\n return {\n dateTz: data?.dateTz,\n duration: data?.duration,\n locations: filteredLocationIds,\n capacity: roomCapacityToValue(data?.roomSize?.value),\n capacity_max: roomMaxCapacityToValue(data?.roomSize?.value),\n\n is_recurrent: data?.is_recurrent,\n is_multiroom: data?.is_multiroom,\n merged: data?.merged,\n recurrence_period: data?.recurrence_period,\n recurrence_interval: data?.recurrence_interval,\n recurrence_endTz: data?.recurrence_endTz,\n recurrence_count: data?.recurrence_count,\n recurrence_starts: data?.recurrence_starts,\n recurrence_exceptions: data?.recurrence_exceptions, //SHOULD BE DATETZ\n recurrence_days: data?.recurrence_days,\n timezone: data?.timezone,\n offset: data?.offset,\n buildings: data?.buildings,\n bookable: data?.bookable,\n ignore: data?.ignore\n };\n};\n","import { BookingFormState } from './booking.form.types';\nimport {\n DEFAULT_BOOKING_DURATION,\n defaultRoomSizeOption,\n internalBookingTypeOption\n} from './booking.form.data';\nimport { DateNow } from '@mckinsey-converge/date-tz';\n\nconst now = DateNow(new Date())\n\nexport const defaultTestBookingForm = (): BookingFormState => ({\n activeForm: {\n dateTz: now,\n duration: 2 * DEFAULT_BOOKING_DURATION,\n location: [],\n bookingType: internalBookingTypeOption,\n roomSize: defaultRoomSizeOption\n },\n activeFormFilters: []\n});\n","import { createSelector } from '@ngrx/store';\nimport {\n RoomStoreState\n} from '../../../../rooms/src/lib/rooms.types';\nimport {\n loadLaterThatDayResults,\n loadNextDayResults,\n loadRoomsForResults,\n roomStateSelector,\n loadByIdResults,\n loadDiffSizeResults,\n loadRoomsForResultsMap\n} from '../../../../rooms/src/lib/rooms.actions';\nimport {\n loadHomepageBookingsResults,\n loadUpcomingBookingsResults,\n loadPastBookingsResults,\n loadCancelledBookingsResults,\n loadBookingByIdResults,\n bookingStateSelector\n} from '../../../../bookings/src/lib/bookings.actions';\nimport { Building } from '../organisation/building.class';\nimport { BuildingStoreState } from '../../../../buildings/src/lib/buildings.types';\nimport { selectLoadBuildingsSuccess } from '../../../../buildings/src/lib/buildings.actions';\nimport { Space } from '../spaces/space.class';\nimport { User } from '../users/user.class';\nimport { selectCurrentUser } from '../../../../user/src/lib/user.actions';\nimport { UserStoreState } from '../../../../user/src/lib/user.types';\nimport { LoadingModel } from '../../../../loading/src/lib/loading.model';\nimport { SelectOption } from '../../../../ui/src/lib/options/select-option';\n\nimport {\n SpaceFilterReason,\n filterSpacesByAppliedFilters,\n FilteredSpaces\n} from './booking-filter.utils';\nimport {\n BookingFormData,\n internalBookingTypeOption,\n} from './booking.form.data';\nimport {\n BookingFormState,\n BookingStoreState,\n} from './booking.form.types';\nimport { DateTZ } from '@mckinsey-converge/date-tz';\nimport { BookingAction, RecurrencePeriod, SeriesAction } from '../recurrence/recurrence.utils';\n\n\nexport const selectBookingFormsData =\n (state: BookingStoreState | RoomStoreState | BuildingStoreState | UserStoreState) =>\n (state as any).bookingForm as BookingFormState;\n\nexport const selectActiveForm = createSelector(selectBookingFormsData, state => {\n // Recreate because serialized form dateTz doesn't have DateTZ class methods\n if (!state.activeForm?.dateTz) {\n return state.activeForm\n }\n const { date, is_local_tz, building_tz} = state.activeForm?.dateTz;\n const endTzDate = state.activeForm?.recurrence_endTz?.date;\n const recurrence_endTz = endTzDate ? {\n recurrence_endTz : new DateTZ({ date: endTzDate.valueOf(), is_local_tz, building_tz })\n } : {};\n\n const recurrence_starts = state.activeForm?.recurrence_starts\n\t\t\t? {\n\t\t\t\t\trecurrence_starts: state.activeForm?.recurrence_starts.map( el =>\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t// at final step of booking creation process recurrence_start becomes Array which need to be Array\n\t\t\t\t\t\t\t// To Do: Refactor - apps/staff/src/app/booking/booking-create/booking-form-base.component.ts lno: 186\n\t\t\t\t\t\t\treturn new DateTZ({ date: typeof el === 'number' ? el * 1000 : el.date.valueOf(), is_local_tz, building_tz })\n\t\t\t\t\t\t}\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t: {};\n\n\n const recEditingSearchPayload = (state.activeForm.action === SeriesAction.EDIT && state.activeForm.is_recurrent)\n\t\t\t?\n\t\t {\n\t\t\t\tid: state.activeForm.id,\n\t\t\t\tignore: state.activeForm.ignore,\n\t\t\t\tbookable: state.activeForm.bookable\n\t\t\t}\n\t\t\t: {};\n\n\n\n\n return {\n ...state.activeForm,\n dateTz: new DateTZ({ date: date.valueOf(), is_local_tz, building_tz }),\n ...recurrence_endTz,\n ...recurrence_starts,\n\t\t\t\t...recEditingSearchPayload\n }\n});\n\nexport const selectBookingType = createSelector(selectActiveForm,\n (form) => form?.bookingType);\n\nexport const selectBookingLocations = createSelector(selectActiveForm,\n (form) => (form?.location || []).filter(f => !f.groupHeader));\n\nexport const selectIsInternal = createSelector(selectBookingType,\n (type) => type?.value === internalBookingTypeOption.value);\n\nexport const selectFormFilters = createSelector(selectBookingFormsData,\n (state) => state.activeFormFilters || []);\n\nconst selectRoomResultsFromState = createSelector(roomStateSelector,\n loadRoomsForResults.selectors.model);\n\nconst selectRoomResultsFromState_map = createSelector(roomStateSelector,\n loadRoomsForResultsMap.selectors.model);\n\nconst selectLaterThatDayResultsFromState = createSelector(roomStateSelector,\n loadLaterThatDayResults.selectors.model);\nconst selectNextDayResultsFromState = createSelector(roomStateSelector,\n loadNextDayResults.selectors.model);\nconst selectByIdResultsFromState = createSelector(roomStateSelector,\n loadByIdResults.selectors.model);\nconst selectDiffSizeResultsFromState = createSelector(roomStateSelector,\n loadDiffSizeResults.selectors.model);\n\nexport const selectHomepageBookingsResultsFromState = createSelector(bookingStateSelector,\n loadHomepageBookingsResults.selectors.model);\nexport const selectUpcomingBookingsResultsFromState = createSelector(bookingStateSelector,\n loadUpcomingBookingsResults.selectors.model);\nexport const selectPastBookingsResultsFromState = createSelector(bookingStateSelector,\n loadPastBookingsResults.selectors.model);\nexport const selectCancelledBookingsResultsFromState = createSelector(bookingStateSelector,\n loadCancelledBookingsResults.selectors.model);\nexport const selectBookingByIdResultsFromState = createSelector(bookingStateSelector,\n loadBookingByIdResults.selectors.model);\n\n/**\n * Wraps {@link filterSpacesByAppliedFilters} with {@link LoadingModel} interop.\n * This will only filter data if there is data to filter, and returns a success {@link LoadingModel}.\n * The \"isStaffMap\" flag- This helps to indicate that all occurrences of this room are booked during the queried times and are currently in use.\n * \"isStaffMap\" flag allows including such rooms in the list, which will be displayed in red on the map.\n */\nconst filterSpacesByAppliedFiltersIfSuccess = (\n results: LoadingModel,\n formFilters: SelectOption[],\n activeForm: BookingFormData,\n buildings: Building[] | undefined,\n currentUser: User | undefined,\n isStaffMap: boolean = false): LoadingModel => {\n const data = results.optionalSuccess;\n\n // don't filter model if no success found\n return !activeForm ? results.mutate({\n spaces: data, // the result here is a raw JSON Space, not the Space class\n reason: SpaceFilterReason.None,\n statusMap: {}\n }) : results.mutate(\n filterSpacesByAppliedFilters(formFilters, data, activeForm, buildings || activeForm.buildings, currentUser, isStaffMap));\n};\n\nexport const selectFilteredResults = createSelector(selectRoomResultsFromState,\n selectFormFilters,\n selectActiveForm,\n selectLoadBuildingsSuccess,\n selectCurrentUser,\n filterSpacesByAppliedFiltersIfSuccess);\n\nexport const selectMapFilteredResults = createSelector(selectRoomResultsFromState_map,\n selectActiveForm,\n selectLoadBuildingsSuccess,\n selectCurrentUser,\n (results, activeForm, buildings, currentUser, isStaffMap) => filterSpacesByAppliedFiltersIfSuccess(results, [], activeForm, buildings, currentUser, true));\n\nexport const selectFilteredLaterDayResults = createSelector(selectLaterThatDayResultsFromState,\n selectFormFilters,\n selectActiveForm,\n selectLoadBuildingsSuccess,\n selectCurrentUser,\n filterSpacesByAppliedFiltersIfSuccess);\n\nexport const selectFilteredNextDayResults = createSelector(selectNextDayResultsFromState,\n selectFormFilters,\n selectActiveForm,\n selectLoadBuildingsSuccess,\n selectCurrentUser,\n filterSpacesByAppliedFiltersIfSuccess);\n\n\nexport const selectFilteredByIdResults = createSelector(selectByIdResultsFromState,\n selectActiveForm,\n selectLoadBuildingsSuccess,\n selectCurrentUser,\n (results, activeForm, buildings, currentUser) => filterSpacesByAppliedFiltersIfSuccess(results, [], activeForm, buildings, currentUser));\n\nexport const selectFilteredDiffSizeResults = createSelector(selectDiffSizeResultsFromState,\n selectFormFilters,\n selectActiveForm,\n selectLoadBuildingsSuccess,\n selectCurrentUser,\n filterSpacesByAppliedFiltersIfSuccess);\n\n/**\n * Maps selected options by loaded buildings.\n */\nexport const selectSelectedBuildingOptions = createSelector(\n selectBookingLocations,\n selectLoadBuildingsSuccess,\n (locations, buildings) => (locations\n .map(l => buildings?.find(b => b.id === l.value))\n .filter(f => !!f) as Building[])\n);\n\n","import { Building, BuildingCity } from '../organisation/building.class';\nimport { Booking } from '../bookings/booking.class';\nimport { BuildingLevel } from '../organisation/level.class';\nimport { Space } from '../spaces/space.class';\nimport { SpacesService } from '../spaces/spaces.service';\nimport {\n timezoneDisplay,\n timezoneNameToDate,\n unique,\n ImageDirective,\n} from '@mckinsey-converge/base';\nimport { SelectOption } from '../../../../ui/src/lib/options/select-option';\nimport { DateNow, DateTZ } from '@mckinsey-converge/date-tz';\n\n/**\n * If more than 3 are selected, we truncate location display.\n */\nconst MAX_ABBREV_LOCATIONS = 3;\n\nexport const mapBuildingToSelectOption = (\n city: string,\n building: Building\n): SelectOption => ({\n display: `${building.name} ${timezoneDisplay(\n timezoneNameToDate(building.timezone)\n )}`,\n dropdownOverride: `${building.code}-${building.name}, ${building.address}`,\n value: building.id,\n groupId: city,\n groupChild: true,\n shortDisplay: `${building.name} ${timezoneDisplay(\n timezoneNameToDate(building.timezone)\n )}`,\n meta: { building, city },\n});\n\nexport const mapCityToAllSelectOption = (\n city: BuildingCity\n): SelectOption => ({\n display: `${city.name} (All Offices) ${timezoneDisplay(\n timezoneNameToDate(city.timezone)\n )}`,\n value: city.name,\n groupHeader: true,\n groupId: city.name,\n shortDisplay: `${city.name} (All Offices) ${timezoneDisplay(\n timezoneNameToDate(city.timezone)\n )}`,\n meta: { city },\n});\n\n/**\n * Flattens a map of {@link BuildingCity} to {@link Building} array into a list of {@link SelectOption}.\n * @param grouped The grouping\n */\nexport const flattenDisplayOffices = (\n grouped: Map\n) => {\n const options: SelectOption[] = [];\n grouped.forEach((value, key) => {\n options.push(mapCityToAllSelectOption(key));\n value.forEach((b) =>\n options.push(mapBuildingToSelectOption(key.name, b))\n );\n });\n return options;\n};\n\n/**\n * This method will either add or remove a selected option from the selectedOptions list based on\n * these conditions:\n * 1. If the option EXISTS in the list AND is a groupHeader, de-select all of its children.\n * 2. If the option EXISTS in the list AND is a groupChild, remove it and its associated header from the list.\n * 3. If the option does NOT EXIST in the list AND is a groupHeader, select all other children.\n * 4. If the option does NOT EXIST in the list AND is a groupChild, add it to the list.\n *\n * Special note regarding item 4:\n * 4a. If that selection completes the children selection, select its associated header as well.\n */\nexport const toggleSelectedByGroup = (\n options: SelectOption[],\n selectedOptions: SelectOption[],\n option: SelectOption\n) => {\n if (selectedOptions.find((s) => s.value === option.value)) {\n return selectedOptions.filter((s) => {\n let filter = s.value !== option.value;\n // if group header, also remove any option that is the child of it.\n if (option.groupHeader) {\n filter =\n filter &&\n (!s.groupChild ||\n (s.groupChild && s.groupId !== option.groupId));\n } else if (option.groupChild) {\n // if child removing, remove the associated header.\n filter =\n filter &&\n (!s.groupHeader ||\n (s.groupHeader && s.groupId !== option.groupId));\n }\n return filter;\n });\n }\n // option does NOT EXIST\n // if adding header, add the other children to the selected list, ensuring no dupes.\n if (option.groupHeader) {\n const toSelect = options.filter(\n (v) =>\n v.groupChild &&\n v.groupId === option.value &&\n !selectedOptions.find((selected) => selected.value === v.value)\n );\n return [...selectedOptions, option, ...toSelect];\n }\n // add group child, add the group header if all satisfied\n const newGroup = [...selectedOptions, option];\n\n // check if we have selected all children from options by filtering down by city and checking if\n // they're in the selected options list.\n const remainingChildrenInGroup = options.filter(\n (o) =>\n o.groupChild &&\n o.groupId === option.groupId &&\n !newGroup.find((ng) => ng.value === o.value)\n );\n // if we dont have remaining children, add the group\n if (remainingChildrenInGroup.length === 0) {\n return [\n ...newGroup,\n options.find((o) => o.groupHeader && o.groupId === option.groupId),\n ];\n }\n return newGroup;\n};\n\n/**\n * If the list of locations are larger than {@link MAX_ABBREV_LOCATIONS}, then truncate\n * and display the remaining count.\n */\nexport const truncateLocationList = (\n locations: readonly SelectOption[]\n): string => {\n // comma separate the locations\n let truncatedLocations = [...locations];\n const shouldTruncate = locations.length > MAX_ABBREV_LOCATIONS;\n if (shouldTruncate) {\n truncatedLocations = truncatedLocations.splice(0, MAX_ABBREV_LOCATIONS);\n }\n let display = truncatedLocations\n .map((l) => l.shortDisplay || l.display)\n .join(', ');\n if (shouldTruncate) {\n display += `...(${locations.length})`;\n }\n return display;\n};\n\nexport const mapBuildingLevelToOption = (\n level?: BuildingLevel\n): SelectOption =>\n level\n ? {\n value: level.id,\n display: level.name,\n }\n : undefined;\n\n/**\n * Returns all levels included with the building, deduped.\n * @param buildings\n */\nexport const flattenBuildingsWithLevels = (buildings: Building[]) => {\n const flattenedLevels = unique(\n buildings.reduce((next: BuildingLevel[], building: Building) => {\n next.push(...building.levels);\n return next;\n }, []),\n 'id'\n );\n return {\n flattenedLevels,\n buildings,\n };\n};\n\n/** Gets route to image placeholder if room image isn't found */\n\nexport const placeholderRoute = (num: number): string => {\n let index: number;\n if (num <= 3) {\n index = num;\n } else if (num % 3 === 0) {\n index = 3;\n } else {\n index = 1;\n }\n return `assets/img/rooms/placeholder-${index}.png`;\n};\n\n/**\n * Method takes the rootFolderURL and fileSlug to create an array of three images\n * that should exist. If a room image exists, it replaces the placehoder image.\n */\nexport const setupRoomImages = (\n componentReference: ImageDirective,\n image_positions: number[],\n rootFolderURL: string,\n fileSlug: string,\n imagesLoaded: boolean = false\n): void => {\n let foundImages: any[] = image_positions.map((i) =>\n i ? { path: placeholderRoute(i) } : false\n );\n image_positions.forEach((n) => {\n const desiredImageName = `${rootFolderURL}${fileSlug}-part-${n}.png`;\n const desiredImage = location.pathname.includes('concierge') ? `${location.origin}/staff/${desiredImageName}` : desiredImageName;\n if (!imagesLoaded) {\n const tester = new Image();\n tester.onload = () => {\n // Will never run on unit test\n foundImages[n - 1] = { path: desiredImage };\n componentReference.foundImages = [...foundImages.slice()];\n componentReference?.loadImages && componentReference?.loadImages.next([...foundImages.slice()]);\n };\n tester.src = desiredImage;\n }\n\n // Force valid image output on unit test\n if (imagesLoaded) {\n foundImages[n - 1] = { path: desiredImage };\n componentReference.foundImages = foundImages.slice();\n }\n });\n};\n\nexport const bookingStatusDetails = (booking: Booking) => {\n const now = DateNow(new Date());\n let image = '';\n let text = '';\n let title = '';\n\n if (booking?.status) {\n const status = now > booking.endDateTz ? 'expired' : booking.status;\n switch (status) {\n case 'unavailable':\n image = 'assets/icon/booking_cancelled.svg';\n title = 'Unavailable';\n text = 'Unavailable';\n break;\n case 'declined':\n image = 'assets/icon/booking_cancelled.svg';\n title = 'Cancelled';\n text = 'Cancelled';\n break;\n case 'cancelled':\n image = 'assets/icon/booking_cancelled.svg';\n title = 'Cancelled';\n text = 'Cancelled';\n break;\n case 'expired':\n image = 'assets/icon/booking_expired.svg';\n title = 'Expired';\n text = 'Expired';\n break;\n case 'tentative':\n image = 'assets/icon/booking_pending.svg';\n title = 'Requested';\n text = 'Pending';\n break;\n case 'accepted':\n image = 'assets/icon/booking_confirmed.svg';\n title = 'Confirmed';\n text = 'Confirmed';\n break;\n case 'approved':\n image = 'assets/icon/booking_confirmed.svg';\n title = 'Confirmed';\n text = 'Confirmed';\n break;\n default:\n // TODO\n image = 'assets/icon/booking_expired.svg';\n title = 'Expired';\n text = 'Expired';\n break;\n }\n }\n return { text, title, image };\n};\n\nexport const roomHasCateringHours = (building: Building, space: Space) => {\n let hasCatering = building\n ? building.has_catering && building.catering_hours\n : false;\n // Room catering status as false overrides building status\n if (space?.has_catering === false) {\n hasCatering = false;\n }\n return hasCatering;\n};\n\nexport const cateringAllowed = (booking: Booking, building: Building) => {\n const status = bookingStatusDetails(booking);\n const disallowedStatus = ['declined', 'cancelled', 'expired'].includes(\n status.text\n );\n const opens = building?.catering_hours?.start;\n const closed = building?.catering_hours?.end;\n\n if (disallowedStatus) {\n return false;\n }\n\n if (typeof opens === 'undefined' || typeof closed === 'undefined') {\n return false;\n }\n\n // Catering time could be a decimal so convert to a date\n const openMinutes = opens * 60;\n const closeMinutes = closed * 60;\n\n const startOfDay = booking.startDateTz.startOfValue('day');\n const cateringOpenTime = startOfDay.addValue({ minutes: openMinutes });\n const cateringCloseTime = startOfDay.addValue({ minutes: closeMinutes });\n\n if (\n cateringOpenTime.ms <= booking.startDateTz.ms ||\n cateringCloseTime.ms > booking.startDateTz.ms\n ) {\n // Booking ends before catering opens\n if (booking.endDateTz.ms <= cateringOpenTime.ms) {\n return false;\n }\n // Booking starts after catering closes\n if (booking.startDateTz.ms > cateringCloseTime.ms) {\n return false;\n }\n\n // Start time is OK\n if (booking.startDateTz.ms > DateNow(new Date()).ms) {\n // Due to COVID, no orders are available once a meeting begins.\n return true;\n }\n }\n\n return false;\n};\n\nexport const checkCollisions = (\n service: SpacesService,\n booking: Booking\n): Promise => {\n return new Promise((resolve, reject) => {\n\n let availabilityParams: {\n room_ids: string;\n dateTz: DateTZ;\n duration: number;\n setup: number;\n breakdown: number;\n hide_bookings: boolean;\n [key: string]: any;\n } = {\n room_ids: booking.space.id,\n dateTz: booking.startDateTz,\n duration: booking.duration,\n setup: booking.setup[booking.space.email],\n breakdown: booking.breakdown[booking.space.email],\n hide_bookings: false,\n };\n\n if (booking.recurrence_type === \"master\" || booking.recurrence_type === null) {\n availabilityParams = {\n ...availabilityParams,\n ignore: booking?.icaluid,\n recurrence_count: booking?.recurrence_count,\n recurrence_period: booking?.recurrence_period,\n recurrence_endTz: booking?.recurrence_endTz,\n recurrence_interval: booking?.recurrence_interval,\n recurrence_starts: booking?.recurrence_starts,\n is_recurrent: booking?.is_recurrent,\n recurrence_days: booking?.recurrence_days\n } as typeof availabilityParams;\n }\n\n service\n .available(\n availabilityParams,\n null,\n false\n )\n .then(\n (list) => {\n const space = list.length ? list[0] : null;\n if (space) {\n if (!space.bookable) return reject('Has conflict');\n const setup = booking?.setup[space?.email] || 0;\n const breakdown = booking?.breakdown[space?.email] || 0;\n const start_time = booking?.startDateTz.seconds - setup;\n const end_time = booking.endDateTz.seconds + breakdown;\n\n const bookings = space.settings.bookings;\n // console.log('Bookings:', bookings);\n // Compare the existing bookings to see if any truely conflict.\n if (bookings.length) {\n const has_conflict = bookings.find(bkn => {\n const b_setup = bkn.setup[space.email] || 0;\n const b_breakdown = bkn.setup[space.email] || 0;\n const start = (bkn.start_epoch || bkn.start) - b_setup;\n const end = (bkn.end_epoch || bkn.end) + b_breakdown;\n return (\n (end_time > start && end_time <= end) || // Booking ends during the meeting\n (start_time >= start && start_time < end) || // Booking starts during a meeting\n (start_time <= start && end_time >= end) // Booking overlaps entire meeting\n ) && \n ( \n bkn.id !== booking.id \n || \n ( !!booking.is_recurrent && (booking.recurrence_type === \"master\" || booking.recurrence_type === null ) ) \n ?\n bkn.recurrence_master_id !== booking.id\n :\n false\n \n )\n\n\n\n });\n // Conflicts found, reject\n if (has_conflict) return reject('has booking conflict');\n }\n return resolve(space.bookable);\n }\n reject('space not found');\n },\n () => reject('available error')\n );\n });\n};\n\nexport const getStatusErrorMessage = (status?: number) => {\n let msg = '';\n switch (status) {\n\t\t\tcase 400:\n\t\t\t\tmsg = 'Apologies, the booking cannot be finalized because the current time has exceeded the scheduled start time for this booking. Please try again.';\n\t\t\t\tbreak;\n\t\t\tcase 403:\n\t\t\t\tmsg = 'Your do not have permission to update this booking.';\n\t\t\t\tbreak;\n\t\t\tcase 409:\n\t\t\t\tmsg = 'Sorry, your booking time conflicts with another booking.';\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tmsg = 'Your booking failed to update, please try again';\n }\n return msg;\n};\n","export * from './room-results/room-results.viewmodel'\nexport * from './booking-filter.utils'\nexport * from './booking.form.actions'\nexport * from './booking.form.data'\nexport * from './booking.form.spec-helpers'\nexport * from './booking.form.types'\nexport * from './booking.selectors'\nexport * from './booking.utils'","import { Space } from '../../spaces/space.class';\nimport { BuildingLevel } from '../../organisation/level.class';\nimport { Building } from '../../organisation/building.class';\n\nexport class RoomResultsViewModel {\n\n public levelDisplay: string;\n public title: string;\n public office: string;\n public capacity: string;\n\n constructor(public space: Space,\n public level?: BuildingLevel,\n public building?: Building) {\n\n this.levelDisplay = `Level ${this.space?.level?.short_name}`;\n this.title = this.space.local_name;\n this.office = this.building ? `(${this.building?.code}) ${this.building?.name}` : '';\n this.capacity = `Capacity: ${this.space.capacity} people`;\n }\n}\n\n/**\n * For each result from results, we find its level and building from the lists.\n */\nexport const mapResultsToViewModels = (results: Space[],\n flattenedLevels: BuildingLevel[],\n buildings: Building[]): RoomResultsViewModel[] =>\n results.map((r: Space) => {\n const level = flattenedLevels.find(l => r.zones.includes(l.id));\n const building = buildings.find(b => r.zones.includes(b.id));\n return new RoomResultsViewModel(\n r,\n level,\n building\n );\n });\n","import { Injectable, NgZone } from '@angular/core';\nimport { BehaviorSubject, combineLatest, of, Observable } from 'rxjs';\nimport {\n catchError,\n debounceTime,\n filter,\n first,\n map,\n shareReplay,\n switchMap,\n} from 'rxjs/operators';\nimport { BaseClass } from '@mckinsey-converge/base';\nimport { replaceBookings, timePeriodsIntersect } from './booking.utilities';\nimport { SpacesService } from '../spaces/spaces.service';\nimport { Booking } from '../bookings/booking.class';\nimport { Space } from '../spaces/space.class';\nimport { DateNow, DateTZ } from '@mckinsey-converge/date-tz';\nimport { OrganisationService } from '../organisation/organisation.service';\n\nexport type BookingType =\n | 'internal'\n | 'client'\n | 'external'\n | 'setup'\n | 'training'\n | 'interview'\n | 'declined';\n\nexport interface BookingFilters {\n /** List of zone ids to get bookings for */\n zone_ids?: string[];\n space_emails?: string[];\n hide_type?: BookingType[];\n}\n\n@Injectable({\n providedIn: 'root',\n})\nexport class BookingStateService extends BaseClass {\n /** List of bookings */\n private _poll = new BehaviorSubject(false);\n /** List of bookings */\n private _long_poll = new BehaviorSubject<'month' | ''>('');\n private _long_poll_week = new BehaviorSubject<'week' | ''>('');\n /** List of bookings */\n private _bookings = new BehaviorSubject([]);\n /** List of meeting count per date */\n public _noOfMeetings = new BehaviorSubject<{}>({});\n /** Filter details for bookings */\n private _filters = new BehaviorSubject({});\n /** Currently active date */\n private _dateTz = new BehaviorSubject(DateNow(new Date()));\n /** Currently displayed zone */\n private _zone = new BehaviorSubject(' ');\n /** Whether booking data is being loaded */\n private _loading = new BehaviorSubject(false);\n /** Observable for filter and booking list changes */\n private _state = combineLatest(\n this._bookings,\n this._filters,\n this._dateTz,\n this._zone\n );\n\n /** Observable for list of bookings */\n public readonly bookings = this._bookings.asObservable();\n /** Observable for active date */\n public readonly dateTz = this._dateTz.asObservable();\n /** Observable for active zone */ // R-- zone was this._date.asObservable(); this looks like a mistake but it exists from the beginning of time\n public readonly zone = this._dateTz.asObservable();\n /** Observable for loading state of bookings */\n public readonly loading = this._loading.asObservable();\n\n public get booking_date() {\n // When this class is initialized this._dateTz is local timezone\n return this._dateTz.value;\n }\n\n public get timezone() {\n return localStorage.getItem('CONCIERGE.timezone');\n }\n\n /** Obsevable for filtered list of bookings */\n public readonly filtered = this._state.pipe(\n map((state) => {\n const bdTz = this.timezone\n ? new DateTZ({\n date: this.booking_date.ms,\n is_local_tz: false,\n building_tz: this.timezone,\n })\n : this.booking_date;\n const startTz = bdTz.startOfValue('day');\n const endTz = startTz.addValue({}).endOfValue('day');\n return this.filterBookings(startTz, endTz);\n }),\n shareReplay(1)\n );\n\n /** Obsevable for filtered list of bookings of the active week */\n public readonly filtered_week = this._state.pipe(\n map(() => {\n // Tested to be valid in building time now.\n const bd = this.timezone\n ? new DateTZ({\n date: this.booking_date.ms,\n is_local_tz: false,\n building_tz: this.timezone,\n })\n : this.booking_date;\n const start = bd.startOfValue('week').startOfValue('day');\n const end = this.getEndOfWeek(bd);\n return this.filterBookings(start, end);\n })\n );\n\n /** Obsevable for filtered list of bookings for active month */\n public readonly filtered_month = this._state.pipe(\n map(() => {\n const start = this.booking_date.startOfValue('month');\n const end = this.booking_date.endOfValue('month');\n return this.filterBookings(start, end);\n })\n );\n\n /** Active filters */\n public get filters() {\n return this._filters.getValue();\n }\n\n constructor(\n private _org: OrganisationService,\n private _spaces: SpacesService,\n private ngZone: NgZone,\n ) {\n super();\n\n this._org.initialised.pipe(first((_) => _)).subscribe(() => {\n // Just to get the timezone correct\n this._dateTz.next(\n new DateTZ({\n date: this._dateTz.value.ms,\n is_local_tz: false,\n building_tz: this._org.building?.timezone,\n })\n );\n });\n\n /** Generate observable for updating bookings */\n const search = combineLatest(this._poll, this._zone, this._dateTz).pipe(\n filter((i) => !!i[0]),\n debounceTime(500),\n switchMap(() => {\n const fzone = this._zone.getValue();\n if (!fzone) {\n return of([]);\n }\n this._loading.next(true);\n const start = this.booking_date.startOfValue('day');\n const end = start.endOfValue('day');\n return this._spaces.queryBooking({\n zone_ids: fzone,\n available_from: start.seconds,\n available_to: end.seconds,\n });\n }),\n catchError(() => of([]))\n );\n\n const search_long_week = combineLatest(\n this._long_poll_week,\n this._zone,\n this._dateTz\n ).pipe(\n filter((i) => !!i[0]),\n debounceTime(500),\n switchMap((props) => {\n const type = props[0];\n const fzone = props[1];\n const dateTz = props[2];\n if (!fzone) {\n return of([]);\n }\n this._loading.next(true);\n return this.querySpace(type, fzone, dateTz, false);\n }),\n catchError((e) => { \n return of([]);\n })\n );\n\n //TO DO: make sure both spaces have same booking\n /** Subscribe to update observable */\n search.subscribe((space_list) => {\n this.processBookings(space_list);\n this._loading.next(false);\n });\n search_long_week.subscribe((space_list) => {\n this.processBookings(space_list, this._long_poll_week.getValue() as any);\n this._loading.next(false);\n });\n }\n\n /**\n * Function to build\n */\n private buildSpaceLongQuery(dailyCount: boolean = false): Observable {\n return combineLatest(\n this._long_poll,\n this._zone,\n this._dateTz\n ).pipe(\n filter((i) => !!i[0]),\n debounceTime(500),\n switchMap((props) => {\n const type = props[0];\n const fzone = dailyCount ? this._org.building.id : props[1];\n const dateTz = props[2];\n if (!fzone) {\n return of([]);\n }\n this._loading.next(true);\n return this.querySpace(type, fzone, dateTz, dailyCount);\n }),\n catchError((e) => { \n return of([]);\n })\n );\n }\n\n private querySpace(type: string, fzone: string, dateTz: DateTZ, dailyCount: boolean = false) : Observable | Observable{\n const start = () => {\n const s = dateTz;\n if (type === 'week') {\n return s.startOfValue('week');\n } else {\n return s.startOfValue('month');\n }\n };\n\n const end = () => {\n const e = start();\n if (type === 'week') {\n /**\n * To Do: date-tz.class.ts endOfValue subtracts 1 day from end of week for 7 days a week type which returns Friday instead of Saturday\n * below is the adjustment -\n */\n return e.addValue({ days: 1 }).endOfValue('week');\n } else {\n return e.endOfValue('month');\n }\n /**\n * R--- when testing March 2022 London offices, the month is an hour short.\n * Is this a Luxon bug or London DST?\n */\n };\n\n // dates here are ok\n return this._spaces.queryBooking({\n zone_ids: fzone,\n available_from: start().seconds,\n available_to: end().seconds,\n ...(dailyCount) ? { daily_count: true } : ''\n });\n }\n\n\n public getDailyMeetingCount() : void {\n this._long_poll.next('month');\n this.buildSpaceLongQuery(true).subscribe((counts) => {\n this._noOfMeetings.next(counts);\n this._loading.next(false);\n });\n }\n\n /**\n * Update the booking filters\n * @param details\n */\n public setFilters(details: BookingFilters) {\n this._filters.next(details);\n }\n\n /**\n * Update the booking date\n * @param details\n */\n public setDate(dateTz: DateTZ) {\n this._dateTz.next(dateTz);\n }\n\n /**\n * Update the booking's zone\n * @param details\n */\n public setZone(zone: string) {\n this._zone.next(zone);\n }\n\n /**\n * update day view once\n */\n public pollOnce(){\n this._poll.next(true);\n }\n \n /**\n * Start polling to update bookings\n * @param delay Duration between polling events in milliseconds\n */\n public startPolling(delay: number = 30 * 1000) {\n this._poll.next(true);\n this.ngZone.runOutsideAngular(() => {\n this.interval('polling', () => this._poll.next(true), delay);\n });\n }\n\n /**\n * Start polling to update bookings\n * @param delay Duration between polling events in milliseconds\n */\n public startPollingWeek(delay: number = 4 * 30 * 1000 ) {\n this._long_poll_week.next('week');\n this.ngZone.runOutsideAngular(() => {\n this.interval(\n 'polling_long',\n () => this._long_poll_week.next('week'),\n delay\n );\n });\n }\n /**\n * Start polling to update bookings\n * @param delay Duration between polling events in milliseconds\n */\n public startPollingEveryFiveMin(delay: number = 60 * 1000) {\n this._poll.next(true);\n this.ngZone.runOutsideAngular(() => {\n this.interval('polling', () => this._poll.next(true), delay);\n });\n }\n \n /**\n * Start polling to update bookings\n * @param delay Duration between polling events in milliseconds\n */\n public startPollingMonth(delay: number = 5 * 60 * 1000) {\n this._long_poll.next('month');\n this.ngZone.runOutsideAngular(() => {\n this.interval(\n 'polling_long',\n () => this._long_poll.next('month'),\n delay\n );\n });\n }\n\n\n /**\n * Stop polling to update bookings;\n */\n public stopPolling() {\n this._poll.next(false);\n this.clearInterval('polling');\n this._long_poll.next('');\n this.clearInterval('polling_long');\n }\n\n public updateRoomList() {\n this._spaces.updateRoomList();\n }\n\n /**\n * Add booking to bookings listing\n * @param booking\n */\n public add(booking: Booking) {\n const bookings = this._bookings.getValue();\n const new_bookings = bookings.concat([booking]);\n this._bookings.next(new_bookings);\n }\n\n /**\n * Update booking in the bookings list\n * @param booking\n */\n public replace(booking: Booking) {\n const bookings = this._bookings.getValue();\n const new_bookings = bookings\n .filter(\n (bkn) =>\n bkn.icaluid !== booking.icaluid && bkn.id !== booking.id\n )\n .concat([booking]);\n this._bookings.next(new_bookings);\n }\n\n /**\n * Remove booking in the bookings list\n * @param booking\n */\n public remove(booking: Booking) {\n const bookings = this._bookings.getValue();\n const new_bookings = bookings.filter(\n (bkn) => bkn.icaluid !== booking.icaluid\n );\n this._bookings.next(new_bookings);\n }\n\n public attentToDelete(\n booking: Booking,\n action: 'series' | 'booking' = 'booking',\n undo: boolean = false\n ) {\n const bookings = this._bookings.getValue();\n const attempted_to_delete = ((action) => {\n return (bkg: Booking) => {\n switch (action) {\n case 'booking': {\n const booking_master = bookings.find(\n (bkn) => bkn.icaluid === booking.icaluid\n );\n if (!booking_master) return false;\n return bkg.id === booking.id;\n }\n case 'series': {\n const booking_master = bookings.find(\n (bkg) =>\n bkg.id ===\n (booking.recurrence_type === 'occurrence'\n ? booking.recurrence_master_id\n : booking.id)\n );\n if (!booking_master) return false;\n return (\n booking_master.id === bkg.id ||\n bkg.recurrence_master_id === booking_master.id\n );\n }\n default:\n false;\n }\n };\n })(action);\n\n const new_bookings = [...bookings].map((bkg) => {\n if (!undo && attempted_to_delete(bkg)) {\n bkg.attempted_to_delete = action;\n }\n\n if (undo && attempted_to_delete(bkg)) {\n bkg.attempted_to_delete = null;\n }\n\n return bkg;\n });\n\n this._bookings.next(new_bookings);\n }\n\n private processBookings(\n space_list: Space[],\n period: 'day' | 'week' | 'month' = 'day'\n ) {\n const start = () => {\n switch (period) {\n case 'month':\n return this.booking_date.startOfValue('month');\n case 'week':\n return this.booking_date.startOfValue('week');\n default:\n return this.booking_date.startOfValue('day');\n }\n };\n const end = () => {\n const s = start();\n switch (period) {\n case 'month':\n return s.endOfValue('month');\n case 'week':\n return s.endOfValue('week');\n default:\n return s.endOfValue('day');\n }\n };\n\n let bookings = this._bookings.getValue();\n space_list.forEach((space) => {\n return (bookings = replaceBookings(\n bookings,\n space.bookings.map((bkn) => new Booking(bkn)),\n {\n room_email: space.email,\n fromTz: start(),\n toTz: end(),\n }\n ));\n });\n this._bookings.next(bookings);\n }\n\n // private filterBookings(startTz: DateTZ, endTz: DateTZ) {\n // const filters = this._filters.getValue();\n // const bookings = this._bookings.getValue();\n // const fzone = this._zone.getValue();\n // return bookings.filter((bkn) => {\n // const intersects = timePeriodsIntersect(\n // startTz.ms,\n // endTz.ms,\n // bkn.startDateTz.ms,\n // bkn.endDateTz.ms\n // );\n // const in_zone = bkn.room.zones.includes(fzone);\n // const has_space =\n // !filters.space_emails?.length ||\n // filters.space_emails.includes(bkn.room.email);\n // const in_zones =\n // !filters.zone_ids?.length ||\n // !!bkn.room.zones.find((zone) =>\n // filters.zone_ids.includes(zone)\n // );\n // const type = bkn.declined ? 'declined' : bkn.getType();\n\n // const show =\n // !filters.hide_type?.length ||\n // !filters.hide_type.includes(type as any);\n // return intersects && has_space && in_zone && in_zones && show;\n // });\n // }\n\n private filterBookings(startTz: DateTZ, endTz: DateTZ) {\n const filters = this._filters.getValue();\n const bookings = this._bookings.getValue();\n const fzone = this._zone.getValue();\n return bookings.filter((bkn) => {\n const intersects = timePeriodsIntersect(\n startTz.ms,\n endTz.ms,\n bkn.startDateTz.ms,\n bkn.endDateTz.ms\n );\n // Check if any room in the booking satisfies the conditions\n const roomSatisfiesConditions = bkn.multi_rooms.some((room) => {\n const in_zone = room.zones.includes(fzone);\n const has_space =\n !filters.space_emails?.length ||\n filters.space_emails.includes(room.email);\n const in_zones =\n !filters.zone_ids?.length ||\n !!room.zones.find((zone) => filters.zone_ids.includes(zone));\n return in_zone && has_space && in_zones;\n });\n \n const type = bkn.declined ? 'declined' : bkn.getType();\n \n const show =\n !filters.hide_type?.length ||\n !filters.hide_type.includes(type as any);\n return intersects && roomSatisfiesConditions && show;\n });\n } \n\n /**If Sunday add one day to get Saturday as end of week */\n private getEndOfWeek(date: DateTZ): DateTZ {\n return date.dateWeekday === 7\n ? date.addValue({ days: 1 }).endOfValue('week').endOfValue('day')\n : date.endOfValue('week').endOfValue('day');\n }\n}\n","import { BaseDataClass } from '../base-api.class';\nimport { CateringOrder } from '../catering';\nimport { User } from '../users';\nimport {\n flatten,\n HashMap,\n humaniseDuration,\n shorterBuildingDateFormatString,\n shorterLocalDateFormatString,\n toTitleCase,\n unique\n} from '@mckinsey-converge/base';\nimport { Space } from '../spaces/space.class';\nimport { ServiceManager } from '../service-manager.class';\nimport { SettingsService } from '../settings.service';\nimport {\n BookingNote,\n} from './booking.types';\nimport { convertLocalTimestampToTimezonedDateTz, DateNow, DateTZ, getTimezoneOffsetString } from '@mckinsey-converge/date-tz';\nimport { DaysOfWeek, getLastDateFromList, handleRecurrenceFields, RecurrencePeriod } from '../recurrence/recurrence.utils';\nimport { findSpace } from '../spaces';\nimport { DateTime } from 'luxon';\n\n\nexport interface IBookingQueryOptions {\n /** booking ID */\n id?: string;\n email?: string;\n target?: string;\n from?: number; // R--- depreciate\n until?: number; // R--- depreciate\n fromTz?: DateTZ;\n untilTz?: DateTZ;\n show_cancelled?: boolean;\n pagination?: boolean;\n limit?: number;\n offset?: number;\n sort?: string,\n filters?: any;\n include_rooms?: boolean;\n building_zone?: string;\n}\n\nexport class Booking extends BaseDataClass {\n /** Unique calendar event ID */\n // public readonly icaluid: string;\n /** Subject or title of the booking */\n public title: string;\n /** Luxon based date class of the booking start time */\n public startDateTz: DateTZ;\n /** Luxon based date class of the booking start time */\n public endDateTz: DateTZ;\n /** Description or details of the booking */\n public readonly body: string;\n /** Type of booking */\n public readonly booking_type: string;\n /** List of catering orders for the booking */\n public catering: readonly CateringOrder[];\n /** Whether booking's duration covers all day */\n public readonly all_day: boolean;\n /** Mapping of emails to approval statuses */\n public approval_status: HashMap;\n /**\n * New booking approval status set in constructor\n *\n * timeBasedStatusLabel is a getter calculated via current time, some parts of the application\n * may use status when it should use timeBasedStatusLabel\n */\n public status: 'tentative' | 'accepted' | 'approved' | 'declined' | 'cancelled' | 'expired' | 'unavailable';\n /** Host/Organiser of the booking */\n public organiser: User;\n /** List of people invited to attend the booking */\n public attendees: User[];\n /** Author of the booking */\n public readonly creator: User;\n\n /** List of notes associated with the booking */\n public notes: readonly BookingNote[];\n /** Mapping of spaces to equipment charge codes */\n public equipment_codes: HashMap;\n /** Mapping of spaces to expected number of attendees */\n public expected_attendees: HashMap;\n /** Map of space emails to the setup time before the meeting in minutes */\n public setup: HashMap;\n /** List of checked in attendees */\n public check_ins: object;\n /** Map of space emails to the breakdown time before the meeting in minutes */\n public breakdown: HashMap;\n /** List of fields edited since creation */\n public readonly edits: string[];\n /** List of users to be notified on visitor arrivals */\n public readonly notify_users: readonly string[];\n /** Whether the time or duration has changed */\n public time_changed = false;\n /**\n * Booking Space\n * Bookings only have one room on MCK\n */\n public room: Space;\n /** building_zone */\n public building_zone: string;\n /** Initialized Timezone */\n public timezone: string;\n /** Array of company names */\n public company?: string[];\n /** Flag for multiroom booking */\n public is_multiroom: boolean;\n /** Multiroom booking ID */\n public multiroom_master_id: string; \n /** Merged - field holds flag for merged-multiroom booking which tightly coupled with multiroom booking only */\n public merged: boolean;\n /** Type of recurrence, Shows whether this is a master or an occurence in the series. */\n public readonly recurrence_type: string;\n /** The frequency of the recurring booking. */\n public readonly recurrence_period: RecurrencePeriod;\n /** The interval time between each period. Defaults to 1. For example, with a period of \"weekly\" and an interval of 2, the recurrencd happen every 2 weeks. */\n public readonly recurrence_interval: number;\n /** Unix epoch in seconds of the recurrence end date */\n private _recurrence_endTz: DateTZ;\n /** The number of times to repeat the recurring booking. */\n public readonly recurrence_count: number;\n /** ID of the booking considered the master */\n public readonly recurrence_master_id: string;\n /** An array of booking IDs which belong to this recurring series. This INCLUDES the master booking ID. */\n public readonly occurrence_ids: string[];\n /** A list of INDIVIDUAL edits to any of the bookings in the series. This is so we can prompt the user if they are going to override previously updated bookings with a whole-series update. */\n public readonly occurrence_edits: string[];\n /** A list of Ocurrences */\n public readonly occurrence_details: { id: string, start_epoch: number }[];\n /** When the booking was created */\n public created_epoch: number;\n /** Is the Application concierge */\n public is_concierge: boolean;\n /**helper to know if recurring toggle is on */\n public is_recurrent: boolean;\n /**array of start dates of each ocurrence in the series */\n public recurrence_starts: Array\n /**array of conflicting dates not to be included in the recurrence series */\n public recurrence_exceptions: Array\n\n public recurrence_days?: Array\n\n /** Master recurrence start */\n public recurrence_start: number;\n\n /** Start Epoch */\n public start_epoch: number;\n public end_epoch: number;\n public level_zone: string;\n public opt_out: boolean;\n public outlook_opt_out: boolean;\n public multi_rooms: Space[];\n public room_setup: any;\n public room_breakdown: any;\n public headcount: number;\n public equipment_code: string;\n\n /** Currently back-end is not properly setting the recurrence_end property, this is a workaround */\n public get recurrence_endTz(): DateTZ {\n return this._recurrence_endTz;\n }\n\n public set recurrence_endTz(date: DateTZ) {\n this._recurrence_endTz = date;\n }\n\n /** Mark a booking for deleting */\n public attempted_to_delete?: 'series' | 'booking' | null;\n\n // No specific reason to set readonly but canm be change if needed to update in future\n public readonly booked_by : string | { name: string };\n public readonly booker: Object;\n public readonly booker_concierge: Object\n\n constructor(raw_data: HashMap = {}) {\n super(raw_data);\n // Needed to check if the current app is Concierge or Staff\n const settingsService = ServiceManager.serviceFor(SettingsService) as unknown as SettingsService;\n this.is_concierge = settingsService.concierge;\n\n /**\n * Setup Defaults when raw_data values are not provided\n */\n const nowTz = new DateTZ();\n\n const defaultTitle = ''; // Blank since the create booking form will init with test values.\n const defaultLocalTimezone = Intl?.DateTimeFormat()?.resolvedOptions()?.timeZone;\n const defaultBuildingZone = null // R --- no idea, should be a building.id but which and how\n const defaultOrgainiser = User.active_user || new User(); // meh, non American use.\n const defaultBookingType = 'internal';\n\n /**\n * Process the simple raw_data with defaults mixed in\n */\n this.title = raw_data.title || defaultTitle;\n this.timezone = raw_data.timezone || defaultLocalTimezone;\n // Used by components to get the building details\n this.building_zone = raw_data.building_zone || defaultBuildingZone;\n this.body = raw_data.body || '';\n // Provided booking type\n this.booking_type = raw_data.booking_type || defaultBookingType;\n // Attendees provided in API booking data\n this.attendees = (raw_data.attendees || []).map((i) => new User(i));\n // Provided organiser, active user, or empty user?\n this.organiser = raw_data.organiser ? new User(raw_data.organiser) : defaultOrgainiser;\n // Creator is provided or defaults to the organizer\n this.creator = (raw_data.booked_by ? new User(raw_data.booked_by) : defaultOrgainiser) || this.organiser;\n // Setup and breakdown times can conflict but not the true event start and end times\n this.setup = raw_data.setup || {};\n this.breakdown = raw_data.breakdown || {};\n this.room_setup = raw_data.room_setup;\n this.room_breakdown = raw_data.room_breakdown;\n // Notes are assigned by room but we only support a single room\n this.notes = raw_data.notes || [];\n this.equipment_codes = raw_data.equipment_codes || {};\n this.equipment_code = raw_data.equipment_code || '';\n this.expected_attendees = raw_data.expected_attendees || {};\n this.headcount = raw_data.headcount;\n this.check_ins = raw_data.check_ins || {};\n this.notify_users = raw_data.notify_users?.length ? raw_data.notify_users : [this.organiser?.name];\n this.company = raw_data.company || [];\n // retain booked by if there : helps to get correct creator above\n this.booked_by = raw_data.booked_by;\n this.booker = raw_data.booker;\n this.booker_concierge = raw_data.booker_concierge;\n this.level_zone = raw_data.level_zone;\n this.opt_out = raw_data.opt_out;\n this.outlook_opt_out = raw_data.outlook_opt_out;\n\n /**\n * \n */\n this.merged = raw_data?.merged || null;\n\n /**\n * Multiroom fields\n */\n this.is_multiroom = raw_data.is_multiroom || null;\n this.multiroom_master_id = raw_data.multiroom_master_id || null;\n\n /**\n * Setup Defaults when raw_data values are not provided\n *\n * all room_ids and space_list inputs can be refactored into just room like an API booking\n *\n * raw_data.room can be provides as the Space class or JSON object\n */\n // this.room = raw_data.room ? new Space(raw_data.room) : new Space();\n // this.room = raw_data?.room ? (Array.isArray(raw_data?.room) && raw_data.room.length > 0) ? raw_data?.room.map((element) => new Space(element))[0]: [new Space(raw_data.room)][0] : [new Space()][0];\n // this.room = raw_data.room ? new Space(raw_data.room) : new Space();\n\n if (raw_data && raw_data.room) {\n if (Array.isArray(raw_data.room) && raw_data.room.length) {\n this.room = raw_data.room.map((element) => new Space(element))[0];\n } else if (Array.isArray(raw_data.room) && raw_data.room.length === 0) {\n this.room = [new Space()][0];\n } else {\n this.room = [new Space(raw_data.room)][0];\n }\n } else {\n this.room = [new Space()][0];\n }\n\n\n const room_id = raw_data.room_id || raw_data.room_ids // Not sure why we have room_id and room_ids\n if (!this.room?.id && room_id?.length) {\n this.room = findSpace(room_id[0]) || this.room;\n }\n\n const roomsData = raw_data?.multi_rooms || raw_data?.room;\n this.multi_rooms = roomsData\n ? Array.isArray(roomsData)\n ? roomsData.map((element) => new Space(element))\n : [new Space(roomsData)]\n : [new Space()];\n\n \n const room_ids = [...(raw_data?.room_id || []), ...(raw_data?.room_ids || [])];\n\n room_ids.forEach(roomId => {\n const newRoom = findSpace(roomId);\n // Check if the room is found and not already included in this.room\n if (newRoom && !this.multi_rooms.some(room => room.id === newRoom.id)) {\n // Add the new room to this.room\n this.multi_rooms.push(newRoom);\n }\n });\n\n // this.room is sometimes getting set to undefined when raw_data?.room is coming as an object\n if(this.multi_rooms.length && this.room === undefined){\n this.room = this.multi_rooms[0];\n }\n \n /**\n * Setup the booking start and end time\n *\n *\n * Booking defaults to now if start is not defined.\n * Booking durration is now a getter\n */\n // now rounded to the next 5 minute increment\n const defaultStartTz = new DateTZ({ date: nowTz.ms, is_local_tz: false, building_tz: this.timezone }).setValue({ minute: Math.ceil(nowTz.minutes / 5) * 5 });\n\n /**\n * When saving a booking the BaseDataClass doens't know to use the toAPIJson\n * so startDateTz isn't being convert to the epoch timestamp\n *\n * I'm not testing for the end time values because we can assume those follow the same pattern.\n */\n const startEpochProvided = !!(raw_data.start_epoch || raw_data.start);\n const startDateTzProvided = !!raw_data.startDateTz;\n\n /**\n * Not trying to be fancy here, just clear.\n */\n if (!startEpochProvided && startDateTzProvided) {\n /**\n * Once a booking is saved the BaseDataClass recreates the Booking\n * but doesn't use the toApiJSON method adapt the class input, mainly the\n * startDateTz isn't converted to the start_epoch timestamp.\n */\n this.startDateTz = raw_data.startDateTz;\n this.endDateTz = raw_data.endDateTz;\n } else if (startEpochProvided) {\n /**\n * A Booking created from the API responses arrives with the booking\n * start_epoch and end_epoch timestamps.\n */\n this.startDateTz = new DateTZ({\n date: ((raw_data.start_epoch || raw_data.start) * 1000),\n is_local_tz: false,\n building_tz: this.timezone\n });\n this.endDateTz = new DateTZ({\n date: ((raw_data.end_epoch || raw_data.end)* 1000),\n is_local_tz: false,\n building_tz: this.timezone\n });\n } else {\n /**\n * And there are uses of new Booking where there are no inputs\n * and defaults are necessary.\n */\n this.startDateTz = defaultStartTz;\n this.endDateTz = defaultStartTz.addValue({ minutes: 60 });;\n }\n\n /**\n * End time was by design ending at one minute before, IE 4:00PM is 3:59PM,\n * because of calendar and conflcit checking\n *\n * Check and finesse it.\n * Not sure why but some booking endtime added extra seconds\n */\n if ((this.endDateTz.minutes % 5) !== 0 || this.endDateTz.second > 0) {\n // Not sure of the source but some bookings do not conform.\n this.endDateTz = this.endDateTz.setValue({ second: 0, minute: Math.round(this.endDateTz.minutes / 5) * 5 });\n }\n\n\n\n /**\n * Setup the booking created_date\n *\n * if it exists as a key, use the value directly.\n * if we initialize without a value, switch to checking duration.\n * Ref: MCK-826\n */\n this.created_epoch = raw_data.created_epoch || nowTz.seconds;\n\n\n /**\n * Setup the booking all_day boolean\n *\n * if it exists as a key, use the value directly.\n * if we initialize without a value, switch to checking duration.\n * Ref: MCK-826\n */\n if ('all_day' in raw_data) {\n this.all_day = raw_data.all_day;\n } else {\n this.all_day = !!raw_data.all_day || this.duration > 23 * 60;\n }\n\n\n // R--- TODO Concierge will use startDateTz so i'm not sure if this is needed.\n if (!this.is_concierge && this.all_day && this.timezone) {\n /**\n * Concierge has a all_day form field, I can see it may need this for that, does it really?\n */\n this.startDateTz = this.startDateTz.startOfValue('day')\n }\n\n /**\n * Setup the booking approval status\n */\n let status = raw_data.status;\n // If not provided default to approved.\n if (!raw_data.status) {\n status = 'accepted';\n }\n // \"show_as\" is a special rule to override how the applications display the status\n if (raw_data.show_as && raw_data.show_as === 'cancelled') {\n status = 'declined';\n }\n const approvalStatus = {};\n this.multi_rooms.forEach(room => {\n approvalStatus[room.email] = status;\n });\n // Tracking new \"status\" and legacy \"approval_status\" from raw_data.status.\n this.status = status;\n this.approval_status = raw_data.approval_status || {};\n\n /**\n * Catering setup\n *\n * Bring in and sort the catering order by delivery time\n */\n\n this.catering = (raw_data.catering instanceof Array ? raw_data.catering : []).map(\n (i) => new CateringOrder(i)\n );\n\n\n\n /**\n * Edited fields setup\n *\n * Bring in and sort the catering order by delivery time\n *\n * cateringOrders can return the order sorted\n */\n const edited_fields = Array.isArray(raw_data.edits)\n ? raw_data.edits\n : unique(\n flatten(\n Object.keys(raw_data.edits || {}).map((room) => {\n return flatten(Object.values(raw_data.edits[room]));\n })\n )\n );\n this.edits = edited_fields;\n\n /**\n * New Recurring booking feilds\n *\n * New fields do not match the BookingRecurrenceDetails type\n * and are simpler to manage this way\n */\n this.recurrence_count = raw_data.recurrence_count || null;\n this.recurrence_days = raw_data.recurrence_days || null;\n this.occurrence_edits = raw_data.occurrence_edits || null;\n this.recurrence_endTz = raw_data.recurrence_end ? new DateTZ({date: raw_data.recurrence_end * 1000, is_local_tz: false, building_tz: this.timezone}) : null;\n this.recurrence_exceptions = raw_data.recurrence_exceptions || null;\n this.occurrence_ids = raw_data.occurrence_ids || null;\n this.recurrence_interval = raw_data.recurrence_interval || null;\n this.recurrence_period = raw_data.recurrence_period || null;\n this.recurrence_type = raw_data.recurrence_type || null;\n this.recurrence_master_id = raw_data.recurrence_master_id\n this.occurrence_details = raw_data.occurrence_details\n this.is_recurrent = raw_data.is_recurrent || (!!this.recurrence_type && !!this.recurrence_period) || null;\n this.recurrence_starts = raw_data.recurrence_starts || []\n this.start_epoch = raw_data.start_epoch\n this.end_epoch = raw_data.end_epoch\n\n /**\n * I'm not clear why, but the booking body, aka description is copied into the notes.\n * R--- In concerge data description is only a key when saving, also in the note array?\n */\n if (raw_data.body && !this.notes.find((i) => i.type === 'description')) {\n this.notes = [\n ...this.notes,\n {\n type: 'description',\n date: 0,\n message: raw_data.body,\n author: this.organiser.email\n }\n ];\n }\n\n /**\n * In case there is a cancellation in progress the property attempted_to_delete is set to false\n * And it is persisted until the booking is gone\n */\n this.attempted_to_delete = raw_data.attempted_to_delete || null;\n }\n\n\n /** Service for managing Bookings */\n protected get _service() {\n return ServiceManager.serviceFor(Booking);\n }\n\n /** Alias to approval_status */\n public get auto_approve(): boolean {\n // if concierge we auto_approve always.\n if (this.is_concierge) {\n return true;\n }\n return !this.multi_rooms.some(room => room.byRequest({\n dateTz: this.startDateTz,\n duration: this.duration,\n host: this.organiser\n }));\n }\n\n /** Whether booking has been approved */\n public get approved(): boolean {\n return !this.declined && !this.tentative;\n }\n\n /** All of the booking attendees including the organizer */\n public get allAttendees(): User[] {\n return unique([this.organiser].concat(this.attendees), 'email');\n }\n\n /** Computer format for booking type */\n public get bookingTypeId(): string {\n return this.booking_type.toLowerCase();\n }\n\n /** Human format for booking type */\n public get bookingTypeLabel(): string {\n return toTitleCase(this.booking_type);\n }\n\n /** Get accessor for the check in object, app expect array */\n public get checkInsArray(): string[] {\n return Object.keys(this.check_ins || {})\n }\n\n /** Legacy getter for \"class\" */ // R-- TODO remove\n public get class(): string {\n return this.booking_type;\n }\n\n /** */\n public get displayEndDateTz(): DateTZ {\n if ((this.endDateTz.minutes % 5) !== 0) {\n // All the bookings should end in 59 seconds\n return this.endDateTz.addValue({ seconds: 1 });\n }\n // but if not they return 00\n return this.endDateTz;\n }\n\n /** Whether booking has been declined */\n public get declined(): boolean {\n /**\n * I'm not sure, nor is Cam if this is ever used to decline\n * all booking for a specific room\n */\n const isAnyRoomDeclined = this.multi_rooms.some(room => room.name.toLowerCase().includes('decline'));\n if (isAnyRoomDeclined) {\n return true;\n }\n\n if (this.status.includes('decline')) {\n return true;\n }\n\n return false;\n }\n\n /** Description of the booking purpose */\n public get description(): string {\n const note = (this.notes || []).find((i) => i.type === 'description');\n return note ? note.message : '';\n }\n\n /** Catering getter */\n public get cateringOrders() {\n // Return the catering orders sorted by delivery time\n return this.catering ? this.catering.slice().sort((a, b) => {\n if (a?.delivery_time > b?.delivery_time) return 1;\n if (b?.delivery_time > a?.delivery_time) return -1;\n\n return 0;\n }) : [];\n }\n\n /** Get the created date as DateTz */\n public get creationDateTz() {\n return new DateTZ({ date: this.created_epoch * 1000, is_local_tz: false, building_tz: this.timezone })\n }\n\n public get tz_offset() {\n return getTimezoneOffsetString(this.timezone);\n }\n\n /** Display value for the date */\n public get date_string(): string {\n return this.startDateTz.formatDate('dd MMM yyyy');\n }\n\n /**\n * Get the booking durration based on start and end times\n */\n public get duration(): number {\n return Math.abs(this.startDateTz.startOfValue('minute').dateDiff(this.displayEndDateTz, 'minutes'));\n }\n\n /** Unix timestamp of the booking start */\n public get date() { // R--- remove if not necessary\n return this.startDateTz.ms;\n }\n\n /** Whether booking contains external visitors in the attendee list */\n public get has_visitors(): boolean {\n return this.attendees.reduce((a, v) => a || v.external, false);\n }\n\n /** Does the booking have catering orders */\n public get has_catering(): boolean {\n return !!this.catering.length;\n }\n\n /**\n * Unique calendar event ID\n * same as this.id, used to support existing component usage\n */\n public get icaluid(): string {\n return this.id;\n }\n\n /**\n * Get the booking room id\n */\n public get room_id(): string {\n return this.room.id;\n }\n\n /** Legacy getter of room, aka space */\n public get space(): Space {\n return this.room;\n }\n\n /** Status of the booking */\n public get timeBasedStatusLabel(): 'future' | 'upcoming' | 'done' | 'started' | 'in_progress' | 'expired_yesterday' {\n const buildingTzDt = DateNow(new Date()).toZone(this.timezone) ; // building time\n\n if (DateNow(new Date()).isBeforeDate(this.startDateTz.subtractValue({ minutes: 15 }))) {\n return 'future';\n } else if (DateNow(new Date()).isBeforeDate(this.startDateTz)) {\n return 'upcoming';\n } else if (DateNow(new Date()).isBeforeDate(this.startDateTz.addValue({ minutes: 15 }))) {\n return 'started';\n } else if (DateNow(new Date()).isBeforeDate(this.startDateTz.addValue({ minutes: this.duration }))) {\n return 'in_progress';\n }\n // if current time is 12 AM (as per timezone) and booking expired yesterday\n else if(buildingTzDt.startOfValue('day').addValue({ minutes: 1 }).isAfterDate(this.startDateTz, 'day') ) {\n return 'expired_yesterday'\n }\n\n return 'done';\n }\n\n /** Whether booking is tentative */\n public get tentative(): boolean {\n if (\n this.status &&\n this.status.indexOf('tentative') >= 0\n ) {\n return true;\n }\n\n return false;\n }\n\n /** Display valuie for the start and end times of the booking */\n public get time_period(): string {\n return `${this.startDateTz.formatDate('h:mma')} - ${this.displayEndDateTz.formatDate('h:mma')}`;\n }\n\n /** Display value for the start time of the booking */\n public get start_time(): string {\n return this.startDateTz.formatDate('h:mma');\n }\n\n /** Display value for the end time of the booking */\n public get end_time(): string {\n return this.endDateTz.formatDate('h:mma');\n }\n\n /** Display value for the duration of the booking */\n public get length_string(): string {\n return humaniseDuration(this.duration);\n }\n\n /** Display value for the location of the booking */\n // public get local_room_name(): string {\n // return this.room?.local_name || 'No location';\n // }\n public get local_room_name(): string {\n if (this.multi_rooms.length === 0) {\n return 'No location';\n } else {\n return this.multi_rooms.map(room => room.local_name).join(', ');\n }\n } \n\n /** Display value for the level of the first space in the booking */\n public get level(): string {\n return this.space.level.name;\n }\n\n /**\n * Make a copy of this object\n */\n public clone(): Booking {\n return new Booking(this.toJSON());\n }\n\n /**\n * Make a copy of this object without identification data\n */\n public duplicate(isEdit = false): Booking {\n return new Booking({\n ...this.toJSON(),\n id: isEdit ? this.id : null,\n });\n }\n\n /**\n *\n * @param status\n * @returns void\n */\n public undo(status?: 'accept' | 'decline', opts: { series?: boolean } = {}): Promise {\n return this._service.undo(\n this.id, status || 'accept',\n opts\n );\n }\n\n /**\n * Delete booking from the server\n */\n public delete(opts: { series?: boolean } = {}): Promise {\n if (this.id) {\n return this._service.delete(this.id, { ...opts });\n\n // R-- ask Cam again is he's positive these extra params are not needed for anything\n // return this._service.delete(this.id, {\n // concierge: this.is_concierge,\n // host: this.organiser.email,\n // room_id: this.space?.id,\n // icaluid: this.icaluid,\n // start: json.start,\n // end: json.end\n // });\n }\n }\n\n /**\n * Convert object into plain object\n */\n public toJSON(this: Booking): HashMap {\n let data = super.toJSON();\n // Remove the description from the notes\n data.notes = Array.isArray(data.notes) ? data.notes.filter((note) => note.type !== 'description') : data.notes;\n // Encode the nested objects\n data.room = data.room.toJSON();\n\n\n\n data.organiser = data.organiser.toJSON();\n data.creator = data.creator?.toJSON ? data.creator.toJSON(): data.creator;\n // Map the attendees User objects\n data.attendees = data.attendees.map((i: User) => i.toJSON());\n\n // New recurring booking\n data = handleRecurrenceFields(data)\n\n if(data.recurrence_list){\n data.recurrence_starts = [...data.recurrence_list]\n delete data.recurrence_list\n }\n\n return data;\n }\n\n /**\n * Convert object into plain object\n */\n public toApiJSON(this: Booking): HashMap {\n let data = super.toJSON();\n\n // Update booking payload contains additional fields\n data.icaluid = \"\";\n if (data.id) {\n data.icaluid = data.id;\n data.location_name = this.multi_rooms.map((item)=>{return item.local_name}).join(', ');\n data.from_room = this.multi_rooms.map((item)=>{return item.email}).join(', ');\n data.building_zone = this.room?.building?.id;\n }\n\n /**\n * These fields are rather in flux\n * According to Cam \"approve\" will be the prefered field\n * and auto_approve and approval_status can be removed.\n */\n data.approve = this.auto_approve; // this value appear incorrect on update\n data.auto_approve = [this.auto_approve]; // Needed to update approval status until BE accepts \"approve\"\n // delete data.approval_status;\n\n // Booking start and end data\n delete data.startDateTz;\n delete data.endDateTz;\n data.start = this.startDateTz.seconds;\n data.end = this.endDateTz.seconds;\n data.old_start = this.startDateTz.seconds; // remove is not necessary\n data.old_end = this.endDateTz.seconds; // remove is not necessary\n\n // Creation date value\n delete data.created_epoch;\n data.creation_date = this.creationDateTz.seconds;\n\n // Catering fields\n // data.catering = data.catering.toJSON();\n data.catering = this.cateringOrders;\n data.has_catering = this.has_catering;\n delete data.cateringOrders\n\n // Attendees appear quite incomplete\n // Map the attendees User objects\n data.attendees = this.allAttendees.map((i: User) => i.toJSON ? i.toJSON() : i);\n\n // Orgainizer and Creator data\n data.organiser = data.organiser.toJSON ? data.organiser.toJSON() : data.organiser;\n data.creator = data.creator.toJSON ? data.creator.toJSON() : data.creator;\n\n // Convert check_ins to checked_in array\n delete data.check_ins;\n data.checked_in = this.checkInsArray;\n\n // edits - edit booking is showing all fields even w/o an edit :(\n\n // Room data\n data.room_ids = this.multi_rooms.map((item)=>{return item.email});\n delete data.room;\n\n // New recurring booking\n data = handleRecurrenceFields(data)\n\n\n if(data.recurrence_list){\n data.recurrence_starts = [...data.recurrence_list]\n delete data.recurrence_list\n }\n\n // Not found in dev data\n delete data.name;\n delete data.email;\n delete data.is_concierge;\n // delete data.building_zone;\n // delete data.status;\n\n return data;\n }\n\n public getType(): string {\n if (!this || this.status?.includes('decline')) {\n return 'cancelled';\n }\n const booking_type = this.booking_type;\n if (booking_type === 'internal' && this.has_visitors) {\n return 'external';\n }\n return booking_type;\n }\n\n /** fallback for a booking when the recurrence_end is null but it's a series booking */\n public recurrenEndFromOcurrences(occurrence_details: { id: string, start_epoch: number }[]): DateTZ {\n const date = occurrence_details?.map(occurence => occurence)\n .sort((a, b) => new Date(a.start_epoch * 1000).getTime() - new Date(b.start_epoch * 1000).getTime())\n .reverse()\n .shift()\n .start_epoch * 1000\n return new DateTZ({date, is_local_tz: false, building_tz: this.timezone});\n }\n\n public get dateString() {\n return shorterLocalDateFormatString(this.startDateTz);\n }\n public get dateBuildingString() {\n return shorterBuildingDateFormatString(this.startDateTz);\n }\n\n public get endDateString(): string {\n return shorterLocalDateFormatString(this.recurrenEndFromOcurrences(this.occurrence_details));\n }\n}\n\n/**\n * Merge catering orders with same time and location\n * @param order_list List of catering orders\n */\nexport function mergeCateringOrders(order_list: CateringOrder[]) {\n // R--- investigate when this is used. Staff doesn't use it when adding catering orders.\n for (let i = 0; i < order_list.length; i++) {\n const orders = order_list.filter(\n (order) =>\n order.location_id === order_list[i].location_id &&\n order.delivery_time === order_list[i].delivery_time\n );\n if (orders.length > 1) {\n const new_list = order_list.filter(\n (order) =>\n !(order.location_id === order_list[i].location_id &&\n order.delivery_time === order_list[i].delivery_time)\n );\n new_list.push(new CateringOrder({\n ...order_list[i],\n items: flatten(orders.map(order => order.items)),\n booking_date: this.startDateTz.ms,\n booking_timezone: this.booking_timezone,\n }));\n order_list = new_list;\n i = 0;\n }\n }\n return order_list;\n}\n","import { DateTZ } from \"@mckinsey-converge/date-tz\";\n\nexport interface SpaceRules {\n auto_approve: boolean; // if false sapce is requestable else bookable\n hide: boolean;\n max_length?: number;\n min_length?: number;\n reason?: string // specially for non-bookable rooms\n}\n\nexport interface BookingNote {\n /** Type of note */\n type: 'equipment' | 'catering' | 'description' | 'private' | 'other' | 'catering-private';\n /** Name of the note's author */\n author: string;\n /** Contents of the note */\n message: string;\n /** Time the note was added to the booking */\n date: number;\n /** Display value for the time */\n time?: string;\n /** ID of the space associated with the note */\n space?: string;\n /** ID of the catering order associated with the note */\n order_id?: string;\n}\n\nexport interface DateDurationData {\n dateTz: DateTZ,\n duration: number,\n mobile: boolean,\n save?: boolean,\n}\n\nexport enum BOOKING_STATUS {\n approved = 'approved',\n declined = 'declined',\n tentative = 'tentative',\n unavailable = 'unavailable'\n}","import {\n AbstractControl,\n FormControl,\n FormGroup,\n Validators\n} from '@angular/forms';\n\nimport { AvailableBookingFields } from '../settings.interfaces';\nimport {\n HashMap,\n humaniseDuration,\n mockDate as mockDateBase,\n resetDate as resetDateBase,\n timeFormatString,\n unique\n} from '@mckinsey-converge/base';\nimport { CateringOrder } from '../catering/catering-order.class';\nimport { ServiceManager } from '../service-manager.class';\nimport { User } from '../users/user.class';\nimport { Booking, IBookingQueryOptions } from './booking.class';\nimport {\n BookingRule,\n SpaceStatus\n} from './space.types';\nimport { validateEndTime } from '../validation.utilities';\nimport { DateNow, DateTZ } from '@mckinsey-converge/date-tz';\nimport { BookingAction, RecurrencePeriod, SeriesAction } from '../recurrence/recurrence.utils';\nimport { Space } from '../spaces';\nimport * as cloneDeep from 'lodash/cloneDeep';\n\nconst MINUTE = 1;\nconst HOUR = 60;\nconst DAY = 24 * HOUR;\nconst WEEK = 7 * DAY;\nconst MONTH = 30 * DAY;\n\nconst DURATION_MAP: { [duration: string]: number } = {\n month: MONTH,\n months: MONTH,\n week: WEEK,\n weeks: WEEK,\n day: DAY,\n days: DAY,\n hour: HOUR,\n hours: HOUR,\n minute: MINUTE,\n minutes: MINUTE\n};\n\n/**\n * Deprecated. Use @mckinsey-converge/base's import.\n */\nexport const mockDate = mockDateBase;\n\n/**\n * Deprecated. Use @mckinsey-converge/base's import.\n */\nexport const resetDate = resetDateBase;\n\nexport interface IBookingSlot {\n start: number;\n end: number;\n}\n\n\nexport function bookingOptionsToQuery(options: IBookingQueryOptions): HashMap {\n let query: HashMap = {};\n if (options) {\n query = { ...options };\n delete query.type;\n if (options.filters) {\n delete query.filters;\n for (const property in options.filters) {\n if (!query[property]) {\n query[property] = options.filters[property]\n }\n }\n }\n\n // id?: string;\n // cancelled?: boolean;\n if (options.until) {\n query.to = options.until;\n delete query.until;\n }\n \n }\n return query;\n}\n\n\n/**\n * Generate a list of free time slots between the given bookings\n * @param list List of bookings to find slots between\n * @param min_size Minimum length of a free slot in minutes\n */\nexport function getFreeBookingSlots(list: Booking[]=[], min_size: number = 30, dateTzMs: number = DateNow(new Date()).ms,): IBookingSlot[] {\n /* istanbul ignore else */\n if (!list.length) {\n return [\n {\n start: 0,\n end: DateNow(new Date()).startOfValue('minute').ms * 10\n }\n ];\n }\n const slots: IBookingSlot[] = [];\n let start = new DateTZ({ date: dateTzMs });\n list.sort((a, b) => a.date - b.date);\n for (const booking of list) {\n const bkn_start = booking.startDateTz;\n const bkn_end = booking.endDateTz;\n if (bkn_start.isAfterDate(start)) {\n const diff = Math.abs(bkn_start.dateDiff(start, 'minutes'));\n /* istanbul ignore else */\n if (diff >= min_size) {\n slots.push({ start: start.ms, end: bkn_start.ms });\n }\n start = bkn_end;\n } else if (start.startOfValue('minute').ms === bkn_start.startOfValue('minute').ms) {\n start = bkn_end;\n }\n }\n slots.push({\n start: start.ms,\n // R--- these X 10's make no sense. why not + 2 years?\n end: DateNow(new Date()).startOfValue('minute').ms * 10\n });\n\n return slots;\n}\n\n/**\n * Get the next free time slot from the given bookings\n * @param list List of bookings to find the next slot\n * @param date Date to find next slot after in ms since UTC epoch\n * @param min_size Minimum length of the free slot in minutes\n * \n * R--- Possibly depreciate, I don't see it used anywhere except for its own test\n */\nexport function getNextFreeBookingSlot(\n list: Booking[],\n date: number = DateNow(new Date()).ms,\n min_size: number = 15\n): IBookingSlot {\n const slots = getFreeBookingSlots(list, min_size, date);\n const date_ = new DateTZ({date})\n const time = date_.startOfValue('minute').setValue({ second: 1 });\n for (const block of slots) {\n const start = new DateTZ({date: block.start});\n const end = new DateTZ({date: block.end});\n if (start.startOfValue('minute').isAfterDate(time)) {\n return block;\n } else if (time.isBeforeDate(end.startOfValue('minute'))) {\n const duration = end.startOfValue('minute').dateDiff(time, 'minutes');\n /* istanbul ignore else */\n if (duration >= min_size) {\n return block;\n }\n }\n }\n return slots[slots.length - 1];\n}\n\n/**\n * Generate form fields for the given booking\n * @param booking Booking to generate form for\n * @param current_user Current user of the system to default as the host\n */\nexport function generateBookingForm(\n booking: Booking,\n use_fields: AvailableBookingFields[],\n isConcierge: boolean = false,\n manualTimezone?: string,\n action?: SeriesAction | BookingAction | null\n ): FormGroup {\n if (!booking) {\n throw Error('No booking passed');\n }\n\n const user_service = ServiceManager.serviceFor(User);\n const current_user =\n user_service.current ||\n new User({ id: 'local-user', name: 'Local User', email: 'local@place.tech' });\n\n const fields: HashMap = {\n id: new FormControl(booking.id || ''),\n space_list: new FormControl( booking.is_multiroom? booking.multi_rooms : [booking.room], []),\n room: new FormControl(booking.room, {}),\n dateTz: new FormControl({ value: booking.startDateTz || DateNow(), disabled: false }, [Validators.required]),\n duration: new FormControl({ value: booking.duration, disabled: false }),\n organiser: new FormControl(booking.organiser || current_user, [Validators.required]),\n attendees: new FormControl(booking.attendees, []),\n title: new FormControl(booking.title || '', { validators: [ Validators.required, Validators.minLength(1) ], updateOn: 'blur' }), //Validators.pattern(\"[^=`~!@#$%*{};:'\\\",_.<>]*\")\n booking_type: new FormControl(booking.booking_type),\n body: new FormControl(booking.body),\n notes: new FormControl(booking.notes),\n equipment_codes: new FormControl(booking.equipment_codes),\n expected_attendees: new FormControl(booking.expected_attendees),\n company: new FormControl(booking.company),\n is_multiroom: new FormControl(booking.is_multiroom),\n merged: new FormControl({value: booking?.merged, disabled: true}),\n catering: new FormControl(\n booking.cateringOrders.map(\n (order) => new CateringOrder({\n ...order,\n booking_date: booking.startDateTz.ms,\n booking_timezone: booking.timezone,\n })\n )\n ),\n all_day: new FormControl(!!booking.all_day),\n has_catering: new FormControl(!!booking.has_catering),\n needs_space: new FormControl(true),\n action: new FormControl(''),\n timezone: new FormControl(manualTimezone || \"\"),\n is_before_date: new FormControl(false)\n };\n if (!isConcierge) {\n fields.duration.setValidators([Validators.required, validateEndTime(fields.dateTz)]);\n }\n /* istanbul ignore else */\n if (booking.id && booking.id !== 'ad-hoc') {\n fields.organiser.disable();\n /* istanbul ignore else */\n if (booking.endDateTz.ms < DateNow(new Date()).ms) {\n fields.dateTz.disable();\n }\n } else {\n const dateValidators = [Validators.required];\n if (!isConcierge) {\n dateValidators.push(isFuture);\n }\n dateValidators.push(validDateTZ)\n fields.dateTz.setValidators(dateValidators);\n fields.dateTz.updateValueAndValidity();\n }\n let list_length = -1;\n fields.space_list.valueChanges.subscribe((list) => {\n const expected = fields.expected_attendees.value || {};\n const matches = Object.keys(expected).filter((key) =>\n list?.find((space) => space.email === key)\n ).length;\n if (list && list.length && matches === 0) {\n const codes = fields.equipment_codes.value || {};\n if (Object.keys(expected).length >= 0 || Object.keys(codes).length >= 0) {\n const key = Object.keys(expected)[0] || Object.keys(codes)[0];\n const new_expected = {};\n const new_codes = {};\n const notes = fields.notes.value;\n notes.forEach((note) => (note.space === key ? (note.space = list[0].email) : ''));\n new_expected[list[0].email] = expected[key];\n new_codes[list[0].email] = codes[key];\n fields.expected_attendees.setValue(new_expected);\n fields.equipment_codes.setValue(new_codes);\n }\n }\n list_length = list?.length;\n });\n fields.dateTz.valueChanges.subscribe((_) => {\n fields.duration.updateValueAndValidity();\n });\n fields.needs_space.valueChanges.subscribe((space_needed) => {\n if (!space_needed) {\n fields.space_list.setValue([]);\n }\n });\n fields.has_catering.valueChanges.subscribe((has_catering) => {\n if (!has_catering) {\n fields.catering.setValue([]);\n }\n });\n const simplified_fields: HashMap = [\n 'id',\n 'space_list',\n 'space_ids',\n 'notes',\n 'dateTz',\n 'booking_type',\n 'equipment_codes',\n 'expected_attendees',\n 'timezone',\n 'is_before_date',\n ...use_fields\n ].reduce((map, key) => {\n /* istanbul ignore else */\n if (fields[key]) {\n map[key] = fields[key];\n }\n return map;\n }, {});\n \n if (simplified_fields.all_day) {\n const handleAllDay = (value) => {\n if (value) {\n let startOfDay = simplified_fields.dateTz.value.clone();\n // When the booking space isn't set we still need a way to determine midnight\n // and here we use the current concierge building selection's timezone.\n if (manualTimezone) {\n startOfDay = startOfDay.toZone(manualTimezone);\n }\n\n simplified_fields.dateTz.setValidators([Validators.required]);\n simplified_fields.dateTz.setValue(simplified_fields.all_day.value\n ? startOfDay.startOfValue('day')\n : simplified_fields.dateTz.value);\n simplified_fields.duration.setValue(simplified_fields.all_day.value\n ? 24 * 60\n : simplified_fields.duration.value);\n simplified_fields.dateTz.updateValueAndValidity();\n simplified_fields.duration.disable();\n // simplified_fields.dateTz.disable();\n } else {\n const dateValidators = [Validators.required];\n if (!isConcierge) {\n dateValidators.push(isFuture);\n }\n dateValidators.push(validDateTZ)\n simplified_fields.dateTz.setValidators(dateValidators);\n simplified_fields.duration.setValue(booking.duration || 30);\n simplified_fields.dateTz.updateValueAndValidity();\n simplified_fields.duration.enable();\n simplified_fields.dateTz.enable();\n }\n };\n simplified_fields.all_day.valueChanges.subscribe(handleAllDay);\n handleAllDay(simplified_fields.all_day.value);\n }\n\n const occurrences = booking.occurrence_details || [];\n\n //Add recurrence fields\n if(action && booking.recurrence_period === RecurrencePeriod.LIST){\n booking.recurrence_starts = occurrences\n .filter(occurrence => occurrence.id !== booking.recurrence_master_id)\n .map((occurrence) => new DateTZ({date: occurrence.start_epoch * 1000, is_local_tz: false, building_tz: booking.timezone})\n ) \n }\n /**If series exists then set the start and end dates from the first and last bookings in the occurrences array\n * this prevents prefilled form errors due to individual edits\n */\n if(action && action !== SeriesAction.CLONE && action !== BookingAction.CLONE && occurrences && occurrences.length && !booking.id){\n const start_dateTz = new DateTZ({\n date: occurrences.sort((d1, d2) => d1.start_epoch - d2.start_epoch)[0].start_epoch * 1000, \n is_local_tz: false, building_tz: booking.timezone\n })\n simplified_fields.dateTz.setValue(start_dateTz)\n const end = occurrences.sort((d1, d2) => d2.start_epoch - d1.start_epoch)[0].start_epoch * 1000\n booking.recurrence_endTz = new DateTZ({date: end, is_local_tz: false, building_tz: booking.timezone}) \n }\n\n // R-- needs TZ aware fields\n const _recurr_end = simplified_fields.dateTz?.value || new DateTZ({ date: 1 }); // new DateTZ({date: simplified_fields.date?.value}) \n const fields_with_recurrence = {\n ...simplified_fields,\n recurrence_period: new FormControl(booking.recurrence_period || RecurrencePeriod.WEEKLY),\n recurrence_interval: new FormControl(booking.recurrence_interval || 1),\n recurrence_endTz: new FormControl(booking.recurrence_endTz || _recurr_end.addValue({days: 1})),\n recurrence_count: new FormControl(booking.recurrence_count || 0),\n is_recurrent: new FormControl(booking.is_recurrent),\n recurrence_starts: new FormControl(booking.recurrence_starts || []),\n recurrence_exceptions: new FormControl([]),\n recurrence_pattern: new FormControl(RecurrencePeriod.WEEKLY), //Helper value to store the last selected pattern (daily, weekly, monthly),\n recurrence_days: new FormControl(booking.recurrence_days || [])\n }\n\n if(booking.is_recurrent && action !== BookingAction.CLONE){\n fields_with_recurrence.recurrence_starts.setValidators([arrayWithValues])\n fields_with_recurrence.recurrence_starts.updateValueAndValidity()\n fields_with_recurrence.recurrence_endTz.setValidators([Validators.required, validDateTZ]);\n }\n // Generate form group for booking item\n const form = new FormGroup(fields_with_recurrence);\n return form;\n}\n\n/**\n * Validate whether date is in the future\n * @param control Control to check value\n */\nexport function isFuture(control: AbstractControl) {\n const dateTz = control.value; // new DateTZ({date: control.value});\n return dateTz.isBeforeDate(DateNow(new Date()).subtractValue({ minutes: 5 }))\n ? { dateTz: 'Date needs to be in the future' }\n : null;\n}\n\n/**\n * Validate whether an array has values\n * @param control Control to check value\n */\nexport function arrayWithValues (control: AbstractControl) {\n return control.value.length !== 0 ? null : { field: 'Needs at least one value'}\n}\n\n/**\n * Validate whether a date has a valid value (future or current date)\n * @param control Control to check value\n */\n export function validDate (control: AbstractControl) {\n if(!control.value){\n return { date: 'Needs a valid date'}\n }\n const date = new DateTZ({date: control.value})\n const now = DateNow(new Date())\n const isValid = now.isBeforeDate(date, 'day') || now.isSameDate(date, 'day')\n return isValid ? null : { date: 'Needs a valid date'}\n}\n\n/**\n * Validate whether a date has a valid DateTZ value (future or current date)\n * @param control Control to check value\n */\n export function validDateTZ (control: AbstractControl) {\n if(!control.value){\n return { date: 'Needs a valid date'}\n }\n const date = DateNow(control.value.JSDate);\n const now = DateNow(new Date());\n const isValid = now.isBeforeDate(date, 'day') || now.isSameDate(date, 'day')\n return isValid ? null : { date: 'Needs a valid date'}\n}\n\n/**\n * Get minimum duration from ruleset in minutes\n * Default to 5min\n */\nexport function getMinLength(rule_list: HashMap): number {\n return Object.values(rule_list).reduce((min, block) => {\n const min_block = block.reduce((min_length, el) => {\n if (el.conditions.min_length && stringToMinutes(el.conditions.min_length) > min) {\n return stringToMinutes(el.conditions.min_length);\n } else {\n return min_length;\n }\n }, 5);\n if (min_block > min) {\n return min_block;\n } else {\n return min;\n }\n }, 5) as number;\n}\n\n/**\n * Whether the first input is greater than the last. Converts duration strings into minutes\n * @param duration_1 First input can be a number in minutes or a duration string e.g. `1 hour`\n * @param duration_2 Second input can be a number in minutes or a duration string e.g. `30 minutes`\n */\nexport function durationGreaterThanOrEqual(\n duration_1: string | number,\n duration_2: string | number\n) {\n const first: number = typeof duration_1 === 'string' ? stringToMinutes(duration_1) : duration_1;\n const second: number =\n typeof duration_2 === 'string' ? stringToMinutes(duration_2) : duration_2;\n return first >= second;\n}\n\n/**\n * Conver time string into minutes\n * @param str timestring e.g. `'1 day'`, `'15 minutes'`, `'2 weeks'`\n */\nexport function stringToMinutes(str: string): number {\n const parts = str.split(' ');\n return +parts[0] * DURATION_MAP[parts[1]];\n}\n\n/**\n * Get current status within bookings\n * @param bookings List of bookings\n * @param host Host of the new event\n * @param date Datetime of the new event\n */\nexport function statusFromBookings(\n bookings: Booking[] = [],\n bookable: boolean = true,\n requestable: boolean = false,\n dateTz: DateTZ = DateNow(),\n space?: Space\n) {\n const free_slots = getFreeBookingSlots(bookings.filter((bkn) => !bkn.declined));\n const now = DateNow() // new DateTZ({date}); // now should be now\n // commeted bellow piece as correct way to get next free booking slot is at line- 488\n // const next_free_slot = free_slots.find((slot) => {\n // return slot && (slot.start > now.ms || now.ms > slot.start - slot.start % 1000 || now.ms < slot.end - slot.end % 60 * 1000);\n // // const start = new DateTZ({date: slot?.start || 0 });\n // // const end = new DateTZ({date: slot?.end || 0}); // slot end isn't late enough? or after now/\n // // return start.isAfterDate(now) || (now.isAfterDate(start.startOfValue('second')) && now.isBeforeDate(end.startOfValue('minute')))\n // });\n\n const next_free_slot = getNextFreeBookingSlot(bookings.filter((bkn) => !bkn.declined), dateTz.ms);\n \n const start = new DateTZ({date: next_free_slot?.start || 0});\n const end = new DateTZ({date: next_free_slot?.end || 0});\n const currently_free = dateTz.isAfterDate(start.startOfValue('second')) && dateTz.isBeforeDate(end.startOfValue('minute'));\n const time_until_next_block = humaniseDuration(\n currently_free ? end.dateDiff(dateTz, 'minutes') : start.dateDiff(dateTz, 'minutes'),\n 'short'\n );\n\n const free_tomorrow = !currently_free && !start.isSameDate(dateTz, 'day');\n const free_today = currently_free && !end.isSameDate(dateTz, 'day');\n \n return {\n status: (!bookable\n ? SpaceStatus.NotBookable\n : currently_free\n ? requestable\n ? SpaceStatus.Requestable\n : SpaceStatus.Available\n : SpaceStatus.InProgress) as SpaceStatus,\n available_until: free_today\n ? 'No meetings today'\n : currently_free\n ? `Free until ${end.formatDate(timeFormatString())}(${time_until_next_block})`\n : free_tomorrow\n ? 'Unavailable today'\n : `Free at ${start.formatDate(timeFormatString())}(${time_until_next_block})`\n };\n}\n\n/**\n * V1 version display.\n */\nexport const spaceStatusToDisplay = (status: SpaceStatus): string => {\n switch (status) {\n case SpaceStatus.NotBookable:\n return 'Not Bookable';\n case SpaceStatus.Requestable:\n return 'Available by Request';\n case SpaceStatus.Unavailable:\n return 'Unavailable';\n case SpaceStatus.InProgress:\n return 'Meeting in Progress';\n case SpaceStatus.Available:\n default:\n return 'Available';\n }\n};\n\nexport function replaceBookings(\n list: Booking[],\n new_bookings: Booking[],\n filter_options: { room_email: string; fromTz: DateTZ; toTz: DateTZ }\n) {\n const filtered_list = list.filter((booking) => {\n return (\n !(booking.multi_rooms.some(room => room?.email === filter_options.room_email)) ||\n !timePeriodsIntersect(filter_options.fromTz.ms, filter_options.toTz.ms, booking.startDateTz.ms, booking.endDateTz.ms)\n );\n });\n const updated_list = filtered_list.concat(new_bookings);\n updated_list.sort((a, b) => a.date - b.date);\n return unique(updated_list, 'icaluid');\n}\n\nexport function timePeriodsIntersect(\n start1: number,\n end1: number,\n start2: number,\n end2: number,\n type = ''\n) {\n return (\n (start1 >= start2 && start1 < end2) ||\n (end1 > start2 && end1 <= end2) ||\n (start2 >= start1 && start2 < end1) ||\n (end2 > start1 && end2 <= end1)\n );\n}\n\nexport function formatWhen({\n all_day,\n startTZ,\n endTZ,\n displayEndTZ\n}): string {\n if (all_day) {\n return `${startTZ.formatDate('dd MMM yyyy')} - All Day`;\n } else {\n if (startTZ.isSameDate(endTZ, 'day')) {\n return `${startTZ.formatDate('dd MMM yyyy, h:mma')} - ${displayEndTZ.formatDate('h:mma')}`;\n } else {\n return `${startTZ.formatDate('dd MMM yyyy, h:mma')} - ${displayEndTZ.formatDate('dd MMM yyyy, h:mma')}`;\n }\n }\n}\n\nexport const isIncludes = (item: any, field: string, searchVaue: string) => (item && item[field] && item[field].toLowerCase().includes(searchVaue));\n\nexport function searchRoomsAndBookings(allItems: (Space | Booking)[], searchValue: string, isConcierge:boolean = false) {\n\n let relevantItems: (Space | Booking)[] = [];\n\n if(isConcierge) {\n relevantItems = allItems.filter( (item: any ) => \n isIncludes(item, 'title', searchValue) ||\n isIncludes(item.creator, 'name', searchValue) ||\n isIncludes(item.organiser, 'name', searchValue) ||\n isIncludes(item, 'simple_name', searchValue) || // search with room name \n (item?.attendees && !!item?.attendees.filter(el => el.name.toLowerCase().includes(searchValue)).length ) \n );\n } else {\n relevantItems = allItems.filter( (item: any ) => \n this.isIncludes(item.creator, 'name', searchValue) ||\n this.isIncludes(item.organiser, 'name', searchValue) ||\n this.isIncludes(item, 'simple_name', searchValue) \n );\n }\n\n const filteredItems = this.duplicateMultiroomEntriesIfAny(relevantItems);\n \n return filteredItems;\n} \n\nexport function duplicateMultiroomEntriesIfAny(bookings) {\n // Initialize an array to hold the original and duplicated entries \n const updatedBookings = [...bookings];\n\n // Loop through the original bookings array \n [...bookings].forEach(booking => {\n if(booking?.is_multiroom) {\n // calculate the number of times to duplicates the booking\n const duplicatesCount = booking.multi_rooms.filter(el => el.id !== booking.space.id);\n\n // Create the duplicate and add them to the updatedBookings array.\n for(let i = 0; i < duplicatesCount.length; i++) {\n // Deep copy the booking to ensure references are not copied\n const duplicatedBooking = cloneDeep(booking); \n duplicatedBooking.room = duplicatesCount[i]; \n updatedBookings.push(duplicatedBooking);\n }\n }\n });\n\n // Return the array with duplicated entries \n return updatedBookings;\n\n}\n \n","\nimport { Injectable } from '@angular/core';\nimport { ComposerService } from '@placeos/composer';\nimport { BaseAPIService } from '../base.service';\nimport { Booking, IBookingQueryOptions } from './booking.class';\nimport { bookingOptionsToQuery } from './booking.utilities';\nimport {\n HashMap\n} from '@mckinsey-converge/base';\nimport { ServiceManager } from '../service-manager.class';\nimport { SettingsService } from '../settings.service';\nimport { BookingCheckinParams } from './space.types';\nimport { DateNow } from \"@mckinsey-converge/date-tz\";\nimport { Observable } from 'rxjs/internal/Observable';\nimport { forkJoin } from 'rxjs';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class BookingsService extends BaseAPIService {\n constructor(protected _composer: ComposerService,\n settingsService: SettingsService,\n ) {\n super(_composer, settingsService);\n ServiceManager.setService(Booking, this);\n this._name = 'Bookings';\n this._api_route = 'bookings';\n this._compare = (a, b) => !(a.id || '').localeCompare(b.id) || !(a.icaluid || '').localeCompare(b.icaluid);\n }\n\n /**\n * Get user bookings\n * @param options\n */\n public userBookings(options: IBookingQueryOptions,\n uniqueId?: string): Promise {\n if (!options) {\n throw new Error('Booking avilability requires request options');\n }\n const now = DateNow(new Date())\n if (!options.from) {\n options.from = now\n .startOfValue('day')\n .subtractValue({ days: 2 })\n .ms;\n }\n if (!options.until) {\n options.until = null;\n }\n\n const key = `bookings|${options.id ? options.id : ''}|${uniqueId ?? ''}`;\n if (!this._promises[key]) {\n this._promises[key] = new Promise((resolve, reject) => {\n const respond = (list: Booking[]) => {\n delete this._promises[key];\n resolve(list);\n };\n const error = e => {\n reject(e);\n delete this._promises[key];\n };\n const query = bookingOptionsToQuery(options);\n if (options.id) {\n this.show(options.id, query).then(i => respond([i]), error);\n } else {\n this.query(query).then(respond, error);\n }\n });\n }\n return this._promises[key];\n }\n\n\n /**\n * Save changes to the booking\n * @param booking Booking update or add to the database\n */\n public save(booking: Booking, series?: boolean): Promise {\n const body = booking.toApiJSON()\n if(series){\n body.series = true\n }\n return booking.id ? this.update(booking.id, body) : this.add(body);\n }\n\n /**\n * Checkin atteendee of a booking\n * @param id ID of the booking\n * @param fields Fields associated with the booking and attendee\n */\n public checkin(id: string, fields: BookingCheckinParams) {\n return this.task(id, 'checkin', fields);\n }\n public checkinIndividualVisitor(id: string, fields: BookingCheckinParams) {\n return this.taskObsr(id, 'checkin', fields); \n }\n\n public accept(id: string, fields?: HashMap) {\n return this.task(id, 'accept', fields);\n }\n\n public undo(id: string, status: 'accept' | 'decline', fields?: HashMap) {\n return this.task(id, status, fields);\n }\n\n public decline(id: string, fields?: HashMap) {\n return this.task(id, this.settingsService.concierge ? 'concierge_decline' : 'decline', fields);\n }\n\n protected process(raw_data: HashMap): Booking {\n return new Booking(raw_data);\n }\n\n public declineMultiple(bookings: Booking[]): Observable {\n const requests = [];\n bookings.forEach(bkg => {\n const booking = bkg.toJSON();\n const id = booking.id;\n const fields = {\n booking_id: booking.id,\n organiser: booking.organiser.email,\n room_email: booking.space?.email,\n icaluid: booking.icaluid,\n start: booking.startDateTz.formatDate('h:mm a').toLocaleLowerCase(),\n end: booking.endDateTz.formatDate('h:mm a').toLocaleLowerCase(),\n };\n if (bkg.is_recurrent && bkg.recurrence_type === 'master') {\n // Add series=true for recurrent bookings\n fields['series'] = true;\n } \n requests.push(this.taskObsr(id, 'concierge_decline', fields) ); \n });\n return forkJoin(requests);\n }\n}\n","import { Injectable } from '@angular/core';\nimport { ComposerService } from '@placeos/composer';\nimport { PaginatedAPIService } from '../paginated.service';\nimport { Booking, IBookingQueryOptions, } from './booking.class';\nimport { bookingOptionsToQuery } from './booking.utilities';\nimport {\n HashMap\n} from '@mckinsey-converge/base';\nimport { ServiceManager } from '../service-manager.class';\nimport { SettingsService } from '../settings.service';\nimport { BookingCheckinParams } from './space.types';\nimport { Observable } from 'rxjs';\n\nexport interface PaginatedBooking {\n results: Booking[];\n total: number;\n}\n\n@Injectable({\n providedIn: 'root'\n})\nexport class BookingsPaginatedService extends PaginatedAPIService {\n constructor(protected _composer: ComposerService,\n settingsService: SettingsService,\n ) {\n super(_composer, settingsService);\n ServiceManager.setService(Booking, this);\n this._name = 'Bookings';\n this._api_route = 'bookings';\n this._compare = (a, b) => !(a.id || '').localeCompare(b.id) || !(a.icaluid || '').localeCompare(b.icaluid);\n }\n\n /**\n * Get user bookings\n * @param options\n */\n public userBookings(options: IBookingQueryOptions,\n uniqueId?: string): Promise {\n if (!options) {\n throw new Error('Booking avilability requires request options');\n }\n\n const key = `bookingsPaginated|${options.id ? options.id : ''}|${uniqueId ?? ''}`;\n if (!this._promises[key]) {\n this._promises[key] = new Promise((resolve, reject) => {\n const respond = (response: any) => {\n delete this._promises[key];\n resolve(response);\n };\n const error = e => {\n reject(e);\n delete this._promises[key];\n };\n const query = bookingOptionsToQuery(options);\n if (options.id) {\n this.show(options.id, query).then(i => respond([i]), error);\n } else {\n this.query(query).then(respond, error);\n }\n });\n }\n\n return this._promises[key];\n }\n\n\n /**\n * Save changes to the booking\n * @param booking Booking update or add to the database\n */\n public save(booking: Booking): Promise {\n return booking.id ? this.update(booking.id, booking.toJSON()) : this.add(booking.toJSON());\n }\n\n /**\n * Checkin atteendee of a booking\n * @param id ID of the booking\n * @param fields Fields associated with the booking and attendee\n */\n public checkin(id: string, fields: BookingCheckinParams) {\n return this.task(id, 'checkin', fields);\n }\n\n public accept(id: string, fields?: HashMap) {\n return this.task(id, 'accept', fields);\n }\n\n public decline(id: string, fields?: HashMap) {\n return this.task(id, this.settingsService.concierge ? 'concierge_decline' : 'decline', fields);\n }\n\n public undo(id: string, status: 'accept' | 'decline', fields?: HashMap) {\n return this.task(id, status, fields);\n }\n\n protected process(raw_data: HashMap): Booking {\n return new Booking(raw_data);\n }\n\n // public getHoldingBay(query: IBookingQueryOptions): Promise {\n // const key = `bookingsPaginated|holding-bay`;\n // if (!this._promises[key]) {\n // this._promises[key] = new Promise((resolve, reject) => {\n // const respond = (response: any) => {\n // delete this._promises[key];\n // resolve(response);\n // };\n // const error = e => {\n // reject(e);\n // delete this._promises[key];\n // }; \n // this.query(query, { url: '/api/staff/bookings/holding_bay' }).then(respond, error);\n // });\n // }\n\n // return this._promises[key];\n // }\n\n public getHoldingBay = (query: IBookingQueryOptions): Observable => this.queryHoldingBay(query, { url: '/api/staff/bookings/holding_bay' });\n}\n","export * from './booking.class'\nexport * from './space.types'\nexport * from './bookings.service'\nexport * from './bookingsPaginated.service'\nexport * from './booking.utilities'\nexport * from './booking-state.service'\nexport * from './space.utilities'\nexport * from './booking.types'\n","import { SpaceRules } from './booking.types';\n\nimport {\n durationGreaterThanOrEqual,\n stringToMinutes\n} from './booking.utilities';\nimport {\n SpaceCheckOptions,\n SpaceRuleOptions\n} from './space.types';\nimport { DateNow, DateTZ } from '@mckinsey-converge/date-tz';\n\n/**\n * Get booking rules for the given user and space\n * @param options\n */\nexport function rulesForSpace(options: SpaceRuleOptions): SpaceRules {\n if (!options) {\n throw Error('Options are needed to check for rule matches');\n }\n const space_rules_for_user: SpaceRules = {\n auto_approve: true,\n hide: true,\n reason: ''\n };\n let match = false;\n /* istanbul ignore else */\n if (options.space) {\n for (const type in options.rules) {\n if (\n options.rules.hasOwnProperty(type) &&\n options.rules[type] instanceof Array &&\n options.space.zones.find((zone) => zone === type)\n ) {\n for (const rule_block of options.rules[type]) {\n /* istanbul ignore else */\n if (\n checkRules({\n user: options.user,\n space: options.space,\n time: options.time,\n duration: options.duration,\n rules: rule_block.conditions\n })\n ) {\n const ruleset = rule_block.rules;\n const conditions = rule_block.conditions;\n space_rules_for_user.hide = false;\n /* istanbul ignore else */\n if (conditions.max_length) {\n space_rules_for_user.max_length = stringToMinutes(\n conditions.max_length as string\n );\n }\n /* istanbul ignore else */\n if (conditions.min_length) {\n space_rules_for_user.min_length = stringToMinutes(\n conditions.min_length as string\n );\n }\n // NOTE: use max_length in conditions instead of book_length in rules\n // if (ruleset.book_length) {\n // space_rules_for_user.max_length = stringToMinutes(ruleset.book_length as string);\n // }\n /* istanbul ignore else */\n if (ruleset.auto_approve !== undefined) {\n space_rules_for_user.auto_approve = ruleset.auto_approve;\n }\n match = true;\n space_rules_for_user.reason = '';\n break;\n }\n else {\n space_rules_for_user.reason = 'OfficeRules';\n }\n }\n }\n \n /* istanbul ignore else */\n if (!space_rules_for_user.hide) {\n break;\n }\n }\n }\n if (!match) {\n space_rules_for_user.hide = true;\n }\n return space_rules_for_user;\n}\n\n\n/**\n * Check if user matches the given ruleset\n * @param options\n */\nfunction checkRules(options: SpaceCheckOptions): boolean {\n /* istanbul ignore else */\n if (options.rules) {\n const time = new DateTZ({date: options.time});\n const count = Object.keys(options.rules).length;\n let matches = 0;\n Object.keys(options.rules).forEach((key) => {\n let counter = 0;\n const condition: string[] =\n options.rules[key] instanceof Array\n ? (options.rules[key] as [])\n : [options.rules[key] as string];\n switch (key) {\n case 'groups':\n /* istanbul ignore else */\n if (options.user && options.user.groups) {\n counter = 0;\n condition.forEach((i) =>\n options.user.groups.find((j) => j === i) ? counter++ : null\n );\n /* istanbul ignore else */\n if (counter > 0) {\n matches++;\n }\n }\n break;\n case 'locations':\n /* istanbul ignore else */\n if (options.user && options.user.location) {\n counter = 0;\n condition.forEach((i) =>\n (options.user.last_location.name || '').indexOf(i) >= 0\n ? counter++\n : null\n );\n /* istanbul ignore else */\n if (counter >= options.rules[key].length) {\n matches++;\n }\n }\n break;\n case 'is_before':\n /* istanbul ignore else */\n if (options.time) {\n const duration = stringToMinutes(condition[0]);\n const check = DateNow(new Date()).addValue({ minutes: duration });\n let match = time.isBeforeDate(check);\n /* istanbul ignore else */\n matches += match ? 1 : 0;\n }\n break;\n case 'is_after':\n /* istanbul ignore else */\n if (options.time) {\n\n const duration = stringToMinutes(condition[0]);\n const check = DateNow(new Date());\n time.isAfterDate(check.addValue({ minutes: duration })) ? matches++ : '';\n }\n break;\n case 'min_length':\n /* istanbul ignore else */\n if (\n options.duration &&\n durationGreaterThanOrEqual(options.duration, condition[0])\n ) {\n matches++;\n }\n break;\n case 'max_length':\n /* istanbul ignore else */\n if (\n options.duration &&\n durationGreaterThanOrEqual(condition[0], options.duration)\n ) {\n matches++;\n }\n break;\n }\n });\n return matches >= count;\n }\n return false;\n}\n","\nimport { Injectable } from '@angular/core';\nimport { ComposerService } from '@placeos/composer';\nimport { CateringCategory } from './catering-category.class';\nimport { BaseAPIService } from '../base.service';\nimport { SettingsService } from '../settings.service';\nimport { ServiceManager } from '../service-manager.class';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class CateringCategoriesService extends BaseAPIService {\n\n constructor(protected _composer: ComposerService,\n settingsService: SettingsService) {\n super(_composer, settingsService);\n ServiceManager.setService(CateringCategory, this);\n this._name = 'catering category/group';\n this._api_route = 'catering/category';\n }\n\n public query(): never {\n throw Error('No index endpoint for catering categories. Use catering menu service.');\n }\n\n public show(): never {\n throw Error('No show endpoint for catering categories. Use catering menu service.');\n }\n\n public processItem(raw_item: any) {\n return new CateringCategory(raw_item);\n }\n\n public format(item: CateringCategory) {\n return { ...item };\n }\n}\n","import { CateringItem } from \"./catering-item.class\";\nimport { HashMap } from '@mckinsey-converge/base';\n\nexport class CateringCategory extends CateringItem {\n /** Whether item is a category */\n public is_category = true;\n public admin_only: boolean;\n public restricted_from: number;\n\n constructor(data: HashMap) {\n super(data);\n this.is_category = true;\n (this as any).must_select = this.must_select || 0;\n (this as any).order_anytime = !!data.order_anytime && this.package;\n }\n\n /**\n * Convert class object into plain object\n */\n public toJSON(this: CateringItem): HashMap {\n const obj = super.toJSON();\n obj.order_anytime = !!obj.order_anytime && obj.package;\n return obj;\n }\n}\n","import { HashMap } from '@mckinsey-converge/base';\nimport { DateNow } from '@mckinsey-converge/date-tz';\n\nexport interface CateringAvailability {\n /** Month of the year that the item starts being available */\n readonly from_month: number;\n /** Month of the year that the item ends being available */\n readonly to_month: number;\n}\n\nexport class CateringItem {\n /** Unique ID of the catering item */\n public readonly id: string;\n /** Display name of the catering item */\n public readonly name: string;\n /** Description of the item */\n public readonly description: string;\n /** URL to the image associated with the item */\n public readonly image_path: string;\n /** Type of catering item */\n public readonly catering_type: string;\n /** Availability of the item */\n public readonly available: CateringAvailability;\n /** Time in hours that is needed to prepare the item beforehand */\n public readonly prior_notice: number;\n /** Cost of the item without decimals */\n private unit_price: number;\n /** Cost of the item without decimals */\n public readonly supplier_cost: number;\n /** Minimum number this item allowed in an order */\n public readonly minimum_quantity: number;\n /** Maximum number this item allowed in an order */\n public readonly maximum_quantity: number;\n /** List of categories that the item is contained in */\n public readonly parent_categories: readonly string[];\n /** Allegen information associated with the item */\n public readonly allergy: string;\n /** Whether the supply of this item has run out */\n public out_of_stock: boolean;\n /** Whether item can be ordered within the time limit set by the building */\n public readonly order_anytime: boolean;\n /** Number of child items that must be selected as part of the package */\n public readonly must_select: number;\n /** List of associated items */\n public readonly items: CateringItem[];\n public readonly availableItems: CateringItem[];\n /** Whether child items are part of a package */\n public readonly package: boolean;\n /** List of zone ids associated with the category */\n public readonly zones: readonly string[];\n /** Number of this item in the assoicated order */\n private _amount = 0;\n /** Unique ID of the catering item */\n private instance_id: string;\n /** Whether item is a category */\n public is_category: boolean = false;\n /** Whether item is disabled in Staff App */\n public admin_only: boolean;\n /** restricted_from indicates category restriction from hours ahead of delivery time */\n public restricted_from: number;\n\n public get can_order_anytime(): boolean {\n return (\n this.order_anytime ||\n !this.package && this.items.reduce(\n (anytime, item) => anytime || item.can_order_anytime,\n false,\n )\n );\n }\n\n /** Number of this item in the assoicated order */\n public get amount(): number {\n return this._amount || 0;\n }\n\n /** Unit price in the assoicated order */\n public get price(): number {\n return this.unit_price || 0;\n }\n\n /** Total cost of the amount of items set */\n public get total(): number {\n return (this._amount * this.unit_price) || 0;\n }\n\n constructor(data: HashMap) {\n this.instance_id = `item-${Math.floor(Math.random() * 999_999_999)}`;\n this.id = data.id || '';\n this.name = data.name || '';\n this.is_category = this.id.includes('category-');\n this.available = {\n from_month: (data.available ? data.available.from_month : data.available_from) || -1,\n to_month: (data.available ? data.available.to_month : data.available_to) || -1,\n };\n this.description = data.description || '';\n this.prior_notice = data.prior_notice || data.notice;\n this.unit_price = data.unit_price || data.price || 0;\n this.supplier_cost = data.supplier_cost;\n this.minimum_quantity = Math.max(0, data.minimum_quantity || 0);\n this.maximum_quantity = Math.max(1, data.maximum_quantity || 999);\n this.parent_categories = data.category_ids || data.parent_categories || data.categories;\n this.order_anytime = !!data.order_anytime;\n this.image_path = data.image_path || '';\n this.admin_only = data.admin_only;\n this.restricted_from = data.restricted_from || data?.root_category_restricted_from || 0;\n \n this.catering_type = data.catering_type;\n this.must_select = data.must_select ?? 0;\n this._amount = data._amount || data.amount || 0;\n this.allergy = data.allergy || '';\n const cateringItems: CateringItem[] = (data.items || []).map(item => new CateringItem(item));\n this.items = cateringItems;\n const filteredCateringItems = cateringItems.filter(c => !c.out_of_stock);\n this.availableItems = filteredCateringItems;\n this.out_of_stock = !!data.out_of_stock ||\n // if original items are empty, we hit the bottom, so if we filter out items then its out of stock.\n (cateringItems.length > 0 && filteredCateringItems.length === 0);\n this.package = data.package === 'true' || data.package === true;\n this.zones = data.zones && data.zones.length ? [...data.zones] : [];\n }\n\n public setAmount(amount: number = 0): void {\n if (amount <= this._amount) {\n if (amount < this.minimum_quantity) {\n amount = 0;\n }\n } else {\n if (amount < this.minimum_quantity) {\n amount = this.minimum_quantity;\n }\n }\n this._amount = Math.max(0, Math.min(this.maximum_quantity, amount));\n }\n\n /**\n * Updates the unit_price in the assoicated order if necessary\n * @param price value to compare to unit_price\n */\n public setPrice(price: number = 0): void {\n if(this.unit_price !== price) this.unit_price = price;\n }\n\n /**\n * Increase amount of the item the order\n * @param amount Amount to increase by\n */\n public addToOrder(amount: number = 1) {\n amount = Math.max(1, amount);\n this._amount += amount;\n if (this._amount < 0) {\n this._amount = 0;\n } else if (this._amount < this.minimum_quantity) {\n this._amount = this.minimum_quantity;\n } else if (this._amount > this.maximum_quantity) {\n this._amount = this.maximum_quantity;\n }\n }\n\n /**\n * Decrease amount of the item the order\n * @param amount Amount to decrease by\n */\n public removeFromOrder(amount: number = 1) {\n amount = Math.max(1, amount);\n this._amount -= amount;\n if (this._amount < 0) {\n this._amount = 0;\n } else if (this._amount < this.minimum_quantity) {\n this._amount = 0;\n }\n }\n\n /**\n * Convert class object into plain object\n */\n public toJSON(this: CateringItem): HashMap {\n const obj: any = { ...this };\n // Remove local private members\n delete obj._changes;\n delete obj._server_names;\n // Convert remaining members to be public\n obj.price = obj.unit_price;\n obj.categories = obj.parent_categories;\n const keys = Object.keys(obj);\n for (const key of keys) {\n if (key[0] === '_') {\n const new_key = key.substr(1);\n obj[new_key] = obj[key];\n delete obj[key];\n } else if (obj[key] === undefined) {\n delete obj[key];\n }\n }\n obj.items = obj.items.map((item: CateringItem) => item.toJSON());\n return obj;\n }\n\n\n\n public within_category_restricted_time(dateTz): boolean {\n /**\n * Update : https://mckinsey.atlassian.net/browse/CNG-574\n * Right now the frontend is restricting catering orders based off the field .settings.discovery_info.catering_restricted_from,\n * so for this ticket change it’s best we keep that field as the default time restriction, then add a new field to catering categories called \n * restricted_from which will override the old field if it’s present!\n * restricted_from can be set by concierge admin to restrict item from being order before set time ex if it been set to 24 then the item should\n * available to order before 24 hours of booking/order time!\n */\n\n if(!!this?.restricted_from) {\n const expired = DateNow(new Date()).addValue({ hours: this?.restricted_from }).startOfValue('minute');\n return dateTz.isBeforeDate(expired);\n } else {\n return false;\n }\n \n }\n\n}\n","\nimport { Injectable } from '@angular/core';\nimport { ComposerService } from '@placeos/composer';\nimport { BaseAPIService } from '../base.service';\nimport { CateringItem } from './catering-item.class';\nimport { SettingsService } from '../settings.service';\nimport { ServiceManager } from '../service-manager.class';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class CateringItemsService extends BaseAPIService {\n constructor(protected _composer: ComposerService,\n settingsService: SettingsService) {\n super(_composer, settingsService);\n ServiceManager.setService(CateringItem, this);\n this._name = 'Catering Menu';\n this._api_route = 'catering/item';\n }\n\n public query(): never {\n throw Error('No index endpoint for catering items. Use menu service.');\n }\n\n public show(): never {\n throw Error('No show endpoint for catering items. Use menu service.');\n }\n\n public processItem(raw_item: any) {\n return new CateringItem(raw_item);\n }\n\n public format(item: CateringItem) {\n return item instanceof CateringItem ? item.toJSON() : item;\n }\n}\n","import { Injectable } from '@angular/core';\nimport { ComposerService } from '@placeos/composer';\n\nimport { CateringItem } from './catering-item.class';\nimport { CateringCategory } from './catering-category.class';\nimport { BaseAPIService } from '../base.service';\nimport { HashMap } from '@mckinsey-converge/base';\nimport { SettingsService } from '../settings.service';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class CateringMenuService extends BaseAPIService {\n constructor(protected _composer: ComposerService,\n settingsService: SettingsService) {\n super(_composer, settingsService);\n this._name = 'Catering Menu';\n this._api_route = 'menu';\n }\n\n /**\n * Convert user data to local format\n * @param user User data\n */\n protected processItem(item: HashMap) {\n return item.items ? new CateringCategory(item) : new CateringItem(item);\n }\n}\n","\nimport { Injectable } from '@angular/core';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class CateringNotesService {\n\n public cateringNotes = [];\n\n \n public setNotes(notes) {\n this.cateringNotes = notes;\n }\n\n public getNotes() {\n return this.cateringNotes;\n }\n}","import { CateringItem } from './catering-item.class';\nimport { HashMap } from '@mckinsey-converge/base';\n\nimport { CateringCategory } from './catering-category.class';\nimport { Booking } from '../bookings';\n\nimport { DateNow, DateTZ } from '@mckinsey-converge/date-tz';\n\nexport type CateringOrderMutableProperties =\n | 'items'\n | 'location_id'\n | 'location'\n | 'delivery_time'\n | 'charge_code'\n | 'notes'\n | 'status';\n\nexport type CateringOrderStatus = 'accepted' | 'preparing' | 'ready' | 'delivered' | 'cancelled';\n\nconst now = DateNow(new Date());\n\n// R--- refactor booking date to be dateTz\nexport class CateringOrder {\n /**\n * Booking fields\n * \n * The app handles booking data inconsisently. Ususally these booking fields are provided\n * for related booking data, but on occasion the full Booking class is used.\n */\n\n /** Unique Booking ID for the catering order */\n readonly booking_id: string;\n /** \n * Timestamp based booking time of the booking associated with the order\n * \n * Must be provide as the true Booking startDateTz or as a new DateTZ with \n * the correct date: timestamp, local_tz = false and bulding_tz values\n */\n readonly booking_date: number;\n /**\n * Timezone of the booking associated with the order\n */\n readonly booking_timezone: string;\n /** ID of the location to deliver the items to */\n location_id: string = '';\n /** Display text for the location to deliver the items to */\n location: string = '';\n /**\n * Smart timezone aware catering datetime with timezone\n */\n readonly bookingDateTz: DateTZ;\n\n /** \n * Booking associated with the order \n * \n * The booking attached to the CateringOrder is a special case used \n * mainly on the Concierge catering reports because someone created \n * a bad pattern to save catering changes to the booking. \n * \n * Avoid when possible.\n */\n public booking: Booking = null;\n\n /**\n * Catering specific fields\n * \n * The app handles booking data inconsisently. Ususally these booking fields are provided\n * for related booking data, but on occasion the full Booking class is used.\n */\n\n /** Whether the order status change is loading */\n public loading: boolean;\n /** Unique ID for the catering order */\n readonly id: string;\n /** Number of minutes after the start of the associated meeting to delivery the order */\n readonly delivery_time: number = 0;\n /** Status of the catering order */\n readonly status: CateringOrderStatus;\n /** Whether items in the order need to be prepared in the kitchen */\n readonly kitchen: boolean;\n /** Whether items in the order are in the pantry */\n readonly pantry: boolean;\n /** List of items */\n readonly items: readonly (CateringItem | CateringCategory)[] = [];\n /** Creation time of the order */\n readonly created_at: number;\n /** Charge code associated with the order */\n readonly charge_code: string;\n /** Notes associated with the order */\n readonly notes: string;\n // /** Mapping of properties to their changes */\n // private _changes: HashMap = {};\n /** Map of local property names to server ones */\n protected _server_names: HashMap = {};\n /** Currency code for the location of the order */\n public symbol: string;\n /** Whether the is an error with the order */\n public error: boolean;\n\n\n constructor(data: HashMap) {\n /** \n * Setup Defaults when raw_data values are not provided\n */\n const nowTz = DateNow(new Date());\n // now rounded to the next 5 minute increment\n const defaultStartTz = nowTz.setValue({ minute: Math.ceil(nowTz.minutes / 5) * 5 });\n const defaultLocalTimezone = Intl?.DateTimeFormat()?.resolvedOptions()?.timeZone;\n\n this.id = data.id || `order-${Math.floor(Math.random() * 999_999_999)}`;\n\n // If the booking is provided default the booking values to the true booking values\n this.booking = data.booking || null;\n\n \n // Has a booking so init with those values\n if (this.hasBookingAttached) {\n this.booking_date = this.booking.startDateTz.ms;\n this.booking_timezone = this.booking.timezone;\n this.bookingDateTz = this.booking.startDateTz;\n this.location_id = data.location_id || '';\n this.location = data.location || '';\n } else {\n this.booking_date = data.booking_date || defaultStartTz.ms;\n this.booking_timezone = data.booking_timezone || defaultLocalTimezone;\n this.bookingDateTz = new DateTZ({ date: this.booking_date, is_local_tz: false, building_tz: this.booking_timezone });\n this.location_id = data.location_id || '';\n this.location = data.location || '';\n }\n \n this.delivery_time = typeof data.delivery_time === 'number' ? data.delivery_time : 0;\n this.status = data.status || 'accepted';\n this.charge_code = data.charge_code || data.code;\n this.notes = data.notes;\n this.kitchen = data.kitchen instanceof Array ? !!data.kitchen.length : data.kitchen;\n this.pantry = data.pantry instanceof Array ? !!data.pantry.length : data.pantry;\n this.symbol = data.symbol || 'USD';\n this.items = (data.items || [])\n .map(item => (item.items ? new CateringCategory(item) : new CateringItem(item)))\n .filter(item => !!item.amount);\n }\n\n /** Special Case, check if a full booking is attached */\n public get hasBookingAttached(): boolean {\n return !!this.booking;\n }\n\n /** Display string for the types of items in the order */\n public get type(): string {\n let type = '';\n /* istanbul ignore else */\n if (this.kitchen) type += 'Kitchen';\n /* istanbul ignore else */\n if (this.pantry) {\n /* istanbul ignore else */\n if (type) type += ' + ';\n type += 'Pantry';\n }\n return type;\n }\n\n /** Total cost of the order */\n public get total(): number {\n return this.items.reduce((total, item) => total + (item.total || 0), 0);\n }\n\n /** Total number of items in the order */\n public get item_count(): number {\n return this.items.reduce((total, item) => total + (item.amount || 0), 0);\n }\n\n public get deliver_date(): DateTZ {\n return (this.bookingDateTz || this.booking?.startDateTz).addValue({ minutes: this.delivery_time })\n }\n\n /** Display string for the delivery time of the order (short version) */\n public get deliver_at(): string {\n return this.deliver_date.formatDate('h:mm a');\n }\n\n /** Display string for the delivery time of the order */\n public get deliver_at_time(): string {\n return this.deliver_date.formatDate('h:mma').toLocaleLowerCase();\n }\n\n /** Display string for the delivery date of the order */\n public get deliver_on_date(): string {\n return this.deliver_date.formatDate('dd MMM yyyy');\n }\n\n /**\n * Make a copy of this object\n */\n public clone(): CateringOrder {\n return new CateringOrder(this.toJSON());\n }\n\n /**\n * Convert class object into plain object\n */\n public toJSON(this: CateringOrder): HashMap {\n const obj: any = { ...this };\n // Remove local private members\n delete obj._server_names;\n // Remove local public members\n delete obj.booking;\n delete obj.loading;\n delete obj.bookingDateTz;\n\n // Convert remaining members to be public\n const keys = Object.keys(obj);\n for (const key of keys) {\n if (key[0] === '_') {\n const new_key = this._server_names[key.substr(1)] || key.substr(1);\n obj[new_key] = obj[key];\n delete obj[key];\n } else if (obj[key] === undefined) {\n delete obj[key];\n }\n }\n obj.items = obj.items.map((item: CateringItem) => item.toJSON());\n\n return obj;\n }\n}\n","import { CateringItem } from './catering-item.class';\n\nexport const mergeCateringItemWithFormData = (existing: CateringItem, override: any) =>\n new CateringItem({\n ...(existing instanceof CateringItem ? existing.toJSON() : existing),\n ...override\n });\n","export * from './catering-item.class';\nexport * from './catering-category.class';\nexport * from './catering-menu.service';\nexport * from './catering-order.class';\nexport * from './catering-categories.service';\nexport * from './catering-items.service';\nexport * from './catering.utilities';\nexport * from './catering-notes.service';\n","import { Injectable } from '@angular/core';\nimport { MatDialogRef } from '@angular/material/dialog';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class CloseDialogService {\n private dialogRefMap = new Map>();\n\n constructor() { }\n\n // Register a dialog with its ID\n registerDialog(id: string, dialogRef: MatDialogRef): void {\n this.dialogRefMap.set(id, dialogRef);\n }\n\n // Close a specific dialog by ID\n closeDialog(id: string): void {\n const dialogRef = this.dialogRefMap.get(id);\n if (dialogRef) {\n dialogRef.close();\n this.dialogRefMap.delete(id);\n }\n }\n\n // Optional: Close all dialogs\n closeAllDialogs(): void {\n this.dialogRefMap.forEach((dialogRef, id) => {\n dialogRef.close();\n this.dialogRefMap.delete(id);\n });\n }\n}\n\nexport enum DialogIds {\n MeetingDetails = 'meetingDetailsDialogId',\n BookingConfirmation = 'bookingConfirmationDialogId',\n}\n","import { Injectable } from '@angular/core';\nimport { BehaviorSubject } from 'rxjs';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class CollapseService {\n private collapseSubject = new BehaviorSubject(false);\n collapse$ = this.collapseSubject.asObservable();\n\n collapsePanel() {\n this.collapseSubject.next(false);\n }\n}\n","import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\n\n@NgModule({\n imports: [CommonModule],\n})\nexport class DataCommonModule {}\n","import { Injectable } from '@angular/core';\nimport {\n BehaviorSubject,\n Observable,\n Subscription\n} from 'rxjs';\n\nimport {\n HashMap,\n unique\n} from '@mckinsey-converge/base';\n\n/** List of keys that cannot be in a combination by themselves or with each other */\nconst INVALID_STANDALONE_KEYS: string[] = ['control', 'shift', 'alt', 'meta', 'os'];\n\n@Injectable({\n providedIn: 'root'\n})\nexport class HotkeysService {\n /** Map of subjects which store press states of keys */\n private keydown_states: HashMap> = {};\n /** Map of obserers for key state subjects */\n private keydown_observers: HashMap> = {};\n /** List of keys at the end of a combination */\n private combo_end: string[] = [];\n /** List of registered hotkey combinations */\n private registered_combos: string[][] = [];\n /** Counter for the number of keydown events. Used for checking order of key presses */\n private counter: number = 0;\n /** Last key code to be pressed */\n private last_down: string;\n\n constructor() {\n window.addEventListener('keydown', (event: KeyboardEvent) => {\n const code = this.mapKey((event.code || '').toLowerCase());\n /* istanbul ignore else */\n if (this.last_down !== code) {\n /* istanbul ignore else */\n if (!this.keydown_states[code]) {\n this.keydown_states[code] = new BehaviorSubject(null);\n this.keydown_observers[code] = this.keydown_states[code].asObservable();\n }\n this.keydown_states[code].next(++this.counter);\n /* istanbul ignore else */\n if (this.combo_end.indexOf(code) >= 0) {\n event.preventDefault();\n }\n this.last_down = code;\n }\n });\n\n window.addEventListener('keyup', (event: KeyboardEvent) => {\n const code = this.mapKey((event.code || '').toLowerCase());\n /* istanbul ignore else */\n if (this.keydown_states[code]) {\n this.keydown_states[code].next(null);\n }\n /* istanbul ignore else */\n if (this.last_down === code) {\n this.last_down = null;\n }\n });\n }\n\n /**\n * Listen to the given key combination\n * @param combo Array of key codes to listen to or a hotkey string e.g. `Alt+Shift+KeyK`\n * @param next Callback for combination presses\n */\n public listen(combo: string | string[], next: () => void): Subscription {\n combo = (combo instanceof Array ? combo : combo.split('+'));\n const combination: string[] = combo.map(i => this.mapKey(i.toLowerCase()));\n /* istanbul ignore else */\n if (combination.length > 0 && this.validCombination(combination)) {\n this.registered_combos.push(combination);\n const last_key = combination[combination.length - 1];\n /* istanbul ignore else */\n if (!this.keydown_states[last_key]) {\n this.keydown_states[last_key] = new BehaviorSubject(null);\n this.keydown_observers[last_key] = this.keydown_states[last_key].asObservable();\n }\n this.updateCombinationEndList();\n return this.keydown_observers[last_key].subscribe((count) => {\n /* istanbul ignore else */\n if (count) {\n const presses: number[] = [];\n /* istanbul ignore else */\n if (combination.length > 0) {\n // Check that keys are pressed\n for (const key of combination) {\n const state = this.keydown_states[key];\n presses.push(state ? state.getValue() || -1 : -1);\n }\n // Check that keys are pressed in the correct order\n for (let i = 0; i < combination.length - 1; i++) {\n if (presses[i] > presses[i + 1]) {\n return;\n }\n }\n }\n const total = presses.reduce((a, v) => a + (v > 0 ? 1 : -1), 0);\n /* istanbul ignore else */\n if (total >= combination.length) {\n next();\n }\n }\n });\n }\n return null;\n }\n\n /**\n * Map key codes with multiple versions to simple form\n * @param code Code to transform\n */\n private mapKey(code: string): string {\n /* istanbul ignore else */\n if (code.indexOf('alt') >= 0 || code.indexOf('shift') >= 0 || code.indexOf('control') >= 0) {\n return code.replace('left', '').replace('right', '');\n }\n return code;\n }\n\n /**\n * Update the list of the last keys in combinations to allow for prevent default actions on pre-existing hotkeys\n */\n private updateCombinationEndList(): void {\n const key_list = [];\n for (const combo of this.registered_combos) {\n this.combo_end.push(combo[combo.length - 1]);\n }\n this.combo_end = unique(key_list);\n }\n\n /**\n * Checks if the given hotkey combination is allowed and valid\n * @param combo Array of key codes\n */\n private validCombination(combo: string[]): boolean {\n let non_meta = 0;\n for (const key of combo) {\n /* istanbul ignore else */\n if (INVALID_STANDALONE_KEYS.indexOf(key) < 0) {\n non_meta++;\n }\n }\n return non_meta > 0;\n }\n}\n","export * from './location.class';\nexport * from './location.service';\n","import { HashMap } from '@mckinsey-converge/base';\n\nimport { Organisation } from '../organisation/organisation.class';\nimport {\n ServiceLike,\n ServiceManager\n} from '../service-manager.class';\nimport { BuildingLevel } from '../organisation/level.class';\n\nexport class MapLocation {\n /** ID of the element on the associated map */\n public readonly id?: string;\n /** Name of the location */\n public readonly name?: string;\n /** X coordinate of the location */\n public readonly x?: number;\n /** Y coordinate of the location */\n public readonly y?: number;\n /** Level details for the location */\n public readonly level: BuildingLevel;\n /** Whether the position is fixed */\n public readonly fixed: boolean;\n /** Accuracy of the location when not fixed */\n public readonly confidence?: number;\n /** Whether location is at a desk */\n public readonly at_desk?: boolean;\n /** Whether location is set */\n public readonly empty: boolean;\n\n /** Service for managing model on the server */\n protected get _service(): ServiceLike {\n return ServiceManager.serviceFor(MapLocation);\n }\n\n /** Display string for the building and level of the location */\n public get display(): string {\n const service = ServiceManager.serviceFor(Organisation);\n if (!service) {\n return this.level.name;\n }\n const bld = service.buildings.find((bld) => bld.id === this.level.building_id);\n return bld ? `${bld.name}, ${this.level.name}` : this.level.name;\n }\n\n /** Whether location is in a different building from the active one */\n public get in_another_building(): boolean {\n const service = ServiceManager.serviceFor(Organisation);\n return service && service.building.id !== this.level.building_id;\n }\n\n constructor(raw_data: HashMap) {\n this.id = raw_data.id || raw_data.map_id || raw_data.desk_id;\n this.name = raw_data.name || '';\n this.x = raw_data.x\n ? Math.floor(this.normalise(raw_data.x, raw_data.x_max || 10000) * 10000)\n : null;\n this.y = raw_data.y\n ? Math.floor(this.normalise(raw_data.y, raw_data.x_max || 10000) * 10000)\n : null;\n const service = ServiceManager.serviceFor(Organisation);\n this.level =\n raw_data.level instanceof BuildingLevel\n ? raw_data.level\n : service\n ? service.levelWithID(raw_data.level)\n : new BuildingLevel(raw_data.level);\n this.fixed = this.x === null && this.y === null;\n this.confidence = Math.max(5, Math.min(15, raw_data.confidence || 0));\n this.at_desk = this.id && this.id.indexOf('area-') === 0;\n this.empty = !(this.name && this.level && (this.id || this.x || this.y));\n }\n\n /** Normalise the given value within the max */\n private normalise(value: number, max: number): number {\n return value / (max * 1.0);\n }\n}\n","import { Injectable } from '@angular/core';\nimport { ComposerService } from '@placeos/composer';\n\nimport { BaseAPIService } from '../base.service';\nimport { MapLocation } from './location.class';\nimport { HashMap } from '@mckinsey-converge/base';\nimport { ServiceManager } from '../service-manager.class';\nimport { SettingsService } from '../settings.service';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class LocationService extends BaseAPIService {\n constructor(protected _composer: ComposerService,\n settingsService: SettingsService) {\n super(_composer, settingsService);\n ServiceManager.setService(MapLocation, this);\n this._name = 'Location';\n this._api_route = 'people';\n }\n\n public add(...args): never {\n throw new Error('Create not allowed for location service')\n }\n\n public update(...args): never {\n throw new Error('Update not allowed for location service')\n }\n\n public delete(...args): never {\n throw new Error('Delete not allowed for location service')\n }\n\n public process(item: HashMap) {\n return new MapLocation(item) as any;\n }\n}\n","import { BaseDataClass } from '../base-api.class';\nimport {\n getItemWithKeys,\n HashMap,\n Identity\n} from '@mckinsey-converge/base';\nimport { BuildingLevel } from './level.class';\nimport { BookingRule } from '../bookings';\nimport { ServiceManager } from '../service-manager.class';\n\nexport interface BuildingCity {\n timezone: string\n name: string\n}\n\nexport interface IBuildingRoleUser {\n name: string;\n email: string;\n phone: string;\n}\n\nexport interface LockerMap {\n [zone: string]: {\n [area: string]: {\n [type: string]: (string | boolean)[][];\n };\n };\n}\n\nexport interface ICoordinates {\n longitude: number;\n latitude: number;\n}\n\nexport interface LevelFeature {\n id: string;\n level_id: string;\n name: string;\n}\n\nexport interface BookingRuleDetails {\n /** List of booking rules details for the building */\n readonly rules: readonly string[];\n /** Custom booking rules for the map */\n readonly map_rules?: readonly string[];\n /** Custom booking rules for the map */\n readonly other_rules?: readonly string[];\n /** Contact email address for the building */\n readonly contact?: string;\n /** Information string to display before the rule listings */\n readonly info?: string;\n /** Link for more details */\n readonly link?: { url?: string, name?: string };\n /**\n * Allow buildings to define their own rules formats where needed or for special messages.\n */\n readonly custom_html?: string;\n}\n\nexport class Building extends BaseDataClass {\n /** Service for managing buildings */\n protected get _service() {\n return ServiceManager.serviceFor(Building);\n }\n\n /** Engine Zone ID for the building */\n public readonly zone_id: string;\n /** Organisation Code for the building */\n public readonly code: string;\n /** Geographical address of the building */\n public readonly address: string;\n /** Details about the booking rules for the building */\n public readonly booking_details: BookingRuleDetails;\n /** Details about the booking rules for the building */\n public readonly booking_rules: HashMap;\n /** Number of hour before a booking catering is restricted */\n public readonly catering_restricted_from: number;\n /** Currency code for the country assoicated with the building */\n public readonly currency: string;\n /** Map of fields that are required on the form */\n public readonly required: HashMap;\n /** IANA timezone database string for the location the building resides */\n public readonly timezone: string;\n /** Whether catering is available in this building */\n public readonly has_catering: boolean;\n /** ID of the system used for the holding bay */\n public readonly holding_bay: string;\n /** ID of the system used for standalone visitors bookings */\n public readonly visitor_space: string;\n /** List of zones to determine sort order spaces */\n public readonly sort_order: readonly string[];\n /** Searchable map features */\n public readonly searchables: readonly LevelFeature[];\n /** List of available extras for the building */\n public readonly extras: readonly Identity[];\n /** List of available extra equipment for loan at the building */\n public readonly loan_items: readonly Identity[];\n /** List of available levels for the building */\n public readonly levels: readonly BuildingLevel[];\n /** The city the building belongs in **/\n public readonly city: string;\n\n public readonly catering_hours: { readonly start: number, readonly end: number };\n /** Map of custom settings for the building */\n private _settings: HashMap;\n /** Map of roles and list of the associated users */\n private _roles: HashMap;\n /** Map of the locker ID arrays */\n private _lockers: LockerMap;\n /** Map of important system ids for the building */\n private _systems: HashMap;\n /** Map of important phone numbers for the building */\n private _phone_numbers: HashMap;\n /** Globe coordiates for the build */\n private _location: ICoordinates;\n /** List of zones associated with the building */\n public readonly zones: readonly string[];\n\n constructor(raw_data: HashMap) {\n super(raw_data);\n const settings = raw_data.settings || {};\n const disc_info = settings.discovery_info || {};\n this.zone_id = raw_data.zone_id || raw_data.zone;\n this.extras = (raw_data.extras || disc_info.extras || []).map(i => ({\n id: i.extra_id || i.id,\n name: i.extra_name || i.name\n }));\n this.loan_items = (raw_data.loan_items || disc_info.loan_items || []).map(i => ({\n id: i.extra_id || i.id,\n name: i.extra_name || i.name\n }));\n this.levels = (raw_data.levels || disc_info.levels || []).map(i => new BuildingLevel({\n ...i,\n building_id: this.id\n }));\n this._roles = raw_data.roles || disc_info.roles || {};\n this._lockers = raw_data.lockers || raw_data.locker_structure || disc_info.locker_structure || {};\n this._systems = raw_data.systems || disc_info.systems || {};\n this._settings = settings;\n this._phone_numbers = raw_data.phone_numbers || disc_info.phone_numbers || {};\n this._location = raw_data.location || disc_info.location || { longitude: null, latitude: null };\n this.catering_hours = raw_data.catering_hours || disc_info.catering_hours || settings.catering_hours || {\n start: 7,\n end: 20\n };\n const searchables = [];\n if (raw_data.neighbourhoods) {\n for (const lvl in raw_data.neighbourhoods) {\n if (raw_data.neighbourhoods.hasOwnProperty(lvl)) {\n const lvl_features = raw_data.neighbourhoods[lvl] || {};\n for (const feature in lvl_features) {\n if (lvl_features.hasOwnProperty(feature)) {\n searchables.push({\n id: lvl_features[feature],\n name: feature,\n level_id: lvl\n });\n }\n }\n }\n }\n }\n this.searchables = searchables;\n this.code = raw_data.code || disc_info.code || settings.code || '';\n this.address = raw_data.address || disc_info.address || settings.address || '';\n this.booking_details = raw_data.booking_details || disc_info.booking_details || settings.booking_details || {};\n this.booking_rules = raw_data.booking_rules || disc_info.booking_rules || settings.booking_rules || {};\n this.catering_restricted_from = raw_data.catering_restricted_from || disc_info.catering_restricted_from || settings.catering_restricted_from || 0;\n this.currency = raw_data.currency || disc_info.currency || settings.currency || 'USD';\n this.required = raw_data.required || disc_info.required || settings.required || {};\n if (disc_info.requires_equipment_code) {\n this.required.equipment_code = true;\n }\n if (disc_info.requires_expected_attendees) {\n this.required.expected_attendees = true;\n }\n this.timezone = raw_data.timezone || disc_info.timezone || settings.timezone || '';\n this.has_catering = raw_data.has_catering || disc_info.has_catering || settings.has_catering || false;\n this.holding_bay = raw_data.holding_bay || disc_info.holding_bay || settings.holding_bay || '';\n this.visitor_space = raw_data.visitor_space || disc_info.visitor_space || settings.visitor_space || '';\n this.sort_order = raw_data.sort_order || disc_info.sort_order || settings.sort_order || [];\n this.city = raw_data.city || disc_info.city || 'No City';\n }\n\n /**\n * Get a custom building setting\n * @param key Name of the setting. i.e. nested items can be grabbed using `.` to seperate key names\n */\n public setting(key: string): any {\n const keys = key.split('.');\n const value = getItemWithKeys(keys, this._settings) || getItemWithKeys(['discovery_info', ...keys], this._settings);\n return value;\n }\n\n /**\n * Get list of users with the associated role\n * @param name Role to find users for\n */\n public role(name: string): IBuildingRoleUser[] {\n return [...(this._roles[name] || [])];\n }\n\n /**\n * Get list of the names of available user role lists\n */\n public get role_names(): string[] {\n return Object.keys(this._roles).filter(i => this._roles.hasOwnProperty(i));\n }\n\n /** Map of the locker ID arrays */\n public get lockers(): LockerMap {\n return { ...(this._lockers || {}) };\n }\n\n /** Map of important system ids for the building */\n public get systems(): HashMap {\n return { ...(this._systems || {}) };\n }\n\n /** Map of important phone numbers for the building */\n public get phone_numbers(): HashMap {\n return { ...(this._phone_numbers || {}) };\n }\n\n /** Real coordinates */\n public get location(): ICoordinates {\n return { ...this._location };\n }\n\n /**\n * Get search map feature for the given level ID\n * @param level_id ID of level to grab features for\n */\n public featuresForLevel(level_id: string): LevelFeature[] {\n return (this.searchables || []).filter(i => i.level_id === level_id);\n }\n\n /**\n * Building objects are readonly and cannot be changed\n */\n public async save(): Promise {\n throw new Error('Building objects are readonly and cannot be changed');\n }\n\n /**\n * Building objects are readonly and cannot be deleted\n */\n public async delete(): Promise {\n throw new Error('Building objects are readonly and cannot be deleted');\n }\n}\n","export * from './building.class';\nexport * from './level.class';\nexport * from './organisation.class';\nexport * from './organisation.service';\n","import { HashMap } from 'libs/base/src/lib/types.utilities';\n\n/** Building Level data */\nexport class BuildingLevel {\n /** ID of the building level zone */\n readonly id: string;\n /** ID of the building zone associated with the level */\n readonly building_id: string;\n /** Name of the level */\n readonly name: string;\n /** Number or short identifier of the level */\n readonly short_name: string;\n /** Map URL for the level */\n readonly map_url: string;\n /** Usage type for the level */\n readonly type: 'staff' | 'client' | 'any';\n /** Setting for the level */\n readonly settings: HashMap;\n\n constructor(_data: HashMap = {}) {\n this.id = _data.id || _data.level_id || '';\n this.building_id = _data.bld_id || _data.building_id || '';\n this.name = _data.name || _data.level_name || '';\n const lower_name = this.name.toLowerCase();\n const num = lower_name.indexOf('level') >= 0 ? lower_name.replace(/ ?level ?/gi, '') : lower_name.substr(0, 1).toUpperCase();\n this.short_name = _data.short_name || num || '';\n this.map_url = _data.map_url || '';\n this.type = _data.type || _data.floor_type || 'any';\n this.settings = _data.settings;\n }\n}\n","import { BaseDataClass } from '../base-api.class';\nimport {\n getItemWithKeys,\n HashMap\n} from '@mckinsey-converge/base';\n\n/* istanbul ignore next */\n\nexport class Organisation extends BaseDataClass {\n /** List of available building zone ids for the organisation */\n public readonly available_buildings: readonly string[];\n /** Map of custom settings for the building */\n private _settings: HashMap;\n\n constructor(raw_data: HashMap = {}) {\n super(raw_data);\n this._settings = raw_data.settings || {};\n this.available_buildings = (raw_data.settings?.discovery_info?.buildings || []).map(\n (bld) => bld.zone_id\n );\n }\n\n /**\n * Get a custom building setting\n * @param key Name of the setting. i.e. nested items can be grabbed using `.` to seperate key names\n */\n public setting(key: string): any {\n const keys = key.split('.');\n const value = getItemWithKeys(keys, this._settings);\n return value;\n }\n}\n","import { Injectable } from '@angular/core';\n\nimport { ComposerService } from '@placeos/composer';\n\nimport { BaseAPIService } from '../base.service';\nimport { Organisation } from './organisation.class';\nimport { Building } from './building.class';\nimport {\n ApplicationLoadingState,\n HashMap,\n Identity\n} from '@mckinsey-converge/base';\nimport { BuildingLevel } from './level.class';\nimport { first } from 'rxjs/operators';\nimport { ApplicationService } from '../app.service';\nimport { ServiceManager } from '../service-manager.class';\nimport { UsersService } from '../users';\nimport { SettingsService } from '../settings.service';\nimport { Observable, Subject } from 'rxjs';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class OrganisationService extends BaseAPIService {\n /** Organisation data for the application */\n private _organisation: Organisation;\n /** Actively displayed building */\n private _active_building: string;\n\n private _spaceTypesSubject: Subject = new Subject();\n\n constructor(\n protected _composer: ComposerService,\n private _service: ApplicationService,\n private _users: UsersService,\n settingsService: SettingsService,\n ) {\n super(_composer, settingsService);\n ServiceManager.setService(Organisation, this);\n ServiceManager.setService(Building, this);\n this._name = 'Organisation';\n this._api_route = 'zones';\n this.set('buildings', []);\n this.set('active_building', null);\n this._users.initialised.pipe(first((_) => _)).subscribe(() => this.init());\n }\n\n /**\n * Add is not available on organisation service\n */\n public async add(form_data: HashMap, query_params?: HashMap): Promise {\n throw new Error('Add is not available on the organisation service');\n }\n\n /**\n * Update is not available on organisation service\n */\n public async update(\n id: string,\n form_data: HashMap,\n query_params?: HashMap\n ): Promise {\n throw new Error('Update is not available on the organisation service');\n }\n\n /**\n * Delete is not available on organisation service\n */\n public async delete(id: string): Promise {\n throw new Error('Delete is not available on the organisation service');\n }\n\n /**\n * Get list of levels for the given building ID\n * @param bld_id Building ID\n */\n public levels(bld_id: string): readonly BuildingLevel[] {\n return (this.buildings.find((i) => i.id === bld_id) || ({} as Building)).levels;\n }\n\n /**\n * Get a setting from the organisation or active building\n * @param key Name of the setting. i.e. nested items can be grabbed using `.` to seperate key names\n */\n public setting(key: string) {\n return this.building.setting(key) || this._organisation.setting(key);\n }\n\n /** Active building */\n public get building(): Building {\n return this.buildings.find((i) => i.id === this._active_building);\n }\n\n /** List of types of spaces */\n public get space_types(): Identity[] {\n return this.get('space_types') || [];\n }\n\n public set building(bld: Building) {\n if (bld instanceof Building) {\n this._active_building = bld.id;\n } else {\n this._active_building = bld;\n }\n this.set('active_building', this.building);\n this.loadSpaceTypes();\n localStorage.setItem('PlaceOS.building', this._active_building);\n localStorage.setItem('CONCIERGE.timezone', bld.timezone);\n }\n\n /** List of buildings for the organisation */\n public get buildings(): Building[] {\n return this.get('buildings') || [];\n }\n\n /**\n * Get list of available equipment\n * @param id ID of the building to get the list from. i.e. Defaults to the active building\n */\n public getExtras(id: string, bld_id?: string) {\n const bld = this.buildings.find((i) => i.id === bld_id) || this.building;\n if (bld && id) {\n return bld.extras.filter((i) => `${i.id}`.indexOf(id) >= 0);\n }\n return [];\n }\n\n /**\n * Get the first level matching the list of given IDs\n * @param ids List of ID to search with\n */\n public levelWithID(ids: string | string[]): BuildingLevel {\n const list = ids instanceof Array ? ids : [ids];\n const bld_list = this.buildings;\n for (const id of list) {\n for (const bld of bld_list) {\n for (const lvl of bld.levels) {\n if (lvl.id === id) {\n return lvl;\n }\n }\n }\n }\n return null;\n }\n\n /**\n * Initialise service data\n */\n protected async load(): Promise {\n /* istanbul ignore else */\n if (localStorage) {\n this._active_building = localStorage.getItem(`${this.settingsService.frontend.toUpperCase()}.building`);\n }\n const loading: ApplicationLoadingState = this._service.get('loading') || {};\n loading.organisation = { message: 'Loading organisation data', state: 'loading' };\n await this.loadOrganisation();\n loading.organisation = { message: 'Loading organisation data', state: 'complete' };\n loading.buildings = { message: 'Loading building data', state: 'loading' };\n this._service.set('loading', loading);\n await this.loadBuildings();\n loading.buildings = { message: 'Loading building data', state: 'complete' };\n loading.levels = { message: 'Loading building floor data', state: 'loading' };\n this._service.set('loading', loading);\n await this.loadLevels();\n loading.levels = { message: 'Loading building floor data', state: 'complete' };\n this._service.set('loading', loading);\n loading.space_types = { message: 'Loading space type data', state: 'loading' };\n this._service.set('loading', loading);\n await this.loadSpaceTypes();\n loading.space_types = { message: 'Loading space type data', state: 'complete' };\n this._service.set('loading', loading);\n const user = this._users.current;\n\n if (user) {\n const id = localStorage.getItem('PlaceOS.building');\n const building = this.buildings.find(bld => id && bld.id === id) || this.buildings.find((bld) => bld.code === user.location);\n if (building) {\n this._active_building = building.id;\n this.set('active_building', building);\n } else if(this.building) {\n // This conditional solves a problem of infinite loop when user building doesn't exist.\n this._active_building = this.building.id;\n this.set('active_building', this.building);\n }\n }\n }\n\n /**\n * Load organisation data for application\n */\n public async loadOrganisation(): Promise {\n const org_data = await this.query({ tags: 'org', engine: true });\n this._organisation = new Organisation(org_data[0]);\n this.set('organisation', this._organisation);\n return this._organisation;\n }\n\n /**\n * Load building data for the organisation already stored in the service..\n */\n public async loadBuildings(): Promise {\n return this.loadBuildingsWithOrg(this._organisation);\n }\n\n /**\n * Load building data for the organisation passed.\n */\n public async loadBuildingsWithOrg(organisation: Organisation): Promise {\n const bld_data = await this.query({ tags: 'building', engine: true, limit: 1000 });\n const buildings = (bld_data as HashMap[])\n .map((i) => new Building(i))\n .filter((bld) => organisation.available_buildings.includes(bld.id));\n this.set('buildings', buildings);\n /* istanbul ignore else */\n if (!this._active_building && buildings && buildings.length > 0) {\n this._active_building = buildings[0].id;\n }\n return buildings;\n }\n\n /**\n * Load level data for the buildings\n */\n public async loadLevels(): Promise {\n const lvl_data = await this.query({ tags: 'level', engine: true, limit: 1000 });\n const levels = (lvl_data as HashMap[]).map((i) => new BuildingLevel(i));\n this.set('levels', levels);\n return levels;\n }\n\n /**\n * Load space type data for the buildings\n */\n public async loadSpaceTypes(): Promise {\n const type_data = await this.query({ tags: 'room', engine: true, limit: 1000, building_id: this._active_building });\n const types = (type_data as HashMap[]).map((i) => ({ id: i.id, name: i.name }));\n this.set('space_types', types);\n // Emit the updated space_types\n this._spaceTypesSubject.next(types);\n }\n\n /**\n * Observable to listen for changes in space_types\n */\n public get spaceTypesObservable(): Observable {\n return this._spaceTypesSubject.asObservable();\n }\n\n public getOrganizationFiltersByRoomType(key: string) {\n if (!this._organisation) {\n return {}\n }\n const discovery_info = this._organisation.setting('discovery_info')\n\n return discovery_info && discovery_info[key] ? discovery_info[key] : {}\n }\n\n public getRoomTypeSubsetDDOptions(type:string) {\n const filters = this.getOrganizationFiltersByRoomType(type) || {}\n return [...Object.keys(filters).map((key) => {\n const display = filters[key].toString();\n return {\n display,\n value: key,\n meta: {}\n }\n }) ];\n }\n\n\n\n /**\n * The function will generate filter options, excluding London. London has its own set of options as per the request. \n * The filter options pertain to a subset of room type options managed by the room admin module.\n * @returns filterOptions: filter options for day view space type filter\n */\n public getRoomFilterOptionsPerLocation() {\n let internalSubSet = this.getRoomTypeSubsetDDOptions('internal_room_types');\n if(this.building.name.toLowerCase().includes('london')) {\n internalSubSet = internalSubSet.filter(e => !e.value.includes('meeting') && !e.value.includes('partner') );\n }\n const filterOptions = [];\n internalSubSet.forEach(el => filterOptions.push({name: el.display, id: el.value} ) ); \n return filterOptions\n }\n\n}\n","import { ComposerService } from '@placeos/composer';\nimport { BehaviorSubject, Observable, of, Subject, Subscriber } from 'rxjs';\n\nimport { BaseAPIService } from './base.service';\nimport { BaseClass, HashMap } from '@mckinsey-converge/base';\nimport { ApplicationService } from './app.service';\nimport { toQueryString } from './api.utilities';\nimport { SettingsService } from './settings.service';\nimport { catchError, map } from 'rxjs/operators';\n\nexport interface IEngineResponse {\n results: HashMap[];\n total: number;\n}\n\nexport class PaginatedAPIService extends BaseClass {\n /** Application service */\n public parent: ApplicationService;\n /** Display name of the service */\n protected _name: string;\n /** API Route of the service */\n protected _api_route: string;\n /** Map of state variables for Service */\n protected _subjects: { [key: string]: BehaviorSubject | Subject } = {};\n /** Map of observables for state variables */\n protected _observers: { [key: string]: Observable } = {};\n /** Map of poll subscribers for API endpoints */\n protected _subscribers: { [key: string]: Subscriber } = {};\n /** Map of promises for Service */\n protected _promises: { [key: string]: Promise } = {};\n /** Comparison function for service items */\n protected _compare: (a: T, b: T) => boolean = (a, b) =>\n a === b || (a as any).id === (b as any).id;\n /** Default filter function for list method */\n protected _list_filter: (a: T) => boolean = (a) => !!a;\n\n /** Http Client */\n protected get http() {\n return this._composer.http;\n }\n\n constructor(protected _composer: ComposerService,\n protected settingsService: SettingsService) {\n super();\n this._name = 'Base';\n this._api_route = 'base';\n this.set('list', []);\n }\n\n /**\n * Injects concierge into form_data.\n */\n private injectConcierge(form_data: HashMap) {\n // we only send it over when concierge, since the BE may check for presence rather than\n // if its true or not.\n if (this.settingsService.concierge) {\n return { ...form_data, concierge: true }\n }\n return form_data;\n }\n\n /**\n * Initailise service\n */\n public init() {\n this.load().then(\n (_) => this._initialised.next(true),\n (err) => this.timeout('init', () => this.init(), 1000)\n );\n }\n\n /**\n * Get API route for the service\n * @param engine Whether endpoint is using the application API or engine API\n */\n public route(engine: boolean = false) {\n const endpoint = engine\n ? this._composer.auth.api_endpoint\n : '/api/staff';\n return `${endpoint}/${this._api_route}`;\n }\n\n /** API Route of the service */\n public get api_route() {\n return this._api_route;\n }\n\n /**\n * Get the current value of the named property\n * @param name Property name\n */\n public get(name: string): U {\n if (!this._observers[name]) {\n this.set(name, null);\n }\n return (this._subjects[name] as BehaviorSubject).getValue();\n }\n\n /**\n * Listen to value change of the named property\n * @param name Property name\n * @param next Callback for value changes\n */\n public listen(name: string): Observable {\n if (!this._observers[name]) {\n this.set(name, null);\n }\n return this._observers[name];\n }\n\n /**\n * Update the value of the named property\n * @param name Property name\n * @param value New value\n */\n protected set(name: string, value: U): void {\n if (!this._subjects[name]) {\n this._subjects[name] = new BehaviorSubject(value);\n this._observers[name] = this._subjects[name].asObservable();\n } else {\n this._subjects[name].next(value);\n }\n }\n\n /**\n * Get list of loaded items\n * @param predicate Function for filtering the list\n */\n public filter(predicate: (a: T) => boolean = this._list_filter): T[] {\n const list: T[] = this.get('list');\n return list.filter(predicate);\n }\n\n /**\n * Get item with the given id from the loaded items\n * @param id ID of the item\n */\n public find(id: string): T {\n const list = this.get('list');\n return list.find((i) => i.id === id || (i.email?.toLowerCase() === id?.toLowerCase()));\n }\n\n /**\n * Query the index of the API route associated with this service\n * @param query_params Map of query paramaters to add to the request URL\n */\n public query(query_params: HashMap = {}, config: { url?: string } = {}): Promise {\n let engine = false;\n let cache = 1000;\n /* istanbul ignore else */\n if (query_params) {\n engine = !!query_params.engine;\n delete query_params.engine;\n cache = query_params.cache || 1000;\n delete query_params.cache;\n }\n const query = toQueryString(query_params);\n const key = `query|${query}`;\n if (!this._promises[key]) {\n this._promises[key] = new Promise((resolve, reject) => {\n const url = config && config.url ? `${config.url}${query ? '?' + query : ''}` : `${this.route(engine)}${query ? '?' + query : ''}`;\n let result: IEngineResponse;\n this.http.get(url).subscribe(\n (d: IEngineResponse | HashMap[]) => {\n if (d && d instanceof Array) {\n const results = d.map((i) => this.process(i));\n result = {\n results,\n total: results.length,\n }\n } else if (d && !(d instanceof Array) && d.results && d.total) {\n const results = d.results.map((i) => this.process(i));\n result = {\n results,\n total: d.total,\n }\n } else if (d && !(d instanceof Array) && d.results) {\n const results = d.results.map((i) => this.process(i));\n result = {\n results,\n total: results.length,\n }\n } else {\n result = {\n results: [],\n total: 0,\n }\n }\n },\n (e) => {\n reject(e);\n this._promises[key] = null;\n },\n () => {\n resolve(result);\n this.timeout(key, () => (this._promises[key] = null), cache);\n }\n );\n });\n }\n return this._promises[key];\n }\n\n /**\n * \n * @param query_params \n * @param config \n * @returns \n */\n queryHoldingBay(query_params: HashMap = {}, config: { url?: string } = {}):Observable {\n const query = toQueryString(query_params);\n const url = `${config.url}${query ? '?' + query : ''}`;\n return this.http.get(url).pipe(\n map((d: IEngineResponse | HashMap[]) => this.processApiResult(d) ),\n catchError((error: any, result?: T) => {\n console.log(error);\n return of(result as T);\n })\n );\n }\n\n /**\n * \n * @param d \n * @returns \n */\n processApiResult(d: IEngineResponse | HashMap[]): IEngineResponse{\n let result: IEngineResponse;\n if (d && d instanceof Array) {\n const results = d.map((i) => this.process(i));\n result = {\n results,\n total: results.length,\n }\n } else if (d && !(d instanceof Array) && d.results && d.total) {\n const results = d.results.map((i) => this.process(i));\n result = {\n results,\n total: d.total,\n }\n } else if (d && !(d instanceof Array) && d.results) {\n const results = d.results.map((i) => this.process(i));\n result = {\n results,\n total: results.length,\n }\n } else {\n result = {\n results: [],\n total: 0,\n }\n }\n return result;\n }\n \n\n /**\n * Query the API route for a sepecific item\n * @param id ID of the item\n * @param query_params Map of query paramaters to add to the request URL\n */\n public show(id: string, query_params: HashMap = {}): Promise {\n let engine = false;\n /* istanbul ignore else */\n if (query_params) {\n engine = !!query_params.engine;\n delete query_params.engine;\n }\n const query = toQueryString(query_params);\n const key = `show|${id}|${query}`;\n /* istanbul ignore else */\n if (!this._promises[key]) {\n this._promises[key] = new Promise((resolve, reject) => {\n const url = `${this.route(engine)}/${id}${query ? '?' + query : ''}`;\n let result: T = null;\n this.http.get(url).subscribe(\n (d) => {\n result = this.process(d); \n },\n (e) => {\n reject(e);\n this._promises.new_item = null;\n },\n () => {\n resolve(result);\n this.timeout(key, () => (this._promises[key] = null), 1000);\n }\n );\n });\n }\n return this._promises[key];\n }\n\n /**\n * Make post request for a new item to the service\n * @param form_data Data to post to the server\n * @param query_params Map of query paramaters to add to the request URL\n */\n public add(form_data: HashMap, query_params: HashMap = {}): Promise {\n /* istanbul ignore else */\n if (!this._promises.new_item) {\n this._promises.new_item = new Promise((resolve, reject) => {\n const query = toQueryString(query_params);\n const url = `${this.route(query_params.engine)}${query ? '?' + query : ''}`;\n let result: T = null;\n this.http.post(url, this.injectConcierge(form_data)).subscribe(\n (d) => (result = this.process(d)),\n (e) => {\n reject(e);\n this.analyticsEvent(`create-${this._name.toLowerCase()}-failed`);\n this._promises.new_item = null;\n },\n () => {\n resolve(result);\n this.set('list', this.updateList(this.get('list'), [result]));\n this.analyticsEvent(`create-${this._name.toLowerCase()}-success`);\n this._promises.new_item = null;\n }\n );\n });\n }\n return this._promises.new_item;\n }\n\n /**\n * Perform API task for the given item ID\n * @param id ID of the item\n * @param task_name Name of the task\n * @param form_data Map of data to pass to the API\n * @param method Verb to use for request\n */\n public task(\n id: string,\n task_name: string,\n form_data: HashMap = {},\n method: 'post' | 'get' = 'post'\n ): Promise {\n const query = toQueryString(this.injectConcierge(form_data));\n const key = `task|${id}|${task_name}|${query}`;\n /* istanbul ignore else */\n if (!this._promises[key]) {\n this._promises[key] = new Promise((resolve, reject) => {\n const post_data = { ...form_data, id, _task: task_name };\n const url = `${this.route(false)}/${id}/${task_name}`;\n let result: any;\n const request =\n method === 'post'\n ? this.http.post(url, post_data)\n : this.http.get(`${url}${query ? '?' + query : ''}`);\n request.subscribe(\n (d) => (result = d),\n (e) => {\n reject(e);\n this.analyticsEvent(\n `${this._name.toLowerCase()}-task-${task_name}-failed`,\n id\n );\n delete this._promises[key];\n },\n () => {\n resolve(result as U);\n this.analyticsEvent(\n `${this._name.toLowerCase()}-task-${task_name}-success`,\n id\n );\n this.timeout(key, () => delete this._promises[key], 1000);\n }\n );\n });\n }\n return this._promises[key];\n }\n\n\n /**\n * Make put request for changes to the item with the given id\n * @param id ID of the item being updated\n * @param form_data New values for the item\n * @param query_params Map of query paramaters to add to the request URL\n */\n public update(id: string, form_data: HashMap, query_params: HashMap = {}): Promise {\n const key = `update|${id}`;\n /* istanbul ignore else */\n if (!this._promises[key]) {\n this._promises[key] = new Promise((resolve, reject) => {\n const query = toQueryString(this.injectConcierge(query_params));\n const url = `${this.route(query_params.engine)}/${id}${query ? '?' + query : ''}`;\n let result: T = null;\n this.http.put(url, this.injectConcierge(form_data)).subscribe(\n (d) => (result = this.process(d)),\n (e) => {\n reject(e);\n this.analyticsEvent(`update-${this._name.toLowerCase()}-failed`, id);\n this._promises[key] = null;\n },\n () => {\n resolve(result);\n this.set(\n 'list',\n this.updateList(this.removeItem(this.get('list'), { id } as any), [\n result\n ])\n );\n this.analyticsEvent(`update-${this._name.toLowerCase()}-success`, id);\n this._promises[key] = null;\n }\n );\n });\n }\n return this._promises[key];\n }\n\n /**\n * Make delete request for the given item\n * @param id ID of item\n */\n public delete(id: string, q: HashMap = {}): Promise {\n const key = `delete|${id}`;\n /* istanbul ignore else */\n if (!this._promises[key]) {\n this._promises[key] = new Promise((resolve, reject) => {\n const query = toQueryString(q);\n const url = `${this.route()}/${id}${query ? '?' + query : ''}`;\n this.http.delete(url).subscribe(\n (_) => null,\n (e) => {\n reject(e);\n this._promises[key] = null;\n },\n () => {\n this.set('list', this.removeItem(this.get('list'), { id } as any));\n this._promises[key] = null;\n resolve();\n }\n );\n });\n }\n return this._promises[key];\n }\n\n /**\n * Load initial data for the service\n */\n protected async load(): Promise {\n }\n\n /**\n * Post analytics event for this service\n * @param action Name of the action to post\n */\n protected analyticsEvent(action: string, label?: string) {\n // if (this.parent && this.parent.Analytics) {\n // this.parent.Analytics.track(this._name, { desc: `${this.parent.name.toLowerCase()}-${action}`, label });\n // }\n }\n\n /**\n * Convert raw API data into a valid API Object\n * @param raw_item Raw API data\n */\n protected process(raw_item: HashMap): T {\n return raw_item as T;\n }\n\n /**\n * Update recorded list of items\n * @param old_list Old list of items\n * @param list List of updated items\n * @param compareFn Function to compare items to remove duplicates\n */\n public updateList(\n old_list: T[],\n list: T[],\n compareFn: (a: T, b: T) => boolean = this._compare\n ): T[] {\n /* istanbul ignore else */\n if (!list || list.length === 0) {\n return old_list;\n }\n const new_list: T[] = [];\n const mixed_list = [...list, ...old_list];\n /* istanbul ignore else */\n if (!compareFn) {\n compareFn = this._compare;\n }\n for (const item of mixed_list) {\n const found = new_list.find((i) => compareFn(i, item));\n /* istanbul ignore else */\n if (!found) {\n new_list.push(item);\n }\n }\n return new_list;\n }\n\n /**\n * Remove the given item from the given list\n * @param list List of items\n * @param item Item to remove\n * @param compareFn Function to compare items\n */\n protected removeItem(list: T[], item: T, compareFn?: (a: T, b: T) => boolean) {\n const new_list = [];\n /* istanbul ignore else */\n if (!compareFn) {\n compareFn = this._compare;\n }\n list.forEach((i) => (compareFn(item, i) ? null : new_list.push(i)));\n return new_list;\n }\n}\n","import { FormGroup } from \"@angular/forms\";\nimport { HashMap } from \"@mckinsey-converge/base\"\nimport { DateNow, DateTZ } from \"@mckinsey-converge/date-tz\"\n\nexport enum RecurrencePeriod {\n LIST = 'list',\n DAILY = 'daily',\n WEEKLY = 'weekly',\n //RELATIVE_MONTHLY = 'relativeMonthly', for the moment we will only support absoluteMonthly\n ABSOLUTE_MONTHLY = 'monthly'\n}\n\nexport enum DaysOfWeek {\n SUNDAY = 'sunday',\n MONDAY = 'monday',\n TUESDAY = 'tuesday',\n WEDNESDAY = 'wednesday',\n THURSDAY = 'thursday',\n FRIDAY = 'friday',\n SATURDAY = 'saturday'\n\n}\n\nexport enum SeriesAction {\n EDIT = 'edit',\n CLONE = 'clone'\n}\n\nexport enum BookingAction {\n CLONE = 'booking_clone',\n EDIT = 'booking_edit'\n}\n\nexport interface WeekDays {\n id: string;\n active: boolean;\n day_index: number;\n full_name: string;\n}\n\nexport interface RepeatPeriod {\n id: RecurrencePeriod;\n label: string\n}\n\nexport enum RepeatsOn {\n PATTERN = 'pattern',\n DATE = 'date'\n}\n\nexport interface RecurrenceType {\n type: RepeatsOn;\n label: string;\n}\n\nexport interface RecurrenceDetails {\n period: RecurrencePeriod,\n end: DateTZ,\n list: Array\n interval: number\n}\n\nexport interface RecurrenceRecurringDetails extends RecurrenceDetails {\n start: number\n}\n\nexport const recurrencePeriodToDurationType = (period: RecurrencePeriod) =>{\n switch (period) {\n case RecurrencePeriod.DAILY:\n return 'days'\n case RecurrencePeriod.WEEKLY:\n return 'weeks' \n case RecurrencePeriod.ABSOLUTE_MONTHLY:\n return 'months' \n default:\n return 'days'\n }\n}\n\n/**\n * Generate weekdays from monday to saturday\n */\nexport const generateWeekDays = (date: number, params: { building_tz?: string } = {}): Array=> {\n const { building_tz } = params;\n let start_week = DateNow(new Date()).startOfValue('week').addValue({days : 1}); //set monday as start of week\n const week_days: Array = []\n for (let i = 1; i <= 7; i++) {\n const activeDate = new DateTZ({date, building_tz, is_local_tz: false })\n const active = activeDate.dateWeekday === i\n const full_name = start_week.formatDate('EEEE')\n const day_index = i === 7 ? 0 : i //Sunday must be indexed as day 0\n week_days.push({id: full_name.substring(0, 1), active, day_index, full_name: full_name.toLocaleLowerCase()});\n start_week = start_week.addValue({ days : 1 });\n }\n return week_days\n}\n\nexport const getFirstDateFromList = (list: Array) => {\n return list.reduce((a, b) => { return a.isBeforeDate(b) ? a : b; });\n}\n\nexport const getLastDateFromList = (list: Array) => {\n return list.reduce((a, b) => { return a.isAfterDate(b) ? a : b; });\n}\n\nexport const recurrenceDetails = (recurr: RecurrenceDetails): string => {\n const end = getLastDateFromList(recurr.list).formatDate('ccc dd MMM yyyy')\n const getPeriod = (period: string) => {\n if(recurr.interval === 1){\n return `Occurs Every ${period} until ${end}`\n }else{\n return `Occurs Every ${recurr.interval} ${period}s until ${end}`\n }\n } \n \n switch (recurr.period) {\n case RecurrencePeriod.DAILY:\n return getPeriod('Day')\n case RecurrencePeriod.WEEKLY:\n return getPeriod('Week')\n case RecurrencePeriod.ABSOLUTE_MONTHLY:\n return getPeriod('Month')\n case RecurrencePeriod.LIST:\n return `Occurs on Specific Dates until ${getLastDateFromList(recurr.list).formatDate('ccc dd MMM yyyy')}`\n default:\n return ''\n }\n}\n\nexport const recurrenceRecurringDetails = (recurr: RecurrenceRecurringDetails): string => {\n const end = recurr.end\n const start = new DateTZ({ date: recurr.start })\n\n const getPeriod = (period: string) => {\n if(recurr.interval === 1){\n return `occurs every ${period} effective ${start.formatDate('dd MMM yyyy')} until ${end.formatDate('dd MMM yyyy')}`\n }else{\n return `occurs every ${recurr.interval} ${period}s effective ${start.formatDate('dd MMM yyyy')} until ${end.formatDate('dd MMM yyyy')}`\n }\n }\n\n switch (recurr.period) {\n case RecurrencePeriod.DAILY:\n return getPeriod('Day')\n case RecurrencePeriod.WEEKLY:\n return getPeriod('Week')\n case RecurrencePeriod.ABSOLUTE_MONTHLY:\n return getPeriod('Month')\n case RecurrencePeriod.LIST:\n return `occurs on Specific Dates`\n default:\n return ''\n }\n}\n\nexport const handleRecurrenceFields = (data: HashMap) => {\n if(data.is_recurrent){\n if(data.recurrence_period !== RecurrencePeriod.LIST){\n delete data.recurrence_starts\n delete data.recurrence_endTz\n if(data.recurrence_period !== RecurrencePeriod.WEEKLY){\n delete data.recurrence_days\n }\n } else if(data.recurrence_period){\n data.recurrence_list = [...(data.recurrence_starts || []).map((date: DateTZ) => date.startOfValue('minute').seconds)]\n delete data.recurrence_interval;\n delete data.recurrence_endTz;\n delete data.recurrence_count;\n delete data.recurrence_starts\n delete data.recurrence_days\n }\n delete data.is_recurrent\n }else{\n delete data.recurrence_period;\n delete data.recurrence_interval;\n delete data.recurrence_endTz;\n delete data.recurrence_count;\n delete data.recurrence_starts\n delete data.is_recurrent\n delete data.recurrence_exceptions\n delete data.recurrence_days\n }\n\n return data\n}\n\n/** Removes expired occurrences and updates start and end dates when cloning a series */\nexport const removeExpiredOcurrences = (form: FormGroup) =>{\n if(form.controls.action?.value === SeriesAction.CLONE ){\n const tz = form.controls.space_list.value[0].timezone\n const date_now = new DateTZ({ is_local_tz: false, building_tz: tz });\n const occurrences = form.controls.recurrence_starts.value.filter((date: DateTZ) =>\n date_now.isBeforeDate(date, 'day') || date_now.isSameDate(date, 'day')\n )\n form.controls.recurrence_starts.setValue(occurrences)\n const { start, end } = setStartAndEndDateFromArray(occurrences, tz)\n form.controls.dateTz.setValue(start)\n form.controls.recurrence_endTz.setValue(end)\n }\n}\n\n/** \n * Returns starts and end dates from an array of dates in milliseconds \n **/\n export const setStartAndEndDateFromArray = (dates: Array, tz: string): {start: DateTZ, end: DateTZ} =>{\n if(dates.length !== 0){\n return {\n start: dates.sort((d1, d2) => d1.ms - d2.ms)[0] ,\n end: dates.sort((d1, d2) => d2.ms - d1.ms)[0]\n }\n }\n //if the dates array is empty set the start and end dates as the current and next day\n const now = DateNow(new Date()).toZone(tz)\n return {start: now, end: now.addValue({days: 1})}\n}\n\nexport const setRecurrenceStartsTime = (form: FormGroup) => {\n const { recurrence_starts, dateTz } = form.value\n const list = (recurrence_starts || []).map((date: DateTZ) => date.setValue({hour: dateTz.dateHour, minute: dateTz.minutes}))\n form.controls.recurrence_starts.setValue(list)\n}","export * from './report.class'\nexport * from './reports.service'\n","import { CurrencyPipe } from '@angular/common';\n\nimport {\n csvToJson,\n downloadFile,\n HashMap,\n humaniseDuration,\n jsonToCsv,\n} from '@mckinsey-converge/base';\n\nimport { ServiceManager } from '../service-manager.class';\nimport { Space } from '../spaces';\nimport { Organisation } from '../organisation';\n\nexport class Report {\n /** Type of report */\n public readonly type: string;\n /** List of data associated with the report */\n public readonly data: T[];\n\n constructor(raw_data: HashMap = {}) {\n this.type = raw_data.type || '';\n this.data = this.cleanData(raw_data.data);\n }\n\n /** Create report data structure from CSV */\n public static fromCSV(type: string, data: string): Report {\n const csv_json = csvToJson(data);\n return new Report({ type, data: csv_json });\n }\n\n /** Download report data as CSV format */\n public downloadCSV(name: string = 'unnamed.csv'): void {\n downloadFile(name, jsonToCsv(this.data));\n }\n\n /** Download report data as JSON format */\n public downloadJSON(name: string = 'unnamed.json'): void {\n downloadFile(name, JSON.stringify(this.data, undefined, 4));\n }\n\n public formatCancelledBy(email: string) {\n if (email !== null && email !== undefined) {\n let name = email.split('@')[0];\n name = name.split('_').join(' '); //get name from email and convert it to title case\n name = name.replace(/\\w\\S*/g, function (txt) {\n return `${txt\n .charAt(0)\n .toUpperCase()}${txt.substr(1).toLowerCase()}`;\n });\n return name;\n } else {\n return email;\n }\n }\n\n public timeConverter( UNIX_timestamp: number ){\n if(!UNIX_timestamp) return '';\n let a = new Date(UNIX_timestamp * 1000);\n let months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];\n let year = a.getFullYear();\n let month = months[a.getMonth()];\n let date = a.getDate();\n let hour = ((a.getHours() + 11) % 12 + 1);\n let suffix = a.getHours() >= 12 ? \"PM\":\"AM\";\n let min = a.getMinutes() === 0 ? '00':a.getMinutes();\n let time = date + ' ' + month + ' ' + year + ' - ' + hour + ':' + min + ' ' + suffix;\n return time;\n }\n\n private cleanData(data: T[] = []) {\n if (data.length > 0 && this.type === 'day') {\n for (let i = 0; i < data.length; i++) {\n data[i]['booker'] = data[i]['booker'].name;\n data[i]['expected_attendees'] = Object.values(data[i]['expected_attendees'])[0];\n }\n } else if (data.length > 0 && this.type === 'audit') {\n for (let i = 0; i < data.length; i++) {\n data[i]['created_at'] = this.timeConverter(data[i]['created_at']);\n }\n } else if (data.length > 0 && this.type === 'catering') {\n for (let i = 0; i < data.length; i++) {\n data[i]['Cancelled By'] = this.formatCancelledBy(\n data[i]['Cancelled By']\n );\n }\n const fields = Object.keys(data[0]);\n const room_field = fields.find(\n (key) =>\n key.toLowerCase().includes('room') &&\n key.toLowerCase().includes('email')\n );\n const price_field = fields.find((key) =>\n key.toLowerCase().includes('price')\n );\n\n for (let row of data) {\n /* istanbul ignore else */\n const space_service = ServiceManager.serviceFor(Space);\n if (room_field && price_field && space_service) {\n const room = space_service.find(\n row[room_field].toLowerCase()\n );\n const org_service = ServiceManager.serviceFor(Organisation);\n const bld = org_service.buildings.find(\n (bld: { id: any }) =>\n bld.id === room?.level?.building_id\n );\n row[price_field] = new CurrencyPipe('en_us').transform(\n row[price_field] / 100,\n bld?.currency\n );\n }\n /* istanbul ignore else */\n for (let field of fields) {\n row[field] =\n typeof row[field] === 'string'\n ? row[field].replace(/\\,/g, '٫')\n : row[field];\n }\n }\n } else if (this.type === 'bookings') {\n data = data.map((i: HashMap) => {\n const booking = i;\n try {\n if (booking.setup instanceof Object) {\n booking.setup = booking.setup[booking.room_email];\n }\n if (booking.breakdown instanceof Object) {\n booking.breakdown =\n booking.breakdown[booking.room_email];\n }\n booking.setup = humaniseDuration((booking.setup || 0) / 60);\n booking.breakdown = humaniseDuration(\n (booking.breakdown || 0) / 60\n );\n booking.description = (booking.description || '')\n .replace(/<[^>]*>?/gm, '')\n .replace(/\\,/g, '٫')\n .replace(/\\r?\\n|\\r/g, ' ');\n booking['Meeting Host'] =\n booking.organizer?.name || booking.organizer;\n booking['Booked By'] =\n booking.booked_by?.name ||\n booking.booked_by?.email ||\n '';\n booking.charge_code =\n booking.equipment_codes[booking.room_email] || '';\n // booking.expected_attendees = Object.keys(booking.expected_attendees).map(key => booking.expected_attendees[key]).join(', ');\n booking.attendees = (booking.attendees || [])\n .map((person) => person.name || person.email || person)\n .join('٫ ');\n booking.notes = (booking.notes || [])\n .map((note) =>\n note.author &&\n (note.type === 'description' ||\n note.type === 'private')\n ? `[${note.author}|${note.type}]${note.message\n .replace(/<[^>]*>?/gm, '')\n .replace(/\\,/g, '٫')\n .replace(/\\r?\\n|\\r/g, ' ')}`\n : ''\n )\n .join(' | ');\n booking.cancelled = booking.isCancelled;\n booking.status = booking.status[booking.room_email] || '';\n booking.company = booking.company.join(', ');\n if (booking.cancelled_by) {\n booking.cancelled_by = this.formatCancelledBy(\n booking.cancelled_by\n );\n }\n booking.cancelled_date = this.timeConverter(booking.cancelled_at_epoch);\n\n } catch (e) {}\n const remove_fields = [\n 'id',\n 'icaluid',\n 'accepted_at',\n 'accepted_by',\n 'check_ins',\n 'changeKey',\n 'created',\n 'booking_type',\n 'edit_history',\n 'end_epoch',\n 'old_attendees',\n 'start_epoch',\n 'expected_attendees',\n 'isAllDay',\n 'isCancelled',\n 'body',\n 'is_free',\n 'lastModifiedDateTime',\n 'locationType',\n 'locations',\n 'organizer',\n 'booked_by',\n 'originalEndTimeZone',\n 'originalStartTimeZone',\n 'room_booking_status',\n 'room_email',\n 'room_emails',\n 'room_id',\n 'sensitivity',\n 'seriesMasterId',\n 'show_as',\n 'notes',\n 'subject',\n 'type',\n 'previous_booking',\n 'catering',\n 'responseStatus',\n 'equipment_codes',\n 'cancelled_at_epoch',\n ];\n for (const field of remove_fields) {\n if (booking[field] !== undefined) {\n delete booking[field];\n }\n }\n const output = {};\n const order = [\n 'title',\n 'start',\n 'end',\n 'location',\n 'Meeting Host',\n 'Booked By',\n 'booked_at',\n 'booked_ahead_by',\n 'all_day',\n 'attendees',\n 'company',\n 'description',\n 'cancelled',\n 'cancelled_by',\n 'cancelled_date',\n ];\n const keys = Object.keys(booking);\n keys.sort((a, b) => {\n const idx_a = order.indexOf(a);\n const idx_b = order.indexOf(b);\n return (\n (idx_a === -1 ? keys.length : idx_a) -\n (idx_b === -1 ? keys.length : idx_b)\n );\n });\n for (const key of keys) {\n output[key] = booking[key];\n }\n return output;\n }) as any;\n }\n return data;\n }\n}\n","import { Injectable } from '@angular/core';\n\nimport { ComposerService } from '@placeos/composer';\n\nimport { Report } from './report.class';\nimport { BaseAPIService } from '../base.service';\nimport { SettingsService } from '../settings.service';\n\n@Injectable({\n providedIn: 'root',\n})\nexport class ReportsService extends BaseAPIService {\n constructor(protected _composer: ComposerService,\n settingsService: SettingsService) {\n super(_composer, settingsService);\n this._name = 'Reports';\n this._api_route = 'reports';\n }\n\n protected process(raw_data: any): Report {\n return raw_data instanceof Array\n ? new Report({ data: raw_data })\n : Report.fromCSV('', raw_data);\n }\n}\n","import { Type } from '@angular/core';\nimport { Observable } from 'rxjs';\n\nimport { HashMap } from '@mckinsey-converge/base';\n\n\nexport interface ServiceLike extends HashMap {\n parent: any;\n add: (_: HashMap) => Promise;\n update: (id: string, _: HashMap) => Promise;\n delete: (id: string, params?: HashMap) => Promise;\n task: (id: string, name: string, data: HashMap) => Promise;\n listen: (prop: string) => Observable;\n}\n\nexport interface ServiceProvider {\n provideFor: Type;\n useValue: T;\n}\n\nexport class ServiceManager {\n /** Map of available services for child classes */\n private static _service_list: ServiceProvider[] = [];\n\n /** Set the services used to handle data model requests */\n public static setService(type: Type, service: any): void {\n if (window.debug) {\n (window as any).ServiceManager = this._service_list;\n }\n const index = ServiceManager._service_list.findIndex(provider => provider.provideFor === type);\n if (index >= 0) {\n ServiceManager._service_list.splice(index, 1, { provideFor: type, useValue: service });\n } else {\n ServiceManager._service_list.push({ provideFor: type, useValue: service });\n }\n }\n\n /** Get the services used to handle data model requests */\n public static serviceFor(type: Type): ServiceLike {\n const provider = ServiceManager._service_list.find(provider => provider.provideFor === type) || { useValue: null };\n return provider.useValue;\n }\n\n constructor() {\n throw new Error('ServiceMananger is static class');\n }\n}\n","import {\n Inject,\n Injectable\n} from '@angular/core';\nimport { Title } from '@angular/platform-browser';\nimport {\n BehaviorSubject,\n Observable\n} from 'rxjs';\n\nimport { VERSION } from '@mckinsey-converge/environment';\nimport {\n BaseClass,\n getItemWithKeys,\n HashMap,\n log\n} from '@mckinsey-converge/base';\nimport { ServiceManager } from './service-manager.class';\nimport { DateNow, DateTZ } from '@mckinsey-converge/date-tz';\n\ndeclare global {\n interface Window {\n debug: boolean;\n }\n}\n\nexport const SETTINGS_TOKEN = 'settings_service_data';\n\ntype SettingsAppData = T & {\n title: string;\n description: string;\n short_name: string;\n logo_light: {\n type: string;\n src: string;\n background: string;\n }\n logo_dark: {\n type: string;\n src: string;\n background: string;\n }\n heap_io: {\n app_id: number;\n force_ssl: boolean;\n secure_cookie: boolean;\n disable_text_capture: boolean;\n cookie_path: string;\n }\n}\n\nexport interface SettingsData {\n debug: boolean;\n mock: boolean;\n frontend: 'staff' | 'concierge' | 'booking';\n composer: {\n domain: string;\n route: string;\n protocol: string;\n port: string;\n use_domain: boolean;\n local_login: boolean;\n }\n app: SettingsAppData;\n}\n\nexport class SettingsObject {\n constructor(public data: SettingsData) {\n }\n}\n\n@Injectable({\n providedIn: 'root'\n})\nexport class SettingsService extends BaseClass {\n /** Name of the application */\n private _app_name = 'Converge';\n /** List of override settings in order of priority */\n private _overrides = new BehaviorSubject([]);\n /** Mapping of behaviour subjects */\n private _subjects: HashMap> = {};\n /** Mapping of observables */\n private _observables: HashMap> = {};\n /** Which frontend we're in. staff, concierge, or bookings **/\n public readonly frontend: string;\n private _bypass_bookings: boolean = false;\n\n /**\n * @hidden\n */\n public set overrides(value: HashMap[]) {\n this._overrides.next(value);\n }\n\n /** Get observable for key */\n public listen(name: string): Observable {\n if (!this._observables[name]) {\n this._subjects[name] = new BehaviorSubject(null);\n this._observables[name] = this._subjects[name].asObservable();\n }\n return this._observables[name];\n }\n\n /** Update observable value for key */\n public post(name: string, value: T): void {\n if (!this._observables[name]) {\n this._subjects[name] = new BehaviorSubject(null);\n this._observables[name] = this._subjects[name].asObservable();\n }\n this._subjects[name].next(value);\n }\n\n public value(name: string): T {\n return !this._observables[name] ? null : this._subjects[name].getValue();\n }\n\n /** Page title */\n public get title() {\n return this._title.getTitle();\n }\n\n public set title(value: string) {\n this._title.setTitle(`${value} | ${this._app_name}`);\n }\n\n constructor(private _title: Title,\n @Inject(SETTINGS_TOKEN)\n private settings: SettingsObject) {\n super();\n ServiceManager.setService(SettingsService, this);\n const time = new DateTZ({date: VERSION.time});\n const built = DateNow(new Date()).isSameDate(time, 'day')\n ? `Today at ${time.formatDate('h:mma')}`\n : time.formatDate('do MMM yyyy, h:mma');\n const frontend = this.get('frontend') || 'Unknown';\n this.frontend = frontend;\n log('CORE', `${VERSION.semver}`, null, 'debug', true, frontend?.toUpperCase());\n log('APP', `${VERSION.hash} | Built: ${built}`, null, 'debug', true, frontend?.toUpperCase());\n this.init();\n }\n\n /**\n * Initialise the settings\n */\n public async init() {\n if (this.get('debug')) {\n window.debug = true;\n }\n if (this.get('app')?.name) {\n this._app_name = this.get('app').name;\n }\n log('Settings', 'Successfully loaded settings');\n this._initialised.next(true);\n }\n\n /** Whether settings service has initialised */\n public get app_name() {\n return this._app_name;\n }\n\n public get concierge() {\n return this.frontend === 'concierge';\n }\n\n public set bypass_bookings(value: boolean) {\n this._bypass_bookings = value;\n }\n\n public get bypass_bookings() {\n return this._bypass_bookings;\n }\n\n /**\n * Get a setting\n * @param key Name of the setting. i.e. nested items can be grabbed using `.` to seperate key names\n */\n public get(key: string): any {\n const keys = key.split('.');\n if (keys[0] !== 'app') {\n return getItemWithKeys(keys, this.settings.data);\n }\n const override_settings = this._overrides.getValue();\n for (const override of override_settings) {\n const value = getItemWithKeys(keys.slice(1), override);\n if (value != null) {\n return value;\n }\n }\n return getItemWithKeys(keys, this.settings.data);\n }\n}\n","export * from './space.class';\nexport * from './spaces.service';\nexport * from './space.utilities';\n","import { BaseDataClass } from '../base-api.class';\nimport {\n HashMap,\n} from '@mckinsey-converge/base';\nimport {\n Building,\n BuildingLevel,\n Organisation\n} from '../organisation';\nimport { User } from '../users';\nimport { ServiceManager } from '../service-manager.class';\nimport { SettingsService } from '../settings.service';\nimport { SpaceRules } from '../bookings/booking.types';\nimport { rulesForSpace } from '../bookings/space.utilities';\nimport { DateTZ } from '@mckinsey-converge/date-tz';\nimport { DaysOfWeek, RecurrencePeriod } from '../recurrence/recurrence.utils';\nimport { DateTime } from 'luxon';\n\nexport interface ISpaceAvailabilityOptions {\n /** Start date and time of the availability block */\n dateTz?: DateTZ;\n /** Length of the availability block */\n duration: number;\n /** */\n id?: string;\n /** List of spaces to look at the availability for */\n room_ids?: string;\n /** Whether the spaces looked at should be bookable */\n bookable?: boolean;\n /** List of spaces or zones to ignore */\n ignore?: string;\n /** List of zones to look at the availability for */\n zone_ids?: string | string[];\n /** Whether space bookings should not be returned */\n hide_bookings?: boolean;\n /** Min capacity on the room **/\n capacity?: number\n clear?: boolean;\n /** Length of the setup */\n setup?: number;\n /** Length of the breakdown */\n breakdown?: number;\n /** Hide declined bookings */\n hide_declined?: boolean;\n /** Max capacity on the room **/\n capacity_max?: number\n\n diff_capacity?: boolean\n\n /** Fields to include recurrence in the availability search */\n recurrence_period?: RecurrencePeriod;\n recurrence_interval?: number;\n recurrence_endTz?: DateTZ;\n recurrence_count?: number;\n recurrence_starts?: Array\n is_recurrent?: boolean\n is_multiroom?: boolean\n merged?: boolean\n recurrence_days?: Array\n}\n\nexport interface SpaceBookingRuleOptions {\n dateTz: DateTZ;\n duration: number;\n host: User;\n}\n\nexport const OPTION_DEFAULTS: ISpaceAvailabilityOptions = {\n duration: 60\n};\n\nexport interface RecurrenceAvailability {\n /** Unix epoch in seconds */\n readonly date: number;\n /** Whetehr space is available at this time */\n readonly available: boolean;\n}\n\n/**\n * Room Settings Type\n *\n * Exmaple:\n * available: true\n * available_until: 1644344999\n * bookings: []\n * catering: false\n * charge_code: false\n * extra_features: \"meeting_room boardroom\"\n * internal_or_external: \"internal\"\n * map_id: \"08.8.15\"\n * natural_light: false\n * room_booking_screen: true\n * room_name: \"8.15 Meeting\"\n * vc: false\n * recurrence_availability: {\n * date: \"1642596300\",\n * available: false,\n * }\n */\nexport interface RoomSettings {\n // Local room name is required.\n room_name?: string\n // TBD if anything else should be required\n available?: boolean\n available_until?: number\n bookings?: HashMap[]\n catering?: boolean\n charge_code?: boolean\n extra_features?: string\n internal_or_external?: string\n map_id?: string\n natural_light?: boolean\n room_booking_screen?: boolean\n vc?: boolean\n recurrence_availability?: RecurrenceAvailability[]\n equipment_code?: boolean\n external?: boolean\n}\n\n/**\n * List of space features that come from settings.\n *\n * Im sure this list will grow. It is not the most comprehensive list.\n */\nexport enum SpaceFeatures {\n BoardRoom = 'boardroom',\n Catering = 'catering',\n CocktailRoom = 'cocktail',\n ConferencePhone = 'conference_phone',\n ConferenceRoom = 'conference',\n ElectronicWhiteboard = 'whiteboard',\n FlipChart = 'flipchart',\n Glassboard = 'glassboard',\n PartnerOffice = 'partner',\n PhoneBooth = 'phone_booth',\n TheatreRoom = 'theatre',\n TeamRoom = 'team_room',\n VideoConference = 'vc',\n NaturalLight = 'natural_light',\n WirelessContentSharing = 'wireless_content_sharing',\n WorkshopRoom = 'workshop',\n UShapeRoom = 'u_shape',\n Miscellaneous = 'miscellaneous'\n}\n\nexport enum SpaceEquipments {\n conference_phone,\n whiteboard,\n vc,\n flipchart,\n glassboard,\n wireless_content_sharing\n}\nexport enum SpaceStyles {\n boardroom,\n cocktail,\n conference,\n partner,\n theatre,\n team_room,\n // natural_light,\n workshop,\n u_shape,\n miscellaneous\n}\n\nconst EMPTY_ARRAY = [];\n\nexport class Space extends BaseDataClass {\n /** Whether space can be booked by users */\n public readonly bookable: boolean;\n /** People capacity of the space */\n public readonly capacity: number;\n /** Index to force order when sorting multiple spaces */\n public readonly sort_priority: number;\n /** Settings has extensive info displayed in the app. */\n public readonly settings: RoomSettings;\n /** URL for the control interface of the space */\n public readonly support_url: string;\n /** Engine zones associated with the space */\n public readonly zones: readonly string[];\n /** Is the Application concierge */\n public is_concierge: boolean = false;\n /** To allow bookings with rooms for staff calendar view only */\n public allowStaffRoomWithBookings: boolean = false;\n\n /** Simple name == local name being using in seeting.room_name\n\t\t *\n\t\t*/\n public simple_name: string;\n\n /** Room style set of all room style one room has\n * [\"boardroom\", \"cocktail\", \"miscellaneous\", \"natural_light\", \"theatre\", \"u-shape\", \"workshop\"] */\n public room_style: string[] = [];\n\n /** Equipment set of all room equipment one room has\n * [\"conference_phone\", \"whiteboard\", \"vc\", \"flipchart\", \"glassboard\", \"whiteboard\", \"wireless_content_sharing\"] */\n public equipment: string[] = [];\n\n /** Filters has charge code settings for booking and catering */\n public filters: RoomSettings;\n\n /** room_type subset of internal or external rooms */\n public room_type: string[] | string;\n /**\n * Features used in identifying room types.\n *\n * Both features and extra_features are space delimited strings.\n *\n * There is a getter featuresArray that returns the\n * combination of the two as an array.\n */\n public readonly features: string;\n public readonly extra_features: string;\n public map_id: string;\n\n\n /** Service for managing spaces */\n protected get _service() {\n return ServiceManager.serviceFor(Building);\n }\n\n /** Return the rooms' building */\n public get building() {\n return this._service?.buildings.find((bld) => {\n return this.zones.includes(bld.id);\n });\n }\n\n /** Return building timezone */\n public get timezone() {\n return this.building?.timezone;\n }\n\n constructor(raw_data: HashMap = {}) {\n super(raw_data);\n\n // Needed to check if the current app is Concierge or Staff\n const settingsService = ServiceManager.serviceFor(SettingsService) as unknown as SettingsService;\n this.is_concierge = !!settingsService?.concierge;\n this.allowStaffRoomWithBookings = settingsService?.bypass_bookings || false;\n\n const defaultGlobalName = raw_data.name || raw_data.email || 'Meeting Room';\n const defaultSettings = {\n room_name: defaultGlobalName\n }\n /**\n * Duplicate of base-api.class just for readability\n * .name is the full, mckinsey standardised name of the room.\n * You can think of it was what people would call the room when\n * considering every office and room globally.\n *\n * this.name = raw_data.name;\n */\n /**\n * Settings has extensive info displayed in the app.\n * setting.room_name is always used when displaying the room name in the applications\n * setting.room_name is the more office-specific room name, you can think of this as\n * the one people in that office would refer to the room as.\n *\n */\n this.settings = raw_data.settings || defaultSettings;\n this.filters = raw_data?.filters;\n // Room fields\n this.bookable = raw_data.bookable || false;\n this.capacity = raw_data.capacity || 0;\n this.support_url = raw_data.support_url;\n this.zones = raw_data.zones instanceof Array ? raw_data.zones : [];\n // Feature fields\n this.features = raw_data.features || '';\n this.extra_features = raw_data.settings?.extra_features || '';\n\n\t\tthis.room_type = raw_data?.room_type;\n this.simple_name = raw_data?.simple_name;\n this.map_id = raw_data?.map_id || raw_data.settings?.map_id || '';\n this.room_style = raw_data?.room_style || [];\n this.equipment = raw_data?.equipment || [];\n\n }\n\n /**\n * Return the global room name\n * IE LDN-4-408-06\n */\n public get global_name(): string {\n return this.name || ''\n }\n\n /**\n * Return the local room name\n * i.e. 4.08\n * TO DO: ROOM-DATA-STRUCTURE-REFACTOR - stop using/remove seetings.room_name\n */\n public get local_name(): string {\n return this.simple_name || this.name || ''; // this.settings?.room_name\n }\n\n public get street_and_city(): string {\n return this.building ? `${this.building.address}, ${this.building.city}` : '';\n }\n\n /**\n * Return the map_id from settings\n * TO DO: ROOM-DATA-STRUCTURE-REFACTOR - stop using/remove seetings.map_id and use map_id from space modal( after testing over prod)\n */\n // public get map_id(): string {\n // return this?.map_id || this.settings?.map_id || '';\n // }\n\n /** Internal / External status */\n public get internal_or_external(): string {\n const external = this.filters?.external;\n return external ? 'external' : 'internal'; // ( external ? 'external' : this.settings?.internal_or_external ) || 'internal';\n }\n\n /**\n * Works in conjunction with available_until.\n *\n * If a room is bookable that only means you can make a booking but not when.\n * Available indicates that it is available to be booked durring the queried times.\n */\n public get available(): boolean {\n return this.settings?.available ?? false;\n }\n\n public get currently_in_use(): boolean {\n const nowMs = new DateTZ({ date: DateTime.now(), is_local_tz: false, building_tz: this.timezone }).ms;\n const runningBookings = this.settings?.bookings?.filter( bks => {\n const startEpoch = new DateTZ({ date: bks.start_epoch * 1000, is_local_tz: false, building_tz: bks.timezone}).ms;\n const endEpoch = new DateTZ({ date: bks.end_epoch * 1000, is_local_tz: false, building_tz: bks.timezone}).ms;\n return startEpoch <= nowMs && endEpoch >= nowMs;\n })\n // console.log(raw_data.settings.room_name+' : '+ nowMs.ms);\n // console.log('IS IN USER CURRENTLY : ', isinUsernow);\n return !!runningBookings.length;\n }\n\n /** Last returned availability time */\n public get available_until(): number {\n return this.settings?.available_until;\n }\n\n /** Bookings associated with the space */\n public get bookings(): HashMap[] {\n // We should need to care if a room as bookings in Staff\n if (!this.is_concierge && !this.allowStaffRoomWithBookings) {\n return EMPTY_ARRAY;\n }\n\n if (this.settings?.bookings?.length) {\n /**\n * When we're dealing with Concierge the API data arriving is rooms, and those rooms contain booking,\n * but those bookings don't contain a room since the room is the root.\n * In order for Concierge to filter bookings by room we need to reattach this room\n * to each of the bookings, with it's own bookings.\n *\n * It's loopy and kooky but correcting for this pattern is beyond the scope of this refactor.\n */\n const spaceWithBookings = new Space(this.toJSON());\n return this.settings?.bookings.map(b => {\n b.room = spaceWithBookings;\n return b;\n });\n }\n return EMPTY_ARRAY;\n }\n\n /**\n\t\t * Whether space has catering\n * TO DO: ROOM-DATA-STRUCTURE-REFACTOR - stop using/remove seetings.catering and use filters?.catering instead\n\t\t */\n public get has_catering(): boolean {\n return this.filters?.catering || this.settings?.catering || false;\n }\n\n /** Returns an array of the space delimited feature and extra_feature strings.\n * TO DO: ROOM-DATA-STRUCTURE-REFACTOR - stop using/remove features, extra_features\n */\n public get featuresArray(): (SpaceFeatures | string)[] {\n // (this.simple_name === \"Entresol M-06\" ) && console.log('raw_data : ', this)\n const roomType = Array.isArray(this.room_type) ? this.room_type : [this.room_type]\n const features = [\n ...this.features.split(' '),\n ...this.extra_features?.split(' '),\n\n ...roomType, // required to work with staff>result>filters\n ...this?.room_style,\n ...this?.equipment,\n\n (this.filters?.natural_light) && SpaceFeatures.NaturalLight || undefined, // || this.settings?.natural_light\n // this.settings?.natural_light && SpaceFeatures.NaturalLight || undefined,\n // this.settings?.vc && SpaceFeatures.VideoConference || undefined,\n ].filter(f => !!f).map(f => f.toLowerCase());\n //will be an array... new Set() turns it into a set, but [... ] turns it back into an array again\n return [ ...new Set(features) ];\n }\n\n /** Level in which the space is associated */\n public get level(): BuildingLevel {\n const service = ServiceManager.serviceFor(Organisation);\n return (service ? service.levelWithID(this.zones as any) : null) || new BuildingLevel({});\n }\n\n /**\n * Return the new recurrence_availability from settings\n */\n public get recurrence_availability(): RecurrenceAvailability[] {\n return this.settings?.recurrence_availability || [];\n }\n\n public get availableOccurrences(): number {\n if(this?.recurrence_availability) {\n return this?.recurrence_availability?.filter(recurr => recurr.available).length;\n }\n }\n\n public get totalOccurrences(): number {\n return this?.recurrence_availability.length;\n }\n\n /**\n * Make a copy of this object\n */\n public clone(): Space {\n return new Space(this);\n }\n\n /**\n * Make a copy of this object without identification data\n */\n public duplicate(): Space {\n const space = { ...this };\n space.settings.bookings = []; // clear out bookings\n return new Space({ ...space, id: null, email: null });\n }\n\n /**\n * Generate the booking rules for space with given options\n * @param options Conditions for generating the space rules\n */\n public rulesFor(options: SpaceBookingRuleOptions): SpaceRules {\n if (!this._service || !this.level) {\n return { auto_approve: true, hide: false };\n }\n\n const building = this.is_concierge ? this.level.building_id : this.building\n\n if (!building) {\n return { auto_approve: true, hide: false };\n }\n const { dateTz, duration, host } = options;\n const rules: SpaceRules = rulesForSpace({\n time: dateTz.ms,\n duration,\n space: this,\n user: host,\n rules: building.booking_rules\n });\n return rules;\n }\n\n /**\n * Whether space can only be booked by request\n * @param options Conditions for checking the space rules\n */\n public byRequest(options: SpaceBookingRuleOptions) {\n const rules = this.rulesFor(options);\n return !rules.auto_approve;\n }\n\n /**\n * Convert object into plain object\n */\n public toJSON(this: Space): HashMap {\n return { ...super.toJSON(), settings: { ...this.settings, bookings: [] }, filters: { ...this.filters }, equipment: [ ...this.equipment] };\n }\n}\n","import { HashMap } from '@mckinsey-converge/base';\nimport {\n ISpaceAvailabilityOptions,\n Space\n} from './space.class';\nimport { Building } from '../organisation/building.class';\nimport { DateTZ } from '@mckinsey-converge/date-tz';\nimport { handleRecurrenceFields, RecurrencePeriod } from '../recurrence/recurrence.utils';\n\nexport function availabilityOptionsToQuery(options: ISpaceAvailabilityOptions): HashMap {\n let query: HashMap = {};\n if (options) {\n query = { ...options };\n if (options.dateTz) {\n const date = options.dateTz.startOfValue('minute');\n query.available_from = date.seconds; // Add one second or API will not allow end to end booking\n query.available_to = date.addValue({ minutes: options.duration || 60 }).subtractValue({ seconds: 1 }).seconds;\n\n if (options.setup) query.setup = options.setup ;\n if (options.breakdown) query.breakdown = options.breakdown;\n \n delete query.dateTz;\n delete query.duration;\n }\n\n if (!options.capacity_max) delete query.capacity_max\n\n if (options.hide_declined) query.hide_declined = options.hide_declined;\n query = handleRecurrenceFields(query)\n }\n return query;\n}\n\n/**\n * Compare two spaces to determine order\n * @param first\n * @param second\n */\nexport function sort(first: Space, second: Space, blds: Building[] = []) {\n const bld = blds.find(bld => first.zones.includes(bld.id));\n const bld_b = blds.find(bld => second.zones.includes(bld.id));\n if (bld) {\n if (bld !== bld_b) {\n return (bld.name).localeCompare(bld_b?.name);\n }\n const sort_order = [...bld.sort_order].reverse();\n for (const zone_id of sort_order) {\n if (zone_id === '*') {\n continue;\n }\n const a_has_zone = first.zones.indexOf(zone_id) >= 0;\n const b_has_zone = second.zones.indexOf(zone_id) >= 0;\n if (a_has_zone && !b_has_zone) {\n return 1;\n } else if (b_has_zone && !a_has_zone) {\n return -1;\n }\n }\n }\n return first.name.localeCompare(second.name);\n}\n","import { Injectable } from '@angular/core';\nimport { ComposerService } from '@placeos/composer';\nimport { first } from 'rxjs/operators';\n\nimport { BaseAPIService } from '../base.service';\nimport { ISpaceAvailabilityOptions, Space } from './space.class';\nimport { ApplicationLoadingState, HashMap } from '@mckinsey-converge/base';\nimport { availabilityOptionsToQuery } from './space.utilities';\nimport { ServiceManager } from '../service-manager.class';\nimport { OrganisationService } from '../organisation';\nimport { ApplicationService } from '../app.service';\nimport { SettingsService } from '../settings.service';\nimport { DateNow } from '@mckinsey-converge/date-tz';\nimport { Observable } from 'rxjs';\nimport { HttpClient } from '@angular/common/http';\n\nlet SPACE_LIST = [];\n\nexport function findSpace(id: string) {\n return SPACE_LIST.find((_) => _.id === id || _.email === id);\n}\n\n@Injectable({\n providedIn: 'root',\n})\nexport class SpacesService extends BaseAPIService {\n constructor(\n protected _composer: ComposerService,\n private _org: OrganisationService,\n private _service: ApplicationService,\n settingsService: SettingsService\n ) {\n \n super(_composer, settingsService);\n ServiceManager.setService(Space, this);\n this._name = 'Space';\n this._api_route = 'rooms';\n this._compare = (a, b) =>\n !a.id.localeCompare(b.id) || !a.email.localeCompare(b.email);\n this._list_filter = (a: Space) => {\n const bld = this._org.building;\n return a.level.building_id === bld.id;\n };\n this._org.initialised\n .pipe(first((_) => _))\n .subscribe(() => this.init());\n }\n\n public async query(query: HashMap = {}, setList:boolean = false) {\n const list = await super.query(query);\n if ( (query.hasOwnProperty('cache') && query.cache === false) || !Object.keys(query).length || setList) {\n this.set('list', list);\n SPACE_LIST = list;\n }\n return list;\n }\n\n public filterAvailableSpaces(list: Space[]) {\n return list.filter((i) => {\n if (i.recurrence_availability?.length) return i; //if recurrence, send the space regardless of availability (needed for edit and cloning series)\n return i.available;\n });\n }\n\n /**\n * Get available spaces\n * @param options\n */\n public available(\n options: ISpaceAvailabilityOptions,\n uniqueId?: string,\n filter: boolean = true\n ): Promise {\n if (!options) {\n throw new Error('Space avilability requires request options');\n }\n\n const now = DateNow(new Date());\n if (!options.dateTz) {\n options.dateTz = now.startOfValue('minute');\n }\n\n if (options.hide_declined === undefined) {\n options.hide_declined = true;\n }\n\n const key = `available|${options.id ? options.id : ''}|${\n uniqueId ?? ''\n }`;\n if (!this._promises[key]) {\n this._promises[key] = new Promise((resolve, reject) => {\n const respond = (list: Space[]) => {\n delete this._promises[key];\n resolve(filter ? this.filterAvailableSpaces(list) : list);\n };\n const error = (e) => {\n reject(e);\n delete this._promises[key];\n };\n const query = availabilityOptionsToQuery(options);\n if (options.id) {\n this.show(options.id, query).then(\n (i) => respond([i]),\n error\n );\n } else {\n this.query(query).then(respond, error);\n }\n });\n }\n\n return this._promises[key];\n }\n\n /**\n * Load initial data for the service\n */\n protected async load(): Promise {\n const loading: ApplicationLoadingState =\n this._service.get('loading') || {};\n if (!loading.spaces) {\n loading.spaces = {\n message: 'Loading space data',\n state: 'loading',\n };\n this._service.set('loading', loading);\n }\n // Adjusted the query by incorporating the \"zone_ids\" option to filter rooms based on specific zone IDs instead of considering all thousands of rooms.\n const option = {\n zone_ids: this._org.building.id\n }\n await this.query(option, true).catch(() => {\n loading.spaces = { message: 'Loading space data', state: 'failed' };\n this._service.set('loading', loading);\n });\n loading.spaces = { message: 'Loading space data', state: 'complete' };\n this._service.set('loading', loading);\n }\n\n /**\n * Convert raw data into API object\n * @param raw_data Raw API data\n */\n public process(raw_data: HashMap): Space {\n return new Space(raw_data);\n }\n /**\n * \n * Special observable function to make http call for rooms\n * @param query \n * @returns observable\n */\n public queryRooms = (query: HashMap = {}): Observable => super.queryRoomsForQR(query);\n\n public setSpaceList(list) {\n this.set('list', list);\n SPACE_LIST = list;\n }\n\n public queryBooking = (query: HashMap = {}) : Observable | Observable => super.queryObsr(query);\n public updateSpace = (id: string, form_data: HashMap, should_inject_concierge: Boolean = false, query_params: HashMap = {}) : Observable | Observable => super.updateObsr(id, form_data, should_inject_concierge, query_params);\n public uploadSpacePhotos = (apiSubRoute: string, form_data: HashMap, should_inject_concierge: Boolean = false, query_params: HashMap = {}) : Observable | Observable => super.uploadSpacePhotos(apiSubRoute, form_data, should_inject_concierge, query_params);\n \n public updateRoomList() {\n this.load().then((_) => null)\n }\n}\n","import {\n BehaviorSubject,\n of\n} from 'rxjs';\nimport {\n Building,\n Organisation\n} from './organisation';\nimport { BaseDataClass } from './base-api.class';\nimport { MapLocation } from './location';\nimport { User } from './users';\nimport { Booking } from './bookings';\nimport {\n RoomSettings,\n Space,\n SpaceFeatures\n} from './spaces';\nimport { ServiceManager } from './service-manager.class';\nimport {\n CateringCategory,\n CateringItem\n} from './catering';\nimport {\n HashMap,\n padZero,\n predictableRandomInt,\n unique\n} from '@mckinsey-converge/base';\n\nimport * as faker from 'faker';\n\nimport {\n SETTINGS_TOKEN,\n SettingsData,\n SettingsObject,\n SettingsService\n} from './settings.service';\nimport { Report } from './reports';\nimport { DateNow, DateTZ } from '@mckinsey-converge/date-tz';\n\nfaker.seed(2560);\n\nlet SERVICE: any;\n\ndeclare global {\n interface Jest {\n fn: () => any\n }\n}\n\n\ndeclare let jest: Jest;\n\n/* istanbul ignore file */\n\nexport function generateMockOrganisationService(): HashMap {\n return {\n levelWithID: jest.fn(),\n listen: jest.fn(),\n loadOrganisation: jest.fn(),\n initialised: of(true),\n building: new Building(generateMockBuilding({ id: 'bld-01' })),\n loadBuildingsWithOrg: jest.fn(),\n getOrganizationFiltersByRoomType: jest.fn(),\n getRoomTypeSubsetDDOptions: jest.fn(),\n getRoomFilterOptionsPerLocation: jest.fn(),\n ...generateMockDataService('OrganisationService')\n };\n}\n\nexport const generateMockSpacesService = (): HashMap => ({\n available: jest.fn(),\n ...generateMockDataService('SpacesService')\n});\n\nexport const generateMockUsersService = (): HashMap => ({\n loadCurrentUser: jest.fn(),\n ...generateMockDataService('UsersService')\n});\n\nexport const bookingState = () => ({\n bookings: of([]),\n filtered: of([]),\n filtered_week: of([]),\n filtered_month: of([]),\n setZone: jest.fn(),\n setDate: jest.fn(),\n setFilters: jest.fn(),\n add: jest.fn(),\n replace: jest.fn(),\n remove: jest.fn(),\n startPolling: jest.fn(),\n startPollingMonth: jest.fn(),\n startPollingWeek: jest.fn(),\n startPollingEveryFiveMin: jest.fn(),\n _noOfMeetings: of([])\n});\n\nconst test_app = {\n title: 'McKinsey & Company',\n description: 'McKinsey & Company Staff UI written with Angular Framework',\n short_name: 'STAFF',\n logo_light: {\n type: 'img',\n src: 'assets/img/logo.svg',\n background: ''\n },\n logo_dark: {\n type: 'img',\n src: 'assets/img/logo-inverse.svg',\n background: ''\n },\n heap_io: {\n app_id: 3540602199,\n force_ssl: true,\n secure_cookie: true,\n disable_text_capture: true,\n cookie_path: '/staff/'\n }\n};\n\nexport const TEST_SETTINGS: SettingsData = {\n debug: true,\n composer: {\n domain: '',\n route: '/test',\n protocol: '',\n port: '',\n use_domain: false,\n local_login: false\n },\n app: test_app,\n mock: false,\n frontend: 'staff'\n};\n\nexport const generateMockSettingsService = (overrides: Partial> = {}) => new SettingsService({ getTitle: () => jest.fn() } as any,\n new SettingsObject({\n ...TEST_SETTINGS,\n ...overrides\n }));\n\n/**\n * Provides an injectable instance for settings.\n */\nexport const provideMockSettingsObject = (settings: Partial