102 lines
4.0 KiB
TypeScript
102 lines
4.0 KiB
TypeScript
/**
|
||
* When a branch delivers.
|
||
*
|
||
* A branch offers at most three windows a day — morning, afternoon, evening —
|
||
* and a shopper picks one at checkout. The window is a PREFERENCE, not a
|
||
* promise: every order is accepted and no window ever fills up.
|
||
*
|
||
* ── A branch with no windows is not broken ──────────────────────────────────
|
||
*
|
||
* Every tenant trading today has none, and all of them keep taking orders.
|
||
* An empty list means "order without a window", never "this shop is closed".
|
||
* Nothing here may treat it as an error state.
|
||
*
|
||
* ── The console edits; it does not decide what is open ──────────────────────
|
||
*
|
||
* Whether a window is still open today is a clock comparison, and the server
|
||
* owns it — see deliverySlotService.go. This module reads what a branch has
|
||
* configured and writes it back. The filtered, dated list a shopper sees is an
|
||
* app concern and is not fetched here.
|
||
*/
|
||
|
||
import { api, WEB } from './client';
|
||
|
||
/** The three, in the order a shopper reads them. */
|
||
export const SLOT_KEYS = ['morning', 'afternoon', 'evening'] as const;
|
||
export type SlotKey = (typeof SLOT_KEYS)[number];
|
||
|
||
export interface DeliverySlot {
|
||
deliveryslotid?: number;
|
||
tenantid?: number;
|
||
locationid?: number;
|
||
slotkey: SlotKey;
|
||
/** What the shopper reads. Blank falls back to the capitalised key. */
|
||
name: string;
|
||
/** "HH:MM", 24-hour, in the shop's own local time. */
|
||
starttime: string;
|
||
/**
|
||
* Also the cut-off. A window takes orders right up to the moment it ends —
|
||
* there is deliberately no separate cut-off to configure.
|
||
*/
|
||
endtime: string;
|
||
status: 'active' | 'inactive';
|
||
}
|
||
|
||
/**
|
||
* What a branch starts with when nobody has set anything.
|
||
*
|
||
* Seeded rather than blank so onboarding asks a shopkeeper to CONFIRM rather
|
||
* than to invent: three empty time fields is a form most people abandon, and a
|
||
* tenant that abandons it has a shop that cannot offer windows at all.
|
||
*/
|
||
export const DEFAULT_SLOTS: DeliverySlot[] = [
|
||
{ slotkey: 'morning', name: 'Morning', starttime: '08:00', endtime: '10:00', status: 'active' },
|
||
{ slotkey: 'afternoon', name: 'Afternoon', starttime: '12:00', endtime: '15:00', status: 'active' },
|
||
{ slotkey: 'evening', name: 'Evening', starttime: '17:00', endtime: '20:00', status: 'active' },
|
||
];
|
||
|
||
export const deliverySlotsApi = {
|
||
/** Everything this branch has configured, active or not. */
|
||
list: (tenantid: number, locationid: number) =>
|
||
api.get<{ details: DeliverySlot[] }>(`${WEB}/deliveryslots`, { tenantid, locationid }),
|
||
|
||
/**
|
||
* All three together, never one at a time.
|
||
*
|
||
* They are edited as a set on one screen, and sending them together is what
|
||
* lets the server reject the whole edit when one row is wrong instead of
|
||
* applying half of it — a shop with two new windows and one old one, and
|
||
* nothing on screen saying which took, is worse than a shop with none.
|
||
*/
|
||
save: (tenantid: number, locationid: number, slots: DeliverySlot[]) =>
|
||
api.put<unknown>(`${WEB}/deliveryslots`, { tenantid, locationid, slots }),
|
||
};
|
||
|
||
/**
|
||
* Is this set fit to send?
|
||
*
|
||
* Mirrors the server's rules so the shopkeeper hears about a mistake while
|
||
* their hands are still on it, rather than as a 409 after pressing save. The
|
||
* server re-checks all of it — this is courtesy, not security.
|
||
*/
|
||
export function slotProblems(slots: DeliverySlot[]): string[] {
|
||
const problems: string[] = [];
|
||
|
||
for (const slot of slots) {
|
||
const label = slot.name.trim() || slot.slotkey;
|
||
|
||
if (!/^\d{2}:\d{2}$/.test(slot.starttime) || !/^\d{2}:\d{2}$/.test(slot.endtime)) {
|
||
problems.push(`${label} needs a start and end time.`);
|
||
continue;
|
||
}
|
||
// An end at or before its start never passes the server's "still running"
|
||
// test, so the window would simply never appear to a shopper, with nothing
|
||
// saying why.
|
||
if (slot.endtime <= slot.starttime) {
|
||
problems.push(`${label} ends at or before it starts (${slot.starttime}–${slot.endtime}).`);
|
||
}
|
||
}
|
||
|
||
return problems;
|
||
}
|