initial: standalone repo from monorepo split
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user