conversion done with astryx
This commit is contained in:
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user