conversion done with astryx

This commit is contained in:
2026-08-13 18:33:22 +05:30
parent d935542b11
commit 0daec5311d
19 changed files with 3509 additions and 5144 deletions

View File

@@ -13,7 +13,7 @@
import React, { useMemo, useState, useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { ShoppingBag, Clock, CheckCircle2, XCircle, Calendar, ChevronLeft, ChevronRight, Package, MapPin, Phone, X, Loader2, Download, UserCheck, ClipboardList, ArrowLeft } from 'lucide-react';
import { ShoppingBag, Clock, CheckCircle2, XCircle, Calendar, ChevronLeft, ChevronRight, Package, MapPin, Phone, X, Download, UserCheck, ClipboardList, ArrowLeft } from 'lucide-react';
import { useFiestaOrderSummary, useFiestaOrders, useFiestaOrderDetails, useFiestaRiders, useFiestaAssignRider, useFiestaNotifyRider } from '../services/fiestaQueries';
import {
FIESTA_TENANT_ID, FIESTA_APPLOCATION_ID, RIDER_MESSAGES, RiderNotReachableError,
@@ -21,9 +21,26 @@ import {
} from '../services/fiestaApi';
import { shortTime } from '../services/fiestaMappers';
import {
GradientHeader, LiveStatus, KpiStrip, Pill, StatusChip, MetricPill, SearchPill, FilterBar, TH_STYLE,
ORDER_STATUS, statusColor, BRAND, BRAND_LIGHT, TEXT, TEXT_2, TEXT_3, BORDER, SURFACE_ALT, tint, soft, edge, ring,
KpiStrip, Pill, StatusChip, SearchPill, FilterBar, Card as ShellCard,
ORDER_STATUS, statusColor, BRAND, TEXT_2, TEXT_3, BORDER, SURFACE_ALT, tint, soft, edge, ring,
} from './consoleUi';
import { Button } from '@astryxdesign/core/Button';
import { IconButton } from '@astryxdesign/core/IconButton';
import { Dialog } from '@astryxdesign/core/Dialog';
import { DateInput } from '@astryxdesign/core/DateInput';
// DateInput's value is a branded `YYYY-MM-DD` template-literal type, while our
// dates are plain strings out of `ymd()`. Importing the real type keeps the cast
// honest and localised instead of reaching for `as any` at four call sites.
import type { ISODateString } from '@astryxdesign/core/utils';
import { Selector } from '@astryxdesign/core/Selector';
import { SegmentedControl, SegmentedControlItem } from '@astryxdesign/core/SegmentedControl';
import { CheckboxInput } from '@astryxdesign/core/CheckboxInput';
import { Heading } from '@astryxdesign/core/Heading';
import { Spinner } from '@astryxdesign/core/Spinner';
import { EmptyState } from '@astryxdesign/core/EmptyState';
import {
Table, TableHeader, TableHeaderCell, TableBody, TableRow, TableCell,
} from '@astryxdesign/core/Table';
interface OrdersViewProps {
searchQuery?: string;
@@ -357,12 +374,28 @@ export default function OrdersView({
</React.Fragment>
))}
</div>
{/* PHASE 6 — two bare `<input type="date">` become DateInputs. Both
were unlabelled, so a screen reader announced only "date"; and the
native picker was the only way in, with no keyboard entry format
hint. The min/max cross-guard between the two is preserved. */}
<div className="flex items-center gap-2 text-xs">
<input type="date" value={fromdate} max={todate} onChange={(e) => setScope({ from: e.target.value })}
className="rounded-full outline-none font-semibold" style={{ padding: '6px 12px', border: `1px solid ${BORDER}`, background: '#fff', color: TEXT_2 }} />
<DateInput
label="From date"
isLabelHidden
size="sm"
value={fromdate as ISODateString}
max={todate as ISODateString}
onChange={(v) => { if (v) setScope({ from: v }); }}
/>
<span style={{ color: TEXT_3 }}>→</span>
<input type="date" value={todate} min={fromdate} onChange={(e) => setScope({ to: e.target.value })}
className="rounded-full outline-none font-semibold" style={{ padding: '6px 12px', border: `1px solid ${BORDER}`, background: '#fff', color: TEXT_2 }} />
<DateInput
label="To date"
isLabelHidden
size="sm"
value={todate as ISODateString}
min={fromdate as ISODateString}
onChange={(v) => { if (v) setScope({ to: v }); }}
/>
</div>
</div>
</FilterBar>
@@ -386,27 +419,28 @@ export default function OrdersView({
</div>
<div className="flex items-center gap-2 w-full lg:w-auto lg:shrink-0">
{branches.length > 1 && (
<select
value={branch}
onChange={(e) => { setBranch(Number(e.target.value)); setPageno(1); }}
title="Filter by branch / app-location"
className="rounded-full font-bold text-xs outline-none cursor-pointer shrink-0"
style={{ padding: '7px 12px', border: `1.5px solid ${edge(BRAND)}`, background: tint(BRAND), color: BRAND }}
>
<option value={0}>All branches</option>
{branches.map((b) => <option key={b.id} value={b.id}>{b.name}</option>)}
</select>
<div className="shrink-0 min-w-[150px]">
<Selector
label="Filter by branch"
isLabelHidden
size="sm"
value={String(branch)}
onChange={(v) => { setBranch(Number(v) || 0); setPageno(1); }}
options={[
{ value: '0', label: 'All branches' },
...branches.map((b) => ({ value: String(b.id), label: b.name })),
]}
/>
</div>
)}
<div className="w-full lg:w-60"><SearchPill value={localSearch} onChange={setLocalSearch} placeholder="Search orders (Ctrl+K)…" inputRef={searchRef} /></div>
<button
<Button
label="CSV"
size="sm"
icon={<Download size={13} />}
isDisabled={rows.length === 0}
onClick={exportCsv}
disabled={rows.length === 0}
title="Export current view to CSV"
className="inline-flex items-center gap-1.5 rounded-full font-extrabold text-white cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed whitespace-nowrap shrink-0"
style={{ padding: '8px 14px', fontSize: 12, background: `linear-gradient(135deg, ${BRAND}, ${BRAND_LIGHT})`, boxShadow: `0 6px 18px ${ring(BRAND)}` }}
>
<Download size={13} /> CSV
</button>
/>
</div>
</div>
</FilterBar>
@@ -417,131 +451,170 @@ export default function OrdersView({
<span className="inline-flex items-center gap-1.5 font-extrabold text-xs" style={{ color: BRAND }}>
<UserCheck size={15} /> {selected.size} selected
</span>
<div className="flex bg-white rounded-full p-0.5 ml-2 border" style={{ borderColor: edge(BRAND) }}>
<button
onClick={() => setRiderSource('all')}
title="Every rider on duty at this branch right now"
className={`px-3 py-1 text-[11px] font-bold rounded-full transition-colors ${riderSource === 'all' ? '' : 'text-slate-500 hover:bg-slate-50'}`}
style={riderSource === 'all' ? { background: tint(BRAND), color: BRAND } : undefined}
{/* PHASE 6 — the two-button rider-source toggle is a SegmentedControl.
It was a pair of buttons that looked like a segmented control but
announced as two unrelated buttons with no indication which was
active; the real one exposes the selection to assistive tech. */}
<div className="ml-2">
<SegmentedControl
label="Rider source"
size="sm"
value={riderSource}
onChange={(v) => setRiderSource(v as 'all' | 'partner')}
>
On Duty
</button>
<button
onClick={() => setRiderSource('partner')}
disabled={!orderPartnerId}
title={orderPartnerId ? 'Only riders from the partner carrying these orders' : 'These orders name no partner'}
className={`px-3 py-1 text-[11px] font-bold rounded-full transition-colors disabled:opacity-40 ${riderSource === 'partner' ? '' : 'text-slate-500 hover:bg-slate-50'}`}
style={riderSource === 'partner' ? { background: tint(BRAND), color: BRAND } : undefined}
>
This Partner
</button>
<SegmentedControlItem value="all" label="On Duty" />
<SegmentedControlItem value="partner" label="This Partner" isDisabled={!orderPartnerId} />
</SegmentedControl>
</div>
<select
value={assignRiderId}
onChange={(e) => setAssignRiderId(Number(e.target.value))}
disabled={selected.size === 0}
title="Choose a rider to assign"
className="rounded-full font-bold text-xs outline-none cursor-pointer disabled:opacity-50"
style={{ padding: '7px 12px', border: `1.5px solid ${edge(BRAND)}`, background: '#fff', color: BRAND, maxWidth: 260 }}
>
<option value={0}>
{ridersQ.isLoading
? 'Loading riders…'
: riderOptions.length
? 'Select rider…'
: riderSource === 'partner'
? 'No riders on duty for this partner'
: 'No riders on duty right now'}
</option>
{riderOptions.map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
</select>
<button
{/* The empty-list wording carried real information — which of the two
scopes came back empty — so it moves to `placeholder` rather than
being flattened into a generic "Select…". */}
<div className="min-w-[200px] max-w-[260px]">
<Selector
label="Choose a rider to assign"
isLabelHidden
size="sm"
value={assignRiderId ? String(assignRiderId) : ''}
onChange={(v) => setAssignRiderId(Number(v) || 0)}
isDisabled={selected.size === 0}
isLoading={ridersQ.isLoading}
placeholder={
ridersQ.isLoading
? 'Loading riders…'
: riderOptions.length
? 'Select rider…'
: riderSource === 'partner'
? 'No riders on duty for this partner'
: 'No riders on duty right now'
}
options={riderOptions.map((o) => ({ value: String(o.id), label: o.label }))}
/>
</div>
<Button
label="Assign rider"
size="sm"
icon={<UserCheck size={13} />}
isLoading={assignMut.isPending}
isDisabled={!assignRiderId || selected.size === 0 || assignMut.isPending}
onClick={handleAssign}
disabled={!assignRiderId || selected.size === 0 || assignMut.isPending}
className="inline-flex items-center gap-1.5 rounded-full font-extrabold text-white cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
style={{ padding: '7px 14px', fontSize: 12, background: `linear-gradient(135deg, ${BRAND}, ${BRAND_LIGHT})`, boxShadow: `0 6px 18px ${ring(BRAND)}` }}
>
{assignMut.isPending ? <Loader2 size={13} className="animate-spin" /> : <UserCheck size={13} />} Assign rider
</button>
/>
{selected.size > 0 && (
<button onClick={() => setSelected(new Set())} className="rounded-full font-bold text-xs cursor-pointer" style={{ padding: '7px 12px', border: `1px solid ${BORDER}`, background: '#fff', color: TEXT_2 }}>
Clear
</button>
<Button label="Clear" variant="secondary" size="sm" onClick={() => setSelected(new Set())} />
)}
{assignMsg && <span className="text-[11px] font-semibold ml-auto" style={{ color: TEXT_2 }}>{assignMsg}</span>}
</div>
)}
{/* Table */}
<div className="bg-white border overflow-hidden" style={{ borderColor: BORDER }}>
{/* Table
PHASE 6 — raw <table> onto the Astryx primitives, and the three bare
`<input type="checkbox">` onto CheckboxInput. The row boxes carried an
aria-label but no visible label and no focus ring beyond the browser
default; the disabled one explained itself only through `title`, which
a keyboard user never sees. */}
<ShellCard className="overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full" style={{ minWidth: 960 }}>
<thead>
<tr>
<th className="px-3 py-2.5 text-left" style={TH_STYLE}>
<input type="checkbox" checked={allSelected} onChange={toggleAll} disabled={assignableRows.length === 0} aria-label="Select all assignable orders" style={{ accentColor: BRAND, cursor: 'pointer', width: 15, height: 15 }} />
</th>
<Table style={{ minWidth: 960 }}>
<TableHeader>
<TableRow>
<TableHeaderCell>
<CheckboxInput
label="Select all assignable orders"
isLabelHidden
value={allSelected}
isDisabled={assignableRows.length === 0}
onChange={toggleAll}
/>
</TableHeaderCell>
{['#', 'Order', 'Branch', 'Pickup', 'Drop', 'Qty', 'COD', 'KMs', 'Charges', 'Status', ''].map((h, i) => (
<th key={i} className="px-3 py-2.5 text-left" style={TH_STYLE}>{h}</th>
<TableHeaderCell key={i}>{h}</TableHeaderCell>
))}
</tr>
</thead>
<tbody>
</TableRow>
</TableHeader>
<TableBody>
{ordersQ.isLoading ? (
<tr><td colSpan={12} className="px-3 py-12 text-center" style={{ color: TEXT_3 }}>
<span className="inline-flex items-center gap-2 text-xs font-semibold"><Loader2 size={15} className="animate-spin" style={{ color: BRAND }} /> Loading orders…</span>
</td></tr>
<TableRow><TableCell colSpan={12}>
<div className="py-8 flex justify-center"><Spinner label="Loading orders…" /></div>
</TableCell></TableRow>
) : rows.length === 0 ? (
<tr><td colSpan={12} className="px-3 py-12 text-center text-xs" style={{ color: TEXT_3 }}>No orders found for this status, date range, or search.</td></tr>
<TableRow><TableCell colSpan={12}>
<EmptyState
isCompact
icon={<Package size={26} />}
title="No orders found"
description="Nothing matches this status, date range, or search."
/>
</TableCell></TableRow>
) : (
rows.map((r, i) => {
const st = fstr(r.orderstatus).toLowerCase();
const cod = fnum(r.collectionamt);
const charges = fnum(r.deliverycharge) || fnum(r.deliverycharges);
const isSel = selected.has(rowKey(r));
return (
<tr key={fstr(r.orderid) || i} className="transition-colors" style={{ borderBottom: `1px solid ${DIVIDER_C}`, background: selected.has(rowKey(r)) ? tint(BRAND) : 'transparent' }}
onMouseEnter={(e) => { if (!selected.has(rowKey(r))) e.currentTarget.style.background = SURFACE_ALT; }} onMouseLeave={(e) => { e.currentTarget.style.background = selected.has(rowKey(r)) ? tint(BRAND) : 'transparent'; }}>
<td className="px-3 py-2.5">
// `rowKey`, not `orderid`. `orderid` is NOT unique — this
// tenant returns several rows sharing one (three "1135-1" on
// the store console alone), so React logged a duplicate-key
// error for most of the page and was free to duplicate or
// drop rows. `rowKey` prefers `orderheaderid`, the actual
// primary key, and is already what the selection Set uses —
// so row identity and checkbox identity now agree instead of
// drifting apart on re-render.
//
// The selected tint stays inline: it is a brand wash keyed to
// selection state, which Table has no prop for.
<TableRow key={rowKey(r) || i} style={isSel ? { background: tint(BRAND) } : undefined}>
<TableCell>
{st === 'created' ? (
<input type="checkbox" checked={selected.has(rowKey(r))} onChange={() => toggleRow(rowKey(r))} aria-label="Select order" style={{ accentColor: BRAND, cursor: 'pointer', width: 15, height: 15 }} />
<CheckboxInput
label="Select order"
isLabelHidden
value={isSel}
onChange={() => toggleRow(rowKey(r))}
/>
) : (
<input type="checkbox" disabled aria-label="Order cannot be assigned" style={{ width: 15, height: 15, opacity: 0.3 }} title="Only 'created' orders can be assigned" />
<CheckboxInput
label="Only 'created' orders can be assigned"
isLabelHidden
value={false}
isDisabled
/>
)}
</td>
<td className="px-3 py-2.5 font-mono" style={{ color: TEXT_3 }}>{(pageno - 1) * PAGE_SIZE + i + 1}</td>
<td className="px-3 py-2.5">
<p className="font-extrabold font-mono text-[13px]" style={{ color: TEXT }}>{fstr(r.orderid) || `#${fstr(r.orderheaderid)}`}</p>
</TableCell>
<TableCell><span className="font-mono" style={{ color: TEXT_3 }}>{(pageno - 1) * PAGE_SIZE + i + 1}</span></TableCell>
<TableCell>
<p className="font-extrabold font-mono text-[13px]">{fstr(r.orderid) || `#${fstr(r.orderheaderid)}`}</p>
<p className="text-[10px]" style={{ color: TEXT_2 }}>{shortTime(r.orderdate || r.deliverydate)}</p>
</td>
<td className="px-3 py-2.5">
</TableCell>
<TableCell>
<span className="inline-flex items-center gap-1 font-bold text-[12px]" style={{ color: BRAND }}>
<MapPin size={11} /> {fstr(r.applocation) || '—'}
</span>
{fstr(r.locationname) && <p className="text-[10px] truncate max-w-[130px]" style={{ color: TEXT_2 }}>{fstr(r.locationname)}</p>}
</td>
<td className="px-3 py-2.5">
<p className="font-bold text-[12px] truncate max-w-[150px]" style={{ color: TEXT }}>{fstr(r.pickupcustomer) || fstr(r.tenantname) || '—'}</p>
</TableCell>
<TableCell>
<p className="font-bold text-[12px] truncate max-w-[150px]">{fstr(r.pickupcustomer) || fstr(r.tenantname) || '—'}</p>
<p className="text-[10px] truncate max-w-[150px]" style={{ color: TEXT_2 }}>{fstr(r.pickupsuburb) || fstr(r.pickupaddress)}</p>
</td>
<td className="px-3 py-2.5">
<p className="font-bold text-[12px] truncate max-w-[150px]" style={{ color: TEXT }}>{fstr(r.deliverycustomer) || '—'}</p>
</TableCell>
<TableCell>
<p className="font-bold text-[12px] truncate max-w-[150px]">{fstr(r.deliverycustomer) || '—'}</p>
<p className="text-[10px] truncate max-w-[150px]" style={{ color: TEXT_2 }}>{fstr(r.deliverysuburb) || fstr(r.deliveryaddress)}</p>
</td>
<td className="px-3 py-2.5 font-mono text-[12px]" style={{ color: TEXT }}>{fnum(r.quantity) || '—'}</td>
<td className="px-3 py-2.5 font-mono text-[12px] font-semibold" style={{ color: cod > 0 ? TEXT : TEXT_3 }}>{cod > 0 ? `₹${cod.toLocaleString('en-IN')}` : '—'}</td>
<td className="px-3 py-2.5 font-mono text-[12px]" style={{ color: fnum(r.kms) ? TEXT_2 : TEXT_3 }}>{fnum(r.kms) ? fnum(r.kms).toFixed(1) : '—'}</td>
<td className="px-3 py-2.5 font-mono text-[12px] font-semibold" style={{ color: charges > 0 ? TEXT : TEXT_3 }}>{charges > 0 ? `₹${charges.toLocaleString('en-IN')}` : '—'}</td>
<td className="px-3 py-2.5"><StatusChip label={st || '—'} color={statusColor(ORDER_STATUS, st)} /></td>
<td className="px-3 py-2.5 text-right">
<button onClick={() => setDetailOrder(r)} className="rounded-full font-extrabold cursor-pointer transition-colors"
style={{ padding: '4px 12px', fontSize: 11, color: BRAND, background: tint(BRAND), border: `1px solid ${edge(BRAND)}` }}>View</button>
</td>
</tr>
</TableCell>
<TableCell><span className="font-mono text-[12px]">{fnum(r.quantity) || '—'}</span></TableCell>
<TableCell><span className="font-mono text-[12px] font-semibold" style={cod > 0 ? undefined : { color: TEXT_3 }}>{cod > 0 ? `₹${cod.toLocaleString('en-IN')}` : '—'}</span></TableCell>
<TableCell><span className="font-mono text-[12px]" style={{ color: fnum(r.kms) ? TEXT_2 : TEXT_3 }}>{fnum(r.kms) ? fnum(r.kms).toFixed(1) : '—'}</span></TableCell>
<TableCell><span className="font-mono text-[12px] font-semibold" style={charges > 0 ? undefined : { color: TEXT_3 }}>{charges > 0 ? `₹${charges.toLocaleString('en-IN')}` : '—'}</span></TableCell>
<TableCell><StatusChip label={st || '—'} color={statusColor(ORDER_STATUS, st)} /></TableCell>
<TableCell>
<div className="text-right">
<Button label="View" variant="secondary" size="sm" onClick={() => setDetailOrder(r)} />
</div>
</TableCell>
</TableRow>
);
})
)}
</tbody>
</table>
</TableBody>
</Table>
</div>
{/* Totals across the filtered rows */}
{rows.length > 0 && (
@@ -554,12 +627,30 @@ export default function OrdersView({
)}
<div className="flex items-center justify-between px-4 py-3 border-t" style={{ borderColor: BORDER, background: SURFACE_ALT }}>
<span className="text-[10px] font-bold uppercase tracking-wider" style={{ color: TEXT_2 }}>Page {pageno} · {rows.length} shown</span>
{/* Astryx `Pagination` wants a known page count; this list is
cursor-shaped (hasNext is inferred from a full page), so Prev/Next
stay two Buttons rather than a pager that would have to invent a
total it does not have. */}
<div className="flex items-center gap-2">
<PagerBtn disabled={pageno === 1} onClick={() => setPageno((p) => Math.max(1, p - 1))}><ChevronLeft size={13} /> Prev</PagerBtn>
<PagerBtn disabled={!hasNext} onClick={() => setPageno((p) => p + 1)}>Next <ChevronRight size={13} /></PagerBtn>
<Button
label="Prev"
variant="secondary"
size="sm"
icon={<ChevronLeft size={13} />}
isDisabled={pageno === 1}
onClick={() => setPageno((p) => Math.max(1, p - 1))}
/>
<Button
label="Next"
variant="secondary"
size="sm"
endContent={<ChevronRight size={13} />}
isDisabled={!hasNext}
onClick={() => setPageno((p) => p + 1)}
/>
</div>
</div>
</div>
</ShellCard>
{detailOrder && <OrderDetailModal order={detailOrder} onClose={() => setDetailOrder(null)} />}
@@ -568,38 +659,37 @@ export default function OrdersView({
Opens the full-page review/assign view on click. */}
{status === 'created' && selected.size > 1 && !showSelected &&
createPortal(
<button
onClick={() => setShowSelected(true)}
title={`Review & assign ${selected.size} selected order${selected.size === 1 ? '' : 's'}`}
className="group fixed right-0 z-[150] flex items-center gap-2 py-3 pl-4 pr-5 text-white font-extrabold text-xs cursor-pointer transition-all duration-200 hover:pr-7 animate-in slide-in-from-right-4"
style={{ top: '70%', background: `linear-gradient(135deg, ${BRAND}, ${BRAND_LIGHT})`, borderTopLeftRadius: 9999, borderBottomLeftRadius: 9999, boxShadow: `0 10px 30px ${ring(BRAND)}` }}
>
<span className="relative inline-flex">
<ClipboardList size={18} />
<span className="absolute -top-2.5 -right-2.5 min-w-[17px] h-[17px] px-1 rounded-full bg-rose-500 text-[9px] font-black flex items-center justify-center ring-2 ring-white">{selected.size}</span>
</span>
<span className="max-w-0 overflow-hidden group-hover:max-w-[80px] transition-all duration-200 whitespace-nowrap">Review</span>
</button>,
// PHASE 6 — the label used to be collapsed to `max-w-0` and revealed
// only on hover, so the button read as an unlabelled icon until you
// pointed at it and was invisible to anyone not using a mouse. The
// Astryx Button carries the count in its label instead.
<div className="fixed right-4 z-[150] animate-in slide-in-from-right-4" style={{ top: '70%' }}>
<Button
label={`Review ${selected.size} selected`}
icon={<ClipboardList size={16} />}
onClick={() => setShowSelected(true)}
/>
</div>,
document.body,
)}
{showSelected &&
createPortal(
<SelectedOrdersPage
rows={selectedRows}
rowKey={rowKey}
riderOptions={riderOptions}
ridersLoading={ridersQ.isLoading}
assignRiderId={assignRiderId}
setAssignRiderId={setAssignRiderId}
assigning={assignMut.isPending}
assignMsg={assignMsg}
onAssign={handleAssign}
onRemove={(k) => toggleRow(k)}
onClose={() => setShowSelected(false)}
/>,
document.body,
)}
{/* No createPortal any more — Dialog renders in the browser's top layer,
which is already outside every ancestor's containing block. */}
{showSelected && (
<SelectedOrdersPage
rows={selectedRows}
rowKey={rowKey}
riderOptions={riderOptions}
ridersLoading={ridersQ.isLoading}
assignRiderId={assignRiderId}
setAssignRiderId={setAssignRiderId}
assigning={assignMut.isPending}
assignMsg={assignMsg}
onAssign={handleAssign}
onRemove={(k) => toggleRow(k)}
onClose={() => setShowSelected(false)}
/>
)}
</div>
);
}
@@ -623,75 +713,114 @@ function SelectedOrdersPage({
onClose: () => void;
}) {
return (
<div className="fixed inset-0 z-[200] overflow-y-auto animate-in fade-in duration-200" style={{ background: '#f8fafc' }}>
{/* Sticky page header with the assign controls */}
<div className="sticky top-0 z-10 border-b" style={{ background: '#fff', borderColor: BORDER }}>
<div className="max-w-5xl mx-auto px-4 md:px-8 py-4 flex flex-wrap items-center gap-3">
<button onClick={onClose} className="inline-flex items-center gap-1.5 rounded-full font-bold text-xs cursor-pointer" style={{ padding: '8px 14px', border: `1px solid ${BORDER}`, color: TEXT_2, background: '#fff' }}>
<ArrowLeft size={14} /> Back to orders
</button>
<div className="flex items-center gap-2">
<span className="h-9 w-9 rounded-xl flex items-center justify-center" style={{ background: tint(BRAND), color: BRAND }}><ClipboardList size={18} /></span>
<div>
<h1 className="font-bold text-lg tracking-tight leading-none" style={{ color: TEXT }}>Selected Orders</h1>
<p className="text-[11px] mt-1" style={{ color: TEXT_2 }}>{rows.length} order{rows.length === 1 ? '' : 's'} ready to assign</p>
// PHASE 6 — was a `fixed inset-0` page takeover with no Escape and no focus
// trap: tabbing from it walked straight back into the orders board
// underneath. Dialog's fullscreen variant gives both, and keeps the same
// full-bleed shape.
//
// What it does NOT give is focus restore — verified in Chrome: closing this
// leaves focus on <body>, not on the control that opened it. consoleUi's
// SlideDrawer restores it because it does so by hand. That gap is Astryx's,
// it applies to every Dialog in the app, and it is a missing gain rather
// than a regression (the old overlay managed no focus at all), so it is not
// patched here one screen at a time.
<Dialog isOpen onOpenChange={(open) => { if (!open) onClose(); }} variant="fullscreen" purpose="info">
<div className="h-full overflow-y-auto">
{/* Sticky page header with the assign controls */}
<div className="sticky top-0 z-10 border-b" style={{ background: 'var(--color-background-card, #fff)', borderColor: BORDER }}>
<div className="max-w-5xl mx-auto px-4 md:px-8 py-4 flex flex-wrap items-center gap-3">
<Button label="Back to orders" variant="secondary" size="sm" icon={<ArrowLeft size={14} />} onClick={onClose} />
<div className="flex items-center gap-2">
<span className="h-9 w-9 rounded-xl flex items-center justify-center" style={{ background: tint(BRAND), color: BRAND }}><ClipboardList size={18} /></span>
<div>
<Heading level={2}>Selected Orders</Heading>
<p className="text-[11px] mt-1" style={{ color: TEXT_2 }}>{rows.length} order{rows.length === 1 ? '' : 's'} ready to assign</p>
</div>
</div>
<div className="flex items-center gap-2 ml-auto">
<div className="min-w-[200px] max-w-[260px]">
<Selector
label="Choose a rider to assign"
isLabelHidden
size="sm"
value={assignRiderId ? String(assignRiderId) : ''}
onChange={(v) => setAssignRiderId(Number(v) || 0)}
isLoading={ridersLoading}
placeholder={ridersLoading ? 'Loading riders…' : riderOptions.length ? 'Select rider…' : 'No riders available'}
options={riderOptions.map((o) => ({ value: String(o.id), label: o.label }))}
/>
</div>
<Button
label="Assign rider"
size="sm"
icon={<UserCheck size={14} />}
isLoading={assigning}
isDisabled={!assignRiderId || rows.length === 0 || assigning}
onClick={onAssign}
/>
</div>
</div>
<div className="flex items-center gap-2 ml-auto">
<select value={assignRiderId} onChange={(e) => setAssignRiderId(Number(e.target.value))} className="rounded-full font-bold text-xs outline-none cursor-pointer" style={{ padding: '8px 12px', border: `1.5px solid ${edge(BRAND)}`, background: '#fff', color: BRAND, maxWidth: 260 }}>
<option value={0}>{ridersLoading ? 'Loading riders…' : riderOptions.length ? 'Select rider…' : 'No riders available'}</option>
{riderOptions.map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
</select>
<button onClick={onAssign} disabled={!assignRiderId || rows.length === 0 || assigning} className="inline-flex items-center gap-1.5 rounded-full font-extrabold text-white cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed" style={{ padding: '8px 16px', fontSize: 12, background: `linear-gradient(135deg, ${BRAND}, ${BRAND_LIGHT})`, boxShadow: `0 6px 18px ${ring(BRAND)}` }}>
{assigning ? <Loader2 size={14} className="animate-spin" /> : <UserCheck size={14} />} Assign rider
</button>
</div>
</div>
</div>
<div className="max-w-5xl mx-auto px-4 md:px-8 py-6">
{assignMsg && <div className="mb-4 rounded-xl px-4 py-2.5 text-xs font-semibold" style={{ background: tint(BRAND), border: `1px solid ${edge(BRAND)}`, color: BRAND }}>{assignMsg}</div>}
{rows.length === 0 ? (
<div className="bg-white border rounded-2xl p-12 text-center text-xs" style={{ borderColor: BORDER, color: TEXT_3 }}>
No orders selected. <button onClick={onClose} className="font-bold underline cursor-pointer" style={{ color: BRAND }}>Go back</button>
</div>
) : (
<div className="bg-white border overflow-hidden" style={{ borderColor: BORDER }}>
<div className="overflow-x-auto">
<table className="w-full" style={{ minWidth: 720 }}>
<thead><tr>{['#', 'Order', 'Pickup', 'Drop', 'Status', ''].map((h, i) => <th key={i} className="px-3 py-2.5 text-left" style={TH_STYLE}>{h}</th>)}</tr></thead>
<tbody>
{rows.map((r, i) => {
const st = fstr(r.orderstatus).toLowerCase();
return (
<tr key={rowKey(r) || i} style={{ borderBottom: `1px solid ${DIVIDER_C}` }}>
<td className="px-3 py-2.5 font-mono" style={{ color: TEXT_3 }}>{i + 1}</td>
<td className="px-3 py-2.5">
<p className="font-extrabold font-mono text-[13px]" style={{ color: TEXT }}>{fstr(r.orderid) || `#${fstr(r.orderheaderid)}`}</p>
<p className="text-[10px]" style={{ color: TEXT_2 }}>{shortTime(r.orderdate || r.deliverydate)}</p>
</td>
<td className="px-3 py-2.5">
<p className="font-bold text-[12px] truncate max-w-[180px]" style={{ color: TEXT }}>{fstr(r.pickupcustomer) || fstr(r.tenantname) || '—'}</p>
<p className="text-[10px] truncate max-w-[180px]" style={{ color: TEXT_2 }}>{fstr(r.pickupsuburb) || fstr(r.pickupaddress)}</p>
</td>
<td className="px-3 py-2.5">
<p className="font-bold text-[12px] truncate max-w-[180px]" style={{ color: TEXT }}>{fstr(r.deliverycustomer) || '—'}</p>
<p className="text-[10px] truncate max-w-[180px]" style={{ color: TEXT_2 }}>{fstr(r.deliverysuburb) || fstr(r.deliveryaddress)}</p>
</td>
<td className="px-3 py-2.5"><StatusChip label={st || '—'} color={statusColor(ORDER_STATUS, st)} /></td>
<td className="px-3 py-2.5 text-right">
<button onClick={() => onRemove(rowKey(r))} title="Remove from selection" className="p-1 rounded-full cursor-pointer" style={{ color: TEXT_3 }}><X size={15} /></button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
)}
<div className="max-w-5xl mx-auto px-4 md:px-8 py-6">
{assignMsg && <div className="mb-4 rounded-xl px-4 py-2.5 text-xs font-semibold" style={{ background: tint(BRAND), border: `1px solid ${edge(BRAND)}`, color: BRAND }}>{assignMsg}</div>}
{rows.length === 0 ? (
<ShellCard className="p-12">
<EmptyState
title="No orders selected"
actions={<Button label="Go back" variant="secondary" onClick={onClose} />}
/>
</ShellCard>
) : (
<ShellCard className="overflow-hidden">
<div className="overflow-x-auto">
<Table style={{ minWidth: 720 }}>
<TableHeader>
<TableRow>
{['#', 'Order', 'Pickup', 'Drop', 'Status', ''].map((h, i) => <TableHeaderCell key={i}>{h}</TableHeaderCell>)}
</TableRow>
</TableHeader>
<TableBody>
{rows.map((r, i) => {
const st = fstr(r.orderstatus).toLowerCase();
return (
<TableRow key={rowKey(r) || i}>
<TableCell><span className="font-mono" style={{ color: TEXT_3 }}>{i + 1}</span></TableCell>
<TableCell>
<p className="font-extrabold font-mono text-[13px]">{fstr(r.orderid) || `#${fstr(r.orderheaderid)}`}</p>
<p className="text-[10px]" style={{ color: TEXT_2 }}>{shortTime(r.orderdate || r.deliverydate)}</p>
</TableCell>
<TableCell>
<p className="font-bold text-[12px] truncate max-w-[180px]">{fstr(r.pickupcustomer) || fstr(r.tenantname) || '—'}</p>
<p className="text-[10px] truncate max-w-[180px]" style={{ color: TEXT_2 }}>{fstr(r.pickupsuburb) || fstr(r.pickupaddress)}</p>
</TableCell>
<TableCell>
<p className="font-bold text-[12px] truncate max-w-[180px]">{fstr(r.deliverycustomer) || '—'}</p>
<p className="text-[10px] truncate max-w-[180px]" style={{ color: TEXT_2 }}>{fstr(r.deliverysuburb) || fstr(r.deliveryaddress)}</p>
</TableCell>
<TableCell><StatusChip label={st || '—'} color={statusColor(ORDER_STATUS, st)} /></TableCell>
<TableCell>
<div className="text-right">
<IconButton
label={`Remove order ${fstr(r.orderid) || fstr(r.orderheaderid)} from selection`}
variant="ghost"
size="sm"
icon={<X size={15} />}
onClick={() => onRemove(rowKey(r))}
/>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
</ShellCard>
)}
</div>
</div>
</div>
</Dialog>
);
}
@@ -704,16 +833,6 @@ function TotalChip({ label, value, color }: { label: string; value: string; colo
);
}
function PagerBtn({ children, disabled, onClick }: { children: React.ReactNode; disabled?: boolean; onClick: () => void }) {
return (
<button onClick={onClick} disabled={disabled}
className="inline-flex items-center gap-1 rounded-full font-bold transition-colors cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
style={{ padding: '6px 12px', fontSize: 11, border: `1px solid ${BORDER}`, background: '#fff', color: TEXT_2 }}>
{children}
</button>
);
}
// ── Order details modal ─────────────────────────────────────────────────────────
function OrderDetailModal({ order, onClose }: { order: Row; onClose: () => void }) {
const orderheaderid = order.orderheaderid ?? order.orderid;
@@ -726,39 +845,42 @@ function OrderDetailModal({ order, onClose }: { order: Row; onClose: () => void
const st = fstr(order.orderstatus).toLowerCase();
const total = fnum(order.deliveryamt) || fnum(order.orderamount);
// Portal to <body> so the overlay escapes any transformed / blurred / overflow
// ancestor in the view tree — otherwise `fixed inset-0` resolves against that
// ancestor (not the viewport) and the panel collapses to a sliver. The explicit
// viewport-relative width is a belt-and-suspenders so sizing never depends on
// percentage resolution against a broken containing block.
return createPortal(
<div className="fixed inset-0 z-[200] flex items-center justify-center p-4" style={{ background: 'rgba(15,23,42,0.4)', backdropFilter: 'blur(4px)' }}
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
<div className="bg-white max-h-[90vh] flex flex-col overflow-hidden animate-in zoom-in-95 duration-200" style={{ width: 'min(32rem, 92vw)', border: `1px solid ${BORDER}`, boxShadow: '0 18px 50px rgba(15,23,42,0.18)' }}>
<div style={{ height: 4, background: `linear-gradient(90deg, ${BRAND} 0%, ${soft(BRAND)} 100%)` }} />
<div className="p-4 border-b flex justify-between items-center shrink-0" style={{ borderColor: BORDER, background: SURFACE_ALT }}>
<h4 className="font-extrabold flex items-center gap-2" style={{ color: TEXT }}><Package size={16} style={{ color: BRAND }} /> Order {fstr(order.orderid) || `#${fstr(order.orderheaderid)}`}</h4>
<button onClick={onClose} className="p-1 rounded-full cursor-pointer" style={{ color: TEXT_3 }}><X size={16} /></button>
// PHASE 6 — the hand-rolled overlay is gone, and with it the portal and the
// `width: min(32rem, 92vw)` workaround. Both existed for one reason: a
// `fixed inset-0` resolves against the nearest transformed/filtered ancestor
// rather than the viewport, and this page has several, so the panel used to
// collapse to a sliver. Dialog renders in the browser's top layer, which no
// ancestor can contain — so the escape hatches are no longer needed.
return (
<Dialog isOpen onOpenChange={(open) => { if (!open) onClose(); }} width={512} maxHeight="90vh" purpose="info">
<div className="flex flex-col max-h-[90vh]">
<div className="p-4 flex justify-between items-center shrink-0">
<Heading level={3}>
<span className="flex items-center gap-2">
<Package size={16} style={{ color: BRAND }} /> Order {fstr(order.orderid) || `#${fstr(order.orderheaderid)}`}
</span>
</Heading>
<IconButton label="Close" variant="ghost" size="sm" icon={<X size={16} />} onClick={onClose} />
</div>
<div className="p-4 space-y-4 overflow-y-auto flex-1">
<div className="flex items-center justify-between">
<StatusChip label={st || '—'} color={statusColor(ORDER_STATUS, st)} />
<span className="text-[11px] font-medium" style={{ color: TEXT_2 }}>{shortTime(order.orderdate || order.deliverydate)}</span>
</div>
<div className="p-3 rounded-xl space-y-1.5" style={{ background: SURFACE_ALT, border: `1px solid ${BORDER}` }}>
<div className="flex items-center gap-2 font-bold" style={{ color: TEXT }}>{fstr(order.deliverycustomer) || 'Customer'}</div>
<div className="p-3 rounded-xl space-y-1.5" style={{ background: 'rgba(127,127,127,0.06)' }}>
<div className="flex items-center gap-2 font-bold">{fstr(order.deliverycustomer) || 'Customer'}</div>
{fstr(order.deliverycontactno) && <div className="flex items-center gap-2 font-mono text-xs" style={{ color: TEXT_2 }}><Phone size={12} /> {fstr(order.deliverycontactno)}</div>}
<div className="flex items-start gap-2 text-xs" style={{ color: TEXT_2 }}><MapPin size={12} className="mt-0.5 shrink-0" /> <span className="leading-relaxed">{fstr(order.deliveryaddress) || fstr(order.deliverysuburb) || 'Address unavailable'}</span></div>
</div>
<div>
<span className="text-[10px] font-extrabold uppercase tracking-wide block mb-2" style={{ color: TEXT_2 }}>Order Items</span>
<div className="rounded-xl p-3" style={{ background: 'rgba(248,250,252,0.6)', border: `1px solid ${BORDER}` }}>
{detailsQ.isLoading && <div className="py-2 flex items-center gap-1.5 text-[11px] font-medium" style={{ color: TEXT_3 }}><Loader2 size={12} className="animate-spin" /> Loading line items…</div>}
<div className="rounded-xl p-3" style={{ background: 'rgba(127,127,127,0.04)' }}>
{detailsQ.isLoading && <div className="py-2 flex justify-center"><Spinner size="sm" label="Loading line items…" /></div>}
{!detailsQ.isLoading && lines.length === 0 && <div className="py-2 text-[11px] font-medium" style={{ color: TEXT_3 }}>No line items returned for this order.</div>}
{lines.map((item, idx) => (
<div key={idx} className="py-2 flex justify-between items-center" style={{ borderTop: idx ? `1px solid ${DIVIDER_C}` : undefined }}>
<div><p className="font-bold text-xs" style={{ color: TEXT }}>{item.name}</p><p className="text-[10px]" style={{ color: TEXT_2 }}>Qty: {item.quantity} × ₹{item.price}</p></div>
<span className="font-extrabold font-mono text-xs" style={{ color: TEXT }}>₹{item.lineTotal.toLocaleString('en-IN')}</span>
<div><p className="font-bold text-xs">{item.name}</p><p className="text-[10px]" style={{ color: TEXT_2 }}>Qty: {item.quantity} × ₹{item.price}</p></div>
<span className="font-extrabold font-mono text-xs">₹{item.lineTotal.toLocaleString('en-IN')}</span>
</div>
))}
{total > 0 && (
@@ -769,13 +891,10 @@ function OrderDetailModal({ order, onClose }: { order: Row; onClose: () => void
</div>
</div>
</div>
<div className="p-3 border-t flex justify-end shrink-0" style={{ borderColor: BORDER, background: SURFACE_ALT }}>
<button onClick={onClose} className="rounded-full font-bold cursor-pointer text-white" style={{ padding: '8px 16px', background: `linear-gradient(135deg, ${BRAND}, ${BRAND_LIGHT_LOCAL})` }}>Close</button>
<div className="p-3 flex justify-end shrink-0">
<Button label="Close" onClick={onClose} />
</div>
</div>
</div>,
document.body,
</Dialog>
);
}
const BRAND_LIGHT_LOCAL = '#9255AB';