ui improvement
This commit is contained in:
@@ -4,12 +4,13 @@ import { CheckboxInput } from '@astryxdesign/core/CheckboxInput';
|
||||
import { Card } from '@astryxdesign/core/Card';
|
||||
import { HStack } from '@astryxdesign/core/HStack';
|
||||
import { Text } from '@astryxdesign/core/Text';
|
||||
import { TextInput } from '@astryxdesign/core/TextInput';
|
||||
import { VStack } from '@astryxdesign/core/VStack';
|
||||
import { Banknote, Bike, Calculator, IndianRupee, MapPin, Package, Percent, Receipt, ReceiptText, ShoppingCart, Truck, UserX } from 'lucide-react';
|
||||
import type { DeliveryRow, OrderRow, PosSale, TenantLocation } from '@/api/types';
|
||||
import { KpiCard } from '@/components/KpiCard';
|
||||
import { PageHeader } from '@/components/PageHeader';
|
||||
import { SearchInput } from '@/components/SearchInput';
|
||||
import { Tab, TabBar } from '@/components/TabBar';
|
||||
import {
|
||||
useDeliveries,
|
||||
useOrders,
|
||||
@@ -37,6 +38,7 @@ import { shortAge } from '../posStatus';
|
||||
import { AssignBar } from '../AssignBar';
|
||||
import { assignability, assignedFrom } from '../assignDelivery';
|
||||
import { useSelection } from '@/components/useSelection';
|
||||
import { Panel } from '@/components/Panel';
|
||||
import { TablePager } from '@/components/TablePager';
|
||||
import { usePaged } from '@/components/usePaged';
|
||||
import { isCounterSale } from '../branchStats';
|
||||
@@ -44,6 +46,21 @@ import './deliveries.css';
|
||||
|
||||
type Tab = 'orders' | 'deliveries' | 'counter';
|
||||
|
||||
/**
|
||||
* What the one search box matches, per view.
|
||||
*
|
||||
* The page used to draw three separate boxes, one inside each view, and the
|
||||
* only thing that differed between them was this line — the value behind them
|
||||
* was already a single `keyword`, filtering orders and deliveries through the
|
||||
* query and counter bills client-side. So the boxes were three renderings of
|
||||
* one control, and the placeholder is what survives of them.
|
||||
*/
|
||||
const SEARCH_HINT: Record<Tab, string> = {
|
||||
orders: 'Order ID, customer…',
|
||||
deliveries: 'Order ID, rider…',
|
||||
counter: 'Invoice, till, cashier…',
|
||||
};
|
||||
|
||||
/**
|
||||
* The bill number a counter sale was imported under.
|
||||
*
|
||||
@@ -287,43 +304,58 @@ export function SalesPage() {
|
||||
<VStack gap={3}>
|
||||
<PageHeader
|
||||
title="Sales"
|
||||
/* Tabs left, search right, on ONE row — see `isTabsInline`. */
|
||||
isTabsInline
|
||||
actions={
|
||||
<SearchInput
|
||||
label="Search sales"
|
||||
value={keyword}
|
||||
onChange={setKeyword}
|
||||
placeholder={SEARCH_HINT[tab]}
|
||||
/>
|
||||
}
|
||||
tabs={
|
||||
<HStack gap={0.5} wrap="wrap">
|
||||
<TabButton
|
||||
label="Online Orders"
|
||||
/* THREE views, not four.
|
||||
|
||||
There used to be an "Online Orders" tab in front of these carrying
|
||||
the combined count — 86 beside Orders 85 and Deliveries 1. It was
|
||||
never a view of its own: it reported active whenever the page was
|
||||
NOT on Counter sales, and clicking it from either of the two tabs
|
||||
it contained did nothing at all. In the old flat pills that passed
|
||||
unnoticed. In a segmented control it does not: a segmented control
|
||||
means "one of these", and Online Orders and Orders were both drawn
|
||||
selected at the same time, which is simply a broken control.
|
||||
|
||||
Nothing is lost by dropping it. Its count was the sum of the two
|
||||
beside it, and the combined figure is already on the KPI row above.
|
||||
|
||||
Counter sales keeps the divider: Orders and Deliveries are two
|
||||
halves of the app's own trade and the till is a different channel,
|
||||
which is the grouping "Online Orders" was there to express. */
|
||||
<TabBar aria-label="Sales views">
|
||||
<Tab
|
||||
label="Orders"
|
||||
icon={<ShoppingCart size={14} />}
|
||||
badge={(allOrders.length || 0) + (allDeliveries.length || 0) || undefined}
|
||||
isActive={tab !== 'counter'}
|
||||
onClick={() => setTab(tab === 'counter' ? 'orders' : tab)}
|
||||
count={allOrders.length || undefined}
|
||||
isActive={tab === 'orders'}
|
||||
onClick={() => setTab('orders')}
|
||||
/>
|
||||
|
||||
{tab !== 'counter' ? (
|
||||
<>
|
||||
<TabButton
|
||||
label="Orders"
|
||||
icon={<ShoppingCart size={14} />}
|
||||
badge={allOrders.length || undefined}
|
||||
isActive={tab === 'orders'}
|
||||
onClick={() => setTab('orders')}
|
||||
/>
|
||||
<TabButton
|
||||
label="Deliveries"
|
||||
icon={<Truck size={14} />}
|
||||
badge={allDeliveries.length || undefined}
|
||||
isActive={tab === 'deliveries'}
|
||||
onClick={() => setTab('deliveries')}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<TabButton
|
||||
<Tab
|
||||
label="Deliveries"
|
||||
icon={<Truck size={14} />}
|
||||
count={allDeliveries.length || undefined}
|
||||
isActive={tab === 'deliveries'}
|
||||
onClick={() => setTab('deliveries')}
|
||||
/>
|
||||
<Tab
|
||||
label="Counter sales"
|
||||
icon={<Receipt size={14} />}
|
||||
badge={billRows.length || undefined}
|
||||
count={billRows.length || undefined}
|
||||
isActive={tab === 'counter'}
|
||||
onClick={() => setTab('counter')}
|
||||
isGroupStart
|
||||
/>
|
||||
</HStack>
|
||||
</TabBar>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -367,9 +399,6 @@ export function SalesPage() {
|
||||
status={status}
|
||||
counts={tabCounts}
|
||||
onStatus={setStatus}
|
||||
search={
|
||||
<Search value={keyword} onChange={setKeyword} placeholder="Order ID, customer…" />
|
||||
}
|
||||
/>
|
||||
{/* Above the table, not below it and not in a drawer.
|
||||
Assigning is something you do WHILE reading the day's orders —
|
||||
@@ -451,9 +480,6 @@ export function SalesPage() {
|
||||
status={status}
|
||||
counts={tabCounts}
|
||||
onStatus={setStatus}
|
||||
search={
|
||||
<Search value={keyword} onChange={setKeyword} placeholder="Order ID, rider…" />
|
||||
}
|
||||
/>
|
||||
<DeliveriesTable
|
||||
rows={deliveryRows}
|
||||
@@ -514,11 +540,9 @@ export function SalesPage() {
|
||||
</div>
|
||||
|
||||
<VStack gap={1.5}>
|
||||
{/* No status strip here: a counter bill is rung and done, so it
|
||||
has no lifecycle to filter by. The search keeps the row. */}
|
||||
<HStack justify="end" wrap="wrap">
|
||||
<Search value={keyword} onChange={setKeyword} placeholder="Invoice, till, cashier…" />
|
||||
</HStack>
|
||||
{/* No status strip and no search row: a counter bill is rung and
|
||||
done, so it has no lifecycle to filter by, and the search is in
|
||||
the page header with the tabs — one box for all three views. */}
|
||||
<BillsTable
|
||||
rows={billRows}
|
||||
queryKey={queryKey}
|
||||
@@ -580,35 +604,46 @@ function freshRow(
|
||||
* in view, the filters decide what is in view, and the table shows it. Reading
|
||||
* downward now follows that order.
|
||||
*/
|
||||
/**
|
||||
* The status ladder, under the view tabs.
|
||||
*
|
||||
* The SECONDARY size of the same control the views use — no track, smaller
|
||||
* type, a brand tint on the selected one. That subordination is the point: two
|
||||
* strips of tabs at equal weight on one screen read as two equally important
|
||||
* choices, when this one only narrows whatever the strip above it selected.
|
||||
*
|
||||
* It used to be a private `StatusTab` — a 30px chip with a 12px radius, a
|
||||
* hairline when idle and a solid brand fill when selected — which was a fourth
|
||||
* treatment of "tab" on a page that now has one.
|
||||
*
|
||||
* The wrapper it sat in is gone with it. It was an `HStack justify="between"`
|
||||
* holding the tabs and the search; the search moved to the page header, so the
|
||||
* row was a space-between with nothing on the right of it.
|
||||
*/
|
||||
function FilterRow({
|
||||
tabs,
|
||||
status,
|
||||
counts,
|
||||
onStatus,
|
||||
search,
|
||||
}: {
|
||||
/** Which ladder to show — orders and deliveries do not share one. */
|
||||
tabs: readonly { readonly key: StatusKey; readonly label: string }[];
|
||||
status: StatusKey;
|
||||
counts: Record<StatusKey, number>;
|
||||
onStatus: (key: StatusKey) => void;
|
||||
search: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<HStack justify="between" align="center" gap={2} wrap="wrap">
|
||||
<HStack gap={0.5} wrap="wrap">
|
||||
{tabs.map((entry) => (
|
||||
<StatusTab
|
||||
key={entry.key}
|
||||
label={entry.label}
|
||||
n={counts[entry.key]}
|
||||
isActive={status === entry.key}
|
||||
onClick={() => onStatus(entry.key)}
|
||||
/>
|
||||
))}
|
||||
</HStack>
|
||||
{search}
|
||||
</HStack>
|
||||
<TabBar size="sm" aria-label="Filter by status">
|
||||
{tabs.map((entry) => (
|
||||
<Tab
|
||||
key={entry.key}
|
||||
label={entry.label}
|
||||
count={counts[entry.key]}
|
||||
isActive={status === entry.key}
|
||||
onClick={() => onStatus(entry.key)}
|
||||
/>
|
||||
))}
|
||||
</TabBar>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -649,7 +684,12 @@ function OrdersTable({
|
||||
}
|
||||
|
||||
return (
|
||||
<Card padding={0} elevation="low">
|
||||
<Panel
|
||||
title="Orders"
|
||||
count={paged.total}
|
||||
countLabel="orders"
|
||||
footer={<TablePager paged={paged} label="orders" />}
|
||||
>
|
||||
<div className="table-scroll">
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
|
||||
<thead>
|
||||
@@ -773,8 +813,7 @@ function OrdersTable({
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<TablePager paged={paged} label="orders" />
|
||||
</Card>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -807,7 +846,12 @@ function DeliveriesTable({
|
||||
}
|
||||
|
||||
return (
|
||||
<Card padding={0} elevation="low">
|
||||
<Panel
|
||||
title="Deliveries"
|
||||
count={paged.total}
|
||||
countLabel="deliveries"
|
||||
footer={<TablePager paged={paged} label="deliveries" />}
|
||||
>
|
||||
<div className="table-scroll">
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
|
||||
<thead>
|
||||
@@ -948,8 +992,7 @@ function DeliveriesTable({
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<TablePager paged={paged} label="deliveries" />
|
||||
</Card>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -980,7 +1023,12 @@ function BillsTable({
|
||||
}
|
||||
|
||||
return (
|
||||
<Card padding={0} elevation="low">
|
||||
<Panel
|
||||
title="Counter sales"
|
||||
count={paged.total}
|
||||
countLabel="bills"
|
||||
footer={<TablePager paged={paged} label="bills" />}
|
||||
>
|
||||
<div className="table-scroll">
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
|
||||
<thead>
|
||||
@@ -1062,8 +1110,7 @@ function BillsTable({
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<TablePager paged={paged} label="bills" />
|
||||
</Card>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1094,29 +1141,8 @@ function clockOf(value: string | number | undefined): string {
|
||||
});
|
||||
}
|
||||
|
||||
function Search({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (next: string) => void;
|
||||
placeholder: string;
|
||||
}) {
|
||||
return (
|
||||
<div style={{ width: 240 }}>
|
||||
<TextInput
|
||||
label="Search"
|
||||
isLabelHidden
|
||||
size="sm"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
hasClear
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
/* The page-local search box lived here. It is `components/SearchInput`
|
||||
now, in the header beside the tabs, and there is one of it. */
|
||||
|
||||
/** A status chip in the lifecycle's own colour. */
|
||||
function Chip({ label, color }: { label: string | undefined; color: string }) {
|
||||
@@ -1222,117 +1248,15 @@ function TotalsBar({
|
||||
);
|
||||
}
|
||||
|
||||
function StatusTab({
|
||||
label,
|
||||
n,
|
||||
isActive,
|
||||
onClick,
|
||||
}: {
|
||||
label: string;
|
||||
n: number;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-pressed={isActive}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
height: 30,
|
||||
padding: '0 11px',
|
||||
borderRadius: 12,
|
||||
border: `1px solid ${isActive ? 'transparent' : 'var(--color-line)'}`,
|
||||
// Transparent when idle, not white. A row of white chips on a tinted
|
||||
// page reads as six raised slabs; only the selected one should carry a
|
||||
// fill, and the hairline is enough to keep the rest legible as chips.
|
||||
background: isActive ? 'var(--color-brand)' : 'transparent',
|
||||
color: isActive ? '#fff' : 'var(--color-ink-2)',
|
||||
fontSize: 12.5,
|
||||
fontWeight: 500,
|
||||
cursor: 'pointer',
|
||||
transition: 'background .2s, color .2s, border-color .2s',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
<span
|
||||
style={{
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
fontWeight: 700,
|
||||
opacity: isActive ? 0.85 : 0.55,
|
||||
}}
|
||||
>
|
||||
{n}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
/* The page-local `StatusTab` lived here — it is the `sm` size of
|
||||
`components/TabBar` now. */
|
||||
|
||||
function TabButton({
|
||||
label,
|
||||
icon,
|
||||
isActive,
|
||||
onClick,
|
||||
badge,
|
||||
}: {
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
badge?: number;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-pressed={isActive}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
height: 32,
|
||||
padding: '0 12px',
|
||||
borderRadius: 12,
|
||||
border: 0,
|
||||
background: isActive ? 'var(--color-brand-tint)' : 'transparent',
|
||||
color: isActive ? 'var(--color-brand)' : 'var(--color-ink-3)',
|
||||
fontSize: 13,
|
||||
fontWeight: isActive ? 600 : 500,
|
||||
cursor: 'pointer',
|
||||
transition: 'background .2s, color .2s',
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
{badge ? (
|
||||
<span
|
||||
style={{
|
||||
minWidth: 18,
|
||||
height: 18,
|
||||
padding: '0 5px',
|
||||
borderRadius: 999,
|
||||
background: isActive ? 'var(--color-brand)' : 'var(--color-surface-sunken)',
|
||||
color: isActive ? '#fff' : 'var(--color-ink-3)',
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
}}
|
||||
>
|
||||
{badge}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
/* The page-local `TabButton` lived here. It is `components/TabBar` now —
|
||||
one control for the six pages that had each grown their own. */
|
||||
|
||||
function Loading({ what }: { what: string }) {
|
||||
return (
|
||||
<Card padding={0} elevation="low">
|
||||
<Card padding={0}>
|
||||
<VStack padding={3}>
|
||||
<Text type="body" size="sm" color="secondary">
|
||||
Reading {what}…
|
||||
@@ -1344,7 +1268,7 @@ function Loading({ what }: { what: string }) {
|
||||
|
||||
function Empty({ icon, title, body }: { icon: React.ReactNode; title: string; body: string }) {
|
||||
return (
|
||||
<Card padding={0} elevation="low">
|
||||
<Card padding={0}>
|
||||
<VStack gap={1} padding={4} align="center">
|
||||
<span style={{ color: 'var(--color-ink-4)' }}>{icon}</span>
|
||||
<Text type="label" size="sm" weight="semibold">
|
||||
|
||||
Reference in New Issue
Block a user