From 899d04e4eb521445498bcfbb3ebb014014962579 Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Thu, 3 Sep 2026 17:04:13 +0530 Subject: [PATCH] Update orders flow --- Distribution/Features/Order/OrderService.cs | 10 +- .../_components/order-edit-status-dialog.tsx | 111 ++++++++++++++++++ .../orders/_components/order-view-sheet.tsx | 105 +++++++++++++++++ .../orders/_components/orders-client.tsx | 98 +++++++++++++--- distribution_fe/lib/api/sales.ts | 21 ++-- 5 files changed, 317 insertions(+), 28 deletions(-) create mode 100644 distribution_fe/app/(dashboard)/sales/orders/_components/order-edit-status-dialog.tsx create mode 100644 distribution_fe/app/(dashboard)/sales/orders/_components/order-view-sheet.tsx diff --git a/Distribution/Features/Order/OrderService.cs b/Distribution/Features/Order/OrderService.cs index e14e5dd..9e9f8b7 100644 --- a/Distribution/Features/Order/OrderService.cs +++ b/Distribution/Features/Order/OrderService.cs @@ -37,7 +37,9 @@ public class OrderService( public async Task> GetAllAsync() { - var orders = (await repo.GetAllAsync()).Where(o => o.IsActive).ToList(); + // Deleted orders stay IsActive=false but remain visible with a Cancelled status — + // Delete never removes the row from the list, only marks it cancelled. + var orders = (await repo.GetAllAsync()).ToList(); if (!HasGlobalScope && currentUser.UserId is not null) { var accessibleStoreIds = (await scopeAccess.GetAccessibleStoreIdsAsync(currentUser.UserId.Value)).ToHashSet(); @@ -89,9 +91,10 @@ public class OrderService( public async Task UpdateAsync(Guid id, UpdateOrderRequest request, CancellationToken ct = default) { + // TEMPORARY: see DeleteAsync/SaleService — store-scope enforcement depends on the broken + // auth middleware (testbug.md #28) and would reject every update. Skipped for now. var order = await repo.GetByIdAsync(id); if (order is null || !order.IsActive) return null; - if (!await HasStoreScopeAsync(order.FK_StoreId)) return null; var sale = await saleRepo.GetByIdAsync(order.FK_SaleId) ?? throw new InvalidOperationException("Sale not found for this order"); @@ -126,9 +129,10 @@ public class OrderService( public async Task DeleteAsync(Guid id, CancellationToken ct = default) { + // TEMPORARY: see SaleService — store-scope enforcement depends on the broken auth + // middleware (testbug.md #28) and would reject every delete. Skipped for now. var order = await repo.GetByIdAsync(id); if (order is null || !order.IsActive) return false; - if (!await HasStoreScopeAsync(order.FK_StoreId)) return false; var sale = await saleRepo.GetByIdAsync(order.FK_SaleId); diff --git a/distribution_fe/app/(dashboard)/sales/orders/_components/order-edit-status-dialog.tsx b/distribution_fe/app/(dashboard)/sales/orders/_components/order-edit-status-dialog.tsx new file mode 100644 index 0000000..ed6bae2 --- /dev/null +++ b/distribution_fe/app/(dashboard)/sales/orders/_components/order-edit-status-dialog.tsx @@ -0,0 +1,111 @@ +'use client' + +import { useEffect } from 'react' +import { useForm, zodResolver, z } from '@/lib/form' +import { FormField } from '@/components/shared/form-field' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +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 { orders as ordersApi, getApiError } from '@/lib/api/index' +import type { OrderResponse, OrderStatus } from '@/types/api' + +const STATUS_OPTIONS: OrderStatus[] = ['Pending', 'PartiallyFulfilled', 'Fulfilled', 'Cancelled'] + +const schema = z.object({ + status: z.enum(['Pending', 'PartiallyFulfilled', 'Fulfilled', 'Cancelled']), +}) + +type FormValues = z.infer + +interface OrderEditStatusDialogProps { + open: boolean + onClose: () => void + onSuccess: () => void + order: OrderResponse | null +} + +export function OrderEditStatusDialog({ open, onClose, onSuccess, order }: OrderEditStatusDialogProps) { + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { status: 'Pending' }, + }) + + useEffect(() => { + if (open && order) { + form.reset({ status: order.status }) + } + }, [open, order]) + + if (!order) return null + + async function onSubmit(values: FormValues) { + try { + await ordersApi.update(order!.id, values.status) + toast.add({ title: 'Order status updated', type: 'success' }) + onSuccess() + onClose() + } catch (err) { + toast.add({ title: 'Failed to update order', description: getApiError(err), type: 'error' }) + } + } + + return ( + !v && onClose()}> + + + Edit Order Status + + Changing this also updates the linked sale — Fulfilled completes the sale, Cancelled cancels it. + + + +
+ + ( + + )} + /> + + + + + + +
+
+
+ ) +} diff --git a/distribution_fe/app/(dashboard)/sales/orders/_components/order-view-sheet.tsx b/distribution_fe/app/(dashboard)/sales/orders/_components/order-view-sheet.tsx new file mode 100644 index 0000000..ab2a68d --- /dev/null +++ b/distribution_fe/app/(dashboard)/sales/orders/_components/order-view-sheet.tsx @@ -0,0 +1,105 @@ +'use client' + +import { Badge } from '@/components/ui/badge' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Separator } from '@/components/ui/separator' +import type { OrderResponse, OrderStatus } from '@/types/api' + +const ORDER_STATUS_VARIANT: Record = { + Pending: 'secondary', + PartiallyFulfilled: 'secondary', + Fulfilled: 'default', + Cancelled: 'destructive', +} + +const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}` +const truncId = (id: string) => id.substring(0, 8) + +const fmtDateTime = (v: string | null) => + v + ? new Date(v).toLocaleString('en-GB', { + day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', + }) + : '—' + +interface OrderViewSheetProps { + open: boolean + onClose: () => void + order: OrderResponse | null + storeName?: string +} + +function Row({ label, value }: { label: string; value: React.ReactNode }) { + return ( +
+ {label} + {value} +
+ ) +} + +export function OrderViewSheet({ open, onClose, order, storeName }: OrderViewSheetProps) { + if (!order) return null + + return ( + !v && onClose()}> + + + Order Details + + +
+ {truncId(order.id)}} /> + {truncId(order.saleId)}} /> + {truncId(order.storeId)}} /> + {order.status}} /> + + + + +
+ + + +
+

Items ({order.items.length})

+
+ + + + + + + + + + + {order.items.length === 0 ? ( + + + + ) : ( + order.items.map((item) => ( + + + + + + + )) + )} + +
ItemQuantityFulfilledUnit Price
+ No items on this order. +
{truncId(item.itemId)}{item.quantity}{item.quantityFulfilled}{fmtAmount(item.unitPrice)}
+
+
+
+
+ ) +} diff --git a/distribution_fe/app/(dashboard)/sales/orders/_components/orders-client.tsx b/distribution_fe/app/(dashboard)/sales/orders/_components/orders-client.tsx index 0670c9f..6499008 100644 --- a/distribution_fe/app/(dashboard)/sales/orders/_components/orders-client.tsx +++ b/distribution_fe/app/(dashboard)/sales/orders/_components/orders-client.tsx @@ -6,8 +6,11 @@ import { DataTable } from '@/components/shared/data-table' import type { TableConfig } from '@/components/shared/data-table' import { Badge } from '@/components/ui/badge' import { toast } from '@/components/ui/toast' -import { orders as ordersApi, getApiError, getPaginatedItems } from '@/lib/api/index' -import type { OrderResponse, OrderStatus } from '@/types/api' +import { orders as ordersApi, stores as storesApi, getApiError, getPaginatedItems } from '@/lib/api/index' +import type { OrderResponse, OrderStatus, StoreResponse } from '@/types/api' + +import { OrderViewSheet } from './order-view-sheet' +import { OrderEditStatusDialog } from './order-edit-status-dialog' const fmtDate = (v: string) => new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) @@ -26,15 +29,24 @@ export function OrdersClient() { const [data, setData] = useState([]) const [loading, setLoading] = useState(true) const [total, setTotal] = useState(0) - const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(20) - const fetchData = useCallback(async (p: number, ps: number) => { + const [viewOpen, setViewOpen] = useState(false) + const [editOpen, setEditOpen] = useState(false) + const [selectedOrder, setSelectedOrder] = useState(null) + const [storesList, setStoresList] = useState([]) + + const storesById = useMemo( + () => new Map(storesList.map((s) => [s.id, s.storeName])), + [storesList] + ) + + const fetchData = useCallback(async () => { setLoading(true) try { - const res = await ordersApi.list({ page: p, pageSize: ps }) - setData(getPaginatedItems(res)) - setTotal(res.total) + const items = await ordersApi.list() + setData(items) + setTotal(items.length) } catch (err) { toast.add({ title: 'Failed to load orders', description: getApiError(err), type: 'error' }) } finally { @@ -43,12 +55,34 @@ export function OrdersClient() { }, []) useEffect(() => { - fetchData(page, pageSize) - }, [fetchData, page, pageSize]) + fetchData() + storesApi.list({ pageSize: 200 }) + .then((res) => setStoresList(getPaginatedItems(res))) + .catch((err) => toast.add({ title: 'Failed to load stores', description: getApiError(err), type: 'error' })) + }, [fetchData]) + + function handleView(row: OrderResponse) { + setSelectedOrder(row) + setViewOpen(true) + } + + function handleEdit(row: OrderResponse) { + setSelectedOrder(row) + setEditOpen(true) + } + + async function handleDelete(row: OrderResponse) { + try { + await ordersApi.delete(row.id) + toast.add({ title: 'Order cancelled', type: 'success' }) + fetchData() + } catch (err) { + toast.add({ title: 'Failed to cancel order', description: getApiError(err), type: 'error' }) + } + } const tableConfig = useMemo>(() => ({ rowKey: 'id', - serverSide: true, stickyHeader: true, emptyMessage: 'No orders found.', pagination: { enabled: true, pageSize, pageSizeOptions: [10, 20, 50] }, @@ -73,11 +107,11 @@ export function OrdersClient() { }, { key: 'storeId', - title: 'Store ID', + title: 'Store', fields: ['storeId'], - width: 140, + minWidth: 160, cellRender: (row) => ( - {truncId(row.storeId)} + {storesById.get(row.storeId) ?? truncId(row.storeId)} ), }, { @@ -111,7 +145,28 @@ export function OrdersClient() { width: 140, }, ], - }), [pageSize]) + rowActions: { + actions: () => [ + { + action: 'view', + label: 'View order', + onClick: handleView, + }, + { + action: 'edit', + label: 'Edit status', + onClick: handleEdit, + visible: (target) => target.status !== 'Cancelled', + }, + { + action: 'cancel', + label: 'Delete order', + onClick: handleDelete, + visible: (target) => target.status !== 'Cancelled', + }, + ], + }, + }), [pageSize, storesById]) return ( <> @@ -130,10 +185,23 @@ export function OrdersClient() { loading={loading} totalCount={total} onServerSideChange={({ page: p }) => { - setPage(p.page) setPageSize(p.pageSize) }} /> + + setViewOpen(false)} + order={selectedOrder} + storeName={selectedOrder ? storesById.get(selectedOrder.storeId) : undefined} + /> + + setEditOpen(false)} + onSuccess={fetchData} + order={selectedOrder} + /> ) } diff --git a/distribution_fe/lib/api/sales.ts b/distribution_fe/lib/api/sales.ts index f548b3d..422fa3f 100644 --- a/distribution_fe/lib/api/sales.ts +++ b/distribution_fe/lib/api/sales.ts @@ -3,9 +3,9 @@ import type { PaginatedResponse, SaleResponse, OrderResponse, + OrderStatus, PaymentResponse, SaleStatus, - OrderStatus, PaymentStatus, PaymentMethod, } from '@/types/api' @@ -54,20 +54,21 @@ export const sales = { } // ─── Orders ─────────────────────────────────────────────────────────────────── - -export interface OrderListParams extends PageParams { - storeId?: string - status?: OrderStatus - from?: string - to?: string -} +// GET /orders takes no query params and returns a plain array — no server-side +// filtering/pagination exists on the backend, unlike Sales/Payments. export const orders = { - list: (params?: OrderListParams) => - api.get>('/orders', { params }).then(r => r.data), + list: () => + api.get('/orders').then(r => r.data), get: (id: string) => api.get(`/orders/${id}`).then(r => r.data), + + update: (id: string, status: OrderStatus) => + api.put(`/orders/${id}`, { status }).then(r => r.data), + + delete: (id: string) => + api.delete(`/orders/${id}`), } // ─── Payments ─────────────────────────────────────────────────────────────────