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 { InvoiceActionDialog } from './invoice-action-dialog'
import { PaymentSheet } from '../../payments/_components/payment-sheet'
const fmtDate = (v: string) =>
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 [actionOpen, setActionOpen] = useState(false)
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) => {
setLoading(true)
@@ -76,6 +79,11 @@ export function OrdersClient() {
setActionOpen(true)
}
function handleRecordPayment(row: InvoiceResponse) {
setPaymentInvoice(row)
setPaymentOpen(true)
}
const tableConfig = useMemo<TableConfig<InvoiceResponse>>(() => ({
rowKey: 'id',
serverSide: true,
@@ -151,6 +159,12 @@ export function OrdersClient() {
onClick: handleFinalize,
visible: (target) => target.status === 'Printed',
},
{
action: 'edit',
label: 'Record payment',
onClick: handleRecordPayment,
visible: (target) => target.paymentStatus !== 'Paid',
},
],
},
}), [pageSize])
@@ -190,6 +204,14 @@ export function OrdersClient() {
invoice={selectedInvoice}
mode={actionMode}
/>
<PaymentSheet
open={paymentOpen}
onClose={() => setPaymentOpen(false)}
onSuccess={() => fetchData(page, pageSize)}
invoices={paymentInvoice ? [paymentInvoice] : []}
defaultInvoiceId={paymentInvoice?.id}
/>
</>
)
}
@@ -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'
@@ -23,8 +23,9 @@ 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, bankAccounts as bankAccountsApi, getApiError } from '@/lib/api/index'
import type { InvoiceResponse } from '@/types/api'
import type { BankAccountResponse } from '@/types/payments'
const schema = z
.object({
@@ -32,8 +33,15 @@ const schema = z
amount: z.number().min(0.01, 'Amount must be greater than 0'),
method: z.enum(['Cash', 'Cheque', 'Credit']),
entryMode: z.enum(['RealTime', 'DayEndManual']),
bankAccountId: z.string().optional(),
chequeNumber: 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, {
message: 'Cheque number is required for cheque payments',
@@ -51,21 +59,27 @@ interface PaymentSheetProps {
onClose: () => void
onSuccess: () => void
invoices: InvoiceResponse[]
defaultInvoiceId?: string
}
const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}`
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>({
resolver: zodResolver(schema),
defaultValues: {
invoiceId: '',
invoiceId: defaultInvoiceId ?? '',
amount: 0,
method: 'Cash',
entryMode: 'RealTime',
bankAccountId: '',
chequeNumber: '',
chequeDate: '',
drawerBankName: '',
accountHolderName: '',
},
})
@@ -74,23 +88,32 @@ export function PaymentSheet({ open, onClose, onSuccess, invoices }: PaymentShee
useEffect(() => {
if (!open) return
form.reset({
invoiceId: '',
invoiceId: defaultInvoiceId ?? '',
amount: 0,
method: 'Cash',
entryMode: 'RealTime',
bankAccountId: '',
chequeNumber: '',
chequeDate: '',
drawerBankName: '',
accountHolderName: '',
})
}, [open])
bankAccountsApi.list()
.then(list => setAccounts(list.filter(a => a.isActive !== false)))
.catch(() => {})
}, [open, defaultInvoiceId])
async function onSubmit(values: FormValues) {
const { invoiceId, method, chequeNumber, chequeDate, ...rest } = values
const { invoiceId, method, bankAccountId, chequeNumber, chequeDate, drawerBankName, accountHolderName, ...rest } = values
try {
await paymentsApi.create(invoiceId, {
...rest,
method,
bankAccountId: method === 'Cash' ? bankAccountId : undefined,
chequeNumber: method === 'Cheque' ? chequeNumber : 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' })
onSuccess()
@@ -118,12 +141,11 @@ export function PaymentSheet({ open, onClose, onSuccess, invoices }: PaymentShee
label="Invoice"
required
render={({ field }) => (
<Select value={field.value as string} onValueChange={field.onChange}>
<Select value={field.value as string} onValueChange={field.onChange} disabled={!!defaultInvoiceId}>
<SelectTrigger>
<SelectValue placeholder="Select invoice..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="" disabled>Select invoice...</SelectItem>
{pendingInvoices.map((inv) => (
<SelectItem key={inv.id} value={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' && (
<>
<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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { toast } from '@/components/ui/toast'
import { TopLoader } from '@/components/shared/top-loader'
function formatDate(value: string) {
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))
return (
<>
<div className="relative">
<TopLoader active={loading} />
<div className="flex items-center justify-between">
<div>
<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() }}
/>
)}
</>
</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
method: PaymentMethod
entryMode?: EntryMode
bankAccountId?: string | null
chequeNumber?: string | null
chequeDate?: string | null
drawerBankName?: string | null
accountHolderName?: string | null
collectedByUserId?: string | null
}