Commit d2bca486465876d2d732f9cff7bb160ca4ab0c71
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.
Showing
2 changed files
with
96 additions
and
25 deletions
web/src/pages/JournalEntriesPage.tsx
| @@ -4,13 +4,13 @@ import type { JournalEntry } from '@/types/api' | @@ -4,13 +4,13 @@ import type { JournalEntry } from '@/types/api' | ||
| 4 | import { PageHeader } from '@/components/PageHeader' | 4 | import { PageHeader } from '@/components/PageHeader' |
| 5 | import { Loading } from '@/components/Loading' | 5 | import { Loading } from '@/components/Loading' |
| 6 | import { ErrorBox } from '@/components/ErrorBox' | 6 | import { ErrorBox } from '@/components/ErrorBox' |
| 7 | -import { DataTable, type Column } from '@/components/DataTable' | ||
| 8 | import { StatusBadge } from '@/components/StatusBadge' | 7 | import { StatusBadge } from '@/components/StatusBadge' |
| 9 | 8 | ||
| 10 | export function JournalEntriesPage() { | 9 | export function JournalEntriesPage() { |
| 11 | const [rows, setRows] = useState<JournalEntry[]>([]) | 10 | const [rows, setRows] = useState<JournalEntry[]>([]) |
| 12 | const [error, setError] = useState<Error | null>(null) | 11 | const [error, setError] = useState<Error | null>(null) |
| 13 | const [loading, setLoading] = useState(true) | 12 | const [loading, setLoading] = useState(true) |
| 13 | + const [expanded, setExpanded] = useState<Set<string>>(new Set()) | ||
| 14 | 14 | ||
| 15 | useEffect(() => { | 15 | useEffect(() => { |
| 16 | finance | 16 | finance |
| @@ -22,38 +22,100 @@ export function JournalEntriesPage() { | @@ -22,38 +22,100 @@ export function JournalEntriesPage() { | ||
| 22 | .finally(() => setLoading(false)) | 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 | return ( | 34 | return ( |
| 49 | <div> | 35 | <div> |
| 50 | <PageHeader | 36 | <PageHeader |
| 51 | title="Journal Entries" | 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 | {loading && <Loading />} | 40 | {loading && <Loading />} |
| 55 | {error && <ErrorBox error={error} />} | 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 | </div> | 119 | </div> |
| 58 | ) | 120 | ) |
| 59 | } | 121 | } |
web/src/types/api.ts
| @@ -264,6 +264,14 @@ export interface Account { | @@ -264,6 +264,14 @@ export interface Account { | ||
| 264 | export type JournalEntryType = 'AR' | 'AP' | 264 | export type JournalEntryType = 'AR' | 'AP' |
| 265 | export type JournalEntryStatus = 'POSTED' | 'SETTLED' | 'REVERSED' | 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 | export interface JournalEntry { | 275 | export interface JournalEntry { |
| 268 | id: string | 276 | id: string |
| 269 | code: string | 277 | code: string |
| @@ -274,4 +282,5 @@ export interface JournalEntry { | @@ -274,4 +282,5 @@ export interface JournalEntry { | ||
| 274 | amount: string | number | 282 | amount: string | number |
| 275 | currencyCode: string | 283 | currencyCode: string |
| 276 | postedAt: string | 284 | postedAt: string |
| 285 | + lines: JournalEntryLine[] | ||
| 277 | } | 286 | } |
-
mentioned in commit 1f6482be