Commit d2bca486465876d2d732f9cff7bb160ca4ab0c71

Authored by zichun
1 parent 3745de3a

feat(web): journal entries page shows double-entry debit/credit lines

JournalEntriesPage now renders expandable debit/credit lines
for each entry. Click a row to toggle the line detail view
showing account code, DR/CR amounts, and description.

Types updated: JournalEntry now includes lines array with
JournalEntryLine (lineNo, accountCode, debit, credit, description).

This makes the GL growth visible in the demo — confirming an SO
shows the AR entry with DR 1100 / CR 4100 balanced lines inline.
web/src/pages/JournalEntriesPage.tsx
... ... @@ -4,13 +4,13 @@ import type { JournalEntry } from '@/types/api'
4 4 import { PageHeader } from '@/components/PageHeader'
5 5 import { Loading } from '@/components/Loading'
6 6 import { ErrorBox } from '@/components/ErrorBox'
7   -import { DataTable, type Column } from '@/components/DataTable'
8 7 import { StatusBadge } from '@/components/StatusBadge'
9 8  
10 9 export function JournalEntriesPage() {
11 10 const [rows, setRows] = useState<JournalEntry[]>([])
12 11 const [error, setError] = useState<Error | null>(null)
13 12 const [loading, setLoading] = useState(true)
  13 + const [expanded, setExpanded] = useState<Set<string>>(new Set())
14 14  
15 15 useEffect(() => {
16 16 finance
... ... @@ -22,38 +22,100 @@ export function JournalEntriesPage() {
22 22 .finally(() => setLoading(false))
23 23 }, [])
24 24  
25   - const columns: Column<JournalEntry>[] = [
26   - {
27   - header: 'Posted',
28   - key: 'postedAt',
29   - render: (r) => (r.postedAt ? new Date(r.postedAt).toLocaleString() : '—'),
30   - },
31   - { header: 'Code', key: 'code', render: (r) => <span className="font-mono text-xs">{r.code}</span> },
32   - { header: 'Type', key: 'type' },
33   - { header: 'Status', key: 'status', render: (r) => <StatusBadge status={r.status} /> },
34   - { header: 'Order', key: 'orderCode', render: (r) => <span className="font-mono">{r.orderCode}</span> },
35   - { header: 'Partner', key: 'partnerCode', render: (r) => <span className="font-mono">{r.partnerCode}</span> },
36   - {
37   - header: 'Amount',
38   - key: 'amount',
39   - render: (r) => (
40   - <span className="font-mono tabular-nums">
41   - {Number(r.amount).toLocaleString(undefined, { minimumFractionDigits: 2 })}{' '}
42   - {r.currencyCode}
43   - </span>
44   - ),
45   - },
46   - ]
  25 + const toggle = (id: string) => {
  26 + setExpanded((prev) => {
  27 + const next = new Set(prev)
  28 + if (next.has(id)) next.delete(id)
  29 + else next.add(id)
  30 + return next
  31 + })
  32 + }
47 33  
48 34 return (
49 35 <div>
50 36 <PageHeader
51 37 title="Journal Entries"
52   - subtitle="AR / AP entries created reactively by pbc-finance from order lifecycle events."
  38 + subtitle="Double-entry GL entries posted by pbc-finance from order lifecycle events. Click a row to see debit/credit lines."
53 39 />
54 40 {loading && <Loading />}
55 41 {error && <ErrorBox error={error} />}
56   - {!loading && !error && <DataTable rows={rows} columns={columns} />}
  42 + {!loading && !error && rows.length === 0 && (
  43 + <div className="card p-6 text-sm text-slate-400">No journal entries yet.</div>
  44 + )}
  45 + {!loading && !error && rows.length > 0 && (
  46 + <div className="card overflow-x-auto">
  47 + <table className="table-base">
  48 + <thead className="bg-slate-50">
  49 + <tr>
  50 + <th>Posted</th>
  51 + <th>Type</th>
  52 + <th>Status</th>
  53 + <th>Order</th>
  54 + <th>Partner</th>
  55 + <th>Amount</th>
  56 + <th>Lines</th>
  57 + </tr>
  58 + </thead>
  59 + <tbody className="divide-y divide-slate-100">
  60 + {rows.map((je) => {
  61 + const isOpen = expanded.has(je.id)
  62 + return (
  63 + <>
  64 + <tr
  65 + key={je.id}
  66 + className="hover:bg-slate-50 cursor-pointer"
  67 + onClick={() => toggle(je.id)}
  68 + >
  69 + <td>{je.postedAt ? new Date(je.postedAt).toLocaleString() : '—'}</td>
  70 + <td>{je.type}</td>
  71 + <td><StatusBadge status={je.status} /></td>
  72 + <td className="font-mono">{je.orderCode}</td>
  73 + <td className="font-mono">{je.partnerCode}</td>
  74 + <td className="font-mono tabular-nums">
  75 + {Number(je.amount).toLocaleString(undefined, { minimumFractionDigits: 2 })}{' '}
  76 + {je.currencyCode}
  77 + </td>
  78 + <td className="text-slate-500">{je.lines?.length ?? 0}</td>
  79 + </tr>
  80 + {isOpen && je.lines?.length > 0 && (
  81 + <tr key={`${je.id}-lines`}>
  82 + <td colSpan={7} className="bg-slate-50 px-6 py-3">
  83 + <table className="min-w-full text-xs">
  84 + <thead>
  85 + <tr className="text-slate-400">
  86 + <th className="text-left px-2 py-1">#</th>
  87 + <th className="text-left px-2 py-1">Account</th>
  88 + <th className="text-right px-2 py-1">Debit</th>
  89 + <th className="text-right px-2 py-1">Credit</th>
  90 + <th className="text-left px-2 py-1">Description</th>
  91 + </tr>
  92 + </thead>
  93 + <tbody>
  94 + {je.lines.map((l) => (
  95 + <tr key={l.lineNo}>
  96 + <td className="px-2 py-1">{l.lineNo}</td>
  97 + <td className="px-2 py-1 font-mono">{l.accountCode}</td>
  98 + <td className="px-2 py-1 font-mono tabular-nums text-right">
  99 + {Number(l.debit) > 0 ? Number(l.debit).toLocaleString(undefined, { minimumFractionDigits: 2 }) : ''}
  100 + </td>
  101 + <td className="px-2 py-1 font-mono tabular-nums text-right">
  102 + {Number(l.credit) > 0 ? Number(l.credit).toLocaleString(undefined, { minimumFractionDigits: 2 }) : ''}
  103 + </td>
  104 + <td className="px-2 py-1 text-slate-500">{l.description ?? ''}</td>
  105 + </tr>
  106 + ))}
  107 + </tbody>
  108 + </table>
  109 + </td>
  110 + </tr>
  111 + )}
  112 + </>
  113 + )
  114 + })}
  115 + </tbody>
  116 + </table>
  117 + </div>
  118 + )}
57 119 </div>
58 120 )
59 121 }
... ...
web/src/types/api.ts
... ... @@ -264,6 +264,14 @@ export interface Account {
264 264 export type JournalEntryType = 'AR' | 'AP'
265 265 export type JournalEntryStatus = 'POSTED' | 'SETTLED' | 'REVERSED'
266 266  
  267 +export interface JournalEntryLine {
  268 + lineNo: number
  269 + accountCode: string
  270 + debit: string | number
  271 + credit: string | number
  272 + description: string | null
  273 +}
  274 +
267 275 export interface JournalEntry {
268 276 id: string
269 277 code: string
... ... @@ -274,4 +282,5 @@ export interface JournalEntry {
274 282 amount: string | number
275 283 currencyCode: string
276 284 postedAt: string
  285 + lines: JournalEntryLine[]
277 286 }
... ...