243 lines
7.3 KiB
TypeScript
243 lines
7.3 KiB
TypeScript
import { useMemo, useState } from 'react'
|
||
import { useStore } from '../store/AppStore'
|
||
import {
|
||
Badge,
|
||
Button,
|
||
Card,
|
||
ConfirmDialog,
|
||
DataTable,
|
||
Field,
|
||
Input,
|
||
Modal,
|
||
PageHeader,
|
||
Select,
|
||
type Column,
|
||
} from '../components/ui'
|
||
import type { Account, AccountType } from '../types'
|
||
import { createId } from '../utils/id'
|
||
import { formatMoney, toFa } from '../utils/format'
|
||
|
||
const typeLabels: Record<AccountType, string> = {
|
||
asset: 'دارایی',
|
||
liability: 'بدهی',
|
||
equity: 'سرمایه',
|
||
income: 'درآمد',
|
||
expense: 'هزینه',
|
||
}
|
||
|
||
const typeTones: Record<AccountType, 'blue' | 'rose' | 'indigo' | 'green' | 'amber'> = {
|
||
asset: 'blue',
|
||
liability: 'rose',
|
||
equity: 'indigo',
|
||
income: 'green',
|
||
expense: 'amber',
|
||
}
|
||
|
||
const emptyDraft = (): Account => ({
|
||
id: '',
|
||
code: '',
|
||
name: '',
|
||
type: 'asset',
|
||
parentId: null,
|
||
isGroup: false,
|
||
openingBalance: 0,
|
||
})
|
||
|
||
export function Accounts() {
|
||
const { data, upsertAccount, removeAccount } = useStore()
|
||
const [search, setSearch] = useState('')
|
||
const [draft, setDraft] = useState<Account | null>(null)
|
||
const [toDelete, setToDelete] = useState<Account | null>(null)
|
||
|
||
const groups = useMemo(
|
||
() => data.accounts.filter((account) => account.isGroup),
|
||
[data.accounts],
|
||
)
|
||
|
||
const filtered = useMemo(() => {
|
||
const query = search.trim().toLowerCase()
|
||
if (!query) return data.accounts
|
||
return data.accounts.filter(
|
||
(account) =>
|
||
account.name.toLowerCase().includes(query) || account.code.includes(query),
|
||
)
|
||
}, [data.accounts, search])
|
||
|
||
const handleSave = () => {
|
||
if (!draft) return
|
||
if (!draft.code.trim() || !draft.name.trim()) return
|
||
upsertAccount({ ...draft, id: draft.id || createId('acc-') })
|
||
setDraft(null)
|
||
}
|
||
|
||
const confirmDelete = () => {
|
||
if (!toDelete) return
|
||
removeAccount(toDelete.id)
|
||
setToDelete(null)
|
||
}
|
||
|
||
const columns: Array<Column<Account>> = [
|
||
{
|
||
key: 'code',
|
||
header: 'کد حساب',
|
||
render: (a) => <span className="font-mono text-slate-500">{toFa(a.code)}</span>,
|
||
},
|
||
{
|
||
key: 'name',
|
||
header: 'عنوان حساب',
|
||
render: (a) => (
|
||
<span className={a.isGroup ? 'font-bold text-slate-800' : 'text-slate-700'}>
|
||
{a.isGroup && <span className="ms-1 text-slate-400">▸</span>} {a.name}
|
||
</span>
|
||
),
|
||
},
|
||
{
|
||
key: 'type',
|
||
header: 'ماهیت',
|
||
render: (a) => <Badge tone={typeTones[a.type]}>{typeLabels[a.type]}</Badge>,
|
||
},
|
||
{
|
||
key: 'kind',
|
||
header: 'نوع',
|
||
render: (a) => (a.isGroup ? <Badge tone="slate">گروه</Badge> : <Badge tone="indigo">معین</Badge>),
|
||
},
|
||
{
|
||
key: 'opening',
|
||
header: 'مانده افتتاحیه',
|
||
align: 'end',
|
||
render: (a) => (a.isGroup ? '—' : <span className="tabular-nums">{formatMoney(a.openingBalance)}</span>),
|
||
},
|
||
{
|
||
key: 'actions',
|
||
header: '',
|
||
align: 'end',
|
||
render: (a) => (
|
||
<div className="flex justify-end gap-1">
|
||
<Button size="sm" variant="ghost" onClick={() => setDraft({ ...a })}>
|
||
ویرایش
|
||
</Button>
|
||
<Button size="sm" variant="ghost" className="text-rose-600" onClick={() => setToDelete(a)}>
|
||
حذف
|
||
</Button>
|
||
</div>
|
||
),
|
||
},
|
||
]
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<PageHeader
|
||
title="حسابها (کدینگ)"
|
||
subtitle="کدینگ استاندارد حسابها مطابق ساختار دارایی، بدهی، سرمایه، درآمد و هزینه"
|
||
actions={
|
||
<Button icon="+" onClick={() => setDraft(emptyDraft())}>
|
||
حساب جدید
|
||
</Button>
|
||
}
|
||
/>
|
||
|
||
<Card className="p-4">
|
||
<Input
|
||
placeholder="جستجو بر اساس کد یا عنوان حساب…"
|
||
value={search}
|
||
onChange={(e) => setSearch(e.target.value)}
|
||
className="mb-4 max-w-sm"
|
||
/>
|
||
<DataTable
|
||
columns={columns}
|
||
rows={filtered}
|
||
rowKey={(a) => a.id}
|
||
emptyTitle="حسابی یافت نشد"
|
||
emptyDescription="برای شروع یک حساب جدید تعریف کنید."
|
||
/>
|
||
</Card>
|
||
|
||
<Modal
|
||
open={draft !== null}
|
||
title={draft?.id ? 'ویرایش حساب' : 'تعریف حساب جدید'}
|
||
onClose={() => setDraft(null)}
|
||
footer={
|
||
<>
|
||
<Button variant="secondary" onClick={() => setDraft(null)}>
|
||
انصراف
|
||
</Button>
|
||
<Button onClick={handleSave}>ذخیره</Button>
|
||
</>
|
||
}
|
||
>
|
||
{draft && (
|
||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||
<Field label="کد حساب">
|
||
<Input
|
||
value={draft.code}
|
||
onChange={(e) => setDraft({ ...draft, code: e.target.value })}
|
||
placeholder="مثال: 1001"
|
||
/>
|
||
</Field>
|
||
<Field label="عنوان حساب">
|
||
<Input
|
||
value={draft.name}
|
||
onChange={(e) => setDraft({ ...draft, name: e.target.value })}
|
||
placeholder="مثال: صندوق"
|
||
/>
|
||
</Field>
|
||
<Field label="ماهیت حساب">
|
||
<Select
|
||
value={draft.type}
|
||
onChange={(e) => setDraft({ ...draft, type: e.target.value as AccountType })}
|
||
>
|
||
{Object.entries(typeLabels).map(([value, label]) => (
|
||
<option key={value} value={value}>
|
||
{label}
|
||
</option>
|
||
))}
|
||
</Select>
|
||
</Field>
|
||
<Field label="حساب گروه (سرفصل)">
|
||
<Select
|
||
value={draft.parentId ?? ''}
|
||
onChange={(e) => setDraft({ ...draft, parentId: e.target.value || null })}
|
||
>
|
||
<option value="">— بدون گروه —</option>
|
||
{groups
|
||
.filter((group) => group.id !== draft.id)
|
||
.map((group) => (
|
||
<option key={group.id} value={group.id}>
|
||
{group.name}
|
||
</option>
|
||
))}
|
||
</Select>
|
||
</Field>
|
||
<Field label="نوع حساب">
|
||
<Select
|
||
value={draft.isGroup ? 'group' : 'leaf'}
|
||
onChange={(e) => setDraft({ ...draft, isGroup: e.target.value === 'group' })}
|
||
>
|
||
<option value="leaf">حساب معین</option>
|
||
<option value="group">گروه / سرفصل</option>
|
||
</Select>
|
||
</Field>
|
||
{!draft.isGroup && (
|
||
<Field label="مانده افتتاحیه (تومان)">
|
||
<Input
|
||
type="number"
|
||
value={draft.openingBalance}
|
||
onChange={(e) => setDraft({ ...draft, openingBalance: Number(e.target.value) || 0 })}
|
||
/>
|
||
</Field>
|
||
)}
|
||
</div>
|
||
)}
|
||
</Modal>
|
||
|
||
<ConfirmDialog
|
||
open={toDelete !== null}
|
||
title="حذف حساب"
|
||
message={`آیا از حذف حساب «${toDelete?.name}» مطمئن هستید؟ این عمل قابل بازگشت نیست.`}
|
||
onCancel={() => setToDelete(null)}
|
||
onConfirm={confirmDelete}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|