Merge pull request 'Refactor/main flow' (#50) from refactor/main_flow into dev
Reviewed-on: #50
This commit was merged in pull request #50.
This commit is contained in:
@@ -14,6 +14,8 @@ public record ShopPOResponse(
|
|||||||
Guid Id,
|
Guid Id,
|
||||||
Guid StoreId,
|
Guid StoreId,
|
||||||
string StoreName,
|
string StoreName,
|
||||||
|
string? StoreAddress,
|
||||||
|
string? StoreContactNumber,
|
||||||
Guid RaisedById,
|
Guid RaisedById,
|
||||||
Guid? InvoiceId,
|
Guid? InvoiceId,
|
||||||
decimal TotalAmount,
|
decimal TotalAmount,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ public class ShopPOService(
|
|||||||
{
|
{
|
||||||
private static ShopPOResponse ToResponse(Models.ShopPO p, Dictionary<Guid, (string Name, string Role)>? users = null) =>
|
private static ShopPOResponse ToResponse(Models.ShopPO p, Dictionary<Guid, (string Name, string Role)>? users = null) =>
|
||||||
new(p.Id, p.FK_StoreId, p.Store?.StoreName ?? string.Empty,
|
new(p.Id, p.FK_StoreId, p.Store?.StoreName ?? string.Empty,
|
||||||
|
p.Store?.StoreAddress, p.Store?.ContactNumber,
|
||||||
p.RaisedById, p.InvoiceId, p.TotalAmount, p.Status,
|
p.RaisedById, p.InvoiceId, p.TotalAmount, p.Status,
|
||||||
p.ConfirmedAt, p.ConfirmedBy, p.FulfilledAt,
|
p.ConfirmedAt, p.ConfirmedBy, p.FulfilledAt,
|
||||||
p.Items.Where(i => i.IsActive).Select(i =>
|
p.Items.Where(i => i.IsActive).Select(i =>
|
||||||
|
|||||||
+293
-105
@@ -20,14 +20,16 @@ import { Checkbox } from '@/components/ui/checkbox'
|
|||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { HugeiconsIcon } from '@hugeicons/react'
|
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 { toast } from '@/components/ui/toast'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
import {
|
import {
|
||||||
shopPos as shopPosApi,
|
shopPos as shopPosApi,
|
||||||
regions as regionsApi,
|
regions as regionsApi,
|
||||||
routes as routesApi,
|
routes as routesApi,
|
||||||
warehouses as warehousesApi,
|
warehouses as warehousesApi,
|
||||||
loadingSheets as loadingSheetsApi,
|
loadingSheets as loadingSheetsApi,
|
||||||
|
stock as stockApi,
|
||||||
getApiError,
|
getApiError,
|
||||||
getPaginatedItems,
|
getPaginatedItems,
|
||||||
} from '@/lib/api/index'
|
} from '@/lib/api/index'
|
||||||
@@ -35,6 +37,26 @@ import type { LoadingSheetResponse, ShopPOResponse, RegionResponse, RouteRespons
|
|||||||
import type { LoadingSheetItemInput } from '@/lib/api/operations'
|
import type { LoadingSheetItemInput } from '@/lib/api/operations'
|
||||||
|
|
||||||
const ALL = '__all__'
|
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 {
|
interface LoadingSheetItemsDialogProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -57,6 +79,8 @@ interface PickedLine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: LoadingSheetItemsDialogProps) {
|
export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: LoadingSheetItemsDialogProps) {
|
||||||
|
const [step, setStep] = useState<1 | 2 | 3>(1)
|
||||||
|
|
||||||
const [regionsList, setRegionsList] = useState<RegionResponse[]>([])
|
const [regionsList, setRegionsList] = useState<RegionResponse[]>([])
|
||||||
const [routesList, setRoutesList] = useState<RouteResponse[]>([])
|
const [routesList, setRoutesList] = useState<RouteResponse[]>([])
|
||||||
const [warehousesList, setWarehousesList] = useState<WarehouseResponse[]>([])
|
const [warehousesList, setWarehousesList] = useState<WarehouseResponse[]>([])
|
||||||
@@ -65,22 +89,27 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa
|
|||||||
const [routeId, setRouteId] = useState('')
|
const [routeId, setRouteId] = useState('')
|
||||||
const [warehouseId, setWarehouseId] = useState('')
|
const [warehouseId, setWarehouseId] = useState('')
|
||||||
const [shopId, setShopId] = useState('')
|
const [shopId, setShopId] = useState('')
|
||||||
const [viewStoreId, setViewStoreId] = useState('')
|
const [expandedStores, setExpandedStores] = useState<Set<string>>(new Set())
|
||||||
|
|
||||||
const [pos, setPos] = useState<ShopPOResponse[]>([])
|
const [pos, setPos] = useState<ShopPOResponse[]>([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [submitting, setSubmitting] = useState(false)
|
const [submitting, setSubmitting] = useState(false)
|
||||||
const [lines, setLines] = useState<Record<string, PickedLine>>({})
|
const [lines, setLines] = useState<Record<string, PickedLine>>({})
|
||||||
|
|
||||||
|
const [stockByItem, setStockByItem] = useState<Record<string, number>>({})
|
||||||
|
const [stockLoading, setStockLoading] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return
|
if (!open) return
|
||||||
|
setStep(1)
|
||||||
setRegionId('')
|
setRegionId('')
|
||||||
setRouteId('')
|
setRouteId('')
|
||||||
setWarehouseId('')
|
setWarehouseId('')
|
||||||
setShopId('')
|
setShopId('')
|
||||||
setViewStoreId('')
|
setExpandedStores(new Set())
|
||||||
setPos([])
|
setPos([])
|
||||||
setLines({})
|
setLines({})
|
||||||
|
setStockByItem({})
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
regionsApi.list({ pageSize: 200 }),
|
regionsApi.list({ pageSize: 200 }),
|
||||||
@@ -108,7 +137,6 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa
|
|||||||
if (!open) return
|
if (!open) return
|
||||||
setLines({})
|
setLines({})
|
||||||
setShopId('')
|
setShopId('')
|
||||||
setViewStoreId('')
|
|
||||||
if (!routeId) {
|
if (!routeId) {
|
||||||
setPos([])
|
setPos([])
|
||||||
return
|
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' }))
|
.catch((err) => toast.add({ title: 'Failed to load Shop POs', description: getApiError(err), type: 'error' }))
|
||||||
.finally(() => setLoading(false))
|
.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 rows = useMemo<PickedLine[]>(() => {
|
||||||
const result: PickedLine[] = []
|
const result: PickedLine[] = []
|
||||||
@@ -164,22 +216,25 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa
|
|||||||
[allShopGroups, shopId]
|
[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 summary = useMemo(() => {
|
||||||
const map = new Map<string, { itemName: string; qty: number; shops: Set<string> }>()
|
const map = new Map<string, { itemId: string; itemName: string; qty: number; available: number; shops: Set<string> }>()
|
||||||
for (const row of rows) {
|
for (const row of selectedRows) {
|
||||||
const line = lines[row.shopPOItemId] ?? row
|
const line = lines[row.shopPOItemId]
|
||||||
if (!line.checked || !(line.quantity > 0)) continue
|
const e = map.get(row.itemId) ?? { itemId: row.itemId, itemName: row.itemName, qty: 0, available: stockByItem[row.itemId] ?? 0, shops: new Set<string>() }
|
||||||
const e = map.get(row.itemId) ?? { itemName: row.itemName, qty: 0, shops: new Set<string>() }
|
|
||||||
e.qty += line.quantity
|
e.qty += line.quantity
|
||||||
e.shops.add(row.storeId)
|
e.shops.add(row.storeId)
|
||||||
map.set(row.itemId, e)
|
map.set(row.itemId, e)
|
||||||
}
|
}
|
||||||
return Array.from(map.values()).sort((a, b) => a.itemName.localeCompare(b.itemName))
|
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 summaryTotal = summary.reduce((s, e) => s + e.qty, 0)
|
||||||
|
const shortItems = summary.filter((e) => e.qty > e.available)
|
||||||
|
|
||||||
function toggleLine(line: PickedLine, checked: boolean) {
|
function toggleLine(line: PickedLine, checked: boolean) {
|
||||||
setLines((prev) => ({ ...prev, [line.shopPOItemId]: { ...line, checked } }))
|
setLines((prev) => ({ ...prev, [line.shopPOItemId]: { ...line, checked } }))
|
||||||
@@ -189,8 +244,17 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa
|
|||||||
setLines((prev) => ({ ...prev, [line.shopPOItemId]: { ...line, quantity } }))
|
setLines((prev) => ({ ...prev, [line.shopPOItemId]: { ...line, quantity } }))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmit() {
|
function toggleExpanded(storeId: string) {
|
||||||
if (!sheet) return
|
setExpandedStores((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
if (next.has(storeId)) next.delete(storeId)
|
||||||
|
else next.add(storeId)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function goNext() {
|
||||||
|
if (step === 1) {
|
||||||
if (!routeId) {
|
if (!routeId) {
|
||||||
toast.add({ title: 'Select a route first', type: 'error' })
|
toast.add({ title: 'Select a route first', type: 'error' })
|
||||||
return
|
return
|
||||||
@@ -199,13 +263,26 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa
|
|||||||
toast.add({ title: 'Select a warehouse first', type: 'error' })
|
toast.add({ title: 'Select a warehouse first', type: 'error' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const selected = rows.filter((r) => (lines[r.shopPOItemId]?.checked ?? false) && lines[r.shopPOItemId].quantity > 0)
|
setStep(2)
|
||||||
if (selected.length === 0) {
|
return
|
||||||
|
}
|
||||||
|
if (step === 2) {
|
||||||
|
if (selectedRows.length === 0) {
|
||||||
toast.add({ title: 'Select at least one item', type: 'error' })
|
toast.add({ title: 'Select at least one item', type: 'error' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
setStep(3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const items: LoadingSheetItemInput[] = selected.map((r) => {
|
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]
|
const line = lines[r.shopPOItemId]
|
||||||
return {
|
return {
|
||||||
fK_ItemId: line.itemId,
|
fK_ItemId: line.itemId,
|
||||||
@@ -247,21 +324,55 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa
|
|||||||
|
|
||||||
if (!sheet) return null
|
if (!sheet) return null
|
||||||
|
|
||||||
|
const selectedWarehouseName = warehousesList.find((w) => w.id === warehouseId)?.name ?? ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||||||
<DialogContent className="overflow-y-auto sm:max-w-3xl">
|
<DialogContent className="overflow-y-auto sm:max-w-3xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Set Loading Sheet Items</DialogTitle>
|
<DialogTitle>Set Loading Sheet Items</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Select a route first. Confirmed Shop POs for that route will then be available to pick
|
{sheet.status === 'Confirmed'
|
||||||
items from.
|
? 'This sheet is already confirmed, so the items you pick are saved on a new planned sheet for the same ride.'
|
||||||
{sheet.status === 'Confirmed' && (
|
: 'Work through the steps below to build the loading sheet.'}
|
||||||
<> This sheet is already confirmed, so the items you pick are saved on a new planned sheet for the same ride.</>
|
|
||||||
)}
|
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-end gap-3">
|
{/* 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>
|
||||||
|
))}
|
||||||
|
</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">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-xs text-muted-foreground">Region</label>
|
<label className="text-xs text-muted-foreground">Region</label>
|
||||||
<Select value={regionId || ALL} onValueChange={(v) => setRegionId(!v || v === ALL ? '' : v)}>
|
<Select value={regionId || ALL} onValueChange={(v) => setRegionId(!v || v === ALL ? '' : v)}>
|
||||||
@@ -291,21 +402,6 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</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">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-xs text-muted-foreground">Warehouse *</label>
|
<label className="text-xs text-muted-foreground">Warehouse *</label>
|
||||||
<Select value={warehouseId} onValueChange={(v) => setWarehouseId(v ?? '')}>
|
<Select value={warehouseId} onValueChange={(v) => setWarehouseId(v ?? '')}>
|
||||||
@@ -320,11 +416,31 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="rounded-xl border">
|
||||||
{!routeId ? (
|
{loading ? (
|
||||||
<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>
|
<p className="px-3 py-6 text-center text-sm text-muted-foreground">Loading…</p>
|
||||||
) : shopGroups.length === 0 ? (
|
) : 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>
|
<p className="px-3 py-6 text-center text-sm text-muted-foreground">No shops with confirmed POs found for this selection.</p>
|
||||||
@@ -333,85 +449,54 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa
|
|||||||
const groupLines = group.rows.map((r) => lines[r.shopPOItemId] ?? r)
|
const groupLines = group.rows.map((r) => lines[r.shopPOItemId] ?? r)
|
||||||
const allChecked = groupLines.every((l) => l.checked)
|
const allChecked = groupLines.every((l) => l.checked)
|
||||||
const someChecked = groupLines.some((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 (
|
return (
|
||||||
<div key={group.storeId} className="flex items-center gap-3 border-t px-3 py-2 first:border-t-0">
|
<div key={group.storeId} className="border-t first:border-t-0">
|
||||||
|
<div className="flex items-center gap-3 px-3 py-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={allChecked}
|
checked={allChecked}
|
||||||
indeterminate={someChecked && !allChecked}
|
indeterminate={someChecked && !allChecked}
|
||||||
|
title="Select all items for this shop"
|
||||||
onCheckedChange={(v) => group.rows.forEach((r) => toggleLine(lines[r.shopPOItemId] ?? r, v === true))}
|
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>
|
<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">
|
<span className="text-xs text-muted-foreground">
|
||||||
{group.poIds.size} PO{group.poIds.size === 1 ? '' : 's'}
|
{group.poIds.size} PO{group.poIds.size === 1 ? '' : 's'}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<span className="hidden text-xs text-muted-foreground sm:inline">
|
||||||
type="button"
|
{expanded ? 'Hide items' : 'View items'}
|
||||||
variant="ghost"
|
</span>
|
||||||
size="icon"
|
<HugeiconsIcon icon={expanded ? ArrowUp01Icon : ArrowDown01Icon} size={16} />
|
||||||
aria-label={`View confirmed PO items for ${group.storeName}`}
|
</button>
|
||||||
onClick={() => setViewStoreId(group.storeId)}
|
|
||||||
>
|
|
||||||
<HugeiconsIcon icon={ViewIcon} size={18} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
{expanded && (
|
||||||
})
|
<div className="overflow-x-auto border-t bg-muted/20">
|
||||||
)}
|
|
||||||
</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">
|
<table className="w-full text-sm">
|
||||||
<tbody>
|
<thead>
|
||||||
{summary.map((e) => (
|
<tr className="text-xs text-muted-foreground">
|
||||||
<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="w-10 px-3 py-2"></th>
|
||||||
<th className="px-3 py-2 text-left font-medium">Item</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-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>
|
<th className="w-32 px-3 py-2 text-right font-medium">Qty to load</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{viewGroup?.rows.map((row) => {
|
{group.rows.map((row) => {
|
||||||
const line = lines[row.shopPOItemId] ?? row
|
const line = lines[row.shopPOItemId] ?? row
|
||||||
|
const available = stockByItem[row.itemId] ?? 0
|
||||||
|
const short = line.checked && line.quantity > available
|
||||||
return (
|
return (
|
||||||
<tr key={row.shopPOItemId} className="border-t">
|
<tr key={row.shopPOItemId} className="border-t">
|
||||||
<td className="px-3 py-2">
|
<td className="px-3 py-2">
|
||||||
@@ -421,17 +506,30 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2">{row.itemName}</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 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">
|
<td className="px-3 py-2">
|
||||||
|
<div className="flex items-center justify-end gap-1">
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
max={row.remainingQty}
|
max={row.remainingQty}
|
||||||
value={line.quantity}
|
value={line.quantity}
|
||||||
disabled={!line.checked}
|
disabled={!line.checked}
|
||||||
|
className={cn('w-24', short && 'border-amber-500 focus-visible:ring-amber-500')}
|
||||||
onChange={(e) => setLineQuantity(row, Number(e.target.value))}
|
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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
@@ -439,12 +537,102 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
)}
|
||||||
<Button type="button" onClick={() => setViewStoreId('')}>Done</Button>
|
</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's
|
||||||
|
in stock at {selectedWarehouseName}. Saving will still work, but confirming this sheet will fail until the
|
||||||
|
quantities are reduced to what'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>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { FieldGroup } from '@/components/ui/field'
|
import { FieldGroup } from '@/components/ui/field'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { toast } from '@/components/ui/toast'
|
import { toast } from '@/components/ui/toast'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
import {
|
import {
|
||||||
shopPos as shopPosApi,
|
shopPos as shopPosApi,
|
||||||
regions as regionsApi,
|
regions as regionsApi,
|
||||||
@@ -53,6 +54,39 @@ import type { LoadingSheetItemInput } from '@/lib/api/operations'
|
|||||||
|
|
||||||
const ALL = '__all__'
|
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 (optional if you're only adding items manually below) and the warehouse every item will be dispatched from.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: 2,
|
||||||
|
label: 'Ride & Assignment',
|
||||||
|
title: 'Which vehicle, and who is populating this sheet?',
|
||||||
|
help: 'Choose the open ride whose vehicle gets loaded, and who is creating this sheet.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: 3,
|
||||||
|
label: 'Items',
|
||||||
|
title: 'What should be loaded from Shop POs?',
|
||||||
|
help: "Expand a shop to tick items and set quantities. If the main warehouse doesn't have enough stock, use \"Take remaining from another warehouse\" to top up the rest.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: 4,
|
||||||
|
label: 'Additional Items',
|
||||||
|
title: "Anything to load that's not on a PO?",
|
||||||
|
help: "Optional — add extra stock for the vehicle that isn't tied to any shop order.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: 5,
|
||||||
|
label: 'Review & Confirm',
|
||||||
|
title: 'Double-check before saving',
|
||||||
|
help: 'This is everything that will be saved to the sheet. Fix anything short before confirming later on the loading sheet list.',
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
interface ExtraAllocation {
|
interface ExtraAllocation {
|
||||||
warehouseId: string
|
warehouseId: string
|
||||||
quantity: number
|
quantity: number
|
||||||
@@ -115,6 +149,8 @@ function LoadingSheetForm() {
|
|||||||
const [rideId, setRideId] = useState('')
|
const [rideId, setRideId] = useState('')
|
||||||
const [populatedByUserId, setPopulatedByUserId] = useState('')
|
const [populatedByUserId, setPopulatedByUserId] = useState('')
|
||||||
|
|
||||||
|
const [step, setStep] = useState<1 | 2 | 3 | 4 | 5>(1)
|
||||||
|
|
||||||
const [viewStoreId, setViewStoreId] = useState('')
|
const [viewStoreId, setViewStoreId] = useState('')
|
||||||
const [itemsList, setItemsList] = useState<ItemSummaryResponse[]>([])
|
const [itemsList, setItemsList] = useState<ItemSummaryResponse[]>([])
|
||||||
const [manualItems, setManualItems] = useState<ManualItem[]>([])
|
const [manualItems, setManualItems] = useState<ManualItem[]>([])
|
||||||
@@ -441,6 +477,34 @@ function LoadingSheetForm() {
|
|||||||
updateExtras(line, line.extras.filter((_, i) => i !== index))
|
updateExtras(line, line.extras.filter((_, i) => i !== index))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goNext() {
|
||||||
|
if (step === 1) {
|
||||||
|
if (!warehouseId) {
|
||||||
|
toast.add({ title: 'Select a warehouse first', type: 'error' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setStep(2)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (step === 2) {
|
||||||
|
if (!rideId) {
|
||||||
|
toast.add({ title: 'Select a ride to load the vehicle', type: 'error' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!populatedByUserId) {
|
||||||
|
toast.add({ title: 'Select who is populating this sheet', type: 'error' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setStep(3)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (step < 5) setStep((s) => (s + 1) as typeof step)
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
setStep((s) => Math.max(1, s - 1) as typeof step)
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
if (!routeId && manualItems.length === 0) {
|
if (!routeId && manualItems.length === 0) {
|
||||||
toast.add({ title: 'Select a route first', type: 'error' })
|
toast.add({ title: 'Select a route first', type: 'error' })
|
||||||
@@ -541,13 +605,43 @@ function LoadingSheetForm() {
|
|||||||
</Button>
|
</Button>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-semibold text-foreground">{isEdit ? 'Edit Loading Sheet' : 'New Loading Sheet'}</h1>
|
<h1 className="text-2xl font-semibold text-foreground">{isEdit ? 'Edit Loading Sheet' : 'New Loading Sheet'}</h1>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">Work through the steps below to build the loading sheet.</p>
|
||||||
Select a route first. Confirmed Shop POs for that route's shops will then be
|
|
||||||
available to pick items from. Pick the ride to load last.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Step indicator */}
|
||||||
|
<div className="flex flex-wrap 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>
|
||||||
|
))}
|
||||||
|
</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 && (
|
||||||
<div className="rounded-2xl border p-6">
|
<div className="rounded-2xl border p-6">
|
||||||
<h3 className="mb-4 text-sm font-medium text-foreground">Route & Warehouse</h3>
|
<h3 className="mb-4 text-sm font-medium text-foreground">Route & Warehouse</h3>
|
||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
@@ -616,7 +710,9 @@ function LoadingSheetForm() {
|
|||||||
</div>
|
</div>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 3 && (
|
||||||
<div className="rounded-2xl border p-6">
|
<div className="rounded-2xl border p-6">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="text-sm font-medium text-foreground">Items</h3>
|
<h3 className="text-sm font-medium text-foreground">Items</h3>
|
||||||
@@ -824,7 +920,9 @@ function LoadingSheetForm() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 4 && (
|
||||||
<div className="rounded-2xl border p-6">
|
<div className="rounded-2xl border p-6">
|
||||||
<div className="mb-4 flex items-center justify-between gap-4">
|
<div className="mb-4 flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -897,7 +995,9 @@ function LoadingSheetForm() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 2 && (
|
||||||
<div className="rounded-2xl border p-6">
|
<div className="rounded-2xl border p-6">
|
||||||
<h3 className="mb-4 text-sm font-medium text-foreground">Ride & Assignment</h3>
|
<h3 className="mb-4 text-sm font-medium text-foreground">Ride & Assignment</h3>
|
||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
@@ -935,7 +1035,9 @@ function LoadingSheetForm() {
|
|||||||
</div>
|
</div>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 5 && (
|
||||||
<div>
|
<div>
|
||||||
<div className="w-full rounded-2xl border p-6">
|
<div className="w-full rounded-2xl border p-6">
|
||||||
<h3 className="text-sm font-medium text-foreground">Loading Sheet Summary</h3>
|
<h3 className="text-sm font-medium text-foreground">Loading Sheet Summary</h3>
|
||||||
@@ -969,15 +1071,31 @@ function LoadingSheetForm() {
|
|||||||
<p className="text-sm font-medium text-foreground">Total Quantity</p>
|
<p className="text-sm font-medium text-foreground">Total Quantity</p>
|
||||||
<p className="text-xl font-semibold text-foreground">{totalQuantity}</p>
|
<p className="text-xl font-semibold text-foreground">{totalQuantity}</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mt-6 flex justify-end gap-2">
|
<div className="flex items-center justify-between rounded-2xl border bg-muted/20 p-4">
|
||||||
<Button type="button" variant="outline" onClick={() => router.push('/operations/loading')}>
|
<div>
|
||||||
|
{step > 1 && (
|
||||||
|
<Button type="button" variant="outline" onClick={goBack}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button type="button" variant="ghost" onClick={() => router.push('/operations/loading')}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
{step < 5 ? (
|
||||||
|
<Button type="button" onClick={goNext}>
|
||||||
|
{step === 1 ? 'Next: Ride & Assignment' : step === 4 ? `Next: Review${(selectedRows.length + manualItems.length) > 0 ? ` (${totalQuantity})` : ''}` : 'Next'}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
<Button type="button" disabled={submitting} onClick={handleSubmit}>
|
<Button type="button" disabled={submitting} onClick={handleSubmit}>
|
||||||
{submitting ? 'Saving…' : isEdit ? 'Save Changes' : 'Create Loading Sheet'}
|
{submitting ? 'Saving…' : isEdit ? 'Save Changes' : 'Create Loading Sheet'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -151,6 +151,20 @@ export function SalesClient() {
|
|||||||
fields: [{ key: 'storeName', primary: true }],
|
fields: [{ key: 'storeName', primary: true }],
|
||||||
minWidth: 180,
|
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',
|
key: 'totalAmount',
|
||||||
title: 'Total',
|
title: 'Total',
|
||||||
@@ -172,7 +186,7 @@ export function SalesClient() {
|
|||||||
{
|
{
|
||||||
key: 'createdBy',
|
key: 'createdBy',
|
||||||
title: 'Created By',
|
title: 'Created By',
|
||||||
fields: ['raisedByName'],
|
fields: ['raisedByName', 'createdAt'],
|
||||||
minWidth: 160,
|
minWidth: 160,
|
||||||
cellRender: (row) => (
|
cellRender: (row) => (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
@@ -180,6 +194,7 @@ export function SalesClient() {
|
|||||||
{row.raisedByRole && (
|
{row.raisedByRole && (
|
||||||
<span className="text-xs text-muted-foreground">{row.raisedByRole}</span>
|
<span className="text-xs text-muted-foreground">{row.raisedByRole}</span>
|
||||||
)}
|
)}
|
||||||
|
<span className="text-xs text-muted-foreground">{fmtDate(row.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -221,12 +236,6 @@ export function SalesClient() {
|
|||||||
<span className="text-sm text-muted-foreground">—</span>
|
<span className="text-sm text-muted-foreground">—</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'createdAt',
|
|
||||||
title: 'Created',
|
|
||||||
fields: [{ key: 'createdAt', transform: (v) => fmtDate(v as string) }],
|
|
||||||
width: 140,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
rowActions: {
|
rowActions: {
|
||||||
actions: () => [
|
actions: () => [
|
||||||
|
|||||||
@@ -639,6 +639,8 @@ export interface ShopPOResponse {
|
|||||||
id: string
|
id: string
|
||||||
storeId: string
|
storeId: string
|
||||||
storeName: string
|
storeName: string
|
||||||
|
storeAddress: string | null
|
||||||
|
storeContactNumber: string | null
|
||||||
raisedById: string
|
raisedById: string
|
||||||
invoiceId: string | null
|
invoiceId: string | null
|
||||||
totalAmount: number
|
totalAmount: number
|
||||||
|
|||||||
Reference in New Issue
Block a user