JournalEntriesPage.tsx 5.1 KB
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>
  )
}