@@ -0,0 +1,243 @@
|
||||
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>
|
||||
)
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user