Files
accounting-frontend/src/pages/Accounts.tsx
2026-07-01 00:29:25 +03:30

243 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
)
}