import { useMemo, useState } from 'react' import { useStore } from '../store/AppStore' import { Badge, Button, Card, ConfirmDialog, DataTable, Field, Input, JalaliDateInput, Modal, PageHeader, Select, type Column, } from '../components/ui' import type { Voucher, VoucherLine } from '../types' import { createId, nextNumber } from '../utils/id' import { formatDate, formatMoney, todayISO, toFa } from '../utils/format' import { voucherBalance } from '../utils/accounting' const newLine = (): VoucherLine => ({ id: createId('vl-'), accountId: '', description: '', debit: 0, credit: 0, }) function createDraft(items: Voucher[]): Voucher { return { id: '', number: nextNumber(items), date: todayISO(), description: '', status: 'draft', lines: [newLine(), newLine()], } } export function Vouchers() { const { data, upsertVoucher, removeVoucher } = useStore() const [draft, setDraft] = useState(null) const [toDelete, setToDelete] = useState(null) const leafAccounts = useMemo( () => data.accounts.filter((account) => !account.isGroup), [data.accounts], ) const accountName = (id: string) => leafAccounts.find((a) => a.id === id)?.name ?? '—' const sorted = useMemo( () => [...data.vouchers].sort((a, b) => b.date.localeCompare(a.date) || b.number - a.number), [data.vouchers], ) const balance = draft ? voucherBalance(draft) : null const updateLine = (lineId: string, patch: Partial) => { if (!draft) return setDraft({ ...draft, lines: draft.lines.map((line) => (line.id === lineId ? { ...line, ...patch } : line)), }) } const handleSave = (status: Voucher['status']) => { if (!draft) return const cleanLines = draft.lines.filter((line) => line.accountId && (line.debit > 0 || line.credit > 0)) if (cleanLines.length < 2) return const candidate: Voucher = { ...draft, lines: cleanLines, status } if (status === 'posted' && !voucherBalance(candidate).balanced) return upsertVoucher({ ...candidate, id: candidate.id || createId('v-') }) setDraft(null) } const columns: Array> = [ { key: 'number', header: 'شماره', headerClassName: 'w-[4.5rem]', className: 'w-[4.5rem] whitespace-nowrap', render: (v) => {toFa(v.number)}, }, { key: 'date', header: 'تاریخ', headerClassName: 'w-[6.5rem]', className: 'w-[6.5rem] whitespace-nowrap tabular-nums', render: (v) => formatDate(v.date), }, { key: 'desc', header: 'شرح', truncate: true, className: 'min-w-0', render: (v) => ( {v.description || '—'} ), }, { key: 'amount', header: 'مبلغ', align: 'end', headerClassName: 'w-[7.5rem]', className: 'w-[7.5rem] whitespace-nowrap tabular-nums', render: (v) => formatMoney(voucherBalance(v).debit), }, { key: 'status', header: 'وضعیت', sticky: { left: '9rem' }, headerClassName: 'w-[6.5rem]', className: 'w-[6.5rem] whitespace-nowrap', render: (v) => v.status === 'posted' ? ثبت قطعی : پیش‌نویس, }, { key: 'actions', header: '', align: 'end', sticky: { left: '0' }, headerClassName: 'w-[9rem]', className: 'w-[9rem] whitespace-nowrap', render: (v) => (
), }, ] return (
setDraft(createDraft(data.vouchers))}> سند جدید } /> v.id} emptyTitle="سندی ثبت نشده است" emptyDescription="اولین سند حسابداری خود را ایجاد کنید." /> setDraft(null)} size="xl" footer={ <>
بدهکار: {formatMoney(balance?.debit ?? 0)} بستانکار: {formatMoney(balance?.credit ?? 0)} {balance?.balanced ? ( تراز است ) : ( اختلاف {formatMoney(Math.abs((balance?.debit ?? 0) - (balance?.credit ?? 0)))} )}
} > {draft && (
setDraft({ ...draft, number: Number(e.target.value) || 0 })} /> setDraft({ ...draft, date })} /> setDraft({ ...draft, description: e.target.value })} />
{draft.lines.map((line) => ( ))}
حساب شرح ردیف بدهکار بستانکار
updateLine(line.id, { description: e.target.value })} placeholder="شرح" /> updateLine(line.id, { debit: Number(e.target.value) || 0, credit: 0 }) } /> updateLine(line.id, { credit: Number(e.target.value) || 0, debit: 0 }) } />
{draft.lines.some((l) => l.accountId) && (

حساب‌های انتخاب‌شده: {draft.lines.filter((l) => l.accountId).map((l) => accountName(l.accountId)).join('، ')}

)}
)}
setToDelete(null)} onConfirm={() => { if (toDelete) removeVoucher(toDelete.id) setToDelete(null) }} />
) }