Files
accounted/components/bookkeeping/EditDraftEntryDialog.tsx
T
Jakob Wennberg 11126d6d56 feat(dimensions): PR3 tagging — voucher-form pickers, MCP dimension tools with resolve-don't-select, engine soft validation (#859)
Phase 3 of dev_docs/dimensions_implementation_plan.md. Companies with
dimensions_enabled=false see zero change; existing free-text API writers keep
working (validation is toggle-governed).

Engine (soft validation):
- validateEntryDimensions() in dimension-resolver: zero queries for untagged
  entries; toggle off → passthrough; toggle on → one settings fetch + two
  registry queries, rejects unknown dims/codes and archived values with
  Swedish per-code messages (DimensionValidationError, 400, details.issues).
  Wired into createDraftEntry + updateDraftEntry before any insert; reversal/
  storno paths untouched (verbatim copies). Fails open on transient registry
  errors — soft validation must never block bookkeeping.

MCP (agent write path):
- New tools: gnubok_list_dimensions, gnubok_list_dimension_values (fuse.js
  fuzzy), gnubok_create_dimension_value (STAGED via pending_operations —
  agents never silently mint reporting values; new op type + CHECK migration
  + executor with duplicate-idempotency).
- create_voucher/correct_entry: per-line dimensions bag + default_dimensions,
  resolve-don't-select server-side (code OR natural-language name; exact →
  fuzzy ≤0.30 with ≥0.15 runner-up margin; non-exact resolutions echoed with
  confidence; ambiguous → ranked candidates, no auto-create).
- gnubok_get_agent_briefing gains a dimensions block (enabled, dims, top
  values) — omitted when registry empty.
- TOOL_SCOPE_MAP entries; risk tier low for staged value creation.

UI:
- JournalEntryForm (manual voucher + TransactionBookingDialog embed): header
  "+ Kostnadsställe/Projekt" progressive disclosure (gäller alla rader with
  documented inheritance rule) + per-row tag popover + compact KS·PR badges;
  gated on dimensions_enabled.
- Voucher detail: display-only dimension badges with registry-name resolution.
- EditDraftEntryDialog carries line dimensions so editing a draft no longer
  strips tags.

categorize/bulk_book dims deferred to PR7 (needs the bulk_book RPC migration).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 13:36:45 +02:00

71 lines
2.5 KiB
TypeScript

'use client'
import { useTranslations } from 'next-intl'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import JournalEntryForm, { type FormLine } from '@/components/bookkeeping/JournalEntryForm'
import type { JournalEntry, JournalEntryLine } from '@/types'
interface Props {
entry: JournalEntry
open: boolean
onOpenChange: (open: boolean) => void
/** Fired after the draft is successfully updated. */
onUpdated: () => void
}
/**
* Edit a DRAFT verifikat. Wraps JournalEntryForm in edit mode, pre-filled from
* the draft's header + lines; the form PATCHes the entry in place and it stays
* a draft (the user posts it separately). Only ever opened for status==='draft'
* entries — the engine + DB triggers reject edits on committed entries anyway.
*/
export default function EditDraftEntryDialog({ entry, open, onOpenChange, onUpdated }: Props) {
const t = useTranslations('bookkeeping')
const initialLines: FormLine[] = ((entry.lines || []) as JournalEntryLine[])
.slice()
.sort((a, b) => a.sort_order - b.sort_order)
.map((l) => ({
account_number: l.account_number,
debit_amount: Number(l.debit_amount) > 0 ? String(l.debit_amount) : '',
credit_amount: Number(l.credit_amount) > 0 ? String(l.credit_amount) : '',
line_description: l.line_description || '',
// Carry the line's dimensions into the form — the PATCH replaces all
// lines, so omitting this would silently strip existing tags.
dimensions:
l.dimensions && Object.keys(l.dimensions).length > 0 ? { ...l.dimensions } : undefined,
}))
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="sm:max-w-3xl max-h-[95dvh] sm:max-h-[90vh] overflow-y-auto"
// Same guard as Ny verifikat: an accidental outside-click must not
// discard in-progress edits.
onPointerDownOutside={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
>
<DialogHeader>
<DialogTitle>{t('edit_draft_dialog_title')}</DialogTitle>
</DialogHeader>
<JournalEntryForm
key={entry.id}
bare
editEntryId={entry.id}
initialLines={initialLines}
initialDate={entry.entry_date}
initialDescription={entry.description}
initialNotes={entry.notes ?? undefined}
initialVoucherSeries={entry.voucher_series}
onUpdated={onUpdated}
/>
</DialogContent>
</Dialog>
)
}