feat: integrate payment functionality with new payment sheet and enhance supplier payments UI
This commit is contained in:
@@ -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"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user