Files
fiberops-web/src/app/(dashboard)/dashboard/settings/chart-of-accounts/page.tsx
2026-04-13 09:36:37 +08:00

130 lines
6.9 KiB
TypeScript

'use client';
import { useState, useEffect, useCallback } from 'react';
import { api } from '@/lib/api';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { FormModal } from '@/components/ui/form-modal';
import { Modal } from '@/components/ui/modal';
import { ActionIcon } from '@/components/ui/action-icon';
import { useToast } from '@/components/ui/toast';
const typeColors: Record<string, 'info' | 'success' | 'purple' | 'warning' | 'error'> = {
asset: 'info', liability: 'error', equity: 'purple', revenue: 'success', expense: 'warning',
};
export default function ChartOfAccountsPage() {
const { toast } = useToast();
const [accounts, setAccounts] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [showCreate, setShowCreate] = useState(false);
const [deleteTarget, setDeleteTarget] = useState<any>(null);
const load = useCallback(async () => {
try { const r = await api.get('/accounting/chart-of-accounts'); setAccounts(r.data.data); }
catch { toast('Failed to load', 'error'); }
finally { setLoading(false); }
}, [toast]);
useEffect(() => { load(); }, [load]);
async function handleDelete() {
if (!deleteTarget) return;
try { await api.delete(`/accounting/chart-of-accounts/${deleteTarget.id}`); toast('Account deleted', 'success'); setDeleteTarget(null); load(); }
catch (e: any) { toast(e.response?.data?.error || 'Failed', 'error'); }
}
// Group by type
const grouped: Record<string, any[]> = {};
for (const a of accounts) {
(grouped[a.type] = grouped[a.type] || []).push(a);
}
const typeOrder = ['asset', 'liability', 'equity', 'revenue', 'expense'];
const typeLabels: Record<string, string> = { asset: 'Assets', liability: 'Liabilities', equity: 'Equity', revenue: 'Revenue', expense: 'Expenses' };
return (
<div>
<div className="flex items-center justify-between mb-5">
<p className="text-sm text-surface-500 dark:text-surface-400">Manage your chart of accounts for double-entry bookkeeping</p>
<Button onClick={() => setShowCreate(true)}>Add Account</Button>
</div>
{loading ? <p className="text-surface-400 dark:text-surface-500">Loading...</p> : (
<div className="space-y-6">
{typeOrder.map((type) => {
const accts = grouped[type];
if (!accts) return null;
return (
<div key={type}>
<h3 className="text-sm font-semibold text-surface-800 dark:text-surface-200 mb-2 flex items-center gap-2">
<Badge label={typeLabels[type]} variant={typeColors[type]} />
</h3>
<div className="bg-white dark:bg-surface-800 rounded-xl border border-surface-200/80 dark:border-surface-700 overflow-hidden">
<table className="min-w-full divide-y divide-surface-200 dark:divide-surface-700">
<tbody className="divide-y divide-surface-100 dark:divide-surface-700/50">
{accts.map((a: any) => (
<tr key={a.id} className="cursor-pointer hover:bg-primary-50/40 dark:hover:bg-surface-700/50 border-l-2 border-l-transparent hover:border-l-primary-400 transition-all duration-150">
<td className="px-5 py-3 text-sm font-mono text-surface-600 dark:text-surface-300 w-24">{a.code}</td>
<td className="px-5 py-3 text-sm text-surface-800 dark:text-surface-200 font-medium">{a.name}</td>
<td className="px-5 py-3 text-sm text-right">
{a.isSystem ? <span className="text-xs text-surface-300 dark:text-surface-500">System</span> : (
<div className="flex justify-end" onClick={(e) => e.stopPropagation()}>
<ActionIcon icon="trash" variant="danger" label="Delete" onClick={() => setDeleteTarget(a)} />
</div>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
})}
</div>
)}
<CreateCoAModal open={showCreate} onClose={() => setShowCreate(false)} onSuccess={load} />
<Modal open={!!deleteTarget} onClose={() => setDeleteTarget(null)} title="Delete Account"
description={`Delete "${deleteTarget?.code}${deleteTarget?.name}"?`} variant="danger"
confirmLabel="Delete" onConfirm={handleDelete} />
</div>
);
}
function CreateCoAModal({ open, onClose, onSuccess }: { open: boolean; onClose: () => void; onSuccess: () => void }) {
const { toast } = useToast();
const [form, setForm] = useState({ code: '', name: '', type: 'asset' });
const [submitting, setSubmitting] = useState(false);
const ic = 'mt-1 block w-full rounded-lg border border-surface-200 dark:border-surface-700 px-3.5 py-2.5 text-sm text-surface-900 dark:text-surface-200 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20 transition-all duration-200';
useEffect(() => { if (open) setForm({ code: '', name: '', type: 'asset' }); }, [open]);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault(); setSubmitting(true);
try { await api.post('/accounting/chart-of-accounts', form); toast('Account created', 'success'); onSuccess(); onClose(); }
catch (e: any) { toast(e.response?.data?.error || 'Failed', 'error'); }
finally { setSubmitting(false); }
}
return (
<FormModal open={open} onClose={onClose} title="Add Account">
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-3 gap-4">
<div><label className="block text-sm font-medium text-surface-700 dark:text-surface-300">Code</label><input type="text" required value={form.code} onChange={(e) => setForm({ ...form, code: e.target.value })} className={ic} placeholder="1050" /></div>
<div className="col-span-2"><label className="block text-sm font-medium text-surface-700 dark:text-surface-300">Name</label><input type="text" required value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} className={ic} placeholder="Account name" /></div>
</div>
<div><label className="block text-sm font-medium text-surface-700 dark:text-surface-300">Type</label>
<select value={form.type} onChange={(e) => setForm({ ...form, type: e.target.value })} className={ic}>
<option value="asset">Asset</option><option value="liability">Liability</option>
<option value="equity">Equity</option><option value="revenue">Revenue</option>
<option value="expense">Expense</option>
</select>
</div>
<div className="flex justify-end gap-3 pt-2"><Button type="button" variant="secondary" onClick={onClose}>Cancel</Button><Button type="submit" loading={submitting}>Create Account</Button></div>
</form>
</FormModal>
);
}