JournalEntriesPage.tsx
5.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import { useEffect, useState } from 'react'
import { finance } from '@/api/client'
import type { JournalEntry } from '@/types/api'
import { PageHeader } from '@/components/PageHeader'
import { Loading } from '@/components/Loading'
import { ErrorBox } from '@/components/ErrorBox'
import { StatusBadge } from '@/components/StatusBadge'
export function JournalEntriesPage() {
const [rows, setRows] = useState<JournalEntry[]>([])
const [error, setError] = useState<Error | null>(null)
const [loading, setLoading] = useState(true)
const [expanded, setExpanded] = useState<Set<string>>(new Set())
useEffect(() => {
finance
.listJournalEntries()
.then((rs) =>
setRows([...rs].sort((a, b) => (b.postedAt ?? '').localeCompare(a.postedAt ?? ''))),
)
.catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e))))
.finally(() => setLoading(false))
}, [])
const toggle = (id: string) => {
setExpanded((prev) => {
const next = new Set(prev)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}
return (
<div>
<PageHeader
title="Journal Entries"
subtitle="Double-entry GL entries posted by pbc-finance from order lifecycle events. Click a row to see debit/credit lines."
/>
{loading && <Loading />}
{error && <ErrorBox error={error} />}
{!loading && !error && rows.length === 0 && (
<div className="card p-6 text-sm text-slate-400">No journal entries yet.</div>
)}
{!loading && !error && rows.length > 0 && (
<div className="card overflow-x-auto">
<table className="table-base">
<thead className="bg-slate-50">
<tr>
<th>Posted</th>
<th>Type</th>
<th>Status</th>
<th>Order</th>
<th>Partner</th>
<th>Amount</th>
<th>Lines</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{rows.map((je) => {
const isOpen = expanded.has(je.id)
return (
<>
<tr
key={je.id}
className="hover:bg-slate-50 cursor-pointer"
onClick={() => toggle(je.id)}
>
<td>{je.postedAt ? new Date(je.postedAt).toLocaleString() : '—'}</td>
<td>{je.type}</td>
<td><StatusBadge status={je.status} /></td>
<td className="font-mono">{je.orderCode}</td>
<td className="font-mono">{je.partnerCode}</td>
<td className="font-mono tabular-nums">
{Number(je.amount).toLocaleString(undefined, { minimumFractionDigits: 2 })}{' '}
{je.currencyCode}
</td>
<td className="text-slate-500">{je.lines?.length ?? 0}</td>
</tr>
{isOpen && je.lines?.length > 0 && (
<tr key={`${je.id}-lines`}>
<td colSpan={7} className="bg-slate-50 px-6 py-3">
<table className="min-w-full text-xs">
<thead>
<tr className="text-slate-400">
<th className="text-left px-2 py-1">#</th>
<th className="text-left px-2 py-1">Account</th>
<th className="text-right px-2 py-1">Debit</th>
<th className="text-right px-2 py-1">Credit</th>
<th className="text-left px-2 py-1">Description</th>
</tr>
</thead>
<tbody>
{je.lines.map((l) => (
<tr key={l.lineNo}>
<td className="px-2 py-1">{l.lineNo}</td>
<td className="px-2 py-1 font-mono">{l.accountCode}</td>
<td className="px-2 py-1 font-mono tabular-nums text-right">
{Number(l.debit) > 0 ? Number(l.debit).toLocaleString(undefined, { minimumFractionDigits: 2 }) : ''}
</td>
<td className="px-2 py-1 font-mono tabular-nums text-right">
{Number(l.credit) > 0 ? Number(l.credit).toLocaleString(undefined, { minimumFractionDigits: 2 }) : ''}
</td>
<td className="px-2 py-1 text-slate-500">{l.description ?? ''}</td>
</tr>
))}
</tbody>
</table>
</td>
</tr>
)}
</>
)
})}
</tbody>
</table>
</div>
)}
</div>
)
}