diff --git a/distribution_fe/app/(dashboard)/procurement/orders/[id]/edit/page.tsx b/distribution_fe/app/(dashboard)/procurement/orders/[id]/edit/page.tsx
new file mode 100644
index 0000000..f5ce354
--- /dev/null
+++ b/distribution_fe/app/(dashboard)/procurement/orders/[id]/edit/page.tsx
@@ -0,0 +1,319 @@
+'use client'
+
+import { useEffect, useMemo, useState } from 'react'
+import { useParams, useRouter } from 'next/navigation'
+import { useFieldArray, useWatch } from 'react-hook-form'
+import { HugeiconsIcon } from '@hugeicons/react'
+import { ArrowLeft01Icon, Add01Icon, Delete02Icon } from '@hugeicons/core-free-icons'
+import { useForm, zodResolver, z } from '@/lib/form'
+import { FormField } from '@/components/shared/form-field'
+import { Input } from '@/components/ui/input'
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select'
+import { Button } from '@/components/ui/button'
+import { FieldGroup } from '@/components/ui/field'
+import { Separator } from '@/components/ui/separator'
+import { toast } from '@/components/ui/toast'
+import {
+ purchaseOrders as purchaseOrdersApi,
+ items as itemsApi,
+ getApiError,
+ getPaginatedItems,
+} from '@/lib/api/index'
+import type { ItemSummaryResponse, PurchaseOrderResponse } from '@/types/api'
+
+const poLineSchema = z.object({
+ purchaseOrderItemId: z.string().optional(),
+ itemId: z.string().min(1, 'Item is required'),
+ quantity: z.number().min(1, 'Quantity must be at least 1'),
+ unitCost: z.number().min(0.01, 'Unit cost must be greater than 0'),
+ isReceived: z.boolean(),
+})
+
+const schema = z.object({
+ items: z.array(poLineSchema).min(1, 'At least one item is required'),
+})
+
+type FormValues = z.infer
+
+function createDefaultLine() {
+ return { purchaseOrderItemId: undefined, itemId: '', quantity: 1, unitCost: 0, isReceived: false }
+}
+
+const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}`
+
+export default function EditPurchaseOrderPage() {
+ const router = useRouter()
+ const params = useParams<{ id: string }>()
+ const [order, setOrder] = useState(null)
+ const [itemsList, setItemsList] = useState([])
+ const [loading, setLoading] = useState(true)
+
+ const form = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: { items: [createDefaultLine()] },
+ })
+
+ const { fields, append, remove } = useFieldArray({ control: form.control, name: 'items' })
+ const watchedItems = useWatch({ control: form.control, name: 'items' })
+ const itemsById = useMemo(() => new Map(itemsList.map((item) => [item.id, item])), [itemsList])
+ const totalCost = useMemo(
+ () => watchedItems.reduce((sum, i) => sum + (Number(i.quantity) || 0) * (Number(i.unitCost) || 0), 0),
+ [watchedItems]
+ )
+
+ useEffect(() => {
+ Promise.all([
+ purchaseOrdersApi.get(params.id),
+ itemsApi.list({ pageSize: 200 }),
+ ])
+ .then(([po, itemsRes]) => {
+ setOrder(po)
+ setItemsList(getPaginatedItems(itemsRes))
+ form.reset({
+ items: po.items
+ .filter((i) => !i.isCancelled)
+ .map((i) => ({
+ purchaseOrderItemId: i.id,
+ itemId: i.itemId,
+ quantity: i.quantity,
+ unitCost: i.unitCost,
+ isReceived: i.receivedQuantity > 0,
+ })),
+ })
+ })
+ .catch((err) => {
+ toast.add({ title: 'Failed to load purchase order', description: getApiError(err), type: 'error' })
+ })
+ .finally(() => setLoading(false))
+ }, [params.id])
+
+ async function onSubmit(values: FormValues) {
+ try {
+ await purchaseOrdersApi.update(params.id, {
+ items: values.items.map((i) => ({
+ purchaseOrderItemId: i.purchaseOrderItemId,
+ itemId: i.itemId,
+ quantity: i.quantity,
+ unitCost: i.unitCost,
+ })),
+ })
+ toast.add({ title: 'Purchase order updated', type: 'success' })
+ router.push('/procurement/orders')
+ } catch (err) {
+ toast.add({ title: 'Failed to update purchase order', description: getApiError(err), type: 'error' })
+ }
+ }
+
+ if (loading) {
+ return Loading…
+ }
+
+ if (!order) {
+ return Purchase order not found.
+ }
+
+ if (order.status !== 'Issued') {
+ return (
+
+
+
+
+
Edit Purchase Order
+
+
+
+ Only a purchase order in the "Issued" status can be edited. This order is currently{' '}
+ {order.status}.
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
Edit Purchase Order
+
{order.supplierName}
+
+
+
+
+
+ )
+}
diff --git a/distribution_fe/app/(dashboard)/procurement/orders/_components/purchase-order-sheet.tsx b/distribution_fe/app/(dashboard)/procurement/orders/_components/purchase-order-sheet.tsx
deleted file mode 100644
index 8e599c3..0000000
--- a/distribution_fe/app/(dashboard)/procurement/orders/_components/purchase-order-sheet.tsx
+++ /dev/null
@@ -1,262 +0,0 @@
-'use client'
-
-import { useEffect, useMemo, useState } from 'react'
-import { useFieldArray } from 'react-hook-form'
-import { useForm, zodResolver, z } from '@/lib/form'
-import { FormField } from '@/components/shared/form-field'
-import { Input } from '@/components/ui/input'
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@/components/ui/select'
-import { Button } from '@/components/ui/button'
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@/components/ui/dialog'
-import { FieldGroup } from '@/components/ui/field'
-import { toast } from '@/components/ui/toast'
-import { HugeiconsIcon } from '@hugeicons/react'
-import { Add01Icon, Delete02Icon } from '@hugeicons/core-free-icons'
-import {
- purchaseOrders as purchaseOrdersApi,
- suppliers as suppliersApi,
- items as itemsApi,
- getApiError,
- getPaginatedItems,
-} from '@/lib/api/index'
-import type { ItemSummaryResponse, SupplierResponse } from '@/types/api'
-
-const poItemSchema = z.object({
- itemId: z.string().min(1, 'Item is required'),
- quantity: z.number().min(1, 'Quantity must be at least 1'),
- unitCost: z.number().min(0.01, 'Unit cost must be greater than 0'),
-})
-
-const schema = z.object({
- supplierId: z.string().min(1, 'Supplier is required'),
- items: z.array(poItemSchema).min(1, 'At least one item is required'),
-})
-
-type FormValues = z.infer
-
-interface PurchaseOrderSheetProps {
- open: boolean
- onClose: () => void
- onSuccess: () => void
-}
-
-function defaultItem() {
- return { itemId: '', quantity: 1, unitCost: 0 }
-}
-
-export function PurchaseOrderSheet({ open, onClose, onSuccess }: PurchaseOrderSheetProps) {
- const [suppliersList, setSuppliersList] = useState([])
- const [itemsList, setItemsList] = useState([])
-
- const form = useForm({
- resolver: zodResolver(schema),
- defaultValues: { supplierId: '', items: [defaultItem()] },
- })
-
- const { fields, append, remove } = useFieldArray({ control: form.control, name: 'items' })
- const watchedItems = form.watch('items')
- const itemsById = useMemo(() => new Map(itemsList.map(i => [i.id, i])), [itemsList])
-
- const totalCost = useMemo(
- () => watchedItems.reduce((sum, i) => sum + (Number(i.quantity) || 0) * (Number(i.unitCost) || 0), 0),
- [watchedItems]
- )
-
- useEffect(() => {
- if (!open) return
- form.reset({ supplierId: '', items: [defaultItem()] })
- Promise.all([
- suppliersApi.list({ pageSize: 200 }),
- itemsApi.list({ pageSize: 200 }),
- ])
- .then(([s, it]) => {
- setSuppliersList(getPaginatedItems(s))
- setItemsList(getPaginatedItems(it))
- })
- .catch(err =>
- toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' })
- )
- }, [open])
-
- async function onSubmit(values: FormValues) {
- try {
- await purchaseOrdersApi.create(values)
- toast.add({ title: 'Purchase order created', type: 'success' })
- onSuccess()
- onClose()
- } catch (err) {
- toast.add({ title: 'Failed to create purchase order', description: getApiError(err), type: 'error' })
- }
- }
-
- return (
-
- )
-}
diff --git a/distribution_fe/app/(dashboard)/procurement/orders/_components/purchase-order-view-sheet.tsx b/distribution_fe/app/(dashboard)/procurement/orders/_components/purchase-order-view-sheet.tsx
new file mode 100644
index 0000000..b91a573
--- /dev/null
+++ b/distribution_fe/app/(dashboard)/procurement/orders/_components/purchase-order-view-sheet.tsx
@@ -0,0 +1,165 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+import { Badge } from '@/components/ui/badge'
+import { Button } from '@/components/ui/button'
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog'
+import { Separator } from '@/components/ui/separator'
+import { toast } from '@/components/ui/toast'
+import { purchaseReturns as purchaseReturnsApi, getApiError } from '@/lib/api/index'
+import type { PurchaseOrderResponse, PurchaseOrderStatus, PurchaseReturnResponse } from '@/types/api'
+import { PurchaseReturnDialog } from './purchase-return-dialog'
+
+const PO_STATUS_VARIANT: Record = {
+ Issued: 'default',
+ PartiallyReceived: 'secondary',
+ Received: 'secondary',
+ Closed: 'secondary',
+ Cancelled: 'destructive',
+}
+
+const fmtDateTime = (v: string) =>
+ new Date(v).toLocaleString('en-GB', {
+ day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit',
+ })
+
+const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}`
+
+interface PurchaseOrderViewSheetProps {
+ open: boolean
+ onClose: () => void
+ order: PurchaseOrderResponse | null
+}
+
+function Row({ label, value }: { label: string; value: React.ReactNode }) {
+ return (
+
+ {label}
+ {value}
+
+ )
+}
+
+export function PurchaseOrderViewSheet({ open, onClose, order }: PurchaseOrderViewSheetProps) {
+ const [returns, setReturns] = useState([])
+ const [returnDialogOpen, setReturnDialogOpen] = useState(false)
+
+ const fetchReturns = () => {
+ if (!order) return
+ purchaseReturnsApi.listByPurchaseOrder(order.id)
+ .then(setReturns)
+ .catch((err) => toast.add({ title: 'Failed to load purchase returns', description: getApiError(err), type: 'error' }))
+ }
+
+ useEffect(() => {
+ if (!open || !order) return
+ fetchReturns()
+ }, [open, order?.id])
+
+ if (!order) return null
+
+ const totalCost = order.items.reduce((sum, i) => sum + i.quantity * i.unitCost, 0)
+ const hasReceivedStock = order.items.some((i) => i.receivedQuantity > 0)
+
+ return (
+
+ )
+}
diff --git a/distribution_fe/app/(dashboard)/procurement/orders/_components/purchase-orders-client.tsx b/distribution_fe/app/(dashboard)/procurement/orders/_components/purchase-orders-client.tsx
index eefdbe8..5f36470 100644
--- a/distribution_fe/app/(dashboard)/procurement/orders/_components/purchase-orders-client.tsx
+++ b/distribution_fe/app/(dashboard)/procurement/orders/_components/purchase-orders-client.tsx
@@ -1,6 +1,7 @@
'use client'
import { useState, useEffect, useCallback, useMemo } from 'react'
+import { useRouter } from 'next/navigation'
import { HugeiconsIcon } from '@hugeicons/react'
import { Add01Icon } from '@hugeicons/core-free-icons'
import { DataTable } from '@/components/shared/data-table'
@@ -10,7 +11,7 @@ import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/toast'
import { purchaseOrders as purchaseOrdersApi, getApiError, getPaginatedItems } from '@/lib/api/index'
import type { PurchaseOrderResponse, PurchaseOrderStatus } from '@/types/api'
-import { PurchaseOrderSheet } from './purchase-order-sheet'
+import { PurchaseOrderViewSheet } from './purchase-order-view-sheet'
const fmtDate = (v: string) =>
new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
@@ -26,12 +27,19 @@ const PO_STATUS_VARIANT: Record([])
const [loading, setLoading] = useState(true)
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
- const [sheetOpen, setSheetOpen] = useState(false)
+ const [viewing, setViewing] = useState(null)
+ const [viewOpen, setViewOpen] = useState(false)
+
+ function openView(row: PurchaseOrderResponse) {
+ setViewing(row)
+ setViewOpen(true)
+ }
const fetchData = useCallback(async (p: number, ps: number) => {
setLoading(true)
@@ -115,9 +123,20 @@ export function PurchaseOrdersClient() {
],
rowActions: {
actions: () => [
+ {
+ action: 'view',
+ label: 'View order',
+ onClick: openView,
+ },
+ {
+ action: 'edit',
+ label: 'Edit order',
+ onClick: (row: PurchaseOrderResponse) => router.push(`/procurement/orders/${row.id}/edit`),
+ visible: (target) => target.status === 'Issued',
+ },
{
action: 'cancel',
- label: 'Cancel order',
+ label: 'Delete order',
tone: 'destructive',
onClick: handleCancel,
visible: (target) => target.status === 'Issued',
@@ -135,7 +154,7 @@ export function PurchaseOrdersClient() {
Orders placed with suppliers to restock inventory
-