diff --git a/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheet-items-dialog.tsx b/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheet-items-dialog.tsx new file mode 100644 index 0000000..5d9b929 --- /dev/null +++ b/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheet-items-dialog.tsx @@ -0,0 +1,292 @@ +'use client' + +import { useEffect, useMemo, useState } from 'react' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from '@/components/ui/dialog' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { Checkbox } from '@/components/ui/checkbox' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import { toast } from '@/components/ui/toast' +import { + shopPos as shopPosApi, + regions as regionsApi, + routes as routesApi, + warehouses as warehousesApi, + loadingSheets as loadingSheetsApi, + getApiError, + getPaginatedItems, +} from '@/lib/api/index' +import type { LoadingSheetResponse, ShopPOResponse, RegionResponse, RouteResponse, WarehouseResponse } from '@/types/api' +import type { LoadingSheetItemInput } from '@/lib/api/operations' + +const ALL = '__all__' + +interface LoadingSheetItemsDialogProps { + open: boolean + onClose: () => void + onSuccess: () => void + sheet: LoadingSheetResponse | null +} + +interface PickedLine { + shopPOItemId: string + itemId: string + itemName: string + storeName: string + orderedQty: number + remainingQty: number + checked: boolean + quantity: number +} + +export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: LoadingSheetItemsDialogProps) { + const [regionsList, setRegionsList] = useState([]) + const [routesList, setRoutesList] = useState([]) + const [warehousesList, setWarehousesList] = useState([]) + + const [regionId, setRegionId] = useState('') + const [routeId, setRouteId] = useState('') + const [warehouseId, setWarehouseId] = useState('') + + const [pos, setPos] = useState([]) + const [loading, setLoading] = useState(false) + const [submitting, setSubmitting] = useState(false) + const [lines, setLines] = useState>({}) + + useEffect(() => { + if (!open) return + setRegionId('') + setRouteId('') + setWarehouseId('') + setPos([]) + setLines({}) + + Promise.all([ + regionsApi.list({ pageSize: 200 }), + warehousesApi.list({ pageSize: 200 }), + ]) + .then(([regionsRes, warehousesRes]) => { + setRegionsList(getPaginatedItems(regionsRes)) + setWarehousesList(getPaginatedItems(warehousesRes)) + }) + .catch((err) => toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' })) + }, [open]) + + useEffect(() => { + if (!open) return + setRouteId('') + routesApi.list({ regionId: regionId || undefined, pageSize: 200 }) + .then((res) => setRoutesList(getPaginatedItems(res))) + .catch((err) => toast.add({ title: 'Failed to load routes', description: getApiError(err), type: 'error' })) + }, [open, regionId]) + + useEffect(() => { + if (!open) return + setLoading(true) + shopPosApi.list({ regionId: regionId || undefined, routeId: routeId || undefined, confirmed: true, pageSize: 200 }) + .then((res) => { + const items = getPaginatedItems(res).filter((po) => po.status === 'Pending' || po.status === 'PartiallyFulfilled') + setPos(items) + }) + .catch((err) => toast.add({ title: 'Failed to load Shop POs', description: getApiError(err), type: 'error' })) + .finally(() => setLoading(false)) + }, [open, regionId, routeId]) + + const rows = useMemo(() => { + const result: PickedLine[] = [] + for (const po of pos) { + for (const item of po.items) { + const remaining = item.quantity - item.quantityFulfilled + if (remaining <= 0) continue + result.push( + lines[item.id] ?? { + shopPOItemId: item.id, + itemId: item.itemId, + itemName: item.itemName, + storeName: po.storeName, + orderedQty: item.quantity, + remainingQty: remaining, + checked: false, + quantity: remaining, + } + ) + } + } + return result + }, [pos, lines]) + + function toggleLine(line: PickedLine, checked: boolean) { + setLines((prev) => ({ ...prev, [line.shopPOItemId]: { ...line, checked } })) + } + + function setLineQuantity(line: PickedLine, quantity: number) { + setLines((prev) => ({ ...prev, [line.shopPOItemId]: { ...line, quantity } })) + } + + async function handleSubmit() { + if (!sheet) return + if (!warehouseId) { + toast.add({ title: 'Select a warehouse first', type: 'error' }) + return + } + const selected = rows.filter((r) => (lines[r.shopPOItemId]?.checked ?? false)) + if (selected.length === 0) { + toast.add({ title: 'Select at least one item', type: 'error' }) + return + } + + const items: LoadingSheetItemInput[] = selected.map((r) => { + const line = lines[r.shopPOItemId] + return { + fK_ItemId: line.itemId, + fK_WarehouseId: warehouseId, + quantity: line.quantity, + fK_ShopPOItemId: line.shopPOItemId, + } + }) + + setSubmitting(true) + try { + await loadingSheetsApi.updateItems(sheet.id, items) + toast.add({ title: 'Loading sheet items saved', type: 'success' }) + onSuccess() + onClose() + } catch (err) { + toast.add({ title: 'Failed to save items', description: getApiError(err), type: 'error' }) + } finally { + setSubmitting(false) + } + } + + if (!sheet) return null + + return ( + !v && onClose()}> + + + Set Loading Sheet Items + + Pick items from confirmed Shop POs to load onto the vehicle for this ride. Narrow the list + by region/route first. + + + +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + + + + + + + + + + + {loading ? ( + + ) : rows.length === 0 ? ( + + ) : ( + rows.map((row) => { + const line = lines[row.shopPOItemId] ?? row + return ( + + + + + + + + ) + }) + )} + +
StoreItemOrderedQty to load
Loading…
No eligible Shop PO items found for this filter.
+ toggleLine(row, v === true)} + /> + {row.storeName}{row.itemName}{row.remainingQty} + setLineQuantity(row, Number(e.target.value))} + /> +
+
+ + + + + +
+
+ ) +} diff --git a/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheets-client.tsx b/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheets-client.tsx index 75ed717..b58ff27 100644 --- a/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheets-client.tsx +++ b/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheets-client.tsx @@ -16,13 +16,14 @@ import { SelectValue, } from '@/components/ui/select' import { toast } from '@/components/ui/toast' -import { loadingSheets as loadingSheetsApi, rides as ridesApi, getApiError, getPaginatedItems } from '@/lib/api/index' -import type { LoadingSheetResponse, LoadingSheetStatus, RideResponse } from '@/types/api' +import { loadingSheets as loadingSheetsApi, rides as ridesApi, regions as regionsApi, routes as routesApi, deliveryTasks as deliveryTasksApi, getApiError, getPaginatedItems } from '@/lib/api/index' +import type { LoadingSheetResponse, LoadingSheetStatus, RideResponse, RegionResponse, RouteResponse } from '@/types/api' const ALL = '__all__' import { LoadingSheetViewSheet } from './loading-sheet-view-sheet' import { LoadingSheetCreateDialog } from './loading-sheet-create-dialog' +import { LoadingSheetItemsDialog } from './loading-sheet-items-dialog' const fmtDate = (v: string) => new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) @@ -43,18 +44,35 @@ export function LoadingSheetsClient() { const [createOpen, setCreateOpen] = useState(false) const [viewOpen, setViewOpen] = useState(false) + const [itemsOpen, setItemsOpen] = useState(false) const [selectedSheet, setSelectedSheet] = useState(null) const [ridesList, setRidesList] = useState([]) const [rideId, setRideId] = useState('') const [statusFilter, setStatusFilter] = useState('') + const [regionsList, setRegionsList] = useState([]) + const [routesList, setRoutesList] = useState([]) + const [regionId, setRegionId] = useState('') + const [routeId, setRouteId] = useState('') + useEffect(() => { ridesApi.list({ pageSize: 200 }) .then((res) => setRidesList(getPaginatedItems(res))) .catch((err) => toast.add({ title: 'Failed to load rides', description: getApiError(err), type: 'error' })) + + regionsApi.list({ pageSize: 200 }) + .then((res) => setRegionsList(getPaginatedItems(res))) + .catch((err) => toast.add({ title: 'Failed to load regions', description: getApiError(err), type: 'error' })) }, []) + useEffect(() => { + setRouteId('') + routesApi.list({ regionId: regionId || undefined, pageSize: 200 }) + .then((res) => setRoutesList(getPaginatedItems(res))) + .catch((err) => toast.add({ title: 'Failed to load routes', description: getApiError(err), type: 'error' })) + }, [regionId]) + const fetchData = useCallback(async (p: number, ps: number) => { setLoading(true) try { @@ -63,6 +81,8 @@ export function LoadingSheetsClient() { pageSize: ps, rideId: rideId || undefined, status: statusFilter || undefined, + regionId: regionId || undefined, + routeId: routeId || undefined, }) setData(getPaginatedItems(res)) setTotal(res.total) @@ -71,17 +91,19 @@ export function LoadingSheetsClient() { } finally { setLoading(false) } - }, [rideId, statusFilter]) + }, [rideId, statusFilter, regionId, routeId]) useEffect(() => { fetchData(page, pageSize) }, [fetchData, page, pageSize]) - const hasFilters = !!rideId || !!statusFilter + const hasFilters = !!rideId || !!statusFilter || !!regionId || !!routeId function clearFilters() { setRideId('') setStatusFilter('') + setRegionId('') + setRouteId('') } function handleView(row: LoadingSheetResponse) { @@ -89,6 +111,11 @@ export function LoadingSheetsClient() { setViewOpen(true) } + function handleSetItems(row: LoadingSheetResponse) { + setSelectedSheet(row) + setItemsOpen(true) + } + async function handleConfirm(row: LoadingSheetResponse) { try { await loadingSheetsApi.confirm(row.id) @@ -99,6 +126,16 @@ export function LoadingSheetsClient() { } } + async function handleGenerateTasks(row: LoadingSheetResponse) { + try { + const tasks = await deliveryTasksApi.generate(row.fK_RideId) + toast.add({ title: `Generated ${tasks.length} delivery task(s)`, type: 'success' }) + fetchData(page, pageSize) + } catch (err) { + toast.add({ title: 'Failed to generate delivery tasks', description: getApiError(err), type: 'error' }) + } + } + const tableConfig = useMemo>(() => ({ rowKey: 'id', serverSide: true, @@ -165,11 +202,23 @@ export function LoadingSheetsClient() { label: 'View sheet', onClick: handleView, }, + { + action: 'edit', + label: 'Set items', + onClick: handleSetItems, + visible: (target) => target.status === 'Planned', + }, { action: 'confirm', label: 'Confirm & commit stock', onClick: handleConfirm, - visible: (target) => target.status === 'Planned', + visible: (target) => target.status === 'Planned' && target.items.length > 0, + }, + { + action: 'generate', + label: 'Generate delivery tasks', + onClick: handleGenerateTasks, + visible: (target) => target.status === 'Confirmed', }, ], }, @@ -226,6 +275,42 @@ export function LoadingSheetsClient() { +
+ + +
+ +
+ + +
+ {hasFilters && (