'use client' // The one pending-op-owned preview (flows prereq, seam 8.3): renders what a // staged pending_operation will do, dispatched on operation_type. Consumed by // /pending (list detail + confirm dialogs), the chat ApprovalCard, and future // flow-run views. Renderers moved verbatim from app/(dashboard)/pending/page.tsx; // keep markup and classNames in lockstep with the design system, not with any // one consumer. import { Fragment, createContext, useContext } from 'react' import { cn, formatCurrency } from '@/lib/utils' import { VTH_CLASS, VTD_CLASS } from '@/components/ui/dry-table' import type { PendingOperation } from '@/types' import { AttachDocumentPreview } from '@/components/bookkeeping/AttachDocumentPreview' import { MatchTransactionInvoicePreview } from '@/components/bookkeeping/MatchTransactionInvoicePreview' // The subset of a PendingOperation the preview actually reads. operation_type // is widened to string: the chat surface derives it from an MCP tool name and // unknown values legitimately fall through to GenericPreview. params carries // tool inputs some renderers need (e.g. attach_document_to_transaction's // document_id); surfaces that only have preview_data may omit it. export interface OperationPreviewInput { operation_type: string preview_data: PendingOperation['preview_data'] params?: PendingOperation['params'] } /** * Account number -> account name, for the proposal previews. The value is * provided by whichever page owns the fetch lifecycle (/pending provides a * per-mount, per-company map; a consumer that provides nothing gets the * default {} and previews show the bare number, never a wrong name). */ export const AccountNamesContext = createContext>({}) /** Render '-' instead of "NaN kr" when a preview payload omits an amount. */ function money(v: unknown, currency: string): string { return typeof v === 'number' && Number.isFinite(v) ? formatCurrency(v, currency) : '-' } function CategorizePreview({ data }: { data: Record }) { const accountNames = useContext(AccountNamesContext) // The exact journal lines the approval will post (net cost line, VAT line, // gross bank line, SEK): staged by the server since the preview-lines fix. const lines = (data.lines as Array<{ account_number?: string; debit_amount?: number; credit_amount?: number; description?: string }>) || [] const vatLines = (data.vat_lines as Array<{ account_number: string; debit_amount: number; credit_amount: number; description: string }>) || [] if (lines.length > 0) { return (

Verifikat

{lines.map((line, i) => { const debitAmt = typeof line.debit_amount === 'number' ? line.debit_amount : 0 const creditAmt = typeof line.credit_amount === 'number' ? line.credit_amount : 0 return (
{line.account_number ?? '?'}{' '} {/* The account's own name first: it is what the posting means. The line text follows only when it adds something the name does not already say. */} {(line.account_number && accountNames[line.account_number]) || line.description || ''} {line.description && line.account_number && accountNames[line.account_number] && line.description !== accountNames[line.account_number] ? ( · {line.description} ) : null} {debitAmt > 0 ? `D ${formatCurrency(debitAmt)}` : `K ${formatCurrency(creditAmt)}`}
) })}
) } // Some operations carry their kontering under the generic `preview_lines` // key instead (the shape every other staged type renders through). Read it // before falling through to the legacy summary, which would otherwise show // blank accounts for a preview that does describe the entry in full. if (isKonteringLines(data.preview_lines)) { return (

Verifikat

) } // Legacy summary for operations staged before the preview carried full // lines: debit/credit accounts + gross amount + separate VAT rows. const legacyAmount = typeof data.amount === 'number' && Number.isFinite(data.amount) ? data.amount : null return (
Debetkonto {String(data.debit_account ?? '')} Kreditkonto {String(data.credit_account ?? '')} Belopp {/* A preview with no usable amount used to render "NaN kr": show the gap as a gap instead of a number that isn't one. */} {legacyAmount === null ? '-' : formatCurrency(legacyAmount, (data.currency as string) || 'SEK')}
{vatLines.length > 0 && (

Momsrader

{vatLines.map((line, i) => (
{line.account_number}{' '} {accountNames[line.account_number] || line.description} {accountNames[line.account_number] && line.description !== accountNames[line.account_number] ? ( · {line.description} ) : null} {line.debit_amount > 0 ? `D ${formatCurrency(line.debit_amount)}` : `K ${formatCurrency(line.credit_amount)}`}
))}
)}
) } function CustomerPreview({ data }: { data: Record }) { return (
Namn {String(data.name ?? '')} Typ {String(data.customer_type ?? '')} {data.email ? ( <> E-post {String(data.email)} ) : null} {data.org_number ? ( <> Org.nr {String(data.org_number)} ) : null}
) } function InvoicePreview({ data }: { data: Record }) { const items = (data.items as Array<{ description: string; quantity: number; unit: string; unit_price: number; line_total: number; vat_rate: number }>) || [] return (
Kund {String(data.customer_name ?? '')} Datum {String(data.invoice_date ?? '')} Förfallodatum {String(data.due_date ?? '')}
{items.length > 0 && (
{items.map((item, i) => (
{item.description} ({item.quantity} {item.unit}) {formatCurrency(item.line_total, (data.currency as string) || 'SEK')}
))}
)}
Netto {money(data.subtotal, (data.currency as string) || 'SEK')} Moms {money(data.vat_amount, (data.currency as string) || 'SEK')} Totalt {money(data.total, (data.currency as string) || 'SEK')}
) } function CreateTransactionPreview({ data }: { data: Record }) { const currency = (data.currency as string) || 'SEK' return (
Datum {String(data.date ?? '')} Beskrivning {String(data.description ?? '')} Belopp {money(data.amount, currency)} {data.external_id ? ( <> Extern referens {String(data.external_id)} ) : null}
) } type VoucherLine = { account_number: string account_name?: string | null debit_amount: number credit_amount: number line_description?: string | null } function VoucherLinesTable({ lines, currency }: { lines: VoucherLine[]; currency?: string }) { return (
{lines.map((line, i) => (
{line.account_number} {line.account_name || line.line_description || '-'} {line.debit_amount > 0 ? formatCurrency(line.debit_amount, currency || 'SEK') : ''} {line.credit_amount > 0 ? formatCurrency(line.credit_amount, currency || 'SEK') : ''}
))}
) } function VoucherPreview({ data }: { data: Record }) { const lines = (data.lines as VoucherLine[]) || [] const totalDebit = data.total_debit as number | undefined const totalCredit = data.total_credit as number | undefined return (
Datum {String(data.entry_date ?? '')} Beskrivning {String(data.description ?? '')} Serie {String(data.voucher_series ?? 'A')}
{lines.length > 0 && (
Konto Text Debet Kredit
)} {totalDebit != null && totalCredit != null && (
Summa {formatCurrency(totalDebit)} {formatCurrency(totalCredit)}
)}
) } function CorrectEntryPreview({ data }: { data: Record }) { const original = (data.original as { voucher?: string entry_date?: string description?: string lines?: VoucherLine[] }) || {} const correction = (data.correction as { total_debit?: number total_credit?: number line_count?: number lines?: VoucherLine[] }) || {} return (

Originalverifikation V{original.voucher ?? ''}, {original.entry_date ?? ''}

{original.description ?? ''}

{original.lines && original.lines.length > 0 && ( )}

Korrigerad verifikation ({correction.line_count ?? correction.lines?.length ?? 0} rader)

{correction.lines && correction.lines.length > 0 && ( )} {correction.total_debit != null && (
Summa {formatCurrency(correction.total_debit)} {formatCurrency(correction.total_credit ?? 0)}
)}
) } // Render a primitive (string/number/bool) or a short summary of an array/object. // Used by GenericPreview to avoid the "[object Object]" stringification that // occurs when an operation_type has no dedicated preview component. function renderPrimitive(value: unknown): string { if (value == null) return '' if (Array.isArray(value)) return `${value.length} rader` if (typeof value === 'object') return JSON.stringify(value) return String(value) } // A preview_data value that is a kontering (array of account/debit/credit // rows). Several staged op types carry one under keys like `preview_lines` // without a dedicated preview component; rendering it as the actual // verifikat rows is what makes the detail panel say what the agent will do. interface PreviewKonteringLine { account?: string account_number?: string description?: string debit?: number credit?: number debit_amount?: number credit_amount?: number } function isKonteringLines(value: unknown): value is PreviewKonteringLine[] { return ( Array.isArray(value) && value.length > 0 && value.every( (line) => line != null && typeof line === 'object' && ('account' in line || 'account_number' in line) && ('debit' in line || 'credit' in line || 'debit_amount' in line || 'credit_amount' in line), ) ) } function PreviewKonteringTable({ lines }: { lines: PreviewKonteringLine[] }) { const amount = (n: number | undefined) => n && n > 0 ? n.toLocaleString('sv-SE', { minimumFractionDigits: 2 }) : '' return ( {lines.map((line, i) => ( ))}
Konto Beskrivning Debet Kredit
{line.account ?? line.account_number} {line.description ?? ''} {amount(line.debit ?? line.debit_amount)} {amount(line.credit ?? line.credit_amount)}
) } function GenericPreview({ data }: { data: Record }) { // Skip period_status here: it's surfaced in the dedicated banner, not the // generic key-value dump (otherwise the approver sees the same fact twice). const entries = Object.entries(data).filter(([k, v]) => v != null && v !== '' && k !== 'period_status') const konteringEntries = entries.filter(([, v]) => isKonteringLines(v)) const rest = entries.filter(([, v]) => !isKonteringLines(v)) return (
{konteringEntries.map(([key, value]) => ( ))} {rest.length > 0 && (
{rest.map(([key, value]) => ( {key.replace(/_/g, ' ')} {renderPrimitive(value)} ))}
)}
) } export function OperationPreview({ op }: { op: OperationPreviewInput }) { const body = (() => { switch (op.operation_type) { case 'categorize_transaction': return case 'create_customer': return case 'create_invoice': return case 'create_transaction': return case 'create_voucher': return case 'correct_entry': return case 'attach_document_to_transaction': return case 'match_transaction_invoice': return default: return } })() return body }