delivery slot updated on orders and deliveries
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user