Merge pull request 'feat: integrate payment functionality with new payment sheet and enhance supplier payments UI' (#73) from dev into production

Reviewed-on: #73
This commit was merged in pull request #73.
This commit is contained in:
2026-10-19 22:02:45 +05:30
5 changed files with 161 additions and 11 deletions
@@ -11,6 +11,7 @@ import type { InvoiceResponse, InvoiceStatus, PaymentStatus } from '@/types/api'
import { OrderViewSheet } from './order-view-sheet' import { OrderViewSheet } from './order-view-sheet'
import { InvoiceActionDialog } from './invoice-action-dialog' import { InvoiceActionDialog } from './invoice-action-dialog'
import { PaymentSheet } from '../../payments/_components/payment-sheet'
const fmtDate = (v: string) => const fmtDate = (v: string) =>
new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
@@ -41,6 +42,8 @@ export function OrdersClient() {
const [selectedInvoice, setSelectedInvoice] = useState<InvoiceResponse | null>(null) const [selectedInvoice, setSelectedInvoice] = useState<InvoiceResponse | null>(null)
const [actionOpen, setActionOpen] = useState(false) const [actionOpen, setActionOpen] = useState(false)
const [actionMode, setActionMode] = useState<'print' | 'finalize'>('print') const [actionMode, setActionMode] = useState<'print' | 'finalize'>('print')
const [paymentOpen, setPaymentOpen] = useState(false)
const [paymentInvoice, setPaymentInvoice] = useState<InvoiceResponse | null>(null)
const fetchData = useCallback(async (p: number, ps: number) => { const fetchData = useCallback(async (p: number, ps: number) => {
setLoading(true) setLoading(true)
@@ -76,6 +79,11 @@ export function OrdersClient() {
setActionOpen(true) setActionOpen(true)
} }
function handleRecordPayment(row: InvoiceResponse) {
setPaymentInvoice(row)
setPaymentOpen(true)
}
const tableConfig = useMemo<TableConfig<InvoiceResponse>>(() => ({ const tableConfig = useMemo<TableConfig<InvoiceResponse>>(() => ({
rowKey: 'id', rowKey: 'id',
serverSide: true, serverSide: true,
@@ -151,6 +159,12 @@ export function OrdersClient() {
onClick: handleFinalize, onClick: handleFinalize,
visible: (target) => target.status === 'Printed', visible: (target) => target.status === 'Printed',
}, },
{
action: 'edit',
label: 'Record payment',
onClick: handleRecordPayment,
visible: (target) => target.paymentStatus !== 'Paid',
},
], ],
}, },
}), [pageSize]) }), [pageSize])
@@ -190,6 +204,14 @@ export function OrdersClient() {
invoice={selectedInvoice} invoice={selectedInvoice}
mode={actionMode} mode={actionMode}
/> />
<PaymentSheet
open={paymentOpen}
onClose={() => setPaymentOpen(false)}
onSuccess={() => fetchData(page, pageSize)}
invoices={paymentInvoice ? [paymentInvoice] : []}
defaultInvoiceId={paymentInvoice?.id}
/>
</> </>
) )
} }
@@ -1,6 +1,6 @@
'use client' 'use client'
import { useEffect } from 'react' import { useEffect, useState } from 'react'
import { useForm, zodResolver, z } from '@/lib/form' import { useForm, zodResolver, z } from '@/lib/form'
import { FormField } from '@/components/shared/form-field' import { FormField } from '@/components/shared/form-field'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
@@ -23,8 +23,9 @@ import {
} from '@/components/ui/dialog' } from '@/components/ui/dialog'
import { FieldGroup } from '@/components/ui/field' import { FieldGroup } from '@/components/ui/field'
import { toast } from '@/components/ui/toast' import { toast } from '@/components/ui/toast'
import { payments as paymentsApi, getApiError } from '@/lib/api/index' import { payments as paymentsApi, bankAccounts as bankAccountsApi, getApiError } from '@/lib/api/index'
import type { InvoiceResponse } from '@/types/api' import type { InvoiceResponse } from '@/types/api'
import type { BankAccountResponse } from '@/types/payments'
const schema = z const schema = z
.object({ .object({
@@ -32,8 +33,15 @@ const schema = z
amount: z.number().min(0.01, 'Amount must be greater than 0'), amount: z.number().min(0.01, 'Amount must be greater than 0'),
method: z.enum(['Cash', 'Cheque', 'Credit']), method: z.enum(['Cash', 'Cheque', 'Credit']),
entryMode: z.enum(['RealTime', 'DayEndManual']), entryMode: z.enum(['RealTime', 'DayEndManual']),
bankAccountId: z.string().optional(),
chequeNumber: z.string().optional(), chequeNumber: z.string().optional(),
chequeDate: z.string().optional(), chequeDate: z.string().optional(),
drawerBankName: z.string().optional(),
accountHolderName: z.string().optional(),
})
.refine((data) => data.method !== 'Cash' || !!data.bankAccountId, {
message: 'Bank/Cash account is required for cash payments',
path: ['bankAccountId'],
}) })
.refine((data) => data.method !== 'Cheque' || !!data.chequeNumber, { .refine((data) => data.method !== 'Cheque' || !!data.chequeNumber, {
message: 'Cheque number is required for cheque payments', message: 'Cheque number is required for cheque payments',
@@ -51,21 +59,27 @@ interface PaymentSheetProps {
onClose: () => void onClose: () => void
onSuccess: () => void onSuccess: () => void
invoices: InvoiceResponse[] invoices: InvoiceResponse[]
defaultInvoiceId?: string
} }
const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}` const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}`
const truncId = (id: string) => id.substring(0, 8) const truncId = (id: string) => id.substring(0, 8)
export function PaymentSheet({ open, onClose, onSuccess, invoices }: PaymentSheetProps) { export function PaymentSheet({ open, onClose, onSuccess, invoices, defaultInvoiceId }: PaymentSheetProps) {
const [accounts, setAccounts] = useState<BankAccountResponse[]>([])
const form = useForm<FormValues>({ const form = useForm<FormValues>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: { defaultValues: {
invoiceId: '', invoiceId: defaultInvoiceId ?? '',
amount: 0, amount: 0,
method: 'Cash', method: 'Cash',
entryMode: 'RealTime', entryMode: 'RealTime',
bankAccountId: '',
chequeNumber: '', chequeNumber: '',
chequeDate: '', chequeDate: '',
drawerBankName: '',
accountHolderName: '',
}, },
}) })
@@ -74,23 +88,32 @@ export function PaymentSheet({ open, onClose, onSuccess, invoices }: PaymentShee
useEffect(() => { useEffect(() => {
if (!open) return if (!open) return
form.reset({ form.reset({
invoiceId: '', invoiceId: defaultInvoiceId ?? '',
amount: 0, amount: 0,
method: 'Cash', method: 'Cash',
entryMode: 'RealTime', entryMode: 'RealTime',
bankAccountId: '',
chequeNumber: '', chequeNumber: '',
chequeDate: '', chequeDate: '',
drawerBankName: '',
accountHolderName: '',
}) })
}, [open]) bankAccountsApi.list()
.then(list => setAccounts(list.filter(a => a.isActive !== false)))
.catch(() => {})
}, [open, defaultInvoiceId])
async function onSubmit(values: FormValues) { async function onSubmit(values: FormValues) {
const { invoiceId, method, chequeNumber, chequeDate, ...rest } = values const { invoiceId, method, bankAccountId, chequeNumber, chequeDate, drawerBankName, accountHolderName, ...rest } = values
try { try {
await paymentsApi.create(invoiceId, { await paymentsApi.create(invoiceId, {
...rest, ...rest,
method, method,
bankAccountId: method === 'Cash' ? bankAccountId : undefined,
chequeNumber: method === 'Cheque' ? chequeNumber : undefined, chequeNumber: method === 'Cheque' ? chequeNumber : undefined,
chequeDate: method === 'Cheque' ? chequeDate : undefined, chequeDate: method === 'Cheque' ? chequeDate : undefined,
drawerBankName: method === 'Cheque' ? drawerBankName || undefined : undefined,
accountHolderName: method === 'Cheque' ? accountHolderName || undefined : undefined,
}) })
toast.add({ title: 'Payment recorded', type: 'success' }) toast.add({ title: 'Payment recorded', type: 'success' })
onSuccess() onSuccess()
@@ -118,12 +141,11 @@ export function PaymentSheet({ open, onClose, onSuccess, invoices }: PaymentShee
label="Invoice" label="Invoice"
required required
render={({ field }) => ( render={({ field }) => (
<Select value={field.value as string} onValueChange={field.onChange}> <Select value={field.value as string} onValueChange={field.onChange} disabled={!!defaultInvoiceId}>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select invoice..." /> <SelectValue placeholder="Select invoice..." />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="" disabled>Select invoice...</SelectItem>
{pendingInvoices.map((inv) => ( {pendingInvoices.map((inv) => (
<SelectItem key={inv.id} value={inv.id}> <SelectItem key={inv.id} value={inv.id}>
{inv.storeName} — {fmtAmount(inv.totalAmount)} ({inv.paymentStatus}) [{truncId(inv.id)}] {inv.storeName} — {fmtAmount(inv.totalAmount)} ({inv.paymentStatus}) [{truncId(inv.id)}]
@@ -174,6 +196,29 @@ export function PaymentSheet({ open, onClose, onSuccess, invoices }: PaymentShee
)} )}
/> />
{method === 'Cash' && (
<FormField
control={form.control}
name="bankAccountId"
label="Deposit Account"
required
render={({ field }) => (
<Select value={(field.value as string) ?? ''} onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue placeholder="Choose where the money lands" />
</SelectTrigger>
<SelectContent>
{accounts.map((a) => (
<SelectItem key={a.accountId} value={a.accountId}>
{a.accountName}{a.bankName ? ` (${a.bankName})` : ''}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
)}
{method === 'Cheque' && ( {method === 'Cheque' && (
<> <>
<FormField <FormField
@@ -213,6 +258,40 @@ export function PaymentSheet({ open, onClose, onSuccess, invoices }: PaymentShee
/> />
)} )}
/> />
<FormField
control={form.control}
name="drawerBankName"
label="Drawer Bank (optional)"
render={({ field }) => (
<Input
id="drawerBankName"
name={field.name}
ref={field.ref}
value={(field.value as string) ?? ''}
onChange={field.onChange}
onBlur={field.onBlur}
placeholder="e.g. Sampath Bank"
/>
)}
/>
<FormField
control={form.control}
name="accountHolderName"
label="Account Holder (optional)"
render={({ field }) => (
<Input
id="accountHolderName"
name={field.name}
ref={field.ref}
value={(field.value as string) ?? ''}
onChange={field.onChange}
onBlur={field.onBlur}
placeholder="Name on the cheque"
/>
)}
/>
</> </>
)} )}
@@ -30,6 +30,7 @@ import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { toast } from '@/components/ui/toast' import { toast } from '@/components/ui/toast'
import { TopLoader } from '@/components/shared/top-loader'
function formatDate(value: string) { function formatDate(value: string) {
return new Date(value).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) return new Date(value).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
@@ -70,7 +71,8 @@ export function SupplierPaymentsClient({ supplierId }: { supplierId: string }) {
const pageCount = Math.max(1, Math.ceil(total / pageSize)) const pageCount = Math.max(1, Math.ceil(total / pageSize))
return ( return (
<> <div className="relative">
<TopLoader active={loading} />
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-semibold text-foreground">Supplier Payments</h1> <h1 className="text-2xl font-semibold text-foreground">Supplier Payments</h1>
@@ -132,7 +134,7 @@ export function SupplierPaymentsClient({ supplierId }: { supplierId: string }) {
onCreated={() => { setCreateOpen(false); loadPayments() }} onCreated={() => { setCreateOpen(false); loadPayments() }}
/> />
)} )}
</> </div>
) )
} }
@@ -0,0 +1,44 @@
'use client'
export function TopLoader({ active }: { active: boolean }) {
if (!active) return null
return (
<div className="pointer-events-none absolute inset-x-0 top-0 h-[2px] overflow-hidden">
<div className="top-loader-bar">
<span className="top-loader-head" />
</div>
<style jsx>{`
.top-loader-bar {
position: absolute;
inset: 0;
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 60, 90, 0.45) 50%,
rgba(255, 90, 120, 0.9) 85%,
#ff2e63 100%
);
transform: translateX(-100%);
animation: top-loader-slide 1.1s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
.top-loader-head {
position: absolute;
right: 0;
top: -2px;
height: 6px;
width: 10px;
border-radius: 9999px;
background: #ff6b8a;
box-shadow:
0 0 6px 1px #ff2e63,
0 0 14px 2px rgba(255, 46, 99, 0.75),
0 0 24px 4px rgba(255, 46, 99, 0.35);
}
@keyframes top-loader-slide {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
`}</style>
</div>
)
}
+3
View File
@@ -97,8 +97,11 @@ export interface PaymentBody {
amount: number amount: number
method: PaymentMethod method: PaymentMethod
entryMode?: EntryMode entryMode?: EntryMode
bankAccountId?: string | null
chequeNumber?: string | null chequeNumber?: string | null
chequeDate?: string | null chequeDate?: string | null
drawerBankName?: string | null
accountHolderName?: string | null
collectedByUserId?: string | null collectedByUserId?: string | null
} }