Files
2026-11-14 09:56:45 +00:00

244 lines
9.7 KiB
TypeScript

import { businessServiceNotices } from "@conusai-kit/business/refusals"
import { type Container, notFound, type TransportCaller } from "@conusai-kit/core"
import type { I18n } from "@conusai-kit/i18n"
import { type ProjectPage, projectPage } from "@conusai-kit/ui"
import { Can } from "@conusai-kit/ui/data"
import {
FactList,
Page,
PageHeader,
PageSection,
type ResourceColumn,
type ServiceNotice,
ServiceStatePage,
serviceNotice,
} from "@conusai-kit/ui/patterns"
import * as c from "../contract/index.js"
import { AccountForm, RecordForm, Transition } from "./forms.js"
import { type LedgerLabels, ledgerLabels } from "./labels.js"
import { LedgerList } from "./resource.js"
type Loaded = { labels: LedgerLabels; verification: c.VerificationView } & (
| {
kind: "list"
accounts: { rows: c.AccountView[]; after?: string | undefined }
transactions: { rows: c.TransactionView[]; after?: string | undefined }
}
| {
kind: "account"
account: c.AccountView
balances: { rows: c.BalanceView[]; after?: string | undefined }
entries: { rows: c.EntryView[]; after?: string | undefined }
}
| { kind: "transaction"; transaction: c.TransactionView }
)
type Data = Loaded | { kind: "state"; labels: LedgerLabels; notice: ServiceNotice }
export type LedgerPage = ProjectPage<TransportCaller, I18n, Container, Data>
export const ledgerPage: LedgerPage = projectPage({
async load({ caller, slugs, segments, i18n }): Promise<Data> {
const labels = ledgerLabels((key) => i18n.message(`ledger.${key}`), i18n.locale)
const input = { org: slugs.org }
const [kind, id, ...rest] = segments
if (rest.length || (kind !== undefined && ((kind !== "accounts" && kind !== "transactions") || !id)))
throw notFound()
try {
const verification = await caller.call(c.verify, input)
if (kind === "accounts" && id) {
const [account, balances, entries] = await Promise.all([
caller.call(c.account, { ...input, accountId: id }),
caller.call(c.balances, { ...input, accountId: id }),
caller.call(c.entries, { ...input, accountId: id }),
])
return { kind: "account", labels, verification, account, balances, entries }
}
if (kind === "transactions" && id)
return {
kind: "transaction",
labels,
verification,
transaction: await caller.call(c.transaction, { ...input, transactionId: id }),
}
const [accounts, transactions] = await Promise.all([
caller.call(c.accounts, input),
caller.call(c.transactions, input),
])
return { kind: "list", labels, verification, accounts, transactions }
} catch (error) {
const notice = serviceNotice(error, businessServiceNotices(i18n.locale))
if (!notice) throw error
return { kind: "state", labels, notice }
}
},
View({ slugs, data }) {
if (data.kind === "state") return <ServiceStatePage title={data.labels.title} notice={data.notice} />
const { labels: l, verification } = data
const base = `/${slugs.org}/${slugs.workspace}/${slugs.project}/ledger`
const org = slugs.org
const fenced = verification.drift.length > 0
const entryColumns: ResourceColumn<c.EntryView>[] = [
{ id: "accountId", header: l.account },
{ id: "direction", header: l.normalBalance, cell: (row: c.EntryView) => l[row.direction] },
{ id: "amountMinor", header: l.amountMinor },
{ id: "currency", header: l.currency },
{ id: "transactionId", header: l.transaction },
]
return (
<Page>
<PageHeader
title={l.title}
description={l.intro}
breadcrumb={data.kind === "list" ? undefined : <a href={base}>{l.back}</a>}
/>
<PageSection title={l.verifier}>
<FactList
facts={[
{ label: l.status, value: fenced ? l.drift : l.healthy },
{ label: l.checked, value: String(verification.checkedAccounts) },
]}
/>
</PageSection>
{data.kind === "list" ? (
<>
<PageSection title={l.accounts}>
<LedgerList
seed={data.accounts}
procedure="ledger.accounts.list"
input={{ org }}
rowId={(row) => row.accountId}
href={(row) => `${base}/accounts/${row.accountId}`}
labels={l}
title={l.accounts}
columns={[
{ id: "code", header: l.code },
{ id: "name", header: l.name },
{ id: "currency", header: l.currency },
{ id: "status", header: l.status, cell: (row) => l[row.status] },
]}
/>
</PageSection>
<Can permission="ledger.accounts.manage">
<AccountForm org={org} labels={l} base={base} />
</Can>
<PageSection title={l.transactions}>
<LedgerList
seed={data.transactions}
procedure="ledger.transactions.list"
input={{ org }}
rowId={(row) => row.transactionId}
href={(row) => `${base}/transactions/${row.transactionId}`}
labels={l}
title={l.transactions}
columns={[
{ id: "transactionId", header: l.transaction },
{ id: "description", header: l.description },
{ id: "status", header: l.status, cell: (row) => l[row.status] },
{ id: "effectiveAt", header: l.effectiveAt },
]}
/>
</PageSection>
<Can permission="ledger.transactions.write">
<RecordForm org={org} labels={l} base={base} fenced={fenced} />
</Can>
</>
) : data.kind === "account" ? (
<>
<FactList
facts={[
{ label: l.account, value: data.account.accountId },
{ label: l.code, value: data.account.code },
{ label: l.name, value: data.account.name },
{ label: l.currency, value: data.account.currency },
{ label: l.type, value: l[data.account.type] },
{ label: l.normalBalance, value: l[data.account.normalBalance] },
{ label: l.allowNegative, value: data.account.allowNegative ? l.yes : l.no },
{ label: l.status, value: l[data.account.status] },
{ label: l.revision, value: data.account.revision },
]}
/>
<PageSection title={l.balances}>
<LedgerList
seed={data.balances}
procedure="ledger.balances.list"
input={{ org, accountId: data.account.accountId }}
rowId={(row) => row.accountId}
labels={l}
title={l.balances}
columns={[
{ id: "currency", header: l.currency },
{ id: "posted", header: l.postedBalance },
{ id: "pending", header: l.pendingBalance },
{ id: "available", header: l.availableBalance },
]}
/>
</PageSection>
<PageSection title={l.entries}>
<LedgerList
seed={data.entries}
procedure="ledger.entries.list"
input={{ org, accountId: data.account.accountId }}
rowId={(row) => row.entryId}
href={(row) => `${base}/transactions/${row.transactionId}`}
labels={l}
title={l.entries}
columns={entryColumns}
/>
</PageSection>
<Can permission="ledger.accounts.manage">
<AccountForm org={org} account={data.account} labels={l} base={base} />
</Can>
</>
) : (
<>
<FactList
facts={[
{ label: l.transaction, value: data.transaction.transactionId },
{ label: l.description, value: data.transaction.description },
{ label: l.reference, value: data.transaction.reference },
{ label: l.effectiveAt, value: data.transaction.effectiveAt },
{ label: l.status, value: l[data.transaction.status] },
{ label: l.revision, value: data.transaction.revision },
{ label: l.reverses, value: data.transaction.reverses },
]}
/>
<LedgerList
seed={{ rows: data.transaction.entries }}
procedure="ledger.transactions.get"
input={{ org, transactionId: data.transaction.transactionId }}
rowId={(row) => row.entryId}
labels={l}
title={l.entries}
columns={entryColumns}
/>
{data.transaction.status === "pending" ? (
<Can permission="ledger.transactions.write">
{(["post", "void"] as const).map((action) => (
<Transition
key={action}
org={org}
transaction={data.transaction}
action={action}
labels={l}
base={base}
fenced={fenced}
/>
))}
</Can>
) : data.transaction.status === "posted" ? (
<Can permission="ledger.transactions.reverse">
<Transition
org={org}
transaction={data.transaction}
action="reverse"
labels={l}
base={base}
fenced={fenced}
/>
</Can>
) : null}
</>
)}
</Page>
)
},
})