drawer and upload
This commit is contained in:
@@ -1,15 +1,13 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Card } from '@astryxdesign/core/Card';
|
||||
import { HStack } from '@astryxdesign/core/HStack';
|
||||
import { Text } from '@astryxdesign/core/Text';
|
||||
import { VStack } from '@astryxdesign/core/VStack';
|
||||
import { Bike, Check, MapPin, Phone, X } from 'lucide-react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { ArrowDown, Bike, Check, MapPin, Phone, X } from 'lucide-react';
|
||||
import type { DeliveryRow, OrderRow } from '@/api/types';
|
||||
import { DeliveryProgress } from './DeliveryProgress';
|
||||
import { useDeliveryMoves } from './DeliveryProgress';
|
||||
import { Drawer } from './Drawer';
|
||||
import { Badge, DrawerButton, DrawerCard, Metric, Metrics, Note, Row, Section } from './drawerKit';
|
||||
import { branchLabel, moneyExact } from './format';
|
||||
import { DELIVERY_STATUS, ORDER_STATUS, orderQuantity, orderValue, statusColor } from './orderStatus';
|
||||
|
||||
type Row = OrderRow | DeliveryRow;
|
||||
type Row_ = OrderRow | DeliveryRow;
|
||||
|
||||
export type RowKind = 'order' | 'delivery';
|
||||
|
||||
@@ -44,11 +42,57 @@ export function OrderDetailDrawer({
|
||||
kind,
|
||||
onClose,
|
||||
}: {
|
||||
row: Row;
|
||||
row: Row_;
|
||||
kind: RowKind;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const delivery = kind === 'delivery';
|
||||
/*
|
||||
Split before the hook, not inside it.
|
||||
|
||||
The moves hook drives both the footer buttons and the caption that
|
||||
reports their errors, and it can only be called when there IS a delivery.
|
||||
Calling it twice — once per renderer — would give each its own pending state
|
||||
and its own error, so a failed write would be reported where nobody is
|
||||
looking. One instance, passed to both.
|
||||
*/
|
||||
return kind === 'delivery' ? (
|
||||
<DeliveryDrawer job={row as DeliveryRow} onClose={onClose} />
|
||||
) : (
|
||||
<Sheet row={row} isDelivery={false} onClose={onClose} />
|
||||
);
|
||||
}
|
||||
|
||||
function DeliveryDrawer({ job, onClose }: { job: DeliveryRow; onClose: () => void }) {
|
||||
const moves = useDeliveryMoves(job);
|
||||
return (
|
||||
<Sheet
|
||||
row={job}
|
||||
isDelivery
|
||||
onClose={onClose}
|
||||
{...(moves.isSettled ? {} : { footer: <MoveActions moves={moves} /> })}
|
||||
caption={<MoveCaption moves={moves} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type Moves = ReturnType<typeof useDeliveryMoves>;
|
||||
|
||||
function Sheet({
|
||||
row,
|
||||
isDelivery,
|
||||
onClose,
|
||||
footer,
|
||||
caption,
|
||||
}: {
|
||||
row: Row_;
|
||||
/** Passed explicitly, never inferred from the presence of a footer — a
|
||||
settled job has no actions and would otherwise read as an order. */
|
||||
isDelivery: boolean;
|
||||
onClose: () => void;
|
||||
footer?: ReactNode;
|
||||
caption?: ReactNode;
|
||||
}) {
|
||||
const delivery = isDelivery;
|
||||
// Narrowed once, here. `kind` comes from the table the row was clicked in, so
|
||||
// it is authoritative in a way that sniffing fields is not — a delivery row
|
||||
// carries an `orderheaderid` too, which is what made the first attempt at
|
||||
@@ -56,16 +100,6 @@ export function OrderDetailDrawer({
|
||||
const job = delivery ? (row as DeliveryRow) : undefined;
|
||||
const order = delivery ? undefined : (row as OrderRow);
|
||||
|
||||
// Escape closes. A dialog that can only be dismissed by finding the scrim
|
||||
// with a mouse is not dismissible for anyone on a keyboard, and this one sits
|
||||
// over the list an operator is working through.
|
||||
useEffect(() => {
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') onClose();
|
||||
}
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
return () => window.removeEventListener('keydown', onKeyDown);
|
||||
}, [onClose]);
|
||||
const title = row.orderid || (job ? `DLV-${job.deliveryid}` : `#${order?.orderheaderid}`);
|
||||
const status = row.orderstatus;
|
||||
const colour = statusColor(delivery ? DELIVERY_STATUS : ORDER_STATUS, status);
|
||||
@@ -74,234 +108,187 @@ export function OrderDetailDrawer({
|
||||
const cancelled = (status ?? '').toLowerCase().includes('cancel');
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={`Order ${title}`}
|
||||
style={{ position: 'fixed', inset: 0, zIndex: 70, display: 'flex', justifyContent: 'flex-end' }}
|
||||
<Drawer
|
||||
title={title}
|
||||
isTitleMono
|
||||
onClose={onClose}
|
||||
meta={
|
||||
<>
|
||||
<Badge label={status || '—'} colour={colour} />
|
||||
<span className="drawer-meta-text">{branchLabel(row.locationname)}</span>
|
||||
</>
|
||||
}
|
||||
{...(footer ? { footer } : {})}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Close"
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
border: 0,
|
||||
background: 'rgb(15 23 42 / .32)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="dialog-panel"
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: 460,
|
||||
maxWidth: '92vw',
|
||||
height: '100%',
|
||||
overflowY: 'auto',
|
||||
background: 'var(--color-surface)',
|
||||
borderLeft: '1px solid var(--color-line)',
|
||||
boxShadow: '-24px 0 56px -12px rgb(15 23 42 / .18)',
|
||||
}}
|
||||
>
|
||||
<HStack
|
||||
justify="between"
|
||||
align="start"
|
||||
gap={1}
|
||||
style={{
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 1,
|
||||
padding: 20,
|
||||
background: 'var(--color-surface)',
|
||||
borderBottom: '1px solid var(--color-line)',
|
||||
}}
|
||||
>
|
||||
<VStack gap={0.5}>
|
||||
<Text
|
||||
type="label"
|
||||
size="lg"
|
||||
weight="semibold"
|
||||
style={{ fontFamily: 'var(--font-mono)' }}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<HStack gap={1} align="center">
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
borderRadius: 999,
|
||||
padding: '2px 9px',
|
||||
fontSize: 11.5,
|
||||
fontWeight: 600,
|
||||
textTransform: 'capitalize',
|
||||
color: colour,
|
||||
background: `color-mix(in oklab, ${colour} 12%, transparent)`,
|
||||
}}
|
||||
>
|
||||
{status || '—'}
|
||||
</span>
|
||||
<Text type="body" size="xsm" color="secondary">
|
||||
{branchLabel(row.locationname)}
|
||||
</Text>
|
||||
</HStack>
|
||||
</VStack>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Close"
|
||||
onClick={onClose}
|
||||
style={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 10,
|
||||
border: 0,
|
||||
flex: 'none',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
background: 'var(--color-surface-sunken)',
|
||||
color: 'var(--color-ink-3)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<X size={15} />
|
||||
</button>
|
||||
</HStack>
|
||||
{/* ── Money ─────────────────────────────────────────────────────────
|
||||
Three equal columns, the shape the spec asks for and the one that
|
||||
answers "what is this worth, who pays what, how much of it". */}
|
||||
<Metrics>
|
||||
<Metric label="Value" value={value > 0 ? moneyExact(value) : '—'} />
|
||||
{job ? (
|
||||
<Metric
|
||||
label="Rider charge"
|
||||
value={job.deliverycharges ? moneyExact(job.deliverycharges) : '—'}
|
||||
isSmall
|
||||
/>
|
||||
) : (
|
||||
<Metric
|
||||
label="Cash to collect"
|
||||
value={order?.collectionamt ? moneyExact(order.collectionamt) : '—'}
|
||||
isSmall
|
||||
/>
|
||||
)}
|
||||
<Metric
|
||||
label="Items"
|
||||
value={job ? String(job.itemcount ?? '—') : String(orderQuantity(row) || '—')}
|
||||
isSmall
|
||||
/>
|
||||
</Metrics>
|
||||
|
||||
<VStack gap={3} padding={2}>
|
||||
{/* ── Money ─────────────────────────────────────────────────── */}
|
||||
<div className="form-grid">
|
||||
<Figure label="Value" value={value > 0 ? moneyExact(value) : '—'} isStrong />
|
||||
{job ? (
|
||||
<Figure
|
||||
label="Rider charge"
|
||||
value={job.deliverycharges ? moneyExact(job.deliverycharges) : '—'}
|
||||
/>
|
||||
) : (
|
||||
<Figure
|
||||
label="Cash to collect"
|
||||
value={order?.collectionamt ? moneyExact(order.collectionamt) : '—'}
|
||||
/>
|
||||
)}
|
||||
<Figure
|
||||
label="Items"
|
||||
value={job ? String(job.itemcount ?? '—') : String(orderQuantity(row) || '—')}
|
||||
/>
|
||||
<Section title="Order journey">
|
||||
<Timeline row={row} isCancelled={cancelled} />
|
||||
</Section>
|
||||
|
||||
<Section title="Route">
|
||||
<DrawerCard>
|
||||
<Party
|
||||
kind="Pickup"
|
||||
name={row.pickupcustomer || branchLabel(row.locationname)}
|
||||
detail={
|
||||
job
|
||||
? job.pickupsuburb || job.pickuplocation || job.Pickupaddress || job.pickupaddress
|
||||
: order?.pickupaddress || order?.pickupsuburb
|
||||
}
|
||||
phone={row.pickupcontactno}
|
||||
/>
|
||||
<div className="drawer-party-link" aria-hidden>
|
||||
<ArrowDown size={13} />
|
||||
</div>
|
||||
<Party
|
||||
kind="Drop"
|
||||
name={row.deliverycustomer}
|
||||
detail={row.deliveryaddress || row.deliverysuburb}
|
||||
phone={row.deliverycontactno}
|
||||
/>
|
||||
</DrawerCard>
|
||||
</Section>
|
||||
|
||||
{/* ── Journey ───────────────────────────────────────────────── */}
|
||||
<VStack gap={1}>
|
||||
<Label>Journey</Label>
|
||||
<Timeline row={row} isCancelled={cancelled} />
|
||||
</VStack>
|
||||
|
||||
{/* ── Pickup and drop ───────────────────────────────────────── */}
|
||||
<VStack gap={1}>
|
||||
<Label>Route</Label>
|
||||
<Party
|
||||
icon={<MapPin size={13} />}
|
||||
heading="Pickup"
|
||||
name={row.pickupcustomer || branchLabel(row.locationname)}
|
||||
detail={
|
||||
job
|
||||
? job.pickupsuburb || job.pickuplocation || job.Pickupaddress || job.pickupaddress
|
||||
: order?.pickupaddress || order?.pickupsuburb
|
||||
}
|
||||
phone={row.pickupcontactno}
|
||||
{/* ── Rider ─────────────────────────────────────────────────────────── */}
|
||||
{job ? (
|
||||
<Section title="Rider">
|
||||
{job.ridername ? (
|
||||
<DrawerCard>
|
||||
<div className="drawer-party">
|
||||
<span className="drawer-party-icon">
|
||||
<Bike size={15} />
|
||||
</span>
|
||||
<div className="drawer-party-body">
|
||||
<span className="drawer-party-name">{job.ridername}</span>
|
||||
{job.ridercontact ? (
|
||||
<a className="drawer-party-phone" href={`tel:${job.ridercontact}`}>
|
||||
<Phone size={12} />
|
||||
{job.ridercontact}
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{/* Planned against actual. The gap is the number worth reading —
|
||||
a job quoted 3km and ridden 9 is either a bad address or a
|
||||
rider taking a detour, and one figure hides both. */}
|
||||
<Metrics cols={job.transitminutes ? 3 : 2}>
|
||||
<Metric label="Planned" value={km(job.kms)} isSmall />
|
||||
<Metric label="Actual" value={km(job.actualkms || job.riderkms)} isSmall />
|
||||
{job.transitminutes ? (
|
||||
<Metric label="Transit" value={`${job.transitminutes} min`} isSmall />
|
||||
) : null}
|
||||
</Metrics>
|
||||
</DrawerCard>
|
||||
) : (
|
||||
<Note>No rider assigned yet.</Note>
|
||||
)}
|
||||
{/* The caption for the footer's actions. It stays in the body while
|
||||
the buttons sit in the bar — it explains when to use them, which
|
||||
is reading matter, not a control. */}
|
||||
{caption}
|
||||
</Section>
|
||||
) : order?.rider ? (
|
||||
<Section title="Rider">
|
||||
<DrawerCard>
|
||||
<Row
|
||||
label="Rider"
|
||||
value={order.rider}
|
||||
icon={<Bike size={14} />}
|
||||
/>
|
||||
<Party
|
||||
icon={<MapPin size={13} />}
|
||||
heading="Drop"
|
||||
name={row.deliverycustomer}
|
||||
detail={row.deliveryaddress || row.deliverysuburb}
|
||||
phone={row.deliverycontactno}
|
||||
/>
|
||||
</VStack>
|
||||
{order.ridercontactno ? (
|
||||
<Row label="Contact" value={order.ridercontactno} icon={<Phone size={14} />} />
|
||||
) : null}
|
||||
</DrawerCard>
|
||||
</Section>
|
||||
) : null}
|
||||
|
||||
{/* ── Rider ─────────────────────────────────────────────────── */}
|
||||
{job ? (
|
||||
<VStack gap={1}>
|
||||
<Label>Rider</Label>
|
||||
{job.ridername ? (
|
||||
<Card padding={0} elevation="low">
|
||||
<VStack gap={1} padding={2}>
|
||||
<HStack gap={1} align="center">
|
||||
<span
|
||||
style={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 999,
|
||||
flex: 'none',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
background: 'color-mix(in oklab, #8b5cf6 14%, transparent)',
|
||||
color: '#8b5cf6',
|
||||
}}
|
||||
>
|
||||
<Bike size={14} />
|
||||
</span>
|
||||
<VStack gap={0}>
|
||||
<Text type="label" size="sm" weight="semibold">
|
||||
{job.ridername}
|
||||
</Text>
|
||||
{job.ridercontact ? (
|
||||
<Text type="body" size="xsm" color="secondary">
|
||||
{job.ridercontact}
|
||||
</Text>
|
||||
) : null}
|
||||
</VStack>
|
||||
</HStack>
|
||||
{/* Planned against actual. The gap is the number worth
|
||||
reading — a job quoted 3km and ridden 9 is either a bad
|
||||
address or a rider taking a detour, and one figure hides
|
||||
both. */}
|
||||
<HStack gap={2}>
|
||||
<Figure label="Planned" value={km(job.kms)} />
|
||||
<Figure label="Actual" value={km(job.actualkms || job.riderkms)} />
|
||||
{job.transitminutes ? (
|
||||
<Figure label="Transit" value={`${job.transitminutes} min`} />
|
||||
) : null}
|
||||
</HStack>
|
||||
</VStack>
|
||||
</Card>
|
||||
) : (
|
||||
<Text type="body" size="sm" color="secondary">
|
||||
No rider assigned yet.
|
||||
</Text>
|
||||
)}
|
||||
{/* Recording progress by hand. Under the rider rather than beside
|
||||
the status chip at the top: it is a correction, not the normal
|
||||
way a job moves, and putting it in the header would invite it
|
||||
to be used as one. */}
|
||||
<DeliveryProgress job={job} />
|
||||
</VStack>
|
||||
) : order?.rider ? (
|
||||
<VStack gap={1}>
|
||||
<Label>Rider</Label>
|
||||
<Text type="body" size="sm">
|
||||
{order.rider}
|
||||
{order.ridercontactno ? ` · ${order.ridercontactno}` : ''}
|
||||
</Text>
|
||||
</VStack>
|
||||
) : null}
|
||||
{row.ordernotes || job?.notes ? (
|
||||
<Section title="Notes">
|
||||
<DrawerCard tone="subtle">
|
||||
<Row label="Note" value={row.ordernotes || job?.notes} isStacked />
|
||||
</DrawerCard>
|
||||
</Section>
|
||||
) : null}
|
||||
|
||||
{/* ── Notes ─────────────────────────────────────────────────── */}
|
||||
{row.ordernotes || job?.notes ? (
|
||||
<VStack gap={1}>
|
||||
<Label>Notes</Label>
|
||||
<Text type="body" size="sm" color="secondary">
|
||||
{row.ordernotes || job?.notes}
|
||||
</Text>
|
||||
</VStack>
|
||||
) : null}
|
||||
<Note>
|
||||
Line items are not in the list read — Fiesta returns them from the order-detail endpoint,
|
||||
which this sheet does not call yet.
|
||||
</Note>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
<Text type="body" size="xsm" color="secondary">
|
||||
Line items are not in the list read — Fiesta returns them from the order-detail
|
||||
endpoint, which this sheet does not call yet.
|
||||
</Text>
|
||||
</VStack>
|
||||
/* ── Recording progress by hand ──────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* The moves, in the drawer's fixed action bar.
|
||||
*
|
||||
* In the footer rather than beside the status chip: it is a correction, not the
|
||||
* normal way a job moves, and putting it in the header would invite it to be
|
||||
* used as one. It is still the drawer's primary action, which is why it is
|
||||
* pinned rather than buried at the end of a scroll.
|
||||
*/
|
||||
function MoveActions({ moves: state }: { moves: Moves }) {
|
||||
const { isPending, moves, status, run } = state;
|
||||
|
||||
return (
|
||||
<>
|
||||
{moves.map((step) => (
|
||||
<DrawerButton
|
||||
key={step.to}
|
||||
label={step.label}
|
||||
variant={step.to === 'cancelled' ? 'danger' : step.to === 'delivered' ? 'primary' : 'secondary'}
|
||||
isDisabled={isPending || step.to === status}
|
||||
onClick={() => run(step.to, step.stamp)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function MoveCaption({ moves }: { moves: Moves }) {
|
||||
const { isSettled, problem, status } = moves;
|
||||
|
||||
if (isSettled) {
|
||||
return <Note>This job is {status}. Nothing further to record.</Note>;
|
||||
}
|
||||
if (problem) {
|
||||
return (
|
||||
<div className="drawer-note" role="alert" style={{ color: 'var(--color-error)' }}>
|
||||
<X size={15} />
|
||||
<span>{problem}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Note>
|
||||
Use the buttons below only when the rider's app has not. Marking a job delivered here also
|
||||
closes the order.
|
||||
</Note>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -315,7 +302,7 @@ export function OrderDetailDrawer({
|
||||
* have a missing stamp, and drawing that step as done would paper over the
|
||||
* data-quality problem the operator opened this sheet to find.
|
||||
*/
|
||||
function Timeline({ row, isCancelled }: { row: Row; isCancelled: boolean }) {
|
||||
function Timeline({ row, isCancelled }: { row: Row_; isCancelled: boolean }) {
|
||||
const steps = STEPS.map((step) => ({
|
||||
label: step.label,
|
||||
// Read by name: the two row shapes do not carry the same stamps — a
|
||||
@@ -326,196 +313,101 @@ function Timeline({ row, isCancelled }: { row: Row; isCancelled: boolean }) {
|
||||
.find(Boolean),
|
||||
}));
|
||||
|
||||
/* The step the job is sitting on: the first one without a stamp. Marked as
|
||||
"current" rather than merely "not done", so the eye lands on where the
|
||||
order actually is. A finished journey has none, which is correct. */
|
||||
const current = isCancelled ? -1 : steps.findIndex((step) => !step.at);
|
||||
|
||||
return (
|
||||
<VStack gap={0}>
|
||||
<div className="drawer-timeline">
|
||||
{steps.map((step, index) => {
|
||||
const done = Boolean(step.at);
|
||||
const isLast = index === steps.length - 1;
|
||||
const isCurrent = index === current;
|
||||
const isLast = index === steps.length - 1 && !isCancelled;
|
||||
return (
|
||||
<HStack key={step.label} gap={1.5} align="start">
|
||||
<VStack gap={0} align="center" style={{ width: 20, flex: 'none' }}>
|
||||
<div key={step.label} className="drawer-step">
|
||||
<span className="drawer-step-rail">
|
||||
<span
|
||||
style={{
|
||||
width: 18,
|
||||
height: 18,
|
||||
borderRadius: 999,
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
flex: 'none',
|
||||
background: done ? 'var(--color-brand)' : 'var(--color-surface-sunken)',
|
||||
color: done ? '#fff' : 'var(--color-ink-4)',
|
||||
border: done ? 0 : '1px solid var(--color-line)',
|
||||
}}
|
||||
className="drawer-step-dot"
|
||||
data-done={done}
|
||||
{...(isCurrent ? { 'data-current': 'true' } : {})}
|
||||
>
|
||||
{done ? <Check size={11} /> : null}
|
||||
</span>
|
||||
{!isLast ? (
|
||||
<span
|
||||
style={{
|
||||
width: 2,
|
||||
height: 22,
|
||||
background: done ? 'var(--color-brand)' : 'var(--color-line)',
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</VStack>
|
||||
<VStack gap={0} style={{ paddingBottom: isLast ? 0 : 8 }}>
|
||||
<Text
|
||||
type="body"
|
||||
size="sm"
|
||||
style={{ color: done ? 'var(--color-ink-1)' : 'var(--color-ink-4)' }}
|
||||
{!isLast ? <span className="drawer-step-line" data-done={done} /> : null}
|
||||
</span>
|
||||
<span className="drawer-step-body">
|
||||
<span
|
||||
className="drawer-step-label"
|
||||
data-done={done}
|
||||
{...(isCurrent ? { 'data-current': 'true' } : {})}
|
||||
>
|
||||
{step.label}
|
||||
</Text>
|
||||
{step.at ? (
|
||||
<Text type="body" size="xsm" color="secondary">
|
||||
{new Date(step.at).toLocaleString('en-IN', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
})}
|
||||
</Text>
|
||||
) : null}
|
||||
</VStack>
|
||||
</HStack>
|
||||
</span>
|
||||
{step.at ? <span className="drawer-step-at">{when(step.at)}</span> : null}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{isCancelled ? (
|
||||
<HStack gap={1.5} align="center" style={{ paddingTop: 8 }}>
|
||||
<span
|
||||
style={{
|
||||
width: 18,
|
||||
height: 18,
|
||||
borderRadius: 999,
|
||||
flex: 'none',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
background: 'color-mix(in oklab, var(--color-error, #d64545) 14%, transparent)',
|
||||
color: 'var(--color-error, #d64545)',
|
||||
}}
|
||||
>
|
||||
<X size={11} />
|
||||
<div className="drawer-step">
|
||||
<span className="drawer-step-rail">
|
||||
<span className="drawer-step-dot" data-tone="danger">
|
||||
<X size={11} />
|
||||
</span>
|
||||
</span>
|
||||
<Text type="body" size="sm" style={{ color: 'var(--color-error, #d64545)' }}>
|
||||
Cancelled
|
||||
{row.canceltime
|
||||
? ` · ${new Date(row.canceltime).toLocaleString('en-IN', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
})}`
|
||||
: ''}
|
||||
</Text>
|
||||
</HStack>
|
||||
<span className="drawer-step-body">
|
||||
<span className="drawer-step-label" style={{ color: 'var(--color-error)', fontWeight: 600 }}>
|
||||
Cancelled
|
||||
</span>
|
||||
{row.canceltime ? <span className="drawer-step-at">{when(row.canceltime)}</span> : null}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</VStack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Party({
|
||||
icon,
|
||||
heading,
|
||||
kind,
|
||||
name,
|
||||
detail,
|
||||
phone,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
heading: string;
|
||||
kind: string;
|
||||
name: string | undefined;
|
||||
detail: string | undefined;
|
||||
phone: string | undefined;
|
||||
}) {
|
||||
return (
|
||||
<HStack gap={1.5} align="start">
|
||||
<span
|
||||
style={{
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: 8,
|
||||
flex: 'none',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
background: 'var(--color-brand-tint)',
|
||||
color: 'var(--color-brand)',
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
<div className="drawer-party">
|
||||
<span className="drawer-party-icon">
|
||||
<MapPin size={15} />
|
||||
</span>
|
||||
<VStack gap={0} style={{ minWidth: 0 }}>
|
||||
<Text
|
||||
type="label"
|
||||
size="xsm"
|
||||
color="secondary"
|
||||
style={{ textTransform: 'uppercase', letterSpacing: '0.09em' }}
|
||||
>
|
||||
{heading}
|
||||
</Text>
|
||||
<Text type="body" size="sm" weight="semibold">
|
||||
{name || '—'}
|
||||
</Text>
|
||||
{detail ? (
|
||||
<Text type="body" size="xsm" color="secondary">
|
||||
{detail}
|
||||
</Text>
|
||||
) : null}
|
||||
<div className="drawer-party-body">
|
||||
<span className="drawer-party-kind">{kind}</span>
|
||||
<span className="drawer-party-name">{name || '—'}</span>
|
||||
{detail ? <span className="drawer-party-detail">{detail}</span> : null}
|
||||
{phone ? (
|
||||
<HStack gap={0.5} align="center">
|
||||
<Phone size={11} style={{ color: 'var(--color-ink-4)' }} />
|
||||
<Text type="body" size="xsm" color="secondary">
|
||||
{phone}
|
||||
</Text>
|
||||
</HStack>
|
||||
<a className="drawer-party-phone" href={`tel:${phone}`}>
|
||||
<Phone size={12} />
|
||||
{phone}
|
||||
</a>
|
||||
) : null}
|
||||
</VStack>
|
||||
</HStack>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Figure({
|
||||
label,
|
||||
value,
|
||||
isStrong,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
isStrong?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<VStack gap={0}>
|
||||
<Text
|
||||
type="label"
|
||||
size="xsm"
|
||||
color="secondary"
|
||||
style={{ textTransform: 'uppercase', letterSpacing: '0.09em' }}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
<Text
|
||||
type="label"
|
||||
size={isStrong ? 'lg' : 'sm'}
|
||||
weight="semibold"
|
||||
style={{ fontVariantNumeric: 'tabular-nums' }}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
const Label = ({ children }: { children: React.ReactNode }) => (
|
||||
<Text
|
||||
type="label"
|
||||
size="xsm"
|
||||
color="secondary"
|
||||
style={{ textTransform: 'uppercase', letterSpacing: '0.09em' }}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
const when = (at: string): string =>
|
||||
new Date(at).toLocaleString('en-IN', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
|
||||
const km = (value: string | undefined): string => {
|
||||
const n = Number(value);
|
||||
|
||||
Reference in New Issue
Block a user