delivery slot updated on orders and deliveries

This commit is contained in:
2026-10-06 19:40:42 +05:30
parent 56e8a95c6a
commit d24df891fb
5 changed files with 407 additions and 3 deletions

View File

@@ -1,4 +1,12 @@
import { useState, useMemo } from 'react';
import {
WINDOW_FILTERS,
matchesWindow,
windowHasPassed,
windowHours,
windowLabel,
type WindowFilterKey,
} from '../deliveryWindow';
import { useDateScope } from '@/components/shell/DateScope';
import { CheckboxInput } from '@astryxdesign/core/CheckboxInput';
import { Card } from '@astryxdesign/core/Card';
@@ -116,6 +124,14 @@ export function SalesPage() {
const { scoped, selected, branches, tenantid } = useBranchScope();
const [tab, setTab] = useState<Tab>('orders');
const [status, setStatus] = useState<StatusKey>('all');
/*
Which delivery window to show.
Separate from `status`, and both apply: "evening orders that are still
pending" is the question a dispatcher actually asks, and folding the two
into one control would make it unaskable.
*/
const [windowFilter, setWindowFilter] = useState<WindowFilterKey>('all');
const dates = useDateScope();
const [keyword, setKeyword] = useState('');
/* What the tables are currently showing. Any change to it sends every pager
@@ -181,11 +197,19 @@ export function SalesPage() {
* carries those tabs now, so the filter follows the column.
*/
const orderRows = useMemo(
() => allOrders.filter((row) => matchesStatus(status, orderStage(row, stages).status)),
() =>
allOrders.filter(
(row) =>
matchesStatus(status, orderStage(row, stages).status) &&
matchesWindow(windowFilter, row),
),
[allOrders, status, stages],
);
const deliveryRows = useMemo(
() => allDeliveries.filter((row) => matchesStatus(status, row.orderstatus)),
() =>
allDeliveries.filter(
(row) => matchesStatus(status, row.orderstatus) && matchesWindow(windowFilter, row),
),
[allDeliveries, status],
);
@@ -453,6 +477,10 @@ export function SalesPage() {
counts={tabCounts}
onStatus={setStatus}
/>
{/* Beside the status tabs, not inside them. Both filters apply at
once, because "evening orders still pending" is the question
somebody actually has. */}
<WindowFilter value={windowFilter} onChange={setWindowFilter} />
{/* Above the table, not below it and not in a drawer.
Assigning is something you do WHILE reading the day's orders —
glancing between the drop addresses and who is free — so the
@@ -772,6 +800,7 @@ function OrdersTable({
<Th>Drop</Th>
<Th>Qty</Th>
<Th>Amount</Th>
<Th>Window</Th>
<Th>Status</Th>
<Th />
</tr>
@@ -855,6 +884,7 @@ function OrdersTable({
<Td isStrong={amount > 0} isMuted={amount === 0}>
<Mono>{amount > 0 ? moneyExact(amount) : '—'}</Mono>
</Td>
<WindowCell row={row} />
<Td>
{/* The stage, not the order's own word — which sits on `pending`
from assignment until delivery. The ladder has to follow
@@ -924,6 +954,7 @@ function DeliveriesTable({
<Th>Rider</Th>
<Th>KM</Th>
<Th>Charge / value</Th>
<Th>Window</Th>
<Th>Status</Th>
<Th>Notes</Th>
</tr>
@@ -1027,6 +1058,7 @@ function DeliveriesTable({
<span style={{ color: 'var(--color-ink-4)' }}>—</span>
) : null}
</Td>
<WindowCell row={row} />
<Td>
<Chip
label={row.orderstatus}
@@ -1457,3 +1489,78 @@ function Td({
);
}
/* ── Delivery window ──────────────────────────────────────────────────────── */
/**
* The window a customer asked for, as a table cell.
*
* Most rows have none, and those read "—" rather than staying blank: an empty
* cell in a table of filled ones looks like something failed to load, and this
* is the ordinary case, not a failure.
*/
function WindowCell({ row }: { row: OrderRow | DeliveryRow }) {
const label = windowLabel(row);
if (!label) {
return (
<Td isMuted>
<span>—</span>
</Td>
);
}
const hours = windowHours(row);
// A window that has ended while the order is still on the board is the one
// thing here worth noticing. Tinted, never hidden — burying it would hide the
// row somebody most needs to act on.
const isLate = windowHasPassed(row);
return (
<Td>
<span
title={hours ?? undefined}
style={isLate ? { color: 'var(--color-error, #d64545)', fontWeight: 600 } : undefined}
>
{label}
</span>
</Td>
);
}
/**
* Which window to show.
*
* "No window" earns its place: it is how an operator finds orders placed
* without choosing one, which is most of them while this rolls out, and is the
* population worth watching to see the feature being used at all.
*/
function WindowFilter({
value,
onChange,
}: {
value: WindowFilterKey;
onChange: (next: WindowFilterKey) => void;
}) {
return (
<select
value={value}
onChange={(event) => onChange(event.target.value as WindowFilterKey)}
aria-label="Filter by delivery window"
style={{
padding: '6px 10px',
fontSize: 12.5,
border: '1px solid var(--color-line, #d7dce5)',
borderRadius: 7,
background: 'var(--color-surface, #fff)',
color: 'var(--color-ink-1)',
}}
>
{WINDOW_FILTERS.map((option) => (
<option key={option.key} value={option.key}>
{option.label}
</option>
))}
</select>
);
}