Initial commit

This commit is contained in:
2026-08-24 20:35:18 +05:30
commit 1dc582ba07
85 changed files with 21081 additions and 0 deletions

View File

@@ -0,0 +1,143 @@
/**
* Order and delivery status, taken verbatim from the old console.
*
* Two ladders, not one, and that is the important part. An order walks
* created → pending → processing → confirmed/ready → delivered, while its
* delivery job walks pending → accepted → arrived → picked → active →
* delivered. Colouring a delivery with the order map paints "picked" and
* "arrived" grey, which is exactly the middle of the journey where an operator
* most needs to see progress.
*
* Values are the old console's `consoleUi.tsx` hex codes, unchanged, so a row
* that was amber there is amber here.
*/
/** The order lifecycle. */
export const ORDER_STATUS: Record<string, string> = {
created: '#0ea5e9',
pending: '#f59e0b',
processing: '#0ea5e9',
modified: '#06b6d4',
confirmed: '#10b981',
accepted: '#6366f1',
ready: '#6366f1',
delivered: '#10b981',
cancelled: '#ef4444',
};
/** The delivery lifecycle. Different words, different colours. */
export const DELIVERY_STATUS: Record<string, string> = {
pending: '#f59e0b',
accepted: '#6366f1',
arrived: '#06b6d4',
picked: '#8b5cf6',
active: '#14b8a6',
skipped: '#f97316',
delivered: '#10b981',
cancelled: '#ef4444',
};
/** Unknown statuses fall to meta grey rather than to a colour that means something. */
export const statusColor = (map: Record<string, string>, status: string | undefined): string =>
map[(status ?? '').trim().toLowerCase()] ?? 'var(--color-ink-3)';
/**
* The status tabs, as the old console groups them.
*
* Tabs rather than a dropdown: there are six, an operator switches between them
* constantly during a shift, and a dropdown hides the counts. Each carries how
* many rows it holds, which is half the reason to look.
*/
export const STATUS_TABS = [
{ key: 'all', label: 'All' },
{ key: 'created', label: 'Created' },
{ key: 'pending', label: 'Pending' },
{ key: 'processing', label: 'Processing' },
{ key: 'delivered', label: 'Delivered' },
{ key: 'cancelled', label: 'Cancelled' },
] as const;
export type StatusKey = (typeof STATUS_TABS)[number]['key'];
/**
* Does a row belong under this tab?
*
* Substring matching, lowercased. Fiesta stores status as free text and the
* casing is inconsistent between writers, so an exact match is how a
* "Delivered" row silently stops counting the day someone writes "delivered".
* "processing" also catches confirmed, preparing and ready — everything between
* accepted and out the door, which is what an operator means by the word.
*/
export function matchesStatus(key: StatusKey, status: string | undefined): boolean {
if (key === 'all') return true;
const s = (status ?? '').trim().toLowerCase();
if (!s) return false;
switch (key) {
case 'created':
return s.includes('created') || s.includes('new');
case 'pending':
return s.includes('pending');
case 'processing':
return (
s.includes('process') ||
s.includes('confirm') ||
s.includes('prepar') ||
s.includes('ready') ||
s.includes('accept') ||
s.includes('picked') ||
s.includes('active') ||
s.includes('arrived') ||
s.includes('out for')
);
case 'delivered':
return s.includes('deliver') && !s.includes('undeliver') && !s.includes('out for');
case 'cancelled':
// `skipped` lives here too. It is its own delivery status — the rider
// reached the address and nobody was in — and it keeps its own orange
// chip, but it has no tab of its own in this six-tab strip. Without this
// line a skipped job appears under "All" and under nothing else, so an
// operator working through the tabs never sees it.
return s.includes('cancel') || s.includes('skip');
}
}
/**
* The money on an order row, in the order the old console reads it.
*
* `ordervalue` is the full figure (goods + tax + charges − promo), `orderamount`
* is the goods alone, and `deliveryamt` is what a delivery job carries. Which
* one is populated depends on which endpoint produced the row, so this walks
* them rather than picking one and showing blanks half the time.
*/
export function orderValue(row: {
ordervalue?: number;
orderamount?: number;
deliveryamt?: number;
}): number {
return row.ordervalue || row.orderamount || row.deliveryamt || 0;
}
/** Items on an order. `quantity` first, `itemcount` as the fallback. */
export function orderQuantity(row: { quantity?: number; itemcount?: number }): number {
return row.quantity || row.itemcount || 0;
}
/**
* How far behind a counter bill was when it reached us.
*
* `billedat` is when the sale was rung; `receivedat` is when it landed here. A
* gap means the till was offline and caught up later, and it is worth showing
* on the row: a day's takings that all arrived at 6pm did not happen at 6pm.
*
* Returns null when the gap is under a minute (normal) or when either stamp is
* missing — a bill with no `receivedat` predates the field, and calling that
* "0 seconds late" would be a claim the data does not support.
*/
export function syncLagMs(bill: { billedat?: string; receivedat?: string }): number | null {
if (!bill.billedat || !bill.receivedat) return null;
const billed = new Date(bill.billedat).getTime();
const received = new Date(bill.receivedat).getTime();
if (Number.isNaN(billed) || Number.isNaN(received)) return null;
const lag = received - billed;
return lag >= 60_000 ? lag : null;
}