diff --git a/distribution_fe/app/(dashboard)/stock/_components/stock-balance-view-sheet.tsx b/distribution_fe/app/(dashboard)/stock/_components/stock-balance-view-sheet.tsx new file mode 100644 index 0000000..0b360b4 --- /dev/null +++ b/distribution_fe/app/(dashboard)/stock/_components/stock-balance-view-sheet.tsx @@ -0,0 +1,58 @@ +'use client' + +import { Badge } from '@/components/ui/badge' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import type { StockBalanceResponse, LocationType } from '@/types/api' + +const LOCATION_TYPE_VARIANT: Record = { + Warehouse: 'default', + Vehicle: 'secondary', + External: 'secondary', +} + +const fmtDateTime = (v: string) => + new Date(v).toLocaleString('en-GB', { + day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', + }) + +interface StockBalanceViewSheetProps { + open: boolean + onClose: () => void + balance: StockBalanceResponse | null +} + +function Row({ label, value }: { label: string; value: React.ReactNode }) { + return ( +
+ {label} + {value} +
+ ) +} + +export function StockBalanceViewSheet({ open, onClose, balance }: StockBalanceViewSheetProps) { + if (!balance) return null + + return ( + !v && onClose()}> + + + Stock Balance Details + + +
+ + + {balance.locationType}} /> + + +
+
+
+ ) +} diff --git a/distribution_fe/app/(dashboard)/stock/_components/stock-balances-client.tsx b/distribution_fe/app/(dashboard)/stock/_components/stock-balances-client.tsx index 10e696b..7249045 100644 --- a/distribution_fe/app/(dashboard)/stock/_components/stock-balances-client.tsx +++ b/distribution_fe/app/(dashboard)/stock/_components/stock-balances-client.tsx @@ -8,6 +8,8 @@ import { Badge } from '@/components/ui/badge' import { toast } from '@/components/ui/toast' import { stock as stockApi, getApiError, getPaginatedItems } from '@/lib/api/index' import type { StockBalanceResponse, LocationType } from '@/types/api' +import { StockBalanceViewSheet } from './stock-balance-view-sheet' +import { StockAdjustDialog } from './stock-adjust-dialog' const fmtDate = (v: string) => new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) @@ -15,6 +17,7 @@ const fmtDate = (v: string) => const LOCATION_TYPE_VARIANT: Record = { Warehouse: 'default', Vehicle: 'secondary', + External: 'secondary', } export function StockBalancesClient() { @@ -23,6 +26,20 @@ export function StockBalancesClient() { const [total, setTotal] = useState(0) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(20) + const [viewing, setViewing] = useState(null) + const [viewOpen, setViewOpen] = useState(false) + const [adjusting, setAdjusting] = useState(null) + const [adjustOpen, setAdjustOpen] = useState(false) + + function openView(row: StockBalanceResponse) { + setViewing(row) + setViewOpen(true) + } + + function openAdjust(row: StockBalanceResponse) { + setAdjusting(row) + setAdjustOpen(true) + } const fetchData = useCallback(async (p: number, ps: number) => { setLoading(true) @@ -55,12 +72,12 @@ export function StockBalancesClient() { minWidth: 200, }, { - key: 'locationId', - title: 'Location ID', - fields: ['locationId'], - width: 160, + key: 'locationName', + title: 'Location', + fields: ['locationName'], + width: 180, cellRender: (row) => ( - {row.locationId.substring(0, 8)} + {row.locationName} ), }, { @@ -88,6 +105,20 @@ export function StockBalancesClient() { width: 140, }, ], + rowActions: { + actions: () => [ + { + action: 'view', + label: 'View balance', + onClick: openView, + }, + { + action: 'edit', + label: 'Adjust stock', + onClick: openAdjust, + }, + ], + }, }), [pageSize]) return ( @@ -111,6 +142,19 @@ export function StockBalancesClient() { setPageSize(p.pageSize) }} /> + + setViewOpen(false)} + balance={viewing} + /> + + setAdjustOpen(false)} + onSuccess={() => fetchData(page, pageSize)} + balance={adjusting} + /> ) } diff --git a/distribution_fe/app/(dashboard)/stock/movements/_components/stock-movement-edit-dialog.tsx b/distribution_fe/app/(dashboard)/stock/movements/_components/stock-movement-edit-dialog.tsx new file mode 100644 index 0000000..6b8bb3f --- /dev/null +++ b/distribution_fe/app/(dashboard)/stock/movements/_components/stock-movement-edit-dialog.tsx @@ -0,0 +1,103 @@ +'use client' + +import { useEffect } from 'react' +import { useForm, zodResolver, z } from '@/lib/form' +import { FormField } from '@/components/shared/form-field' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from '@/components/ui/dialog' +import { FieldGroup } from '@/components/ui/field' +import { toast } from '@/components/ui/toast' +import { stock as stockApi, getApiError } from '@/lib/api/index' +import type { StockMovementResponse } from '@/types/api' + +const schema = z.object({ + reasonCode: z.string().min(1, 'Reason is required'), +}) + +type FormValues = z.infer + +interface StockMovementEditDialogProps { + open: boolean + onClose: () => void + onSuccess: () => void + movement: StockMovementResponse | null +} + +export function StockMovementEditDialog({ open, onClose, onSuccess, movement }: StockMovementEditDialogProps) { + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { reasonCode: '' }, + }) + + useEffect(() => { + if (!open || !movement) return + form.reset({ reasonCode: movement.reasonCode ?? '' }) + }, [open, movement]) + + if (!movement) return null + + async function onSubmit(values: FormValues) { + try { + await stockApi.updateMovementReason(movement!.id, values.reasonCode) + toast.add({ title: 'Reason updated', type: 'success' }) + onSuccess() + onClose() + } catch (err) { + toast.add({ title: 'Failed to update reason', description: getApiError(err), type: 'error' }) + } + } + + return ( + !v && onClose()}> + + + Edit Movement — {movement.itemName} + + Movements are an immutable ledger — quantity, locations, and type can never be changed + once recorded. Only the reason/note can be edited. + + + +
+ + ( + + )} + /> + + + + + + +
+
+
+ ) +} diff --git a/distribution_fe/app/(dashboard)/stock/movements/_components/stock-movement-view-sheet.tsx b/distribution_fe/app/(dashboard)/stock/movements/_components/stock-movement-view-sheet.tsx new file mode 100644 index 0000000..e31fcae --- /dev/null +++ b/distribution_fe/app/(dashboard)/stock/movements/_components/stock-movement-view-sheet.tsx @@ -0,0 +1,68 @@ +'use client' + +import { Badge } from '@/components/ui/badge' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import type { StockMovementResponse, MovementType } from '@/types/api' + +const MOVEMENT_TYPE_VARIANT: Record = { + Sale: 'default', + CustomerReturn: 'secondary', + Transfer: 'secondary', + Damage: 'destructive', + Adjustment: 'secondary', + Load: 'default', + PurchaseReceipt: 'default', + PurchaseReturn: 'secondary', +} + +const fmtDateTime = (v: string) => + new Date(v).toLocaleString('en-GB', { + day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', + }) + +interface StockMovementViewSheetProps { + open: boolean + onClose: () => void + movement: StockMovementResponse | null +} + +function Row({ label, value }: { label: string; value: React.ReactNode }) { + return ( +
+ {label} + {value} +
+ ) +} + +export function StockMovementViewSheet({ open, onClose, movement }: StockMovementViewSheetProps) { + if (!movement) return null + + return ( + !v && onClose()}> + + + Stock Movement Details + + +
+ + {movement.type}} /> + + + + + + + {movement.referenceId} : '—'} /> + +
+
+
+ ) +} diff --git a/distribution_fe/app/(dashboard)/stock/movements/_components/stock-movements-client.tsx b/distribution_fe/app/(dashboard)/stock/movements/_components/stock-movements-client.tsx index 73c4bcc..10c0826 100644 --- a/distribution_fe/app/(dashboard)/stock/movements/_components/stock-movements-client.tsx +++ b/distribution_fe/app/(dashboard)/stock/movements/_components/stock-movements-client.tsx @@ -8,6 +8,8 @@ import { Badge } from '@/components/ui/badge' import { toast } from '@/components/ui/toast' import { stock as stockApi, getApiError, getPaginatedItems } from '@/lib/api/index' import type { StockMovementResponse, MovementType } from '@/types/api' +import { StockMovementViewSheet } from './stock-movement-view-sheet' +import { StockMovementEditDialog } from './stock-movement-edit-dialog' const fmtDate = (v: string) => new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) @@ -29,6 +31,20 @@ export function StockMovementsClient() { const [total, setTotal] = useState(0) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(20) + const [viewing, setViewing] = useState(null) + const [viewOpen, setViewOpen] = useState(false) + const [editing, setEditing] = useState(null) + const [editOpen, setEditOpen] = useState(false) + + function openView(row: StockMovementResponse) { + setViewing(row) + setViewOpen(true) + } + + function openEdit(row: StockMovementResponse) { + setEditing(row) + setEditOpen(true) + } const fetchData = useCallback(async (p: number, ps: number) => { setLoading(true) @@ -70,24 +86,24 @@ export function StockMovementsClient() { ), }, { - key: 'fromLocationType', + key: 'fromLocationName', title: 'From', - fields: ['fromLocationType'], - width: 140, + fields: ['fromLocationName'], + width: 160, cellRender: (row) => ( - {row.fromLocationType ?? '—'} + {row.fromLocationName ?? '—'} ), }, { - key: 'toLocationType', + key: 'toLocationName', title: 'To', - fields: ['toLocationType'], - width: 140, + fields: ['toLocationName'], + width: 160, cellRender: (row) => ( - {row.toLocationType ?? '—'} + {row.toLocationName ?? '—'} ), }, @@ -107,6 +123,20 @@ export function StockMovementsClient() { width: 140, }, ], + rowActions: { + actions: () => [ + { + action: 'view', + label: 'View movement', + onClick: openView, + }, + { + action: 'edit', + label: 'Edit reason', + onClick: openEdit, + }, + ], + }, }), [pageSize]) return ( @@ -130,6 +160,19 @@ export function StockMovementsClient() { setPageSize(p.pageSize) }} /> + + setViewOpen(false)} + movement={viewing} + /> + + setEditOpen(false)} + onSuccess={() => fetchData(page, pageSize)} + movement={editing} + /> ) }