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

306 lines
11 KiB
TypeScript

"use client"
import { useWrite } from "@conusai-kit/ui/data"
import {
CheckField,
ConfirmDialog,
FactList,
FormLayout,
type FormValues,
Page,
PageSection,
ResourceList,
SelectField,
TextField,
usePageNavigation,
} from "@conusai-kit/ui/patterns"
import { Button } from "@conusai-kit/ui/primitives"
import { useRef, useState } from "react"
import { type AccountView, recordTerms, type TransactionView } from "../contract/index.js"
import { ledgerFailure } from "./errors.js"
import type { LedgerLabels, LedgerMessages } from "./labels.js"
import { retryIdentity } from "./retry.js"
const options = (labels: LedgerLabels, values: (keyof LedgerMessages)[]) =>
values.map((value) => ({ value, label: labels[value] }))
export function AccountForm({
org,
account,
labels,
base,
}: {
org: string
account?: AccountView
labels: LedgerLabels
base: string
}) {
const navigation = usePageNavigation()
const write = useWrite<Record<string, unknown>, AccountView>(
account ? "ledger.accounts.update" : "ledger.accounts.create",
)
const identity = useRef(retryIdentity())
const [pending, setPending] = useState<Record<string, unknown>>()
return (
<PageSection title={account ? labels.configure : labels.create}>
<FormLayout
describeError={(error) => ledgerFailure(error, labels)}
submitLabel={labels.review}
busyLabel={labels.busy}
onSubmit={async (values: FormValues) => {
setPending(
account
? {
org,
accountId: account.accountId,
expectedVersion: account.revision,
name: values.name,
status: values.status,
allowNegative: values.allowNegative === "on",
}
: { org, ...values, allowNegative: values.allowNegative === "on" },
)
}}
>
{!account ? (
<>
<TextField name="code" label={labels.code} required pattern="[A-Za-z0-9][A-Za-z0-9._\-]{0,63}" />
<TextField name="currency" label={labels.currency} required pattern="[A-Z]{3}" />
<SelectField
name="type"
label={labels.type}
defaultValue="asset"
options={options(labels, ["asset", "liability", "equity", "income", "expense"])}
/>
<SelectField
name="normalBalance"
label={labels.normalBalance}
defaultValue="debit"
options={options(labels, ["debit", "credit"])}
/>
</>
) : (
<SelectField
name="status"
label={labels.status}
defaultValue={account.status}
options={options(labels, ["active", "archived"])}
/>
)}
<TextField name="name" label={labels.name} defaultValue={account?.name} required maxLength={200} />
<CheckField name="allowNegative" label={labels.allowNegative} defaultChecked={account?.allowNegative} />
</FormLayout>
<ConfirmDialog
describeError={(error) => ledgerFailure(error, labels)}
open={pending !== undefined}
onOpenChange={(open) => {
if (!open) setPending(undefined)
}}
title={labels.confirm}
description={labels.archiveWarning}
confirmLabel={labels.save}
cancelLabel={labels.cancel}
onConfirm={async () => {
if (!pending) return
const saved = await write.mutate({ ...pending, requestId: identity.current(pending) })
navigation.navigate(`${base}/accounts/${saved.accountId}`)
}}
>
<Page>
{pending ? (
<FactList
facts={[
{ label: labels.name, value: String(pending.name) },
{ label: labels.code, value: account?.code ?? String(pending.code) },
{ label: labels.currency, value: account?.currency ?? String(pending.currency) },
{ label: labels.type, value: labels[(account?.type ?? String(pending.type)) as keyof LedgerMessages] },
{
label: labels.normalBalance,
value: labels[(account?.normalBalance ?? String(pending.normalBalance)) as keyof LedgerMessages],
},
{ label: labels.status, value: pending.status === "archived" ? labels.archived : labels.active },
{ label: labels.allowNegative, value: pending.allowNegative ? labels.yes : labels.no },
]}
/>
) : null}
</Page>
</ConfirmDialog>
</PageSection>
)
}
export function RecordForm({
org,
labels,
base,
fenced,
}: {
org: string
labels: LedgerLabels
base: string
fenced: boolean
}) {
const navigation = usePageNavigation()
const write = useWrite<Record<string, unknown>, TransactionView>("ledger.transactions.record")
const identity = useRef(retryIdentity())
const [lines, setLines] = useState(2)
const [pending, setPending] = useState<ReturnType<typeof recordTerms.parse>>()
return (
<PageSection title={labels.record}>
<FormLayout
describeError={(error) => ledgerFailure(error, labels)}
submitLabel={labels.review}
busyLabel={labels.busy}
submitDisabled={fenced}
onSubmit={async (values: FormValues) => {
if (fenced) throw new Error(labels.drift)
const result = recordTerms.safeParse({
effectiveAt: values.effectiveAt,
description: values.description,
...(values.reference ? { reference: values.reference } : {}),
status: values.status,
entries: Array.from({ length: lines }, (_, i) => ({
accountId: values[`account-${i}`],
direction: values[`direction-${i}`],
amountMinor: values[`amount-${i}`],
})),
})
if (!result.success) throw new Error(labels.invalid)
setPending(result.data)
}}
>
<TextField name="effectiveAt" label={labels.effectiveAt} required />
<TextField name="description" label={labels.description} required maxLength={200} />
<TextField name="reference" label={labels.reference} maxLength={200} />
<SelectField
name="status"
label={labels.status}
defaultValue="pending"
options={options(labels, ["pending", "posted"])}
/>
{Array.from({ length: lines }, (_, i) => (
<PageSection key={i} title={`${labels.entries} ${i + 1}`}>
<TextField name={`account-${i}`} label={labels.account} required />
<SelectField
name={`direction-${i}`}
label={labels.normalBalance}
defaultValue={i === 0 ? "debit" : "credit"}
options={options(labels, ["debit", "credit"])}
/>
<TextField
name={`amount-${i}`}
label={labels.amountMinor}
required
pattern="[1-9][0-9]{0,18}"
inputMode="numeric"
/>
</PageSection>
))}
<Button type="button" disabled={lines >= 200 || fenced} onClick={() => setLines((n) => n + 1)}>
{labels.addLine}
</Button>
<Button type="button" disabled={lines <= 2} onClick={() => setLines((n) => n - 1)}>
{labels.removeLine}
</Button>
</FormLayout>
<ConfirmDialog
describeError={(error) => ledgerFailure(error, labels)}
open={pending !== undefined}
onOpenChange={(open) => {
if (!open) setPending(undefined)
}}
title={labels.confirm}
description={labels.confirmDescription}
confirmLabel={labels.record}
cancelLabel={labels.cancel}
onConfirm={async () => {
if (!pending || fenced) throw new Error(labels.drift)
const saved = await write.mutate({
org,
terms: pending,
confirmed: true,
requestId: identity.current({ org, terms: pending }),
})
navigation.navigate(`${base}/transactions/${saved.transactionId}`)
}}
>
<Page>
{pending ? (
<>
<FactList
facts={[
{ label: labels.effectiveAt, value: pending.effectiveAt },
{ label: labels.description, value: pending.description },
{ label: labels.reference, value: pending.reference },
{ label: labels.status, value: labels[pending.status] },
]}
/>
<ResourceList
label={labels.entries}
data={pending.entries.map((entry, index) => ({ ...entry, id: String(index) }))}
columns={[
{ id: "accountId", header: labels.account },
{ id: "direction", header: labels.normalBalance, cell: (row) => labels[row.direction] },
{ id: "amountMinor", header: labels.amountMinor },
]}
empty={{ title: labels.empty }}
/>
</>
) : null}
</Page>
</ConfirmDialog>
</PageSection>
)
}
export function Transition({
org,
transaction,
action,
labels,
base,
fenced,
}: {
org: string
transaction: TransactionView
action: "post" | "void" | "reverse"
labels: LedgerLabels
base: string
fenced: boolean
}) {
const navigation = usePageNavigation()
const write = useWrite<Record<string, unknown>, TransactionView>(`ledger.transactions.${action}`)
const identity = useRef(retryIdentity())
const payload = {
org,
transactionId: transaction.transactionId,
expectedVersion: transaction.revision,
confirmed: true,
}
return (
<ConfirmDialog
describeError={(error) => ledgerFailure(error, labels)}
title={labels.confirm}
description={labels.confirmDescription}
confirmLabel={labels[action]}
cancelLabel={labels.cancel}
trigger={<Button disabled={fenced && action !== "void"}>{labels[action]}</Button>}
onConfirm={async () => {
if (fenced && action !== "void") throw new Error(labels.drift)
const saved = await write.mutate({ ...payload, requestId: identity.current(payload) })
navigation.navigate(`${base}/transactions/${saved.transactionId}`)
}}
>
<Page>
<FactList
facts={[
{ label: labels.transaction, value: transaction.transactionId },
{ label: labels.revision, value: transaction.revision },
{ label: labels.status, value: labels[transaction.status] },
]}
/>
</Page>
</ConfirmDialog>
)
}