From 8864f8ad6d36525975b86080a962fdf97182d4e2 Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Thu, 3 Sep 2026 17:22:04 +0530 Subject: [PATCH] Refactor payments UI: dropdowns, view sheet --- .../payments/_components/payment-sheet.tsx | 126 +++++++++--------- .../_components/payment-view-sheet.tsx | 65 +++++++++ .../payments/_components/payments-client.tsx | 59 +++++++- 3 files changed, 182 insertions(+), 68 deletions(-) create mode 100644 distribution_fe/app/(dashboard)/sales/payments/_components/payment-view-sheet.tsx diff --git a/distribution_fe/app/(dashboard)/sales/payments/_components/payment-sheet.tsx b/distribution_fe/app/(dashboard)/sales/payments/_components/payment-sheet.tsx index cd23ffe..28e945f 100644 --- a/distribution_fe/app/(dashboard)/sales/payments/_components/payment-sheet.tsx +++ b/distribution_fe/app/(dashboard)/sales/payments/_components/payment-sheet.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import { useForm, zodResolver, z } from '@/lib/form' import { FormField } from '@/components/shared/form-field' import { Input } from '@/components/ui/input' @@ -22,14 +22,14 @@ import { } from '@/components/ui/dialog' import { FieldGroup } from '@/components/ui/field' import { toast } from '@/components/ui/toast' -import { payments as paymentsApi, getApiError } from '@/lib/api/index' +import { payments as paymentsApi, sales as salesApi, users as usersApi, getApiError, getPaginatedItems } from '@/lib/api/index' +import type { SaleResponse, UserResponse } from '@/types/api' const schema = z.object({ - saleId: z.string().min(1, 'Sale ID is required'), - storeId: z.string().min(1, 'Store ID is required'), + saleId: z.string().min(1, 'Sale is required'), amount: z.number().min(0.01, 'Amount must be greater than 0'), method: z.enum(['Cash', 'Credit']), - collectedAt: z.string().min(1, 'Date is required'), + collectedByUserId: z.string().min(1, 'Collected by is required'), }) type FormValues = z.infer @@ -40,28 +40,44 @@ interface PaymentSheetProps { onSuccess: () => void } +const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}` +const truncId = (id: string) => id.substring(0, 8) + export function PaymentSheet({ open, onClose, onSuccess }: PaymentSheetProps) { + const [salesList, setSalesList] = useState([]) + const [usersList, setUsersList] = useState([]) + const form = useForm({ resolver: zodResolver(schema), defaultValues: { saleId: '', - storeId: '', amount: 0, method: 'Cash', - collectedAt: '', + collectedByUserId: '', }, }) useEffect(() => { - if (open) { - form.reset({ - saleId: '', - storeId: '', - amount: 0, - method: 'Cash', - collectedAt: '', + if (!open) return + + form.reset({ + saleId: '', + amount: 0, + method: 'Cash', + collectedByUserId: '', + }) + + Promise.all([ + salesApi.list({ pageSize: 200 }), + usersApi.list({ pageSize: 200 }), + ]) + .then(([salesRes, usersRes]) => { + setSalesList(getPaginatedItems(salesRes).filter((s) => s.status !== 'Cancelled')) + setUsersList(getPaginatedItems(usersRes)) + }) + .catch((err) => { + toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' }) }) - } }, [open]) async function onSubmit(values: FormValues) { @@ -80,7 +96,7 @@ export function PaymentSheet({ open, onClose, onSuccess }: PaymentSheetProps) { Record Payment - Record a cash or credit payment from a store. + Record a cash or credit payment against a sale.
@@ -88,38 +104,22 @@ export function PaymentSheet({ open, onClose, onSuccess }: PaymentSheetProps) { ( - - )} - /> - - ( - + render={({ field }) => ( + )} /> @@ -135,7 +135,7 @@ export function PaymentSheet({ open, onClose, onSuccess }: PaymentSheetProps) { ref={field.ref} type="number" step="0.01" - value={Number(field.value as string)} + value={Number(field.value as number)} onChange={(e) => field.onChange(Number(e.target.value))} onBlur={field.onBlur} placeholder="0.00" @@ -167,20 +167,24 @@ export function PaymentSheet({ open, onClose, onSuccess }: PaymentSheetProps) { ( - + render={({ field }) => ( + )} /> diff --git a/distribution_fe/app/(dashboard)/sales/payments/_components/payment-view-sheet.tsx b/distribution_fe/app/(dashboard)/sales/payments/_components/payment-view-sheet.tsx new file mode 100644 index 0000000..1468570 --- /dev/null +++ b/distribution_fe/app/(dashboard)/sales/payments/_components/payment-view-sheet.tsx @@ -0,0 +1,65 @@ +'use client' + +import { Badge } from '@/components/ui/badge' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import type { PaymentResponse, PaymentMethod } from '@/types/api' + +const PAYMENT_METHOD_VARIANT: Record = { + Cash: 'default', + Credit: 'secondary', +} + +const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}` +const truncId = (id: string) => id.substring(0, 8) + +const fmtDateTime = (v: string) => + new Date(v).toLocaleString('en-GB', { + day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', + }) + +interface PaymentViewSheetProps { + open: boolean + onClose: () => void + payment: PaymentResponse | null + storeName?: string + collectedByName?: string +} + +function Row({ label, value }: { label: string; value: React.ReactNode }) { + return ( +
+ {label} + {value} +
+ ) +} + +export function PaymentViewSheet({ open, onClose, payment, storeName, collectedByName }: PaymentViewSheetProps) { + if (!payment) return null + + return ( + !v && onClose()}> + + + Payment Details + + +
+ {truncId(payment.id)}} /> + {truncId(payment.saleId)}} /> + {truncId(payment.storeId)}} /> + + {payment.method}} /> + + {truncId(payment.collectedBy)}} /> + +
+
+
+ ) +} diff --git a/distribution_fe/app/(dashboard)/sales/payments/_components/payments-client.tsx b/distribution_fe/app/(dashboard)/sales/payments/_components/payments-client.tsx index debf358..8327b16 100644 --- a/distribution_fe/app/(dashboard)/sales/payments/_components/payments-client.tsx +++ b/distribution_fe/app/(dashboard)/sales/payments/_components/payments-client.tsx @@ -9,10 +9,11 @@ import type { TableConfig } from '@/components/shared/data-table' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { toast } from '@/components/ui/toast' -import { payments as paymentsApi, getApiError, getPaginatedItems } from '@/lib/api/index' -import type { PaymentResponse, PaymentMethod } from '@/types/api' +import { payments as paymentsApi, stores as storesApi, users as usersApi, getApiError, getPaginatedItems } from '@/lib/api/index' +import type { PaymentResponse, PaymentMethod, StoreResponse, UserResponse } from '@/types/api' import { PaymentSheet } from './payment-sheet' +import { PaymentViewSheet } from './payment-view-sheet' const fmtDate = (v: string) => new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) @@ -33,6 +34,19 @@ export function PaymentsClient() { const [pageSize, setPageSize] = useState(20) const [sheetOpen, setSheetOpen] = useState(false) + const [viewOpen, setViewOpen] = useState(false) + const [selectedPayment, setSelectedPayment] = useState(null) + const [storesList, setStoresList] = useState([]) + const [usersList, setUsersList] = useState([]) + + const storeNameById = useMemo( + () => new Map(storesList.map((s) => [s.id, s.storeName])), + [storesList] + ) + const userNameById = useMemo( + () => new Map(usersList.map((u) => [u.id, u.fullName])), + [usersList] + ) const fetchData = useCallback(async (p: number, ps: number) => { setLoading(true) @@ -51,6 +65,20 @@ export function PaymentsClient() { fetchData(page, pageSize) }, [fetchData, page, pageSize]) + useEffect(() => { + storesApi.list({ pageSize: 200 }) + .then((res) => setStoresList(getPaginatedItems(res))) + .catch((err) => toast.add({ title: 'Failed to load stores', description: getApiError(err), type: 'error' })) + usersApi.list({ pageSize: 200 }) + .then((res) => setUsersList(getPaginatedItems(res))) + .catch((err) => toast.add({ title: 'Failed to load users', description: getApiError(err), type: 'error' })) + }, []) + + function handleView(row: PaymentResponse) { + setSelectedPayment(row) + setViewOpen(true) + } + const tableConfig = useMemo>(() => ({ rowKey: 'id', serverSide: true, @@ -69,11 +97,11 @@ export function PaymentsClient() { }, { key: 'storeId', - title: 'Store ID', + title: 'Store', fields: ['storeId'], - width: 140, + minWidth: 160, cellRender: (row) => ( - {truncId(row.storeId)} + {storeNameById.get(row.storeId) ?? truncId(row.storeId)} ), }, { @@ -106,11 +134,20 @@ export function PaymentsClient() { fields: ['collectedBy'], minWidth: 160, cellRender: (row) => ( - {row.collectedBy} + {userNameById.get(row.collectedBy) ?? truncId(row.collectedBy)} ), }, ], - }), [pageSize]) + rowActions: { + actions: () => [ + { + action: 'view', + label: 'View payment', + onClick: handleView, + }, + ], + }, + }), [pageSize, storeNameById, userNameById]) return ( <> @@ -143,6 +180,14 @@ export function PaymentsClient() { onClose={() => setSheetOpen(false)} onSuccess={() => fetchData(page, pageSize)} /> + + setViewOpen(false)} + payment={selectedPayment} + storeName={selectedPayment ? storeNameById.get(selectedPayment.storeId) : undefined} + collectedByName={selectedPayment ? userNameById.get(selectedPayment.collectedBy) : undefined} + /> ) }