feat: add cheque books and received cheques management
- Implemented ChequeBooksPage for viewing and filtering cheque books. - Added NewReceivedChequePage for recording received cheques. - Created ReceivedChequesPage for listing and managing received cheques. - Introduced API endpoints for bank accounts, cheque books, and received cheques. - Defined types for payments including bank accounts, cheque books, and received cheques.
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { ArrowLeft } from 'lucide-react'
|
||||
|
||||
import { bankAccounts as bankAccountsApi, getApiError } from '@/lib/api/index'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { BankAccountType, type CashAccountTypeDto } from '@/types/payments'
|
||||
|
||||
import { Button, buttonVariants } from '@/components/ui/button'
|
||||
import { Field, FieldError, FieldLabel } from '@/components/ui/field'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { toast } from '@/components/ui/toast'
|
||||
|
||||
const OTHER_CASH_TYPE = '__other__'
|
||||
|
||||
export default function NewBankAccountPage() {
|
||||
const router = useRouter()
|
||||
const [accountType, setAccountType] = useState<BankAccountType>(BankAccountType.Bank)
|
||||
|
||||
const [cashAccountTypes, setCashAccountTypes] = useState<CashAccountTypeDto[] | null>(null)
|
||||
const [cashAccountTypesError, setCashAccountTypesError] = useState<string | null>(null)
|
||||
|
||||
const [accountName, setAccountName] = useState('')
|
||||
const [bankName, setBankName] = useState('')
|
||||
const [cashAccountTypeChoice, setCashAccountTypeChoice] = useState('')
|
||||
const [customCashAccountTypeName, setCustomCashAccountTypeName] = useState('')
|
||||
const [accountNumber, setAccountNumber] = useState('')
|
||||
const [currencyCode, setCurrencyCode] = useState('LKR')
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (accountType !== BankAccountType.Cash || cashAccountTypes !== null) return
|
||||
bankAccountsApi.cashAccountTypes()
|
||||
.then(setCashAccountTypes)
|
||||
.catch(err => setCashAccountTypesError(getApiError(err)))
|
||||
}, [accountType, cashAccountTypes])
|
||||
|
||||
const cashAccountTypeName =
|
||||
cashAccountTypeChoice === OTHER_CASH_TYPE ? customCashAccountTypeName.trim() : cashAccountTypeChoice
|
||||
|
||||
async function handleSubmit() {
|
||||
const next: Record<string, string> = {}
|
||||
if (!accountName.trim()) next.accountName = 'Account name is required'
|
||||
if (accountType === BankAccountType.Cash && !cashAccountTypeName) {
|
||||
next.cashAccountTypeName = 'Select or enter a cash account type'
|
||||
}
|
||||
setErrors(next)
|
||||
if (Object.keys(next).length > 0) return
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const created =
|
||||
accountType === BankAccountType.Bank
|
||||
? await bankAccountsApi.createBank({
|
||||
accountName,
|
||||
bankName: bankName || null,
|
||||
accountNumber: accountNumber || null,
|
||||
currencyCode: currencyCode || undefined,
|
||||
})
|
||||
: await bankAccountsApi.createCash({
|
||||
accountName,
|
||||
cashAccountTypeName,
|
||||
accountNumber: accountNumber || null,
|
||||
currencyCode: currencyCode || undefined,
|
||||
})
|
||||
toast.add({ title: `${accountType} account created`, description: created.accountName, type: 'success' })
|
||||
router.push('/payments/bank-accounts')
|
||||
} catch (err) {
|
||||
toast.add({ title: `Could not create ${accountType.toLowerCase()} account`, description: getApiError(err), type: 'error' })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href="/payments/bank-accounts" className={cn(buttonVariants({ variant: 'ghost', size: 'icon' }))}>
|
||||
<ArrowLeft className="size-4" />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-foreground">New Cash / Bank Account</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">Record a new cash or bank account for use in payments.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border bg-card p-6">
|
||||
<div className="mb-6 flex max-w-sm gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant={accountType === BankAccountType.Bank ? 'default' : 'outline'}
|
||||
className="flex-1"
|
||||
onClick={() => setAccountType(BankAccountType.Bank)}
|
||||
>Bank</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={accountType === BankAccountType.Cash ? 'default' : 'outline'}
|
||||
className="flex-1"
|
||||
onClick={() => setAccountType(BankAccountType.Cash)}
|
||||
>Cash</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-x-6 gap-y-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<Field data-invalid={!!errors.accountName}>
|
||||
<FieldLabel htmlFor="ba-name">Account name</FieldLabel>
|
||||
<Input
|
||||
id="ba-name"
|
||||
value={accountName}
|
||||
onChange={(e) => setAccountName(e.target.value)}
|
||||
placeholder={accountType === BankAccountType.Bank ? 'Main Account' : 'Head Office Petty Cash'}
|
||||
aria-invalid={!!errors.accountName}
|
||||
/>
|
||||
<FieldError errors={[errors.accountName ? { message: errors.accountName } : undefined]} />
|
||||
</Field>
|
||||
|
||||
{accountType === BankAccountType.Bank ? (
|
||||
<>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="ba-bank">Bank name (optional)</FieldLabel>
|
||||
<Input id="ba-bank" value={bankName} onChange={(e) => setBankName(e.target.value)} placeholder="Commercial Bank" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="ba-acct-no">Account number (optional)</FieldLabel>
|
||||
<Input id="ba-acct-no" value={accountNumber} onChange={(e) => setAccountNumber(e.target.value)} placeholder="8001234567" />
|
||||
</Field>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Field data-invalid={!!errors.cashAccountTypeName}>
|
||||
<FieldLabel htmlFor="ba-cash-type">Cash account type</FieldLabel>
|
||||
<Select<string> value={cashAccountTypeChoice} onValueChange={(v) => setCashAccountTypeChoice(v ?? '')}>
|
||||
<SelectTrigger id="ba-cash-type" className="w-full" aria-invalid={!!errors.cashAccountTypeName}>
|
||||
<SelectValue placeholder={cashAccountTypes === null ? 'Loading…' : 'Select a type'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(cashAccountTypes ?? []).map(t => (
|
||||
<SelectItem key={t.name} value={t.name} label={t.name}>{t.name}</SelectItem>
|
||||
))}
|
||||
<SelectItem value={OTHER_CASH_TYPE} label="Other, please specify">Other, please specify…</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{cashAccountTypeChoice === OTHER_CASH_TYPE && (
|
||||
<Input
|
||||
value={customCashAccountTypeName}
|
||||
onChange={(e) => setCustomCashAccountTypeName(e.target.value)}
|
||||
placeholder="e.g. Site Cash"
|
||||
className="mt-2"
|
||||
/>
|
||||
)}
|
||||
{cashAccountTypesError && (
|
||||
<p className="text-sm text-destructive">{cashAccountTypesError}</p>
|
||||
)}
|
||||
<FieldError errors={[errors.cashAccountTypeName ? { message: errors.cashAccountTypeName } : undefined]} />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="ba-acct-no">Account number (optional)</FieldLabel>
|
||||
<Input id="ba-acct-no" value={accountNumber} onChange={(e) => setAccountNumber(e.target.value)} />
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="ba-currency">Currency</FieldLabel>
|
||||
<Input id="ba-currency" value={currencyCode} onChange={(e) => setCurrencyCode(e.target.value)} maxLength={3} placeholder="LKR" />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-end gap-3">
|
||||
<Link href="/payments/bank-accounts" className={cn(buttonVariants({ variant: 'outline' }))}>Cancel</Link>
|
||||
<Button onClick={handleSubmit} disabled={submitting}>{submitting ? 'Creating…' : 'Create'}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { CheckCircle2, Pencil, Plus, Search, Trash2, Wallet, XCircle } from 'lucide-react'
|
||||
|
||||
import { bankAccounts as bankAccountsApi, getApiError } from '@/lib/api/index'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { BankAccountType, type BankAccountResponse } from '@/types/payments'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button, buttonVariants } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Field, FieldError, FieldGroup, FieldLabel } from '@/components/ui/field'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { toast } from '@/components/ui/toast'
|
||||
|
||||
type AccountTypeFilter = BankAccountType | 'All'
|
||||
|
||||
function formatDate(value: string): string {
|
||||
return new Date(value).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
|
||||
}
|
||||
|
||||
export default function BankAccountsPage() {
|
||||
const [accounts, setAccounts] = useState<BankAccountResponse[] | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [search, setSearch] = useState('')
|
||||
const [accountType, setAccountType] = useState<AccountTypeFilter>('All')
|
||||
|
||||
const [editTarget, setEditTarget] = useState<BankAccountResponse | null>(null)
|
||||
const [editName, setEditName] = useState('')
|
||||
const [editError, setEditError] = useState<string | null>(null)
|
||||
const [editSubmitting, setEditSubmitting] = useState(false)
|
||||
const [statusPendingId, setStatusPendingId] = useState<string | null>(null)
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null)
|
||||
const [confirmDelete, setConfirmDelete] = useState<BankAccountResponse | null>(null)
|
||||
|
||||
function load() {
|
||||
const filter = accountType === 'All' ? undefined : accountType
|
||||
bankAccountsApi
|
||||
.list(filter)
|
||||
.then(setAccounts)
|
||||
.catch(err => setError(getApiError(err)))
|
||||
}
|
||||
|
||||
useEffect(load, [accountType])
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (!accounts) return null
|
||||
const q = search.trim().toLowerCase()
|
||||
if (!q) return accounts
|
||||
return accounts.filter(a =>
|
||||
a.accountName.toLowerCase().includes(q) ||
|
||||
(a.bankName ?? '').toLowerCase().includes(q) ||
|
||||
(a.cashAccountTypeName ?? '').toLowerCase().includes(q) ||
|
||||
(a.accountNumber ?? '').toLowerCase().includes(q) ||
|
||||
a.currencyCode.toLowerCase().includes(q),
|
||||
)
|
||||
}, [accounts, search])
|
||||
|
||||
function openEdit(a: BankAccountResponse) {
|
||||
setEditTarget(a)
|
||||
setEditName(a.accountName)
|
||||
setEditError(null)
|
||||
}
|
||||
|
||||
async function handleRename() {
|
||||
if (!editTarget) return
|
||||
if (!editName.trim()) {
|
||||
setEditError('Account name is required')
|
||||
return
|
||||
}
|
||||
setEditSubmitting(true)
|
||||
try {
|
||||
await bankAccountsApi.updateCash(editTarget.accountId, editName.trim())
|
||||
toast.add({ title: 'Cash account renamed', type: 'success' })
|
||||
setEditTarget(null)
|
||||
load()
|
||||
} catch (err) {
|
||||
setEditError(getApiError(err))
|
||||
} finally {
|
||||
setEditSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleStatus(a: BankAccountResponse) {
|
||||
setStatusPendingId(a.accountId)
|
||||
try {
|
||||
await bankAccountsApi.setCashStatus(a.accountId, !a.isActive)
|
||||
toast.add({ title: a.isActive ? 'Cash account deactivated' : 'Cash account activated', type: 'success' })
|
||||
load()
|
||||
} catch (err) {
|
||||
toast.add({ title: 'Could not update status', description: getApiError(err), type: 'error' })
|
||||
} finally {
|
||||
setStatusPendingId(null)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(a: BankAccountResponse) {
|
||||
setDeletingId(a.accountId)
|
||||
try {
|
||||
await bankAccountsApi.deleteCash(a.accountId)
|
||||
toast.add({ title: 'Cash account deleted', type: 'success' })
|
||||
load()
|
||||
} catch (err) {
|
||||
toast.add({ title: 'Could not delete cash account', description: getApiError(err), type: 'error' })
|
||||
} finally {
|
||||
setDeletingId(null)
|
||||
setConfirmDelete(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-foreground">Cash / Bank Accounts</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">Cash and bank accounts used for payments, deposits, and cheques.</p>
|
||||
</div>
|
||||
<Link href="/payments/bank-accounts/new" className={cn(buttonVariants())}>
|
||||
<Plus className="size-4" />
|
||||
New Account
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<div className="relative flex-1">
|
||||
<Search className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Search name, bank/type, account no. or currency…"
|
||||
className="pl-9"
|
||||
aria-label="Search cash/bank accounts"
|
||||
/>
|
||||
</div>
|
||||
<Select<AccountTypeFilter> value={accountType} onValueChange={(v) => setAccountType(v ?? 'All')}>
|
||||
<SelectTrigger className="w-full sm:w-48">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="All" label="All types">All types</SelectItem>
|
||||
<SelectItem value={BankAccountType.Bank} label="Bank">Bank</SelectItem>
|
||||
<SelectItem value={BankAccountType.Cash} label="Cash">Cash</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive">{error}</div>
|
||||
)}
|
||||
|
||||
{!error && filtered === null && (
|
||||
<div className="flex flex-col gap-2">
|
||||
{Array.from({ length: 4 }).map((_, i) => <Skeleton key={i} className="h-12 w-full" />)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!error && filtered !== null && filtered.length === 0 && (
|
||||
<div className="flex flex-col items-center gap-3 rounded-xl border border-dashed py-16 text-center">
|
||||
<Wallet className="size-10 text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{search ? 'No accounts match your search.' : 'No cash/bank accounts yet.'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!error && filtered !== null && filtered.length > 0 && (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Account name</TableHead>
|
||||
<TableHead>Bank / Cash type</TableHead>
|
||||
<TableHead>Account no.</TableHead>
|
||||
<TableHead>Currency</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filtered.map(a => (
|
||||
<TableRow key={`${a.accountType}-${a.accountId}`}>
|
||||
<TableCell>
|
||||
<Badge variant={a.accountType === BankAccountType.Cash ? 'secondary' : 'default'}>
|
||||
{a.accountType}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">{a.accountName}</TableCell>
|
||||
<TableCell>{a.bankName ?? a.cashAccountTypeName ?? '—'}</TableCell>
|
||||
<TableCell>{a.accountNumber ?? '—'}</TableCell>
|
||||
<TableCell>{a.currencyCode}</TableCell>
|
||||
<TableCell>{formatDate(a.createdAt)}</TableCell>
|
||||
<TableCell>
|
||||
{a.accountType === BankAccountType.Cash ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<Button variant="ghost" size="icon" onClick={() => openEdit(a)} aria-label={`Edit ${a.accountName}`}>
|
||||
<Pencil className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(a.isActive ? 'text-destructive hover:bg-destructive/10' : 'text-emerald-600 hover:bg-emerald-100')}
|
||||
aria-label={a.isActive ? `Deactivate ${a.accountName}` : `Activate ${a.accountName}`}
|
||||
title={a.isDefault && a.isActive ? "Default cash account can't be deactivated" : undefined}
|
||||
disabled={statusPendingId === a.accountId || (a.isDefault === true && a.isActive === true)}
|
||||
onClick={() => toggleStatus(a)}
|
||||
>
|
||||
{a.isActive ? <XCircle className="size-4" /> : <CheckCircle2 className="size-4" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-destructive hover:bg-destructive/10"
|
||||
aria-label={`Delete ${a.accountName}`}
|
||||
title={a.isDefault ? "Default cash account can't be deleted" : undefined}
|
||||
disabled={deletingId === a.accountId || a.isDefault === true}
|
||||
onClick={() => setConfirmDelete(a)}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">—</span>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
|
||||
<Dialog open={!!editTarget} onOpenChange={(v) => { if (!v) setEditTarget(null) }}>
|
||||
<DialogContent className="sm:max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Rename cash account</DialogTitle>
|
||||
<DialogDescription>
|
||||
{editTarget?.isDefault ? 'This is the default cash account — only its name can be changed.' : "Update this cash account's display name."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<FieldGroup>
|
||||
<Field data-invalid={!!editError}>
|
||||
<FieldLabel htmlFor="cash-account-name">Account name</FieldLabel>
|
||||
<Input id="cash-account-name" value={editName} onChange={(e) => setEditName(e.target.value)} aria-invalid={!!editError} />
|
||||
<FieldError errors={[editError ? { message: editError } : undefined]} />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<div className="flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end">
|
||||
<Button variant="outline" onClick={() => setEditTarget(null)} disabled={editSubmitting}>Cancel</Button>
|
||||
<Button onClick={handleRename} disabled={editSubmitting}>{editSubmitting ? 'Saving…' : 'Save'}</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={!!confirmDelete} onOpenChange={(v) => { if (!v) setConfirmDelete(null) }}>
|
||||
<DialogContent className="sm:max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete {confirmDelete?.accountName}?</DialogTitle>
|
||||
<DialogDescription>This permanently removes the cash account. Accounts referenced elsewhere cannot be deleted.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end">
|
||||
<Button variant="outline" onClick={() => setConfirmDelete(null)} disabled={!!deletingId}>Cancel</Button>
|
||||
<Button variant="destructive" onClick={() => confirmDelete && handleDelete(confirmDelete)} disabled={!!deletingId}>
|
||||
{deletingId ? 'Deleting…' : 'Delete'}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { ArrowLeft, BookText } from 'lucide-react'
|
||||
|
||||
import { bankAccounts as bankAccountsApi, chequeBooks as chequeBooksApi, chequePages as chequePagesApi, getApiError } from '@/lib/api/index'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
BankAccountType,
|
||||
ChequePageIssueStatus,
|
||||
ChequePageStatusAction,
|
||||
PayeeType,
|
||||
type BankAccountResponse,
|
||||
type ChequeBookResponse,
|
||||
type ChequePageResponse,
|
||||
} from '@/types/payments'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button, buttonVariants } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Field, FieldError, FieldLabel } from '@/components/ui/field'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { toast } from '@/components/ui/toast'
|
||||
|
||||
const STATUS_BADGE_VARIANT: Record<ChequePageIssueStatus, 'default' | 'secondary' | 'destructive' | 'outline'> = {
|
||||
[ChequePageIssueStatus.Unused]: 'outline',
|
||||
[ChequePageIssueStatus.Issued]: 'default',
|
||||
[ChequePageIssueStatus.Cleared]: 'secondary',
|
||||
[ChequePageIssueStatus.Bounced]: 'destructive',
|
||||
[ChequePageIssueStatus.Cancelled]: 'destructive',
|
||||
[ChequePageIssueStatus.Void]: 'outline',
|
||||
}
|
||||
|
||||
function formatDate(value: string | null): string {
|
||||
if (!value) return '—'
|
||||
return new Date(value).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
|
||||
}
|
||||
|
||||
function formatAmount(value: number | null): string {
|
||||
if (value === null) return '—'
|
||||
return value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
export default function ChequeBookDetailPage() {
|
||||
const params = useParams<{ chequeBookNo: string }>()
|
||||
const chequeBookNo = decodeURIComponent(params.chequeBookNo)
|
||||
|
||||
const [book, setBook] = useState<ChequeBookResponse | null>(null)
|
||||
const [bankAccount, setBankAccount] = useState<BankAccountResponse | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [selectedPage, setSelectedPage] = useState<ChequePageResponse | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
chequeBooksApi
|
||||
.get(chequeBookNo, true)
|
||||
.then(async res => {
|
||||
if (cancelled) return
|
||||
setBook(res)
|
||||
try {
|
||||
const banks = await bankAccountsApi.list(BankAccountType.Bank)
|
||||
if (!cancelled) setBankAccount(banks.find(b => b.accountId === res.bankAccountId) ?? null)
|
||||
} catch { /* ignore */ }
|
||||
})
|
||||
.catch(err => { if (!cancelled) setError(getApiError(err)) })
|
||||
return () => { cancelled = true }
|
||||
}, [chequeBookNo])
|
||||
|
||||
function handlePageUpdated(updated: ChequePageResponse) {
|
||||
setBook(prev => prev ? { ...prev, pages: prev.pages.map(p => p.chequeNo === updated.chequeNo ? updated : p) } : prev)
|
||||
setSelectedPage(updated)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href="/payments/cheque-books" className={cn(buttonVariants({ variant: 'ghost', size: 'icon' }))}>
|
||||
<ArrowLeft className="size-4" />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-foreground">Cheque Book {chequeBookNo}</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">Every leaf in this book — click one to view or take an action.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive">{error}</div>
|
||||
)}
|
||||
|
||||
{!error && book === null && (
|
||||
<div className="flex flex-col gap-2">
|
||||
{Array.from({ length: 4 }).map((_, i) => <Skeleton key={i} className="h-12 w-full" />)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!error && book !== null && (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-4 rounded-xl border bg-card p-5 sm:grid-cols-4">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Bank account</p>
|
||||
<p className="text-sm font-medium">{bankAccount ? bankAccount.accountName : `#${book.bankAccountId.slice(0, 8)}`}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Branch</p>
|
||||
<p className="text-sm font-medium">{book.branchId}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Range</p>
|
||||
<p className="text-sm font-medium">{book.startChequeNo} – {book.endChequeNo}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Received</p>
|
||||
<p className="text-sm font-medium">{formatDate(book.receivedDate)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{book.pages.length === 0 ? (
|
||||
<div className="flex flex-col items-center gap-3 rounded-xl border border-dashed py-16 text-center">
|
||||
<BookText className="size-10 text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground">No pages found for this book.</p>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Cheque no.</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Payee</TableHead>
|
||||
<TableHead>Issue date</TableHead>
|
||||
<TableHead className="text-right">Amount</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{book.pages.map(p => (
|
||||
<TableRow key={p.chequeNo} className="cursor-pointer" onClick={() => setSelectedPage(p)}>
|
||||
<TableCell className="font-medium">{p.chequeNo}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={STATUS_BADGE_VARIANT[p.issueStatus]}>{p.issueStatus}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{p.payeeName ?? '—'}</TableCell>
|
||||
<TableCell>{formatDate(p.issueDate)}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">{formatAmount(p.amount)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<ChequePageDialog page={selectedPage} onClose={() => setSelectedPage(null)} onUpdated={handlePageUpdated} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Inline cheque page action dialog — Distribution doesn't ship a shared ChequePageDialog component.
|
||||
function ChequePageDialog({
|
||||
page,
|
||||
onClose,
|
||||
onUpdated,
|
||||
}: {
|
||||
page: ChequePageResponse | null
|
||||
onClose: () => void
|
||||
onUpdated: (p: ChequePageResponse) => void
|
||||
}) {
|
||||
const open = page !== null
|
||||
const [mode, setMode] = useState<'view' | 'issue' | 'status'>('view')
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
// Issue form
|
||||
const [payeeType, setPayeeType] = useState<PayeeType>(PayeeType.Supplier)
|
||||
const [payeeName, setPayeeName] = useState('')
|
||||
const [issueDate, setIssueDate] = useState('')
|
||||
const [amount, setAmount] = useState('')
|
||||
const [purpose, setPurpose] = useState('')
|
||||
const [issueErrors, setIssueErrors] = useState<Record<string, string>>({})
|
||||
|
||||
// Status form
|
||||
const [action, setAction] = useState<ChequePageStatusAction>(ChequePageStatusAction.Clear)
|
||||
const [clearedDate, setClearedDate] = useState('')
|
||||
const [cancelReason, setCancelReason] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (!page) return
|
||||
setMode('view')
|
||||
setPayeeType(page.payeeType ?? PayeeType.Supplier)
|
||||
setPayeeName(page.payeeName ?? '')
|
||||
setIssueDate(page.issueDate ? page.issueDate.slice(0, 10) : '')
|
||||
setAmount(page.amount != null ? String(page.amount) : '')
|
||||
setPurpose(page.purpose ?? '')
|
||||
setIssueErrors({})
|
||||
setAction(ChequePageStatusAction.Clear)
|
||||
setClearedDate('')
|
||||
setCancelReason('')
|
||||
}, [page])
|
||||
|
||||
if (!page) return null
|
||||
|
||||
async function handleIssue() {
|
||||
if (!page) return
|
||||
const next: Record<string, string> = {}
|
||||
if (!payeeName.trim()) next.payeeName = 'Payee name is required'
|
||||
if (!issueDate) next.issueDate = 'Issue date is required'
|
||||
const amountNum = Number(amount)
|
||||
if (!amount || Number.isNaN(amountNum) || amountNum <= 0) next.amount = 'Amount must be greater than zero'
|
||||
setIssueErrors(next)
|
||||
if (Object.keys(next).length > 0) return
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const updated = await chequePagesApi.issue(page.chequeNo, {
|
||||
payeeType,
|
||||
payeeName,
|
||||
issueDate,
|
||||
amount: amountNum,
|
||||
purpose: purpose || undefined,
|
||||
})
|
||||
toast.add({ title: 'Cheque issued', description: `${updated.chequeNo} — ${updated.payeeName}`, type: 'success' })
|
||||
onUpdated(updated)
|
||||
setMode('view')
|
||||
} catch (err) {
|
||||
toast.add({ title: 'Could not issue cheque', description: getApiError(err), type: 'error' })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStatus() {
|
||||
if (!page) return
|
||||
if (action === ChequePageStatusAction.Clear && !clearedDate) {
|
||||
toast.add({ title: 'Cleared date is required', type: 'error' })
|
||||
return
|
||||
}
|
||||
if (action === ChequePageStatusAction.Cancel && !cancelReason.trim()) {
|
||||
toast.add({ title: 'Cancel reason is required', type: 'error' })
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const updated = await chequePagesApi.updateStatus(page.chequeNo, {
|
||||
action,
|
||||
clearedDate: action === ChequePageStatusAction.Clear ? clearedDate : undefined,
|
||||
cancelReason: action === ChequePageStatusAction.Cancel ? cancelReason : undefined,
|
||||
})
|
||||
toast.add({ title: `Cheque ${action.toLowerCase()}ed`, type: 'success' })
|
||||
onUpdated(updated)
|
||||
setMode('view')
|
||||
} catch (err) {
|
||||
toast.add({ title: 'Could not update status', description: getApiError(err), type: 'error' })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const canIssue = page.issueStatus === ChequePageIssueStatus.Unused
|
||||
const canChangeStatus = page.issueStatus === ChequePageIssueStatus.Issued || page.issueStatus === ChequePageIssueStatus.Unused
|
||||
|
||||
const availableActions: ChequePageStatusAction[] =
|
||||
page.issueStatus === ChequePageIssueStatus.Issued
|
||||
? [ChequePageStatusAction.Clear, ChequePageStatusAction.Bounce, ChequePageStatusAction.Cancel]
|
||||
: page.issueStatus === ChequePageIssueStatus.Unused
|
||||
? [ChequePageStatusAction.Void]
|
||||
: []
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(v) => { if (!v) onClose() }}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Cheque {page.chequeNo}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Status: <Badge variant={STATUS_BADGE_VARIANT[page.issueStatus]}>{page.issueStatus}</Badge>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{mode === 'view' && (
|
||||
<div className="flex flex-col gap-3 text-sm">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Payee</p>
|
||||
<p>{page.payeeName ?? '—'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Payee type</p>
|
||||
<p>{page.payeeType ?? '—'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Issue date</p>
|
||||
<p>{formatDate(page.issueDate)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Amount</p>
|
||||
<p className="tabular-nums">{formatAmount(page.amount)}</p>
|
||||
</div>
|
||||
{page.purpose && (
|
||||
<div className="col-span-2">
|
||||
<p className="text-xs text-muted-foreground">Purpose</p>
|
||||
<p>{page.purpose}</p>
|
||||
</div>
|
||||
)}
|
||||
{page.cancelReason && (
|
||||
<div className="col-span-2">
|
||||
<p className="text-xs text-muted-foreground">Cancel reason</p>
|
||||
<p>{page.cancelReason}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2 pt-2">
|
||||
{canIssue && <Button onClick={() => setMode('issue')}>Issue cheque</Button>}
|
||||
{canChangeStatus && availableActions.length > 0 && (
|
||||
<Button variant="outline" onClick={() => { setAction(availableActions[0]); setMode('status') }}>
|
||||
Change status
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === 'issue' && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="pt">Payee type</FieldLabel>
|
||||
<Select<PayeeType> value={payeeType} onValueChange={(v) => setPayeeType(v ?? PayeeType.Supplier)}>
|
||||
<SelectTrigger id="pt" className="w-full"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.values(PayeeType).map(t => (
|
||||
<SelectItem key={t} value={t} label={t}>{t}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field data-invalid={!!issueErrors.payeeName}>
|
||||
<FieldLabel htmlFor="pn">Payee name</FieldLabel>
|
||||
<Input id="pn" value={payeeName} onChange={(e) => setPayeeName(e.target.value)} aria-invalid={!!issueErrors.payeeName} />
|
||||
<FieldError errors={[issueErrors.payeeName ? { message: issueErrors.payeeName } : undefined]} />
|
||||
</Field>
|
||||
<Field data-invalid={!!issueErrors.issueDate}>
|
||||
<FieldLabel htmlFor="id">Issue date</FieldLabel>
|
||||
<Input id="id" type="date" value={issueDate} onChange={(e) => setIssueDate(e.target.value)} aria-invalid={!!issueErrors.issueDate} />
|
||||
<FieldError errors={[issueErrors.issueDate ? { message: issueErrors.issueDate } : undefined]} />
|
||||
</Field>
|
||||
<Field data-invalid={!!issueErrors.amount}>
|
||||
<FieldLabel htmlFor="am">Amount</FieldLabel>
|
||||
<Input id="am" type="number" value={amount} onChange={(e) => setAmount(e.target.value)} aria-invalid={!!issueErrors.amount} />
|
||||
<FieldError errors={[issueErrors.amount ? { message: issueErrors.amount } : undefined]} />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="pp">Purpose (optional)</FieldLabel>
|
||||
<Input id="pp" value={purpose} onChange={(e) => setPurpose(e.target.value)} />
|
||||
</Field>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button variant="outline" onClick={() => setMode('view')} disabled={submitting}>Back</Button>
|
||||
<Button onClick={handleIssue} disabled={submitting}>{submitting ? 'Issuing…' : 'Issue'}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === 'status' && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="ac">Action</FieldLabel>
|
||||
<Select<ChequePageStatusAction> value={action} onValueChange={(v) => setAction(v ?? availableActions[0])}>
|
||||
<SelectTrigger id="ac" className="w-full"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableActions.map(a => (
|
||||
<SelectItem key={a} value={a} label={a}>{a}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
{action === ChequePageStatusAction.Clear && (
|
||||
<Field>
|
||||
<FieldLabel htmlFor="cd">Cleared date</FieldLabel>
|
||||
<Input id="cd" type="date" value={clearedDate} onChange={(e) => setClearedDate(e.target.value)} />
|
||||
</Field>
|
||||
)}
|
||||
{action === ChequePageStatusAction.Cancel && (
|
||||
<Field>
|
||||
<FieldLabel htmlFor="cr">Cancel reason</FieldLabel>
|
||||
<Input id="cr" value={cancelReason} onChange={(e) => setCancelReason(e.target.value)} />
|
||||
</Field>
|
||||
)}
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button variant="outline" onClick={() => setMode('view')} disabled={submitting}>Back</Button>
|
||||
<Button onClick={handleStatus} disabled={submitting}>{submitting ? 'Saving…' : 'Confirm'}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { ArrowLeft } from 'lucide-react'
|
||||
|
||||
import { bankAccounts as bankAccountsApi, chequeBooks as chequeBooksApi, getApiError } from '@/lib/api/index'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { BankAccountType, type BankAccountResponse } from '@/types/payments'
|
||||
|
||||
import { Button, buttonVariants } from '@/components/ui/button'
|
||||
import { Field, FieldError, FieldLabel } from '@/components/ui/field'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { toast } from '@/components/ui/toast'
|
||||
|
||||
export default function NewChequeBookPage() {
|
||||
const router = useRouter()
|
||||
const [bankAccounts, setBankAccounts] = useState<BankAccountResponse[] | null>(null)
|
||||
const [bankAccountsError, setBankAccountsError] = useState<string | null>(null)
|
||||
|
||||
const [branchId, setBranchId] = useState('')
|
||||
const [bankAccountId, setBankAccountId] = useState('')
|
||||
const [chequeBookNo, setChequeBookNo] = useState('')
|
||||
const [startChequeNo, setStartChequeNo] = useState('')
|
||||
const [endChequeNo, setEndChequeNo] = useState('')
|
||||
const [totalLeaves, setTotalLeaves] = useState('')
|
||||
const [receivedDate, setReceivedDate] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [createdBy, setCreatedBy] = useState('')
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
bankAccountsApi.list(BankAccountType.Bank)
|
||||
.then(setBankAccounts)
|
||||
.catch(err => setBankAccountsError(getApiError(err)))
|
||||
}, [])
|
||||
|
||||
async function handleSubmit() {
|
||||
const next: Record<string, string> = {}
|
||||
if (!bankAccountId) next.bankAccountId = 'Bank account is required'
|
||||
if (!branchId) next.branchId = 'Branch is required'
|
||||
if (!chequeBookNo.trim()) next.chequeBookNo = 'Cheque book number is required'
|
||||
if (!startChequeNo.trim()) next.startChequeNo = 'Start cheque number is required'
|
||||
if (!endChequeNo.trim()) next.endChequeNo = 'End cheque number is required'
|
||||
const leavesNum = Number(totalLeaves)
|
||||
if (!totalLeaves || Number.isNaN(leavesNum) || leavesNum <= 0) next.totalLeaves = 'Total leaves must be greater than zero'
|
||||
if (!receivedDate) next.receivedDate = 'Received date is required'
|
||||
setErrors(next)
|
||||
if (Object.keys(next).length > 0) return
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const created = await chequeBooksApi.create({
|
||||
branchId: Number(branchId),
|
||||
bankAccountId,
|
||||
chequeBookNo,
|
||||
startChequeNo,
|
||||
endChequeNo,
|
||||
totalLeaves: Number(totalLeaves),
|
||||
receivedDate,
|
||||
description: description || undefined,
|
||||
createdBy: createdBy || undefined,
|
||||
})
|
||||
toast.add({ title: 'Cheque book created', description: `${created.chequeBookNo} — ${created.totalLeaves} leaves`, type: 'success' })
|
||||
router.push(`/payments/cheque-books/${encodeURIComponent(created.chequeBookNo)}`)
|
||||
} catch (err) {
|
||||
toast.add({ title: 'Could not create cheque book', description: getApiError(err), type: 'error' })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href="/payments/cheque-books" className={cn(buttonVariants({ variant: 'ghost', size: 'icon' }))}>
|
||||
<ArrowLeft className="size-4" />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-foreground">New Cheque Book</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
Every leaf from the start to end cheque number is generated automatically, all "Unused".
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{bankAccountsError && (
|
||||
<div className="rounded-lg border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive">{bankAccountsError}</div>
|
||||
)}
|
||||
|
||||
<div className="rounded-xl border bg-card p-6">
|
||||
<div className="grid grid-cols-1 gap-x-6 gap-y-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<Field data-invalid={!!errors.bankAccountId}>
|
||||
<FieldLabel htmlFor="cb-bank">Bank account</FieldLabel>
|
||||
<Select<string> value={bankAccountId} onValueChange={(v) => setBankAccountId(v ?? '')}>
|
||||
<SelectTrigger id="cb-bank" className="w-full" aria-invalid={!!errors.bankAccountId}>
|
||||
<SelectValue placeholder={bankAccounts === null ? 'Loading…' : 'Select a bank account'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(bankAccounts ?? []).map(a => (
|
||||
<SelectItem key={a.accountId} value={a.accountId} label={a.accountName}>
|
||||
{a.accountName}{a.bankName ? ` — ${a.bankName}` : ''}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldError errors={[errors.bankAccountId ? { message: errors.bankAccountId } : undefined]} />
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={!!errors.branchId}>
|
||||
<FieldLabel htmlFor="cb-branch">Branch</FieldLabel>
|
||||
<Input id="cb-branch" type="number" value={branchId} onChange={(e) => setBranchId(e.target.value)} placeholder="1" aria-invalid={!!errors.branchId} />
|
||||
<FieldError errors={[errors.branchId ? { message: errors.branchId } : undefined]} />
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={!!errors.chequeBookNo}>
|
||||
<FieldLabel htmlFor="cb-no">Cheque book number</FieldLabel>
|
||||
<Input id="cb-no" value={chequeBookNo} onChange={(e) => setChequeBookNo(e.target.value)} placeholder="CB-0001" aria-invalid={!!errors.chequeBookNo} />
|
||||
<FieldError errors={[errors.chequeBookNo ? { message: errors.chequeBookNo } : undefined]} />
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={!!errors.startChequeNo}>
|
||||
<FieldLabel htmlFor="cb-start">Start cheque no.</FieldLabel>
|
||||
<Input id="cb-start" value={startChequeNo} onChange={(e) => setStartChequeNo(e.target.value)} placeholder="000001" aria-invalid={!!errors.startChequeNo} />
|
||||
<FieldError errors={[errors.startChequeNo ? { message: errors.startChequeNo } : undefined]} />
|
||||
</Field>
|
||||
<Field data-invalid={!!errors.endChequeNo}>
|
||||
<FieldLabel htmlFor="cb-end">End cheque no.</FieldLabel>
|
||||
<Input id="cb-end" value={endChequeNo} onChange={(e) => setEndChequeNo(e.target.value)} placeholder="000025" aria-invalid={!!errors.endChequeNo} />
|
||||
<FieldError errors={[errors.endChequeNo ? { message: errors.endChequeNo } : undefined]} />
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={!!errors.totalLeaves}>
|
||||
<FieldLabel htmlFor="cb-leaves">Total leaves</FieldLabel>
|
||||
<Input id="cb-leaves" type="number" value={totalLeaves} onChange={(e) => setTotalLeaves(e.target.value)} placeholder="25" aria-invalid={!!errors.totalLeaves} />
|
||||
<p className="text-xs text-muted-foreground">Must equal end − start + 1.</p>
|
||||
<FieldError errors={[errors.totalLeaves ? { message: errors.totalLeaves } : undefined]} />
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={!!errors.receivedDate}>
|
||||
<FieldLabel htmlFor="cb-received">Received date</FieldLabel>
|
||||
<Input id="cb-received" type="date" value={receivedDate} onChange={(e) => setReceivedDate(e.target.value)} aria-invalid={!!errors.receivedDate} />
|
||||
<FieldError errors={[errors.receivedDate ? { message: errors.receivedDate } : undefined]} />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="cb-desc">Description (optional)</FieldLabel>
|
||||
<Input id="cb-desc" value={description} onChange={(e) => setDescription(e.target.value)} />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="cb-by">Created by (optional)</FieldLabel>
|
||||
<Input id="cb-by" value={createdBy} onChange={(e) => setCreatedBy(e.target.value)} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-end gap-3">
|
||||
<Link href="/payments/cheque-books" className={cn(buttonVariants({ variant: 'outline' }))}>Cancel</Link>
|
||||
<Button onClick={handleSubmit} disabled={submitting}>{submitting ? 'Creating…' : 'Create'}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { BookText, Plus } from 'lucide-react'
|
||||
|
||||
import { bankAccounts as bankAccountsApi, chequeBooks as chequeBooksApi, getApiError } from '@/lib/api/index'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
BankAccountType,
|
||||
ChequeBookStatus,
|
||||
type BankAccountResponse,
|
||||
type ChequeBookResponse,
|
||||
} from '@/types/payments'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
|
||||
type StatusFilter = ChequeBookStatus | 'All'
|
||||
|
||||
const STATUS_BADGE_VARIANT: Record<ChequeBookStatus, 'default' | 'secondary' | 'destructive'> = {
|
||||
[ChequeBookStatus.Active]: 'default',
|
||||
[ChequeBookStatus.Completed]: 'secondary',
|
||||
[ChequeBookStatus.Cancelled]: 'destructive',
|
||||
}
|
||||
|
||||
function formatDate(value: string): string {
|
||||
return new Date(value).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
|
||||
}
|
||||
|
||||
export default function ChequeBooksPage() {
|
||||
const router = useRouter()
|
||||
const [books, setBooks] = useState<ChequeBookResponse[] | null>(null)
|
||||
const [bankAccountMap, setBankAccountMap] = useState<Map<string, BankAccountResponse>>(new Map())
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [status, setStatus] = useState<StatusFilter>('All')
|
||||
|
||||
useEffect(() => {
|
||||
bankAccountsApi
|
||||
.list(BankAccountType.Bank)
|
||||
.then(list => setBankAccountMap(new Map(list.map(a => [a.accountId, a]))))
|
||||
.catch(() => setBankAccountMap(new Map()))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
chequeBooksApi
|
||||
.list({ status: status === 'All' ? undefined : status, pageSize: 100 })
|
||||
.then(res => { if (!cancelled) setBooks(res.items) })
|
||||
.catch(err => { if (!cancelled) setError(getApiError(err)) })
|
||||
return () => { cancelled = true }
|
||||
}, [status])
|
||||
|
||||
const sortedBooks = useMemo(() => books, [books])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-foreground">Cheque Books</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">Cheque books issued from the company's own supply.</p>
|
||||
</div>
|
||||
<Link href="/payments/cheque-books/new" className={cn(buttonVariants())}>
|
||||
<Plus className="size-4" />
|
||||
New Cheque Book
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<Select<StatusFilter> value={status} onValueChange={(v) => setStatus(v ?? 'All')}>
|
||||
<SelectTrigger className="w-full sm:w-48">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="All" label="All statuses">All statuses</SelectItem>
|
||||
<SelectItem value={ChequeBookStatus.Active} label="Active">Active</SelectItem>
|
||||
<SelectItem value={ChequeBookStatus.Completed} label="Completed">Completed</SelectItem>
|
||||
<SelectItem value={ChequeBookStatus.Cancelled} label="Cancelled">Cancelled</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive">{error}</div>
|
||||
)}
|
||||
|
||||
{!error && sortedBooks === null && (
|
||||
<div className="flex flex-col gap-2">
|
||||
{Array.from({ length: 4 }).map((_, i) => <Skeleton key={i} className="h-12 w-full" />)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!error && sortedBooks !== null && sortedBooks.length === 0 && (
|
||||
<div className="flex flex-col items-center gap-3 rounded-xl border border-dashed py-16 text-center">
|
||||
<BookText className="size-10 text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground">No cheque books yet.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!error && sortedBooks !== null && sortedBooks.length > 0 && (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Cheque book no.</TableHead>
|
||||
<TableHead>Bank account</TableHead>
|
||||
<TableHead>Branch</TableHead>
|
||||
<TableHead>Range</TableHead>
|
||||
<TableHead>Leaves</TableHead>
|
||||
<TableHead>Received</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{sortedBooks.map(b => {
|
||||
const bank = bankAccountMap.get(b.bankAccountId)
|
||||
return (
|
||||
<TableRow
|
||||
key={b.chequeBookNo}
|
||||
className="cursor-pointer"
|
||||
onClick={() => router.push(`/payments/cheque-books/${encodeURIComponent(b.chequeBookNo)}`)}
|
||||
>
|
||||
<TableCell className="font-medium">{b.chequeBookNo}</TableCell>
|
||||
<TableCell>{bank ? bank.accountName : `#${b.bankAccountId.slice(0, 8)}`}</TableCell>
|
||||
<TableCell>{b.branchId || '—'}</TableCell>
|
||||
<TableCell>{b.startChequeNo} – {b.endChequeNo}</TableCell>
|
||||
<TableCell>{b.totalLeaves}</TableCell>
|
||||
<TableCell>{formatDate(b.receivedDate)}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={STATUS_BADGE_VARIANT[b.status]}>{b.status}</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { ArrowLeft } from 'lucide-react'
|
||||
|
||||
import { receivedCheques as receivedChequesApi, getApiError } from '@/lib/api/index'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ReceivedFromType } from '@/types/payments'
|
||||
|
||||
import { Button, buttonVariants } from '@/components/ui/button'
|
||||
import { Field, FieldError, FieldLabel } from '@/components/ui/field'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { toast } from '@/components/ui/toast'
|
||||
|
||||
export default function NewReceivedChequePage() {
|
||||
const router = useRouter()
|
||||
|
||||
const [companyId, setCompanyId] = useState('')
|
||||
const [branchId, setBranchId] = useState('')
|
||||
const [receivedFromType, setReceivedFromType] = useState<ReceivedFromType>(ReceivedFromType.Customer)
|
||||
const [receivedFromId, setReceivedFromId] = useState('')
|
||||
const [receivedFromName, setReceivedFromName] = useState('')
|
||||
const [drawerBankName, setDrawerBankName] = useState('')
|
||||
const [drawerBankBranch, setDrawerBankBranch] = useState('')
|
||||
const [accountHolderName, setAccountHolderName] = useState('')
|
||||
const [chequeNo, setChequeNo] = useState('')
|
||||
const [chequeDate, setChequeDate] = useState('')
|
||||
const [amount, setAmount] = useState('')
|
||||
const [receivedDate, setReceivedDate] = useState('')
|
||||
const [referenceType, setReferenceType] = useState('')
|
||||
const [referenceId, setReferenceId] = useState('')
|
||||
const [notes, setNotes] = useState('')
|
||||
const [createdBy, setCreatedBy] = useState('')
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
async function handleSubmit() {
|
||||
const next: Record<string, string> = {}
|
||||
const companyNum = Number(companyId)
|
||||
if (!companyId || Number.isNaN(companyNum) || companyNum <= 0) next.companyId = 'Company ID is required'
|
||||
if (!receivedFromName.trim()) next.receivedFromName = 'Received from name is required'
|
||||
if (!chequeNo.trim()) next.chequeNo = 'Cheque number is required'
|
||||
if (!chequeDate) next.chequeDate = 'Cheque date is required'
|
||||
const amountNum = Number(amount)
|
||||
if (!amount || Number.isNaN(amountNum) || amountNum <= 0) next.amount = 'Amount must be greater than zero'
|
||||
if (!receivedDate) next.receivedDate = 'Received date is required'
|
||||
setErrors(next)
|
||||
if (Object.keys(next).length > 0) return
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const created = await receivedChequesApi.create({
|
||||
companyId: companyNum,
|
||||
branchId: branchId ? Number(branchId) : undefined,
|
||||
receivedFromType,
|
||||
receivedFromId: receivedFromId ? Number(receivedFromId) : undefined,
|
||||
receivedFromName,
|
||||
drawerBankName: drawerBankName || undefined,
|
||||
drawerBankBranch: drawerBankBranch || undefined,
|
||||
accountHolderName: accountHolderName || undefined,
|
||||
chequeNo,
|
||||
chequeDate,
|
||||
amount: amountNum,
|
||||
receivedDate,
|
||||
referenceType: referenceType || undefined,
|
||||
referenceId: referenceId ? Number(referenceId) : undefined,
|
||||
notes: notes || undefined,
|
||||
createdBy: createdBy || undefined,
|
||||
})
|
||||
toast.add({ title: 'Received cheque recorded', description: `${created.chequeNo} — ${created.receivedFromName}`, type: 'success' })
|
||||
router.push('/payments/received-cheques')
|
||||
} catch (err) {
|
||||
toast.add({ title: 'Could not record received cheque', description: getApiError(err), type: 'error' })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href="/payments/received-cheques" className={cn(buttonVariants({ variant: 'ghost', size: 'icon' }))}>
|
||||
<ArrowLeft className="size-4" />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-foreground">New Received Cheque</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">Record a cheque received from a customer, supplier, or other party.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border bg-card p-6">
|
||||
<div className="grid grid-cols-1 gap-x-6 gap-y-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<Field data-invalid={!!errors.companyId}>
|
||||
<FieldLabel htmlFor="rc-company">Company ID</FieldLabel>
|
||||
<Input id="rc-company" type="number" value={companyId} onChange={(e) => setCompanyId(e.target.value)} placeholder="1" aria-invalid={!!errors.companyId} />
|
||||
<FieldError errors={[errors.companyId ? { message: errors.companyId } : undefined]} />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="rc-branch">Branch (optional)</FieldLabel>
|
||||
<Input id="rc-branch" type="number" value={branchId} onChange={(e) => setBranchId(e.target.value)} />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="rc-from-type">Received from type</FieldLabel>
|
||||
<Select<ReceivedFromType> value={receivedFromType} onValueChange={(v) => setReceivedFromType(v ?? ReceivedFromType.Customer)}>
|
||||
<SelectTrigger id="rc-from-type" className="w-full"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.values(ReceivedFromType).map(t => (
|
||||
<SelectItem key={t} value={t} label={t}>{t}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={!!errors.receivedFromName}>
|
||||
<FieldLabel htmlFor="rc-from-name">Received from name</FieldLabel>
|
||||
<Input id="rc-from-name" value={receivedFromName} onChange={(e) => setReceivedFromName(e.target.value)} aria-invalid={!!errors.receivedFromName} />
|
||||
<FieldError errors={[errors.receivedFromName ? { message: errors.receivedFromName } : undefined]} />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="rc-from-id">Received from ID (optional)</FieldLabel>
|
||||
<Input id="rc-from-id" type="number" value={receivedFromId} onChange={(e) => setReceivedFromId(e.target.value)} />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="rc-drawer-bank">Drawer bank (optional)</FieldLabel>
|
||||
<Input id="rc-drawer-bank" value={drawerBankName} onChange={(e) => setDrawerBankName(e.target.value)} />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="rc-drawer-branch">Drawer branch (optional)</FieldLabel>
|
||||
<Input id="rc-drawer-branch" value={drawerBankBranch} onChange={(e) => setDrawerBankBranch(e.target.value)} />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="rc-holder">Account holder name (optional)</FieldLabel>
|
||||
<Input id="rc-holder" value={accountHolderName} onChange={(e) => setAccountHolderName(e.target.value)} />
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={!!errors.chequeNo}>
|
||||
<FieldLabel htmlFor="rc-cheque-no">Cheque number</FieldLabel>
|
||||
<Input id="rc-cheque-no" value={chequeNo} onChange={(e) => setChequeNo(e.target.value)} aria-invalid={!!errors.chequeNo} />
|
||||
<FieldError errors={[errors.chequeNo ? { message: errors.chequeNo } : undefined]} />
|
||||
</Field>
|
||||
<Field data-invalid={!!errors.chequeDate}>
|
||||
<FieldLabel htmlFor="rc-cheque-date">Cheque date</FieldLabel>
|
||||
<Input id="rc-cheque-date" type="date" value={chequeDate} onChange={(e) => setChequeDate(e.target.value)} aria-invalid={!!errors.chequeDate} />
|
||||
<FieldError errors={[errors.chequeDate ? { message: errors.chequeDate } : undefined]} />
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={!!errors.amount}>
|
||||
<FieldLabel htmlFor="rc-amount">Amount</FieldLabel>
|
||||
<Input id="rc-amount" type="number" value={amount} onChange={(e) => setAmount(e.target.value)} aria-invalid={!!errors.amount} />
|
||||
<FieldError errors={[errors.amount ? { message: errors.amount } : undefined]} />
|
||||
</Field>
|
||||
<Field data-invalid={!!errors.receivedDate}>
|
||||
<FieldLabel htmlFor="rc-received-date">Received date</FieldLabel>
|
||||
<Input id="rc-received-date" type="date" value={receivedDate} onChange={(e) => setReceivedDate(e.target.value)} aria-invalid={!!errors.receivedDate} />
|
||||
<FieldError errors={[errors.receivedDate ? { message: errors.receivedDate } : undefined]} />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="rc-ref-type">Reference type (optional)</FieldLabel>
|
||||
<Input id="rc-ref-type" value={referenceType} onChange={(e) => setReferenceType(e.target.value)} placeholder="Invoice" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="rc-ref-id">Reference ID (optional)</FieldLabel>
|
||||
<Input id="rc-ref-id" type="number" value={referenceId} onChange={(e) => setReferenceId(e.target.value)} />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="rc-notes">Notes (optional)</FieldLabel>
|
||||
<Input id="rc-notes" value={notes} onChange={(e) => setNotes(e.target.value)} />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="rc-created-by">Created by (optional)</FieldLabel>
|
||||
<Input id="rc-created-by" value={createdBy} onChange={(e) => setCreatedBy(e.target.value)} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-end gap-3">
|
||||
<Link href="/payments/received-cheques" className={cn(buttonVariants({ variant: 'outline' }))}>Cancel</Link>
|
||||
<Button onClick={handleSubmit} disabled={submitting}>{submitting ? 'Recording…' : 'Record'}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Inbox, Plus } from 'lucide-react'
|
||||
|
||||
import { bankAccounts as bankAccountsApi, receivedCheques as receivedChequesApi, getApiError } from '@/lib/api/index'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
BankAccountType,
|
||||
ReceivedChequeStatus,
|
||||
ReceivedChequeStatusAction,
|
||||
type BankAccountResponse,
|
||||
type ReceivedChequeResponse,
|
||||
} from '@/types/payments'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button, buttonVariants } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Field, FieldLabel } from '@/components/ui/field'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { toast } from '@/components/ui/toast'
|
||||
|
||||
type StatusFilter = ReceivedChequeStatus | 'All'
|
||||
|
||||
const STATUS_BADGE_VARIANT: Record<ReceivedChequeStatus, 'default' | 'secondary' | 'destructive' | 'outline'> = {
|
||||
[ReceivedChequeStatus.Received]: 'outline',
|
||||
[ReceivedChequeStatus.Deposited]: 'default',
|
||||
[ReceivedChequeStatus.Cleared]: 'secondary',
|
||||
[ReceivedChequeStatus.Returned]: 'destructive',
|
||||
[ReceivedChequeStatus.Cancelled]: 'destructive',
|
||||
}
|
||||
|
||||
function formatDate(value: string | null): string {
|
||||
if (!value) return '—'
|
||||
return new Date(value).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
|
||||
}
|
||||
|
||||
function formatAmount(value: number): string {
|
||||
return value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
export default function ReceivedChequesPage() {
|
||||
const [cheques, setCheques] = useState<ReceivedChequeResponse[] | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [status, setStatus] = useState<StatusFilter>('All')
|
||||
const [selected, setSelected] = useState<ReceivedChequeResponse | null>(null)
|
||||
|
||||
function load() {
|
||||
receivedChequesApi
|
||||
.list({ status: status === 'All' ? undefined : status, pageSize: 100 })
|
||||
.then(res => setCheques(res.items))
|
||||
.catch(err => setError(getApiError(err)))
|
||||
}
|
||||
|
||||
useEffect(load, [status])
|
||||
|
||||
function handleUpdated(updated: ReceivedChequeResponse) {
|
||||
setCheques(prev => prev ? prev.map(c => c.receivedChequeId === updated.receivedChequeId ? updated : c) : prev)
|
||||
setSelected(updated)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-foreground">Received Cheques</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">Cheques received from customers, suppliers, or others.</p>
|
||||
</div>
|
||||
<Link href="/payments/received-cheques/new" className={cn(buttonVariants())}>
|
||||
<Plus className="size-4" />
|
||||
New Received Cheque
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<Select<StatusFilter> value={status} onValueChange={(v) => setStatus(v ?? 'All')}>
|
||||
<SelectTrigger className="w-full sm:w-48">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="All" label="All statuses">All statuses</SelectItem>
|
||||
{Object.values(ReceivedChequeStatus).map(s => (
|
||||
<SelectItem key={s} value={s} label={s}>{s}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive">{error}</div>
|
||||
)}
|
||||
|
||||
{!error && cheques === null && (
|
||||
<div className="flex flex-col gap-2">
|
||||
{Array.from({ length: 4 }).map((_, i) => <Skeleton key={i} className="h-12 w-full" />)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!error && cheques !== null && cheques.length === 0 && (
|
||||
<div className="flex flex-col items-center gap-3 rounded-xl border border-dashed py-16 text-center">
|
||||
<Inbox className="size-10 text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground">No received cheques yet.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!error && cheques !== null && cheques.length > 0 && (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Cheque no.</TableHead>
|
||||
<TableHead>Received from</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead className="text-right">Amount</TableHead>
|
||||
<TableHead>Received</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{cheques.map(c => (
|
||||
<TableRow key={c.receivedChequeId} className="cursor-pointer" onClick={() => setSelected(c)}>
|
||||
<TableCell className="font-medium">{c.chequeNo}</TableCell>
|
||||
<TableCell>{c.receivedFromName}</TableCell>
|
||||
<TableCell>{c.receivedFromType}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">{formatAmount(c.amount)}</TableCell>
|
||||
<TableCell>{formatDate(c.receivedDate)}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={STATUS_BADGE_VARIANT[c.status]}>{c.status}</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
|
||||
<ReceivedChequeDialog cheque={selected} onClose={() => setSelected(null)} onUpdated={handleUpdated} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ReceivedChequeDialog({
|
||||
cheque,
|
||||
onClose,
|
||||
onUpdated,
|
||||
}: {
|
||||
cheque: ReceivedChequeResponse | null
|
||||
onClose: () => void
|
||||
onUpdated: (c: ReceivedChequeResponse) => void
|
||||
}) {
|
||||
const open = cheque !== null
|
||||
const [mode, setMode] = useState<'view' | 'action'>('view')
|
||||
const [action, setAction] = useState<ReceivedChequeStatusAction>(ReceivedChequeStatusAction.Deposit)
|
||||
const [depositBankAccountId, setDepositBankAccountId] = useState('')
|
||||
const [depositDate, setDepositDate] = useState('')
|
||||
const [notes, setNotes] = useState('')
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [bankOptions, setBankOptions] = useState<BankAccountResponse[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (!cheque) return
|
||||
setMode('view')
|
||||
setAction(ReceivedChequeStatusAction.Deposit)
|
||||
setDepositBankAccountId('')
|
||||
setDepositDate('')
|
||||
setNotes('')
|
||||
}, [cheque])
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== 'action' || action !== ReceivedChequeStatusAction.Deposit || bankOptions.length > 0) return
|
||||
bankAccountsApi.list(BankAccountType.Bank).then(setBankOptions).catch(() => setBankOptions([]))
|
||||
}, [mode, action, bankOptions.length])
|
||||
|
||||
if (!cheque) return null
|
||||
|
||||
const availableActions: ReceivedChequeStatusAction[] =
|
||||
cheque.status === ReceivedChequeStatus.Received
|
||||
? [ReceivedChequeStatusAction.Deposit, ReceivedChequeStatusAction.Cancel]
|
||||
: cheque.status === ReceivedChequeStatus.Deposited
|
||||
? [ReceivedChequeStatusAction.Clear, ReceivedChequeStatusAction.Return]
|
||||
: []
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!cheque) return
|
||||
if (action === ReceivedChequeStatusAction.Deposit && (!depositBankAccountId || !depositDate)) {
|
||||
toast.add({ title: 'Deposit bank account and date are required', type: 'error' })
|
||||
return
|
||||
}
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const updated = await receivedChequesApi.updateStatus(cheque.receivedChequeId, {
|
||||
action,
|
||||
depositBankAccountId: action === ReceivedChequeStatusAction.Deposit ? depositBankAccountId : undefined,
|
||||
depositDate: action === ReceivedChequeStatusAction.Deposit ? depositDate : undefined,
|
||||
notes: notes || undefined,
|
||||
})
|
||||
toast.add({ title: `Cheque ${action.toLowerCase()}ed`, type: 'success' })
|
||||
onUpdated(updated)
|
||||
setMode('view')
|
||||
} catch (err) {
|
||||
toast.add({ title: 'Could not update status', description: getApiError(err), type: 'error' })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(v) => { if (!v) onClose() }}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Cheque {cheque.chequeNo}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Status: <Badge variant={STATUS_BADGE_VARIANT[cheque.status]}>{cheque.status}</Badge>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{mode === 'view' && (
|
||||
<div className="flex flex-col gap-3 text-sm">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div><p className="text-xs text-muted-foreground">Received from</p><p>{cheque.receivedFromName}</p></div>
|
||||
<div><p className="text-xs text-muted-foreground">Type</p><p>{cheque.receivedFromType}</p></div>
|
||||
<div><p className="text-xs text-muted-foreground">Cheque date</p><p>{formatDate(cheque.chequeDate)}</p></div>
|
||||
<div><p className="text-xs text-muted-foreground">Amount</p><p className="tabular-nums">{formatAmount(cheque.amount)}</p></div>
|
||||
<div><p className="text-xs text-muted-foreground">Drawer bank</p><p>{cheque.drawerBankName ?? '—'}</p></div>
|
||||
<div><p className="text-xs text-muted-foreground">Deposit date</p><p>{formatDate(cheque.depositDate)}</p></div>
|
||||
{cheque.notes && (
|
||||
<div className="col-span-2"><p className="text-xs text-muted-foreground">Notes</p><p>{cheque.notes}</p></div>
|
||||
)}
|
||||
</div>
|
||||
{availableActions.length > 0 && (
|
||||
<div className="pt-2">
|
||||
<Button onClick={() => { setAction(availableActions[0]); setMode('action') }}>Change status</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === 'action' && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="rc-action">Action</FieldLabel>
|
||||
<Select<ReceivedChequeStatusAction> value={action} onValueChange={(v) => setAction(v ?? availableActions[0])}>
|
||||
<SelectTrigger id="rc-action" className="w-full"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableActions.map(a => (
|
||||
<SelectItem key={a} value={a} label={a}>{a}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
{action === ReceivedChequeStatusAction.Deposit && (
|
||||
<>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="rc-deposit-bank">Deposit bank account</FieldLabel>
|
||||
<Select<string> value={depositBankAccountId} onValueChange={(v) => setDepositBankAccountId(v ?? '')}>
|
||||
<SelectTrigger id="rc-deposit-bank" className="w-full">
|
||||
<SelectValue placeholder="Select a bank account" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{bankOptions.map(b => (
|
||||
<SelectItem key={b.accountId} value={b.accountId} label={b.accountName}>{b.accountName}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="rc-deposit-date">Deposit date</FieldLabel>
|
||||
<Input id="rc-deposit-date" type="date" value={depositDate} onChange={(e) => setDepositDate(e.target.value)} />
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
<Field>
|
||||
<FieldLabel htmlFor="rc-notes">Notes (optional)</FieldLabel>
|
||||
<Input id="rc-notes" value={notes} onChange={(e) => setNotes(e.target.value)} />
|
||||
</Field>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button variant="outline" onClick={() => setMode('view')} disabled={submitting}>Back</Button>
|
||||
<Button onClick={handleSubmit} disabled={submitting}>{submitting ? 'Saving…' : 'Confirm'}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -127,6 +127,15 @@ export const NAV_GROUPS: NavGroup[] = [
|
||||
{ title: 'Suppliers', href: '/suppliers', icon: UserGroupIcon, description: 'Supplier accounts used for raising purchase orders', permission: 'suppliers.view' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Accounts',
|
||||
icon: BankIcon,
|
||||
items: [
|
||||
{ title: 'Bank Accounts', href: '/payments/bank-accounts', icon: BankIcon, description: 'Cash and bank accounts used for payments and deposits' },
|
||||
{ title: 'Cheque Books', href: '/payments/cheque-books', icon: ClipboardListIcon, description: 'Cheque books issued from the company’s own supply' },
|
||||
{ title: 'Received Cheques', href: '/payments/received-cheques', icon: DollarSquareIcon, description: 'Cheques received from customers, suppliers, or others' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Returns & Claims',
|
||||
icon: ClipboardListIcon,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { api } from '@/lib/api'
|
||||
import type {
|
||||
BankAccountResponse,
|
||||
BankAccountType,
|
||||
CashAccountTypeDto,
|
||||
CreateBankAccountRequest,
|
||||
CreateCashAccountRequest,
|
||||
} from '@/types/payments'
|
||||
|
||||
export const bankAccounts = {
|
||||
list: (accountType?: BankAccountType) =>
|
||||
api
|
||||
.get<BankAccountResponse[]>('/bank-accounts', {
|
||||
params: accountType ? { accountType } : undefined,
|
||||
})
|
||||
.then(r => r.data),
|
||||
|
||||
createBank: (body: CreateBankAccountRequest) =>
|
||||
api.post<BankAccountResponse>('/bank-accounts', body).then(r => r.data),
|
||||
|
||||
createCash: (body: CreateCashAccountRequest) =>
|
||||
api.post<BankAccountResponse>('/cash-accounts', body).then(r => r.data),
|
||||
|
||||
updateCash: (id: string, accountName: string) =>
|
||||
api.put<BankAccountResponse>(`/cash-accounts/${id}`, { accountName }).then(r => r.data),
|
||||
|
||||
setCashStatus: (id: string, isActive: boolean) =>
|
||||
api.patch<BankAccountResponse>(`/cash-accounts/${id}/status`, { isActive }).then(r => r.data),
|
||||
|
||||
deleteCash: (id: string) => api.delete(`/cash-accounts/${id}`),
|
||||
|
||||
cashAccountTypes: () =>
|
||||
api.get<CashAccountTypeDto[]>('/cash-account-types').then(r => r.data),
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { api } from '@/lib/api'
|
||||
import type {
|
||||
ChequeBookResponse,
|
||||
ChequeBookStatus,
|
||||
ChequePageResponse,
|
||||
CreateChequeBookRequest,
|
||||
IssueChequePageRequest,
|
||||
PagedResult,
|
||||
UpdateChequePageStatusRequest,
|
||||
} from '@/types/payments'
|
||||
|
||||
export const chequeBooks = {
|
||||
list: (params?: {
|
||||
bankAccountId?: string
|
||||
branchId?: number
|
||||
status?: ChequeBookStatus
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}) =>
|
||||
api
|
||||
.get<PagedResult<ChequeBookResponse>>('/cheque-books', { params })
|
||||
.then(r => r.data),
|
||||
|
||||
get: (chequeBookNo: string, expandPages = false) =>
|
||||
api
|
||||
.get<ChequeBookResponse>(`/cheque-books/${encodeURIComponent(chequeBookNo)}`, {
|
||||
params: expandPages ? { expand: 'pages' } : undefined,
|
||||
})
|
||||
.then(r => r.data),
|
||||
|
||||
create: (body: CreateChequeBookRequest) =>
|
||||
api.post<ChequeBookResponse>('/cheque-books', body).then(r => r.data),
|
||||
}
|
||||
|
||||
export const chequePages = {
|
||||
issue: (chequeNo: string, body: IssueChequePageRequest) =>
|
||||
api
|
||||
.put<ChequePageResponse>(
|
||||
`/cheque-pages/${encodeURIComponent(chequeNo)}/issue`,
|
||||
body,
|
||||
)
|
||||
.then(r => r.data),
|
||||
|
||||
updateStatus: (chequeNo: string, body: UpdateChequePageStatusRequest) =>
|
||||
api
|
||||
.put<ChequePageResponse>(
|
||||
`/cheque-pages/${encodeURIComponent(chequeNo)}/status`,
|
||||
body,
|
||||
)
|
||||
.then(r => r.data),
|
||||
}
|
||||
@@ -12,6 +12,9 @@ export { productReports } from './reports'
|
||||
export { shopPos, invoices, payments } from './sales'
|
||||
export { saleReturns, claims, supplierClaims } from './returns-claims'
|
||||
export { sysConfig, ITEM_TRACKING_KEYS, GRN_TAX_KEYS, grnTaxSettings, DEFAULT_GRN_TAX_SETTINGS } from './sys-config'
|
||||
export { bankAccounts } from './bank-accounts'
|
||||
export { chequeBooks, chequePages } from './cheques'
|
||||
export { receivedCheques } from './received-cheques'
|
||||
export { getPaginatedItems } from './paginated'
|
||||
|
||||
export type { PageParams } from './master'
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { api } from '@/lib/api'
|
||||
import type {
|
||||
CreateReceivedChequeRequest,
|
||||
PagedResult,
|
||||
ReceivedChequeResponse,
|
||||
ReceivedChequeStatus,
|
||||
ReceivedFromType,
|
||||
UpdateReceivedChequeStatusRequest,
|
||||
} from '@/types/payments'
|
||||
|
||||
export const receivedCheques = {
|
||||
list: (params?: {
|
||||
companyId?: number
|
||||
branchId?: number
|
||||
status?: ReceivedChequeStatus
|
||||
receivedFromType?: ReceivedFromType
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}) =>
|
||||
api
|
||||
.get<PagedResult<ReceivedChequeResponse>>('/received-cheques', { params })
|
||||
.then(r => r.data),
|
||||
|
||||
create: (body: CreateReceivedChequeRequest) =>
|
||||
api.post<ReceivedChequeResponse>('/received-cheques', body).then(r => r.data),
|
||||
|
||||
updateStatus: (id: string, body: UpdateReceivedChequeStatusRequest) =>
|
||||
api.put<ReceivedChequeResponse>(`/received-cheques/${id}/status`, body).then(r => r.data),
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
// Mirrors Distribution backend DTOs in Features/{BankAccount,ChequeBook,ReceivedCheque}.
|
||||
// Entities use Guid Ids on the backend — represented as strings here.
|
||||
|
||||
export enum BankAccountType {
|
||||
Cash = 'Cash',
|
||||
Bank = 'Bank',
|
||||
}
|
||||
|
||||
export interface BankAccountResponse {
|
||||
accountType: BankAccountType
|
||||
accountId: string
|
||||
accountName: string
|
||||
bankName: string | null
|
||||
cashAccountTypeName: string | null
|
||||
accountNumber: string | null
|
||||
currencyCode: string
|
||||
createdAt: string
|
||||
isActive: boolean | null
|
||||
isDefault: boolean | null
|
||||
}
|
||||
|
||||
export interface CreateBankAccountRequest {
|
||||
accountName: string
|
||||
bankName?: string | null
|
||||
accountNumber?: string | null
|
||||
currencyCode?: string
|
||||
}
|
||||
|
||||
export interface CreateCashAccountRequest {
|
||||
accountName: string
|
||||
cashAccountTypeName: string
|
||||
accountNumber?: string | null
|
||||
currencyCode?: string
|
||||
}
|
||||
|
||||
export interface CashAccountTypeDto {
|
||||
name: string
|
||||
}
|
||||
|
||||
// Cheque Books / Pages
|
||||
|
||||
export enum ChequeBookStatus {
|
||||
Active = 'Active',
|
||||
Completed = 'Completed',
|
||||
Cancelled = 'Cancelled',
|
||||
}
|
||||
|
||||
export enum ChequePageIssueStatus {
|
||||
Unused = 'Unused',
|
||||
Issued = 'Issued',
|
||||
Cleared = 'Cleared',
|
||||
Bounced = 'Bounced',
|
||||
Cancelled = 'Cancelled',
|
||||
Void = 'Void',
|
||||
}
|
||||
|
||||
export enum ChequePageStatusAction {
|
||||
Clear = 'Clear',
|
||||
Bounce = 'Bounce',
|
||||
Cancel = 'Cancel',
|
||||
Void = 'Void',
|
||||
}
|
||||
|
||||
export enum PayeeType {
|
||||
Supplier = 'Supplier',
|
||||
Customer = 'Customer',
|
||||
Employee = 'Employee',
|
||||
Other = 'Other',
|
||||
}
|
||||
|
||||
export interface ChequePageResponse {
|
||||
chequeNo: string
|
||||
chequeBookNo: string
|
||||
issueStatus: ChequePageIssueStatus
|
||||
payeeType: PayeeType | null
|
||||
payeeId: number | null
|
||||
payeeName: string | null
|
||||
issueDate: string | null
|
||||
amount: number | null
|
||||
currencyCode: string | null
|
||||
voucherId: number | null
|
||||
referenceNo: string | null
|
||||
purpose: string | null
|
||||
isCrossCheque: boolean | null
|
||||
isAccountPayee: boolean | null
|
||||
isPostDated: boolean | null
|
||||
notes: string | null
|
||||
printedBy: string | null
|
||||
printedAt: string | null
|
||||
clearedDate: string | null
|
||||
clearedByBank: boolean | null
|
||||
cancelReason: string | null
|
||||
}
|
||||
|
||||
export interface ChequeBookResponse {
|
||||
chequeBookNo: string
|
||||
branchId: number
|
||||
bankAccountId: string
|
||||
startChequeNo: string
|
||||
endChequeNo: string
|
||||
totalLeaves: number
|
||||
receivedDate: string
|
||||
description: string | null
|
||||
createdBy: string | null
|
||||
status: ChequeBookStatus
|
||||
pages: ChequePageResponse[]
|
||||
}
|
||||
|
||||
export interface PagedResult<T> {
|
||||
items: T[]
|
||||
totalCount: number
|
||||
page: number | null
|
||||
pageSize: number | null
|
||||
}
|
||||
|
||||
export interface CreateChequeBookRequest {
|
||||
branchId: number
|
||||
bankAccountId: string
|
||||
chequeBookNo: string
|
||||
startChequeNo: string
|
||||
endChequeNo: string
|
||||
totalLeaves: number
|
||||
receivedDate: string
|
||||
description?: string
|
||||
createdBy?: string
|
||||
}
|
||||
|
||||
export interface IssueChequePageRequest {
|
||||
payeeType: PayeeType
|
||||
payeeId?: number
|
||||
payeeName: string
|
||||
issueDate: string
|
||||
amount: number
|
||||
currencyCode?: string
|
||||
voucherId?: number
|
||||
referenceNo?: string
|
||||
purpose?: string
|
||||
isCrossCheque?: boolean
|
||||
isAccountPayee?: boolean
|
||||
isPostDated?: boolean
|
||||
notes?: string
|
||||
printedBy?: string
|
||||
}
|
||||
|
||||
export interface UpdateChequePageStatusRequest {
|
||||
action: ChequePageStatusAction
|
||||
clearedDate?: string
|
||||
cancelReason?: string
|
||||
performedBy?: string
|
||||
}
|
||||
|
||||
// Received Cheques
|
||||
|
||||
export enum ReceivedChequeStatus {
|
||||
Received = 'Received',
|
||||
Deposited = 'Deposited',
|
||||
Cleared = 'Cleared',
|
||||
Returned = 'Returned',
|
||||
Cancelled = 'Cancelled',
|
||||
}
|
||||
|
||||
export enum ReceivedChequeStatusAction {
|
||||
Deposit = 'Deposit',
|
||||
Clear = 'Clear',
|
||||
Return = 'Return',
|
||||
Cancel = 'Cancel',
|
||||
}
|
||||
|
||||
export enum ReceivedFromType {
|
||||
Customer = 'Customer',
|
||||
Supplier = 'Supplier',
|
||||
Other = 'Other',
|
||||
}
|
||||
|
||||
export interface ReceivedChequeResponse {
|
||||
receivedChequeId: string
|
||||
companyId: number
|
||||
branchId: number | null
|
||||
receivedFromType: ReceivedFromType
|
||||
receivedFromId: number | null
|
||||
receivedFromName: string
|
||||
drawerBankName: string | null
|
||||
drawerBankBranch: string | null
|
||||
accountHolderName: string | null
|
||||
chequeNo: string
|
||||
chequeDate: string
|
||||
amount: number
|
||||
receivedDate: string
|
||||
referenceType: string | null
|
||||
referenceId: number | null
|
||||
notes: string | null
|
||||
createdBy: string | null
|
||||
status: ReceivedChequeStatus
|
||||
depositBankAccountId: string | null
|
||||
depositDate: string | null
|
||||
}
|
||||
|
||||
export interface CreateReceivedChequeRequest {
|
||||
companyId: number
|
||||
branchId?: number
|
||||
receivedFromType: ReceivedFromType
|
||||
receivedFromId?: number
|
||||
receivedFromName: string
|
||||
drawerBankName?: string
|
||||
drawerBankBranch?: string
|
||||
accountHolderName?: string
|
||||
chequeNo: string
|
||||
chequeDate: string
|
||||
amount: number
|
||||
receivedDate: string
|
||||
referenceType?: string
|
||||
referenceId?: number
|
||||
notes?: string
|
||||
createdBy?: string
|
||||
}
|
||||
|
||||
export interface UpdateReceivedChequeStatusRequest {
|
||||
action: ReceivedChequeStatusAction
|
||||
depositBankAccountId?: string
|
||||
depositDate?: string
|
||||
notes?: string
|
||||
performedBy?: string
|
||||
}
|
||||
Reference in New Issue
Block a user