feat(transactions): AI booking proposal in the quick-review dialog (cascade step 3) (#1782)

Wires the auto-booking cascade to the UI. When the quick-review dialog opens
for a transaction, AiCategorizeProposal fetches POST /api/agent/categorize
(Tier 1 deterministic candidates → Tier 2 model selector, provider-agnostic)
and:

- pre-fills the dialog's account + VAT from the model's pick (through
  handleAccountChange, so class-2 VAT clearing is preserved),
- shows a confidence band (säker / trolig / välj konto), a short "Varför", and
  the candidate alternatives (click one to re-apply),
- falls back SILENTLY to the deterministic defaults on error, and shows a soft
  note when no AI backend is configured — the dialog always works without AI.

Surfaced inside the existing dialog (not a new card) so it reuses the proven,
balanced commit path. No silent auto-posting: "säker" is pre-filled and booked
with the dialog's existing one-tap Bokför; hands-off auto-book waits for
calibration (avoids any storno-on-undo).

lint + guards clean; 94 transaction/categorize tests green (no regressions).
Strings inline Swedish for now (lift to messages/{sv,en} pre-merge). UI PR:
awaiting founder visual sign-off before merge.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-21 15:04:03 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Opus 4.8
parent e7a5e65ecf
commit 0531576807
3 changed files with 216 additions and 0 deletions
@@ -27,6 +27,7 @@ import InboxDocumentPicker from '@/components/bookkeeping/InboxDocumentPicker'
import type { UploadedFile } from '@/components/bookkeeping/DocumentUploadZone'
import type { AvailableInboxDoc } from '@/components/bookkeeping/InboxDocumentPicker'
import VatTreatmentSelect from './VatTreatmentSelect'
import AiCategorizeProposal from './AiCategorizeProposal'
import { VAT_TREATMENT_OPTIONS } from './transaction-types'
import type { TransactionWithInvoice } from './transaction-types'
import type { TransactionCategory, VatTreatment, BASAccount, EntityType, LinePatternEntry } from '@/types'
@@ -439,6 +440,22 @@ export default function QuickReviewDialog({
</div>
)}
{/* AI booking proposal: pre-fills account + VAT and explains why.
Falls back silently to the deterministic defaults on error. */}
{tx.id && (
<AiCategorizeProposal
key={tx.id}
transactionId={tx.id}
open={open}
onApply={(account, vat) => {
handleAccountChange(account)
// handleAccountChange clears VAT for class-2 accounts; for the
// rest, apply the proposed treatment.
if (!account.startsWith('2')) setVatTreatment(vat)
}}
/>
)}
{/* Template or Category */}
<div>
<label className="text-sm font-medium text-muted-foreground">