delivery slot creation
This commit is contained in:
101
src/api/deliverySlots.ts
Normal file
101
src/api/deliverySlots.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user