+
-
+
+
+
+
+
+
+
+ Caterer is required
+
+
+
1"
+ class="hidden sm:block px-2 py-2"
+ >
+
+
+
+
+ {{ caterer }}
+
+
+
+
Options
@@ -167,13 +188,14 @@ export class CateringItemFiltersComponent
public readonly setFilters = (f) => this._state.setFilters(f);
public readonly categories = this._state.categories;
+ public readonly caterers = this._state.caterers;
public readonly exact_tooltip =
'Deliver at exactly specified time. \nNote that changes to the booking will not be \nreflected in the order if this is set.';
public get start_of_date() {
return startOfDay(
- addDays(this._state.getFilters().date, this.offset_day)
+ addDays(this._state.getFilters().date, this.offset_day),
).valueOf();
}
@@ -188,12 +210,12 @@ export class CateringItemFiltersComponent
public get max_offset() {
const end = Math.min(
endOfDay(
- addDays(this._state.getFilters().date, this.offset_day)
+ addDays(this._state.getFilters().date, this.offset_day),
).valueOf(),
addMinutes(
this._state.getFilters().date,
- this._state.getFilters().duration
- ).valueOf()
+ this._state.getFilters().duration,
+ ).valueOf(),
);
const diff = differenceInMinutes(end, this._state.getFilters().date);
return Math.min(diff, Math.min(24 * 60 - 1, this._max_offset));
@@ -207,7 +229,7 @@ export class CateringItemFiltersComponent
constructor(
private _state: CateringOrderStateService,
- private _settings: SettingsService
+ private _settings: SettingsService,
) {
super();
}
@@ -215,7 +237,7 @@ export class CateringItemFiltersComponent
public ngOnInit() {
this._min_offset = Math.max(
this._settings.get('app.catering.min_offset'),
- 0
+ 0,
);
this.subscription(
'filters',
@@ -223,10 +245,10 @@ export class CateringItemFiltersComponent
this._max_offset = Math.max(
15,
(this._state.getFilters().duration || 60) -
- this._settings.get('app.catering.end_offset')
+ this._settings.get('app.catering.end_offset'),
);
this._updateDayOptions();
- })
+ }),
);
this._updateDayOptions();
}
diff --git a/libs/catering/src/lib/catering-order-modal/catering-order-state.service.ts b/libs/catering/src/lib/catering-order-modal/catering-order-state.service.ts
index 4d854bf164..c375f65fa1 100644
--- a/libs/catering/src/lib/catering-order-modal/catering-order-state.service.ts
+++ b/libs/catering/src/lib/catering-order-modal/catering-order-state.service.ts
@@ -5,6 +5,7 @@ import { PlaceMetadata, showMetadata } from '@placeos/ts-client';
import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
import {
catchError,
+ debounceTime,
filter,
map,
shareReplay,
@@ -30,6 +31,7 @@ export interface CateringOrderFilters {
search: string;
tags: string[];
categories: string[];
+ caterer?: string;
}
@Injectable({
@@ -41,6 +43,7 @@ export class CateringOrderStateService {
search: '',
tags: [],
categories: [],
+ caterer: '',
});
private _loading = new BehaviorSubject('');
@@ -51,21 +54,21 @@ export class CateringOrderStateService {
filter((_) => !!_),
switchMap((_) =>
showMetadata(_.id, 'catering-settings').pipe(
- catchError((_) => of({} as PlaceMetadata))
- )
+ catchError((_) => of({} as PlaceMetadata)),
+ ),
),
map((_) => _.details as CateringSettings),
tap((_) =>
- this._settings.post('require_catering_notes', !!_?.require_notes)
+ this._settings.post('require_catering_notes', !!_?.require_notes),
),
- shareReplay(1)
+ shareReplay(1),
);
public readonly charge_codes = this.settings.pipe(
- map((_) => _.charge_codes || [])
+ map((_) => _.charge_codes || []),
);
public readonly availability = this.settings.pipe(
- map((_) => _.disabled_rooms || [])
+ map((_) => _.disabled_rooms || []),
);
public readonly available_menu: Observable
= combineLatest([
@@ -74,28 +77,50 @@ export class CateringOrderStateService {
]).pipe(
filter(([_, bld]) => !!bld),
switchMap(([{ zone }, bld]) => {
- this._loading.next('[Menu]');
+ this._loading.next('[MENU]');
return showMetadata(zone || bld.id, 'catering').pipe(
map((d) =>
(d.details instanceof Array ? d.details : []).map(
- (_) => new CateringItem(_)
- )
+ (_) => new CateringItem(_),
+ ),
),
- catchError((_) => [])
+ catchError((_) => []),
);
}),
- tap((_) => this._loading.next('')),
- shareReplay(1)
+ tap((items) => {
+ this._loading.next(this._loading.getValue().replace('[MENU]', ''));
+ if (this._settings.get('app.catering_provider')) {
+ this.setFilters({
+ caterer: this._settings.get('app.catering_provider'),
+ });
+ } else {
+ const caterer_list = unique(
+ items.map((i) => i.caterer).filter((_) => !!_),
+ );
+ if (caterer_list.length <= 1) return;
+ this.setFilters({ caterer: caterer_list[0] || '' });
+ }
+ }),
+ shareReplay(1),
);
public readonly categories = this.available_menu.pipe(
- map((_) => unique(_.map((i) => i.category)))
+ map((_) => unique(_.map((i) => i.category))),
+ );
+
+ public readonly caterers = this.available_menu.pipe(
+ map((_) => {
+ return this._settings.get('app.catering_provider')
+ ? []
+ : unique(_.map((i) => i.caterer));
+ }),
);
public readonly filtered_menu = combineLatest([
this._filters,
this.available_menu,
]).pipe(
+ debounceTime(300),
switchMap(
async ([
{
@@ -106,12 +131,12 @@ export class CateringOrderStateService {
date,
duration,
resources,
+ caterer,
},
l,
]) => {
- const rules = await getCateringRulesForZone(
- zone_id
- ).toPromise();
+ const rules =
+ await getCateringRulesForZone(zone_id).toPromise();
search = search.toLowerCase();
let list = search
? l.filter((_) => _.name.toLowerCase().includes(search))
@@ -122,17 +147,20 @@ export class CateringOrderStateService {
list = categories.length
? list.filter((_) => categories.includes(_.category))
: list;
+ list = caterer
+ ? list.filter((_) => _.caterer === caterer)
+ : list;
list = list.filter((_) =>
cateringItemAvailable(_, rules, {
date,
duration,
resources,
- } as any)
+ } as any),
);
return list;
- }
+ },
),
- shareReplay(1)
+ shareReplay(1),
);
public get currency_code() {
@@ -141,7 +169,7 @@ export class CateringOrderStateService {
constructor(
private _org: OrganisationService,
- private _settings: SettingsService
+ private _settings: SettingsService,
) {}
public setOptions(opts: Partial) {
diff --git a/libs/catering/src/lib/catering-order-modal/new-catering-order-modal.component.ts b/libs/catering/src/lib/catering-order-modal/new-catering-order-modal.component.ts
index c01c5e1c62..f4bbf0f1b5 100644
--- a/libs/catering/src/lib/catering-order-modal/new-catering-order-modal.component.ts
+++ b/libs/catering/src/lib/catering-order-modal/new-catering-order-modal.component.ts
@@ -177,18 +177,22 @@ export class NewCateringOrderModalComponent {
exact_time?: boolean;
offset?: number;
offset_day?: number;
- }
+ caterer?: string;
+ },
) {
const { duration } = this._data.details;
this._order.setFilters(this._data.details);
this.offset = Math.min(
Math.max(
this._settings.get('app.catering.min_offset'),
- this._data.offset || 0
+ this._data.offset || 0,
),
- (duration || 60) - this._settings.get('app.catering.end_offset')
+ (duration || 60) - this._settings.get('app.catering.end_offset'),
);
this.offset_day = this._data.offset_day || 0;
+ if (this._data.caterer) {
+ this._order.setFilters({ caterer: this._data.caterer });
+ }
}
public isSelected(id: string) {
@@ -197,7 +201,9 @@ export class NewCateringOrderModalComponent {
public setSelected(item: CateringItem, state: boolean) {
const list = this.selected.filter(
- (_) => _.custom_id !== item.custom_id
+ (_) =>
+ _.custom_id !== item.custom_id &&
+ (!item.caterer || item.caterer === _.caterer),
);
if (state) {
const new_item = new CateringItem({ ...item, in_order: true });
@@ -218,7 +224,7 @@ export class NewCateringOrderModalComponent {
} else {
this._settings.saveUserSetting(
'favourite_menu_items',
- fav_list.filter((_) => _ !== item.id)
+ fav_list.filter((_) => _ !== item.id),
);
}
}
diff --git a/libs/catering/src/lib/catering-order.class.ts b/libs/catering/src/lib/catering-order.class.ts
index 627f739e96..541f4ad1a0 100644
--- a/libs/catering/src/lib/catering-order.class.ts
+++ b/libs/catering/src/lib/catering-order.class.ts
@@ -44,6 +44,8 @@ export class CateringOrder {
public readonly deliver_time?: number;
/** Notes associated with the order */
public readonly notes: string;
+ /** Caterer associated with the order */
+ public readonly caterer: string;
/** Event associated with the order */
public readonly event: CalendarEvent | null;
public readonly deliver_at_time: number;
@@ -68,16 +70,20 @@ export class CateringOrder {
this.id = data.id || `order-${randomInt(9_999_999, 1_000_000)}`;
this.system_id = data.system_id || '';
this.event_id = data.event_id || data.event?.id || '';
+ this.caterer = data.caterer || '';
this.items = (data.items || []).map((i) =>
- i instanceof CateringItem ? i : new CateringItem(i)
+ i instanceof CateringItem ? i : new CateringItem(i),
+ );
+ this.items = this.items.filter(
+ (i) => i.quantity > 0 && this.caterer === i.caterer,
);
this.item_count = this.items.reduce(
(amount, item) => amount + item.quantity,
- 0
+ 0,
);
this.total_cost = this.items.reduce(
(amount, item) => amount + (item.total_cost || 0),
- 0
+ 0,
);
this.charge_code = data.charge_code || '';
this.status =
diff --git a/libs/catering/src/lib/catering-orders.service.ts b/libs/catering/src/lib/catering-orders.service.ts
index ab66f2fcd1..a5f46a6724 100644
--- a/libs/catering/src/lib/catering-orders.service.ts
+++ b/libs/catering/src/lib/catering-orders.service.ts
@@ -10,7 +10,13 @@ import {
} from 'rxjs/operators';
import { startOfDay, endOfDay, getUnixTime, format } from 'date-fns';
-import { AsyncHandler, currentUser, flatten } from '@placeos/common';
+import {
+ AsyncHandler,
+ currentUser,
+ flatten,
+ SettingsService,
+ unique,
+} from '@placeos/common';
import {
queryEvents,
saveEvent,
@@ -28,19 +34,21 @@ export interface CateringOrderFilters {
zones?: string[];
/** Search string to filter orders on */
search?: string;
+ /** Caterer to filter orders on */
+ caterer?: string;
}
function checkOrder(
order: CateringOrder,
- filters: CateringOrderFilters
+ filters: CateringOrderFilters,
): boolean {
const s = (filters.search || '').toLowerCase();
return !!order.items.find(
(item) =>
item.name.toLowerCase().includes(s) ||
!!item.options.find((option) =>
- option.name.toLowerCase().includes(s)
- )
+ option.name.toLowerCase().includes(s),
+ ),
);
}
@@ -50,14 +58,16 @@ function checkOrder(
export class CateringOrdersService extends AsyncHandler {
private _poll = new BehaviorSubject(0);
private _loading = new BehaviorSubject(false);
- private _filters = new BehaviorSubject({});
+ private _filters = new BehaviorSubject({
+ caterer: '',
+ });
/** Observable for list of orders */
public readonly orders: Observable = combineLatest([
this._filters,
this._poll,
]).pipe(
- debounceTime(1000),
+ debounceTime(300),
switchMap(([{ date, zones }]) => {
this._loading.next(true);
const start = getUnixTime(startOfDay(date || Date.now()));
@@ -72,25 +82,56 @@ export class CateringOrdersService extends AsyncHandler {
flatten(
events.map((event) =>
event.valid_catering.map(
- (o) => new CateringOrder({ ...o, event })
- )
- )
- )
+ (o) => new CateringOrder({ ...o, event }),
+ ),
+ ),
+ ),
),
map((orders) =>
orders.filter(
(o) =>
format(o.deliver_at, 'yyyy-MM-dd') ===
- format(start * 1000, 'yyyy-MM-dd')
- )
- )
+ format(start * 1000, 'yyyy-MM-dd'),
+ ),
+ ),
);
}),
tap(() => this._loading.next(false)),
- shareReplay(1)
+ shareReplay(1),
);
/** Observable for loading status of orders */
public readonly loading = this._loading.asObservable();
+
+ public readonly order_filters = this._filters.asObservable();
+
+ public readonly caterers = this.orders.pipe(
+ map((_) => {
+ const provider_groups =
+ this._settings.get('app.catering_provider_groups') || {};
+ let provider_list = Object.keys(provider_groups);
+ const is_admin =
+ currentUser().groups.includes('placeos_admin') ||
+ currentUser().groups.includes('placeos_support');
+ if (!provider_list.length || is_admin)
+ return unique(_.map((i) => i.caterer));
+ provider_list = provider_list.filter((caterer) =>
+ provider_groups[caterer].find((group) =>
+ currentUser().groups.includes(group),
+ ),
+ );
+ if (
+ provider_list.length <= 1 &&
+ this._filters.getValue()?.caterer !== provider_list[0]
+ ) {
+ this._filters.next({
+ ...this._filters.getValue(),
+ caterer: provider_list[0],
+ });
+ }
+ return unique(provider_list);
+ }),
+ shareReplay(1),
+ );
/** Order filters */
public get filters() {
return this._filters.getValue();
@@ -104,11 +145,11 @@ export class CateringOrdersService extends AsyncHandler {
map((list) =>
list
.filter((order) => checkOrder(order, this._filters.getValue()))
- .sort((a, b) => a.deliver_at - b.deliver_at)
- )
+ .sort((a, b) => a.deliver_at - b.deliver_at),
+ ),
);
- constructor() {
+ constructor(private _settings: SettingsService) {
super();
this.subscription('changes', this.orders.subscribe());
}
@@ -118,7 +159,7 @@ export class CateringOrdersService extends AsyncHandler {
this.interval(
'polling',
() => this._poll.next(new Date().valueOf()),
- delay
+ delay,
);
}
@@ -134,7 +175,7 @@ export class CateringOrdersService extends AsyncHandler {
*/
public async updateStatus(
order: CateringOrder,
- status: CateringOrderStatus
+ status: CateringOrderStatus,
) {
order.status = status;
const updated_order = new CateringOrder({
@@ -144,7 +185,7 @@ export class CateringOrdersService extends AsyncHandler {
});
const catering = [
...(order.event.extension_data.catering || []).filter(
- (o) => o.id !== order.id
+ (o) => o.id !== order.id,
),
updated_order,
].map((i) => new CateringOrder({ ...i }));
@@ -156,7 +197,7 @@ export class CateringOrdersService extends AsyncHandler {
const booking = await updateEventMetadata(
event.id,
system_id,
- event.extension_data
+ event.extension_data,
).toPromise();
this.timeout('refresh-list', () => this._poll.next(Date.now()), 1000);
(order as any).status = status;
diff --git a/libs/catering/src/lib/catering-state.service.ts b/libs/catering/src/lib/catering-state.service.ts
index a8fe02307a..836a9d67f8 100644
--- a/libs/catering/src/lib/catering-state.service.ts
+++ b/libs/catering/src/lib/catering-state.service.ts
@@ -19,6 +19,7 @@ import {
import {
AsyncHandler,
+ currentUser,
flatten,
notifyError,
notifySuccess,
@@ -53,6 +54,7 @@ import {
CateringOrderOptionsModalData,
} from './catering-order-options-modal.component';
import { CateringImportMenuModalComponent } from './catering-import-menu-modal.component';
+import { CateringOrdersService } from './catering-orders.service';
export interface CateringSettings {
require_notes?: boolean;
@@ -87,21 +89,42 @@ export class CateringStateService extends AsyncHandler {
filter(([_]) => !!_),
switchMap(([_]) =>
showMetadata(_.id, 'catering-settings').pipe(
- catchError((_) => of({} as PlaceMetadata))
- )
+ catchError((_) => of({} as PlaceMetadata)),
+ ),
),
map((_) => (_.details as CateringSettings) || {}),
tap((_) =>
- this._settings.post('require_catering_notes', !!_?.require_notes)
+ this._settings.post('require_catering_notes', !!_?.require_notes),
),
- shareReplay(1)
+ shareReplay(1),
);
public readonly charge_codes = this.settings.pipe(
- map((_) => _.charge_codes || [])
+ map((_) => _.charge_codes || []),
);
public readonly availability = this.settings.pipe(
- map((_) => _.disabled_rooms || [])
+ map((_) => _.disabled_rooms || []),
+ );
+
+ public readonly caterers = combineLatest([
+ this._menu,
+ this._orders.caterers,
+ ]).pipe(
+ map(([menu_items]) => {
+ const provider_groups =
+ this._settings.get('app.catering_provider_groups') || {};
+ let provider_list = Object.keys(provider_groups);
+ if (!provider_list.length) {
+ return unique(menu_items.map((i) => i.caterer));
+ }
+ provider_list = provider_list.filter((caterer) =>
+ provider_groups[caterer].find((group) =>
+ currentUser().groups.includes(group),
+ ),
+ );
+ return unique(provider_list);
+ }),
+ shareReplay(1),
);
public zone = '';
@@ -115,27 +138,37 @@ export class CateringStateService extends AsyncHandler {
return unique(menu.map((i) => i.category));
}
+ public get caterer_list() {
+ const menu = this._menu.getValue();
+ return unique(menu.map((i) => i.caterer));
+ }
+
constructor(
private _org: OrganisationService,
private _dialog: MatDialog,
- private _settings: SettingsService
+ private _settings: SettingsService,
+ private _orders: CateringOrdersService,
) {
super();
this.subscription(
'building',
this._org.active_building.subscribe(async (bld: Building) => {
if (bld) {
- const menu = (await this.getCateringForZone(bld.id)).map(
- (i) => new CateringItem(i)
- );
+ this._loading.next(true);
+ this._menu.next([]);
+ const menu = (
+ await this.getCateringForZone(bld.id).catch((_) => [])
+ ).map((i) => new CateringItem(i));
this._currency.next(
this._settings.get('app.currency') ||
bld.currency ||
- 'USD'
+ 'USD',
);
- this._menu.next(menu);
+ this._loading.next(false);
+
+ this.timeout('loaded', () => this._menu.next(menu), 1000);
}
- })
+ }),
);
}
@@ -175,6 +208,7 @@ export class CateringStateService extends AsyncHandler {
data: {
item,
categories: this.categories,
+ caterers: this.caterer_list,
},
});
const details = await Promise.race([
@@ -196,7 +230,7 @@ export class CateringStateService extends AsyncHandler {
this._menu.next([...menu]);
ref.close();
},
- () => (ref.componentInstance.loading = false)
+ () => (ref.componentInstance.loading = false),
);
}
@@ -206,13 +240,13 @@ export class CateringStateService extends AsyncHandler {
if (index >= 0) menu.splice(index, 1, item);
else menu.push(item);
this.updateMenu(this._org.building.id, menu).then(() =>
- this._menu.next([...menu])
+ this._menu.next([...menu]),
);
}
public async addOption(
item: CateringItem,
- option: CateringOption = {} as any
+ option: CateringOption = {} as any,
) {
const types = unique(item.options.map((i) => i.group));
const ref = this._dialog.open<
@@ -244,7 +278,7 @@ export class CateringStateService extends AsyncHandler {
this._menu.next([...menu]);
ref.close();
},
- () => (ref.componentInstance.loading = false)
+ () => (ref.componentInstance.loading = false),
);
}
@@ -280,7 +314,7 @@ export class CateringStateService extends AsyncHandler {
content: 'delete',
},
},
- this._dialog
+ this._dialog,
);
if (details.reason !== 'done') return;
details.loading('Removing catering item...');
@@ -290,7 +324,7 @@ export class CateringStateService extends AsyncHandler {
this._menu.next([...menu]);
details.close();
},
- () => details.loading('')
+ () => details.loading(''),
);
}
@@ -305,7 +339,7 @@ export class CateringStateService extends AsyncHandler {
content: 'delete',
},
},
- this._dialog
+ this._dialog,
);
if (details.reason !== 'done') return;
details.loading('Removing catering item option...');
@@ -316,14 +350,14 @@ export class CateringStateService extends AsyncHandler {
new CateringItem({
...item,
options: item.options.filter((opt) => opt.id !== option.id),
- })
+ }),
);
this.updateMenu(this._org.building.id, menu).then(
() => {
this._menu.next([...menu]);
details.close();
},
- () => details.loading('')
+ () => details.loading(''),
);
}
@@ -352,7 +386,7 @@ export class CateringStateService extends AsyncHandler {
if (details?.reason !== 'done') return;
this.updateConfig(this._org.building.id, details.metadata).then(
() => ref.close(),
- () => (ref.componentInstance.loading = false)
+ () => (ref.componentInstance.loading = false),
);
}
@@ -375,7 +409,7 @@ export class CateringStateService extends AsyncHandler {
throw _;
});
notifySuccess(
- `Successfully imported catering menu. ${details.metadata.length} item(s) added.`
+ `Successfully imported catering menu. ${details.metadata.length} item(s) added.`,
);
ref.close();
}
@@ -408,7 +442,7 @@ export class CateringStateService extends AsyncHandler {
}
public async getCateringConfig(
- zone_id: string = this._org.building.id
+ zone_id: string = this._org.building.id,
): Promise {
const rules = (
await showMetadata(zone_id, 'catering_config').toPromise()
@@ -437,8 +471,8 @@ export class CateringStateService extends AsyncHandler {
(new_item.options.find((opt) => o.id === opt.id)
? 1
: 0),
- 0
- )
+ 0,
+ ),
);
match
? ((match as any).quantity += 1)