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