delivery slot creation

This commit is contained in:
2026-10-06 17:37:02 +05:30
parent 990dd0dc23
commit 56e8a95c6a
7 changed files with 498 additions and 2 deletions

101
src/api/deliverySlots.ts Normal file
View 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;
}