From 1f6d63a0b3b15699a6d365086ce72fff4eea0f5e Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Fri, 18 Sep 2026 11:47:34 +0530 Subject: [PATCH] Add cheque to payment UI --- .../docs/refactor_docs/ride_lifecycle.md | 34 +++++- .../payments/_components/payment-sheet.tsx | 108 ++++++++++++++++-- .../_components/payment-view-sheet.tsx | 23 +++- .../payments/_components/payments-client.tsx | 21 +++- 4 files changed, 168 insertions(+), 18 deletions(-) diff --git a/Distribution/docs/refactor_docs/ride_lifecycle.md b/Distribution/docs/refactor_docs/ride_lifecycle.md index be99cba..7f64e1d 100644 --- a/Distribution/docs/refactor_docs/ride_lifecycle.md +++ b/Distribution/docs/refactor_docs/ride_lifecycle.md @@ -1,11 +1,36 @@ # [Refactor] Ride Lifecycle — Route-Free, Type-Free Rides -**Status**: Proposed (design only — no code changes yet) +**Status**: Mostly implemented — see [Implementation Status](#implementation-status-2026-09-17) **Refactors**: [../03_ride_lifecycle.md](../03_ride_lifecycle.md) (kept as-is, this is the target design) **Depends On**: 02 — Stock Ledger --- +## Implementation Status (2026-09-17) + +**Done**, verified against `Models/DeliveryRun.cs`, `Features/Ride/*`: +- `RideStatus` collapsed to `Open`/`Cancelled`/`Closed` — no Draft/Active stage anywhere. +- `FK_RouteId`, `RideType`, `ActivatedAt`, `FK_SourceWarehouseId` are all gone from `DeliveryRun`. +- `POST /api/rides` creates directly into `Open` with `StartedAt` set; no `/open`/`/activate` routes. +- `POST /api/rides/{id}/cancel` cancels an `Open` ride directly — no approval step, no + `CancelRequested` stage. +- Driver compliance check (`CheckDriverComplianceAsync`) and the "vehicle can't have two Open rides" + guard are both wired into `RideService.CreateAsync`. +- Dead `RideCancelRequest` model/repository/table removed (2026-09-17) — see Migration Notes. + +**Not done / diverges from this doc:** +1. **`GET /api/rides/breakdown-assist/{rideId}`** — not implemented at all (no route in + `RideController.cs`), not even the trimmed "vehicle stock levels only" version this doc describes. +2. **Impact item 1 (delivery task generation) confirmed still broken, honestly**: + `DeliveryTaskService.GenerateAsync` is a one-line `throw new NotImplementedException("... route- + based dispatch not yet implemented.")` — matches this doc's prediction exactly, not yet designed. +3. **Impact item 4 (stock reconciliation at Close/Cancel without a source warehouse)** — still + unaddressed; `CloseAsync`/`CancelAsync` do not touch `StockBalance` at all. +4. **Impact item 2 (loading sheet auto-population)** — resolved differently than either this doc or + `loading_event.md` predicted; see that document's own Implementation Status section. + +--- + ## Goal Ride should not require a `Route`, should not carry a `RideType`, should not be gated by a warehouse's `Type`, and should not even pin down a source warehouse on the ride record itself. Per direction from the business owner, the ride record carries only what a real dispatch needs to track — **"a ride only carries vehicle, driver assign, and meter reading from start to end."** Everything route-shaped (which stores get visited, which orders/items travel on this trip) and everything warehouse-shaped (where stock loads from/returns to) moves out of the Ride entity's responsibility entirely; neither is replaced by another matching mechanism here. @@ -215,9 +240,10 @@ Not part of this docs-only pass, but captured for the eventual implementation st `DeliveryRun`/`Ride` and their Fluent/EF configuration. `Status` stays, re-typed to the narrowed `RideStatus` (`Open`/`Cancelled`/`Closed`). Add `CancelTypeCode`, `CancelReason`, `CancelledAt` columns to `DeliveryRun`. -- Drop the `RideCancelRequest` table, its Fluent/EF configuration, and - `RideCancelRequestRepository`/`IRideCancelRequestRepository` — the request/approve/reject flow is - gone. +- ~~Drop the `RideCancelRequest` table, its Fluent/EF configuration, and + `RideCancelRequestRepository`/`IRideCancelRequestRepository`~~ — **Done (2026-09-17)**: model, + repository, `DbSet`, and seed data removed; `CancelRequestStatus` enum removed as unused; + migration `RemoveRideCancelRequest` applied, dropping the table. - Remove `RouteId`, `RideType`, and `SourceWarehouseId`/`SourceWarehouseName` from `CreateRideRequest`, `RideResponse`, and `BreakdownAssistRideResponse` in `RideDTOs.cs`. Remove the open-orders list from `BreakdownAssistResponse`. Remove the `RaiseCancelRequest`/cancel-request 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 1d47c16..0878203 100644 --- a/distribution_fe/app/(dashboard)/sales/payments/_components/payment-sheet.tsx +++ b/distribution_fe/app/(dashboard)/sales/payments/_components/payment-sheet.tsx @@ -11,6 +11,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' +import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group' import { Button } from '@/components/ui/button' import { Dialog, @@ -25,11 +26,23 @@ import { toast } from '@/components/ui/toast' import { payments as paymentsApi, getApiError } from '@/lib/api/index' import type { InvoiceResponse } from '@/types/api' -const schema = z.object({ - invoiceId: z.string().min(1, 'Invoice is required'), - amount: z.number().min(0.01, 'Amount must be greater than 0'), - method: z.enum(['Cash', 'Credit']), -}) +const schema = z + .object({ + invoiceId: z.string().min(1, 'Invoice is required'), + amount: z.number().min(0.01, 'Amount must be greater than 0'), + method: z.enum(['Cash', 'Cheque', 'Credit']), + entryMode: z.enum(['RealTime', 'DayEndManual']), + chequeNumber: z.string().optional(), + chequeDate: z.string().optional(), + }) + .refine((data) => data.method !== 'Cheque' || !!data.chequeNumber, { + message: 'Cheque number is required for cheque payments', + path: ['chequeNumber'], + }) + .refine((data) => data.method !== 'Cheque' || !!data.chequeDate, { + message: 'Cheque date is required for cheque payments', + path: ['chequeDate'], + }) type FormValues = z.infer @@ -50,18 +63,35 @@ export function PaymentSheet({ open, onClose, onSuccess, invoices }: PaymentShee invoiceId: '', amount: 0, method: 'Cash', + entryMode: 'RealTime', + chequeNumber: '', + chequeDate: '', }, }) + const method = form.watch('method') + useEffect(() => { if (!open) return - form.reset({ invoiceId: '', amount: 0, method: 'Cash' }) + form.reset({ + invoiceId: '', + amount: 0, + method: 'Cash', + entryMode: 'RealTime', + chequeNumber: '', + chequeDate: '', + }) }, [open]) async function onSubmit(values: FormValues) { - const { invoiceId, ...rest } = values + const { invoiceId, method, chequeNumber, chequeDate, ...rest } = values try { - await paymentsApi.create(invoiceId, rest) + await paymentsApi.create(invoiceId, { + ...rest, + method, + chequeNumber: method === 'Cheque' ? chequeNumber : undefined, + chequeDate: method === 'Cheque' ? chequeDate : undefined, + }) toast.add({ title: 'Payment recorded', type: 'success' }) onSuccess() onClose() @@ -77,7 +107,7 @@ export function PaymentSheet({ open, onClose, onSuccess, invoices }: PaymentShee Record Payment - Record a cash or credit payment against an invoice. + Record a cash, cheque, or credit payment against an invoice.
@@ -137,11 +167,71 @@ export function PaymentSheet({ open, onClose, onSuccess, invoices }: PaymentShee Cash + Cheque Credit )} /> + + {method === 'Cheque' && ( + <> + ( + + )} + /> + + ( + + )} + /> + + )} + + ( + value[0] && field.onChange(value[0])} + > + Real-time + Day-end entry + + )} + /> 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 index 2aa4a01..bc00bd1 100644 --- a/distribution_fe/app/(dashboard)/sales/payments/_components/payment-view-sheet.tsx +++ b/distribution_fe/app/(dashboard)/sales/payments/_components/payment-view-sheet.tsx @@ -7,13 +7,19 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog' -import type { PaymentResponse, PaymentMethod } from '@/types/api' +import type { PaymentResponse, PaymentMethod, EntryMode } from '@/types/api' -const PAYMENT_METHOD_VARIANT: Record = { +const PAYMENT_METHOD_VARIANT: Record = { Cash: 'default', + Cheque: 'outline', Credit: 'secondary', } +const ENTRY_MODE_LABEL: Record = { + RealTime: 'Real-time', + DayEndManual: 'Day-end entry', +} + const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}` const truncId = (id: string) => id.substring(0, 8) @@ -22,6 +28,9 @@ const fmtDateTime = (v: string) => day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', }) +const fmtDate = (v: string) => + new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) + interface PaymentViewSheetProps { open: boolean onClose: () => void @@ -53,8 +62,18 @@ export function PaymentViewSheet({ open, onClose, payment, invoiceName }: Paymen {truncId(payment.invoiceId)}} /> {payment.method}} /> + {payment.method === 'Cheque' && ( + <> + + + + )} + {ENTRY_MODE_LABEL[payment.entryMode]}} /> {truncId(payment.collectedBy)}} /> + {payment.recordedBy && ( + {truncId(payment.recordedBy)}} /> + )} 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 cf1ed72..daef14a 100644 --- a/distribution_fe/app/(dashboard)/sales/payments/_components/payments-client.tsx +++ b/distribution_fe/app/(dashboard)/sales/payments/_components/payments-client.tsx @@ -11,7 +11,7 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { toast } from '@/components/ui/toast' import { invoices as invoicesApi, payments as paymentsApi, getApiError, getPaginatedItems } from '@/lib/api/index' -import type { PaymentResponse, PaymentMethod, InvoiceResponse } from '@/types/api' +import type { PaymentResponse, PaymentMethod, EntryMode, InvoiceResponse } from '@/types/api' import { PaymentSheet } from './payment-sheet' import { PaymentViewSheet } from './payment-view-sheet' @@ -22,11 +22,17 @@ const fmtDate = (v: string) => const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}` const truncId = (id: string) => id.substring(0, 8) -const PAYMENT_METHOD_VARIANT: Record = { +const PAYMENT_METHOD_VARIANT: Record = { Cash: 'default', + Cheque: 'outline', Credit: 'secondary', } +const ENTRY_MODE_LABEL: Record = { + RealTime: 'Real-time', + DayEndManual: 'Day-end entry', +} + export function PaymentsClient() { const [invoicesList, setInvoicesList] = useState([]) const [selectedInvoiceId, setSelectedInvoiceId] = useState('') @@ -119,6 +125,15 @@ export function PaymentsClient() { {row.method} ), }, + { + key: 'entryMode', + title: 'Entry Mode', + fields: ['entryMode'], + width: 130, + cellRender: (row) => ( + {ENTRY_MODE_LABEL[row.entryMode]} + ), + }, { key: 'collectedAt', title: 'Collected At', @@ -143,7 +158,7 @@ export function PaymentsClient() {

Payments

- Cash and credit payments collected against invoices + Cash, cheque, and credit payments collected against invoices