This commit is contained in:
2026-09-29 17:07:16 +05:30
parent ef4e0e0873
commit 89c0c17be7
5 changed files with 424 additions and 222 deletions
@@ -20,14 +20,16 @@ import { Checkbox } from '@/components/ui/checkbox'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { HugeiconsIcon } from '@hugeicons/react'
import { ViewIcon } from '@hugeicons/core-free-icons'
import { ArrowDown01Icon, ArrowUp01Icon, Alert02Icon } from '@hugeicons/core-free-icons'
import { toast } from '@/components/ui/toast'
import { cn } from '@/lib/utils'
import {
shopPos as shopPosApi,
regions as regionsApi,
routes as routesApi,
warehouses as warehousesApi,
loadingSheets as loadingSheetsApi,
stock as stockApi,
getApiError,
getPaginatedItems,
} from '@/lib/api/index'
@@ -35,6 +37,26 @@ import type { LoadingSheetResponse, ShopPOResponse, RegionResponse, RouteRespons
import type { LoadingSheetItemInput } from '@/lib/api/operations'
const ALL = '__all__'
const STEPS = [
{
n: 1,
label: 'Route & Warehouse',
title: 'Where is this load coming from?',
help: 'Pick the route this ride will deliver, and the warehouse the vehicle will be loaded from. Every item on this sheet will be dispatched from that one warehouse.',
},
{
n: 2,
label: 'Pick Items',
title: 'What should be loaded?',
help: "Click a shop to open its pending orders, tick the items to load, and adjust quantities if needed. The \"In stock\" column shows what's on hand at the warehouse you picked.",
},
{
n: 3,
label: 'Review & Confirm',
title: 'Double-check before saving',
help: 'This is everything that will be saved to the sheet. Items marked "Short" have more requested than is in stock — confirming the sheet will fail until those are fixed.',
},
] as const
interface LoadingSheetItemsDialogProps {
open: boolean
@@ -57,6 +79,8 @@ interface PickedLine {
}
export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: LoadingSheetItemsDialogProps) {
const [step, setStep] = useState<1 | 2 | 3>(1)
const [regionsList, setRegionsList] = useState<RegionResponse[]>([])
const [routesList, setRoutesList] = useState<RouteResponse[]>([])
const [warehousesList, setWarehousesList] = useState<WarehouseResponse[]>([])
@@ -65,22 +89,27 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa
const [routeId, setRouteId] = useState('')
const [warehouseId, setWarehouseId] = useState('')
const [shopId, setShopId] = useState('')
const [viewStoreId, setViewStoreId] = useState('')
const [expandedStores, setExpandedStores] = useState<Set<string>>(new Set())
const [pos, setPos] = useState<ShopPOResponse[]>([])
const [loading, setLoading] = useState(false)
const [submitting, setSubmitting] = useState(false)
const [lines, setLines] = useState<Record<string, PickedLine>>({})
const [stockByItem, setStockByItem] = useState<Record<string, number>>({})
const [stockLoading, setStockLoading] = useState(false)
useEffect(() => {
if (!open) return
setStep(1)
setRegionId('')
setRouteId('')
setWarehouseId('')
setShopId('')
setViewStoreId('')
setExpandedStores(new Set())
setPos([])
setLines({})
setStockByItem({})
Promise.all([
regionsApi.list({ pageSize: 200 }),
@@ -108,7 +137,6 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa
if (!open) return
setLines({})
setShopId('')
setViewStoreId('')
if (!routeId) {
setPos([])
return
@@ -121,7 +149,31 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa
})
.catch((err) => toast.add({ title: 'Failed to load Shop POs', description: getApiError(err), type: 'error' }))
.finally(() => setLoading(false))
}, [open, routeId])
}, [open, routeId, regionId])
// Stock in the chosen warehouse is fetched once the user moves past step 1, so
// step 2 can show — and warn against — quantities that exceed what's on hand.
useEffect(() => {
if (!open || !warehouseId || step < 2) return
setStockLoading(true)
stockApi.balances({ locationId: warehouseId, locationType: 'Warehouse', pageSize: 1000 })
.then((res) => {
const map: Record<string, number> = {}
for (const b of getPaginatedItems(res)) map[b.itemId] = b.quantity
setStockByItem(map)
})
.catch((err) => toast.add({ title: 'Failed to load warehouse stock', description: getApiError(err), type: 'error' }))
.finally(() => setStockLoading(false))
}, [open, warehouseId, step])
// First-time users land on step 2 with everything collapsed and no obvious next
// move, so the first shop is opened automatically to show what "pick items" means.
useEffect(() => {
if (step !== 2 || pos.length === 0 || expandedStores.size > 0) return
const firstStoreId = pos[0]?.storeId
if (firstStoreId) setExpandedStores(new Set([firstStoreId]))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [step, pos])
const rows = useMemo<PickedLine[]>(() => {
const result: PickedLine[] = []
@@ -164,22 +216,25 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa
[allShopGroups, shopId]
)
const viewGroup = allShopGroups.find((g) => g.storeId === viewStoreId)
const selectedRows = useMemo(
() => rows.filter((r) => (lines[r.shopPOItemId]?.checked ?? false) && lines[r.shopPOItemId].quantity > 0),
[rows, lines]
)
const summary = useMemo(() => {
const map = new Map<string, { itemName: string; qty: number; shops: Set<string> }>()
for (const row of rows) {
const line = lines[row.shopPOItemId] ?? row
if (!line.checked || !(line.quantity > 0)) continue
const e = map.get(row.itemId) ?? { itemName: row.itemName, qty: 0, shops: new Set<string>() }
const map = new Map<string, { itemId: string; itemName: string; qty: number; available: number; shops: Set<string> }>()
for (const row of selectedRows) {
const line = lines[row.shopPOItemId]
const e = map.get(row.itemId) ?? { itemId: row.itemId, itemName: row.itemName, qty: 0, available: stockByItem[row.itemId] ?? 0, shops: new Set<string>() }
e.qty += line.quantity
e.shops.add(row.storeId)
map.set(row.itemId, e)
}
return Array.from(map.values()).sort((a, b) => a.itemName.localeCompare(b.itemName))
}, [rows, lines])
}, [selectedRows, lines, stockByItem])
const summaryTotal = summary.reduce((s, e) => s + e.qty, 0)
const shortItems = summary.filter((e) => e.qty > e.available)
function toggleLine(line: PickedLine, checked: boolean) {
setLines((prev) => ({ ...prev, [line.shopPOItemId]: { ...line, checked } }))
@@ -189,23 +244,45 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa
setLines((prev) => ({ ...prev, [line.shopPOItemId]: { ...line, quantity } }))
}
async function handleSubmit() {
if (!sheet) return
if (!routeId) {
toast.add({ title: 'Select a route first', type: 'error' })
return
}
if (!warehouseId) {
toast.add({ title: 'Select a warehouse first', type: 'error' })
return
}
const selected = rows.filter((r) => (lines[r.shopPOItemId]?.checked ?? false) && lines[r.shopPOItemId].quantity > 0)
if (selected.length === 0) {
toast.add({ title: 'Select at least one item', type: 'error' })
return
}
function toggleExpanded(storeId: string) {
setExpandedStores((prev) => {
const next = new Set(prev)
if (next.has(storeId)) next.delete(storeId)
else next.add(storeId)
return next
})
}
const items: LoadingSheetItemInput[] = selected.map((r) => {
function goNext() {
if (step === 1) {
if (!routeId) {
toast.add({ title: 'Select a route first', type: 'error' })
return
}
if (!warehouseId) {
toast.add({ title: 'Select a warehouse first', type: 'error' })
return
}
setStep(2)
return
}
if (step === 2) {
if (selectedRows.length === 0) {
toast.add({ title: 'Select at least one item', type: 'error' })
return
}
setStep(3)
}
}
function goBack() {
setStep((s) => (s === 3 ? 2 : 1) as 1 | 2)
}
async function handleConfirmSave() {
if (!sheet) return
const items: LoadingSheetItemInput[] = selectedRows.map((r) => {
const line = lines[r.shopPOItemId]
return {
fK_ItemId: line.itemId,
@@ -247,203 +324,314 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa
if (!sheet) return null
const selectedWarehouseName = warehousesList.find((w) => w.id === warehouseId)?.name ?? ''
return (
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
<DialogContent className="overflow-y-auto sm:max-w-3xl">
<DialogHeader>
<DialogTitle>Set Loading Sheet Items</DialogTitle>
<DialogDescription>
Select a route first. Confirmed Shop POs for that route will then be available to pick
items from.
{sheet.status === 'Confirmed' && (
<> This sheet is already confirmed, so the items you pick are saved on a new planned sheet for the same ride.</>
)}
{sheet.status === 'Confirmed'
? 'This sheet is already confirmed, so the items you pick are saved on a new planned sheet for the same ride.'
: 'Work through the steps below to build the loading sheet.'}
</DialogDescription>
</DialogHeader>
<div className="flex flex-wrap items-end gap-3">
<div className="flex flex-col gap-1">
<label className="text-xs text-muted-foreground">Region</label>
<Select value={regionId || ALL} onValueChange={(v) => setRegionId(!v || v === ALL ? '' : v)}>
<SelectTrigger className="w-48">
<SelectValue placeholder="All regions" />
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL}>All regions</SelectItem>
{regionsList.map((r) => (
<SelectItem key={r.id} value={r.id}>{r.name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs text-muted-foreground">Route *</label>
<Select value={routeId} onValueChange={(v) => setRouteId(v ?? '')}>
<SelectTrigger className="w-48">
<SelectValue placeholder="Select route..." />
</SelectTrigger>
<SelectContent>
{routesList.map((r) => (
<SelectItem key={r.id} value={r.id}>{r.routeName}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs text-muted-foreground">Shop</label>
<Select value={shopId || ALL} onValueChange={(v) => setShopId(!v || v === ALL ? '' : v)} disabled={!routeId}>
<SelectTrigger className="w-48">
<SelectValue placeholder="All shops" />
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL}>All shops</SelectItem>
{allShopGroups.map((g) => (
<SelectItem key={g.storeId} value={g.storeId}>{g.storeName}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs text-muted-foreground">Warehouse *</label>
<Select value={warehouseId} onValueChange={(v) => setWarehouseId(v ?? '')}>
<SelectTrigger className="w-52">
<SelectValue placeholder="Select warehouse..." />
</SelectTrigger>
<SelectContent>
{warehousesList.map((w) => (
<SelectItem key={w.id} value={w.id}>{w.name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="rounded-xl border">
{!routeId ? (
<p className="px-3 py-6 text-center text-sm text-muted-foreground">Select a route to see shops with confirmed POs.</p>
) : loading ? (
<p className="px-3 py-6 text-center text-sm text-muted-foreground">Loading…</p>
) : shopGroups.length === 0 ? (
<p className="px-3 py-6 text-center text-sm text-muted-foreground">No shops with confirmed POs found for this selection.</p>
) : (
shopGroups.map((group) => {
const groupLines = group.rows.map((r) => lines[r.shopPOItemId] ?? r)
const allChecked = groupLines.every((l) => l.checked)
const someChecked = groupLines.some((l) => l.checked)
return (
<div key={group.storeId} className="flex items-center gap-3 border-t px-3 py-2 first:border-t-0">
<Checkbox
checked={allChecked}
indeterminate={someChecked && !allChecked}
onCheckedChange={(v) => group.rows.forEach((r) => toggleLine(lines[r.shopPOItemId] ?? r, v === true))}
/>
<span className="min-w-0 flex-1 truncate font-medium">{group.storeName}</span>
<span className="text-xs text-muted-foreground">
{group.poIds.size} PO{group.poIds.size === 1 ? '' : 's'}
</span>
<Button
type="button"
variant="ghost"
size="icon"
aria-label={`View confirmed PO items for ${group.storeName}`}
onClick={() => setViewStoreId(group.storeId)}
>
<HugeiconsIcon icon={ViewIcon} size={18} />
</Button>
</div>
)
})
)}
</div>
<div className="rounded-xl border">
<div className="flex items-center justify-between bg-muted/50 px-3 py-2 text-sm font-medium">
<span>Loading Sheet Summary</span>
<span className="text-muted-foreground">{summary.length} item{summary.length === 1 ? '' : 's'} · {summaryTotal} units</span>
</div>
{summary.length === 0 ? (
<p className="px-3 py-4 text-center text-sm text-muted-foreground">Tick items above to build the summary.</p>
) : (
<table className="w-full text-sm">
<tbody>
{summary.map((e) => (
<tr key={e.itemName} className="border-t first:border-t-0">
<td className="px-3 py-2">{e.itemName}</td>
<td className="px-3 py-2 text-muted-foreground">{e.shops.size} shop{e.shops.size === 1 ? '' : 's'}</td>
<td className="px-3 py-2 text-right font-semibold">{e.qty}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={onClose}>
Cancel
</Button>
<Button type="button" disabled={submitting} onClick={handleSubmit}>
{submitting ? 'Saving…' : 'Save Items'}
</Button>
</DialogFooter>
<Dialog open={!!viewGroup} onOpenChange={(v) => !v && setViewStoreId('')}>
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>{viewGroup?.storeName}</DialogTitle>
<DialogDescription>
Confirmed PO items. Tick the items to load and set the quantity.
</DialogDescription>
</DialogHeader>
<div className="overflow-x-auto rounded-xl border">
<table className="w-full text-sm">
<thead className="bg-muted/50">
<tr>
<th className="w-10 px-3 py-2"></th>
<th className="px-3 py-2 text-left font-medium">Item</th>
<th className="w-24 px-3 py-2 text-left font-medium">PO</th>
<th className="w-24 px-3 py-2 text-right font-medium">Ordered</th>
<th className="w-32 px-3 py-2 text-right font-medium">Qty to load</th>
</tr>
</thead>
<tbody>
{viewGroup?.rows.map((row) => {
const line = lines[row.shopPOItemId] ?? row
return (
<tr key={row.shopPOItemId} className="border-t">
<td className="px-3 py-2">
<Checkbox
checked={line.checked}
onCheckedChange={(v) => toggleLine(row, v === true)}
/>
</td>
<td className="px-3 py-2">{row.itemName}</td>
<td className="px-3 py-2 font-mono text-xs text-muted-foreground">{row.shopPOId.substring(0, 8)}</td>
<td className="px-3 py-2 text-right text-muted-foreground">{row.remainingQty}</td>
<td className="px-3 py-2">
<Input
type="number"
min={1}
max={row.remainingQty}
value={line.quantity}
disabled={!line.checked}
onChange={(e) => setLineQuantity(row, Number(e.target.value))}
/>
</td>
</tr>
)
})}
</tbody>
</table>
{/* Step indicator */}
<div className="flex items-center gap-2 text-xs">
{STEPS.map((s, idx) => (
<div key={s.n} className="flex items-center gap-2">
<button
type="button"
disabled={s.n > step}
onClick={() => s.n < step && setStep(s.n)}
className={cn(
'flex h-6 w-6 items-center justify-center rounded-full border font-semibold transition-colors',
step === s.n
? 'border-primary bg-primary text-primary-foreground'
: step > s.n
? 'border-primary/40 bg-primary/10 text-primary hover:bg-primary/20'
: 'border-muted-foreground/30 text-muted-foreground',
s.n < step && 'cursor-pointer'
)}
>
{step > s.n ? '✓' : s.n}
</button>
<span className={cn(step === s.n ? 'font-medium text-foreground' : 'text-muted-foreground')}>{s.label}</span>
{idx < STEPS.length - 1 && <div className="mx-1 h-px w-8 bg-border" />}
</div>
<DialogFooter>
<Button type="button" onClick={() => setViewStoreId('')}>Done</Button>
</DialogFooter>
</DialogContent>
</Dialog>
))}
</div>
{/* Step title & guidance */}
<div className="rounded-lg bg-muted/40 px-3 py-2">
<p className="text-sm font-medium">{STEPS[step - 1].title}</p>
<p className="mt-0.5 text-xs text-muted-foreground">{STEPS[step - 1].help}</p>
</div>
{/* Step 1: Route & Warehouse */}
{step === 1 && (
<div className="flex flex-wrap items-end gap-3 py-2">
<div className="flex flex-col gap-1">
<label className="text-xs text-muted-foreground">Region</label>
<Select value={regionId || ALL} onValueChange={(v) => setRegionId(!v || v === ALL ? '' : v)}>
<SelectTrigger className="w-48">
<SelectValue placeholder="All regions" />
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL}>All regions</SelectItem>
{regionsList.map((r) => (
<SelectItem key={r.id} value={r.id}>{r.name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs text-muted-foreground">Route *</label>
<Select value={routeId} onValueChange={(v) => setRouteId(v ?? '')}>
<SelectTrigger className="w-48">
<SelectValue placeholder="Select route..." />
</SelectTrigger>
<SelectContent>
{routesList.map((r) => (
<SelectItem key={r.id} value={r.id}>{r.routeName}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs text-muted-foreground">Warehouse *</label>
<Select value={warehouseId} onValueChange={(v) => setWarehouseId(v ?? '')}>
<SelectTrigger className="w-52">
<SelectValue placeholder="Select warehouse..." />
</SelectTrigger>
<SelectContent>
{warehousesList.map((w) => (
<SelectItem key={w.id} value={w.id}>{w.name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
)}
{/* Step 2: Pick Items */}
{step === 2 && (
<div className="flex flex-col gap-3 py-2">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="text-sm text-muted-foreground">
Route <span className="font-medium text-foreground">{routesList.find((r) => r.id === routeId)?.routeName}</span>
{' · '}Warehouse <span className="font-medium text-foreground">{selectedWarehouseName}</span>
</div>
<Select value={shopId || ALL} onValueChange={(v) => setShopId(!v || v === ALL ? '' : v)}>
<SelectTrigger className="w-48">
<SelectValue placeholder="All shops" />
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL}>All shops</SelectItem>
{allShopGroups.map((g) => (
<SelectItem key={g.storeId} value={g.storeId}>{g.storeName}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="rounded-xl border">
{loading ? (
<p className="px-3 py-6 text-center text-sm text-muted-foreground">Loading…</p>
) : shopGroups.length === 0 ? (
<p className="px-3 py-6 text-center text-sm text-muted-foreground">No shops with confirmed POs found for this selection.</p>
) : (
shopGroups.map((group) => {
const groupLines = group.rows.map((r) => lines[r.shopPOItemId] ?? r)
const allChecked = groupLines.every((l) => l.checked)
const someChecked = groupLines.some((l) => l.checked)
const checkedCount = groupLines.filter((l) => l.checked).length
const expanded = expandedStores.has(group.storeId)
return (
<div key={group.storeId} className="border-t first:border-t-0">
<div className="flex items-center gap-3 px-3 py-2">
<Checkbox
checked={allChecked}
indeterminate={someChecked && !allChecked}
title="Select all items for this shop"
onCheckedChange={(v) => group.rows.forEach((r) => toggleLine(lines[r.shopPOItemId] ?? r, v === true))}
/>
<button
type="button"
className="flex min-w-0 flex-1 items-center gap-2 text-left"
onClick={() => toggleExpanded(group.storeId)}
>
<span className="min-w-0 flex-1 truncate font-medium">{group.storeName}</span>
{checkedCount > 0 && (
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
{checkedCount} selected
</span>
)}
<span className="text-xs text-muted-foreground">
{group.poIds.size} PO{group.poIds.size === 1 ? '' : 's'}
</span>
<span className="hidden text-xs text-muted-foreground sm:inline">
{expanded ? 'Hide items' : 'View items'}
</span>
<HugeiconsIcon icon={expanded ? ArrowUp01Icon : ArrowDown01Icon} size={16} />
</button>
</div>
{expanded && (
<div className="overflow-x-auto border-t bg-muted/20">
<table className="w-full text-sm">
<thead>
<tr className="text-xs text-muted-foreground">
<th className="w-10 px-3 py-2"></th>
<th className="px-3 py-2 text-left font-medium">Item</th>
<th className="w-24 px-3 py-2 text-right font-medium">Ordered</th>
<th className="w-28 px-3 py-2 text-right font-medium">In stock</th>
<th className="w-32 px-3 py-2 text-right font-medium">Qty to load</th>
</tr>
</thead>
<tbody>
{group.rows.map((row) => {
const line = lines[row.shopPOItemId] ?? row
const available = stockByItem[row.itemId] ?? 0
const short = line.checked && line.quantity > available
return (
<tr key={row.shopPOItemId} className="border-t">
<td className="px-3 py-2">
<Checkbox
checked={line.checked}
onCheckedChange={(v) => toggleLine(row, v === true)}
/>
</td>
<td className="px-3 py-2">{row.itemName}</td>
<td className="px-3 py-2 text-right text-muted-foreground">{row.remainingQty}</td>
<td className={cn('px-3 py-2 text-right', stockLoading ? 'text-muted-foreground' : available === 0 ? 'text-destructive' : 'text-muted-foreground')}>
{stockLoading ? '…' : available}
</td>
<td className="px-3 py-2">
<div className="flex items-center justify-end gap-1">
<Input
type="number"
min={1}
max={row.remainingQty}
value={line.quantity}
disabled={!line.checked}
className={cn('w-24', short && 'border-amber-500 focus-visible:ring-amber-500')}
onChange={(e) => setLineQuantity(row, Number(e.target.value))}
/>
{short && (
<HugeiconsIcon icon={Alert02Icon} size={16} className="shrink-0 text-amber-500" />
)}
</div>
{short && (
<p className="mt-1 text-right text-[11px] text-amber-600">
Only {available} available — rest stays pending
</p>
)}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)}
</div>
)
})
)}
</div>
<p className="text-xs text-muted-foreground">
{selectedRows.length} item line{selectedRows.length === 1 ? '' : 's'} selected. Expand a shop to tick items and set quantities.
</p>
</div>
)}
{/* Step 3: Review & Confirm */}
{step === 3 && (
<div className="flex flex-col gap-3 py-2">
<div className="text-sm text-muted-foreground">
Route <span className="font-medium text-foreground">{routesList.find((r) => r.id === routeId)?.routeName}</span>
{' · '}Warehouse <span className="font-medium text-foreground">{selectedWarehouseName}</span>
</div>
{shortItems.length > 0 && (
<div className="flex items-start gap-2 rounded-lg border border-amber-500/40 bg-amber-500/10 px-3 py-2 text-xs text-amber-700 dark:text-amber-400">
<HugeiconsIcon icon={Alert02Icon} size={16} className="mt-0.5 shrink-0" />
<span>
{shortItems.length} item{shortItems.length === 1 ? '' : 's'} exceed{shortItems.length === 1 ? 's' : ''} what&apos;s
in stock at {selectedWarehouseName}. Saving will still work, but confirming this sheet will fail until the
quantities are reduced to what&apos;s available — go back to adjust them.
</span>
</div>
)}
<div className="rounded-xl border">
<div className="flex items-center justify-between bg-muted/50 px-3 py-2 text-sm font-medium">
<span>Loading Sheet Summary</span>
<span className="text-muted-foreground">{summary.length} item{summary.length === 1 ? '' : 's'} · {summaryTotal} units</span>
</div>
{summary.length === 0 ? (
<p className="px-3 py-4 text-center text-sm text-muted-foreground">No items selected.</p>
) : (
<table className="w-full text-sm">
<thead>
<tr className="text-xs text-muted-foreground">
<th className="px-3 py-2 text-left font-medium">Item</th>
<th className="px-3 py-2 text-left font-medium">Shops</th>
<th className="px-3 py-2 text-right font-medium">In stock</th>
<th className="px-3 py-2 text-right font-medium">Qty to load</th>
<th className="px-3 py-2 text-right font-medium">Short</th>
</tr>
</thead>
<tbody>
{summary.map((e) => {
const shortQty = Math.max(0, e.qty - e.available)
return (
<tr key={e.itemId} className="border-t">
<td className="px-3 py-2">{e.itemName}</td>
<td className="px-3 py-2 text-muted-foreground">{e.shops.size} shop{e.shops.size === 1 ? '' : 's'}</td>
<td className="px-3 py-2 text-right text-muted-foreground">{e.available}</td>
<td className="px-3 py-2 text-right font-semibold">{e.qty}</td>
<td className={cn('px-3 py-2 text-right', shortQty > 0 ? 'font-medium text-amber-600' : 'text-muted-foreground')}>
{shortQty > 0 ? shortQty : '—'}
</td>
</tr>
)
})}
</tbody>
</table>
)}
</div>
</div>
)}
<DialogFooter className="flex items-center justify-between sm:justify-between">
<div>
{step > 1 && (
<Button type="button" variant="outline" onClick={goBack}>
Back
</Button>
)}
</div>
<div className="flex gap-2">
<Button type="button" variant="ghost" onClick={onClose}>
Cancel
</Button>
{step < 3 ? (
<Button type="button" onClick={goNext}>
{step === 1 ? 'Next: Pick items' : `Next: Review${selectedRows.length > 0 ? ` (${selectedRows.length})` : ''}`}
</Button>
) : (
<Button type="button" disabled={submitting} onClick={handleConfirmSave}>
{submitting ? 'Saving…' : 'Save Items'}
</Button>
)}
</div>
</DialogFooter>
</DialogContent>
</Dialog>
)
@@ -151,6 +151,20 @@ export function SalesClient() {
fields: [{ key: 'storeName', primary: true }],
minWidth: 180,
},
{
key: 'storeDetails',
title: 'Store Contact',
fields: ['storeAddress', 'storeContactNumber'],
minWidth: 200,
cellRender: (row) => (
<div className="flex flex-col">
<span className="text-sm">{row.storeAddress ?? '—'}</span>
{row.storeContactNumber && (
<span className="text-xs text-muted-foreground">{row.storeContactNumber}</span>
)}
</div>
),
},
{
key: 'totalAmount',
title: 'Total',
@@ -172,7 +186,7 @@ export function SalesClient() {
{
key: 'createdBy',
title: 'Created By',
fields: ['raisedByName'],
fields: ['raisedByName', 'createdAt'],
minWidth: 160,
cellRender: (row) => (
<div className="flex flex-col">
@@ -180,6 +194,7 @@ export function SalesClient() {
{row.raisedByRole && (
<span className="text-xs text-muted-foreground">{row.raisedByRole}</span>
)}
<span className="text-xs text-muted-foreground">{fmtDate(row.createdAt)}</span>
</div>
),
},
@@ -221,12 +236,6 @@ export function SalesClient() {
<span className="text-sm text-muted-foreground">—</span>
),
},
{
key: 'createdAt',
title: 'Created',
fields: [{ key: 'createdAt', transform: (v) => fmtDate(v as string) }],
width: 140,
},
],
rowActions: {
actions: () => [
+2
View File
@@ -639,6 +639,8 @@ export interface ShopPOResponse {
id: string
storeId: string
storeName: string
storeAddress: string | null
storeContactNumber: string | null
raisedById: string
invoiceId: string | null
totalAmount: number