perf(forms): supplier-invoice form, register forms and review dialogs read the session cache (#1938)

The supplier-invoice editor issued four requests on every mount (suppliers,
accounts, settings, fiscal periods) and defaulted vatRegistered=true,
entity type and rounding until /api/settings landed, so the moms controls
visibly flipped. The register forms fetched the whole chart of accounts to
fill one konto combobox, and each transaction review dialog refetched
accounts, cash accounts or settings per open.

- use-supplier-invoice-data: thin composition of useSuppliers, useAccounts,
  useCompanySettings and useFiscalPeriods; the settings-driven gates come
  from a pure deriveSupplierInvoiceDefaults() (tested) instead of state
  that flips when the fetch returns; the per-invoice öresavrundning toggle
  is the one local override. Inline supplier create invalidates the shared
  list instead of patching local state.
- SupplierForm, ArticleForm (posting accounts), QuickReviewDialog,
  InvoiceMatchDialog, supplier-invoices/[id] (payment dialog chart):
  useAccounts; ArticleForm's inline account create invalidates the chart.
- BulkBookDialog, MatchVoucherDialog, DuplicateBookingDialog: cash
  accounts from useCashAccounts (resolveAccount over the cached list; an
  empty list still resolves to 1930 with the fallback note).
- QuickReviewDialog, BulkBookDialog, NewEmployeeDialog, customers list
  (default payment terms), salary run page (payment format, bank, IBAN,
  dimensions): derived from useCompanySettings; the salary page's
  post-settings-modal refetch becomes a cache invalidation.

raw-reference-fetch ratchet: 45 -> 35 files.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-26 14:50:26 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5
parent 40e773548c
commit 4560ccbfc9
16 changed files with 228 additions and 332 deletions
+7 -35
View File
@@ -1,6 +1,7 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { useAccounts, useCompanySettings } from '@/lib/reference-data/hooks'
import { useRouter } from 'next/navigation'
import { useTranslations } from 'next-intl'
import { Badge } from '@/components/ui/badge'
@@ -31,7 +32,7 @@ import VatTreatmentSelect from './VatTreatmentSelect'
import AiCategorizeProposal, { type AiProposalMeta } from './AiCategorizeProposal'
import { VAT_TREATMENT_OPTIONS } from './transaction-types'
import type { TransactionWithInvoice } from './transaction-types'
import type { TransactionCategory, VatTreatment, BASAccount, EntityType, LinePatternEntry } from '@/types'
import type { TransactionCategory, VatTreatment, EntityType, LinePatternEntry } from '@/types'
import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message'
interface QuickReviewDialogProps {
@@ -101,7 +102,10 @@ export default function QuickReviewDialog({
// never throws.
const [accountOverride, setAccountOverride] = useState(defaultAccount ?? '')
const [vatTreatment, setVatTreatment] = useState<VatTreatment | 'none'>(defaultVat)
const [accounts, setAccounts] = useState<BASAccount[]>([])
// Session-cached (lib/reference-data): the kontoväljare is populated on
// the first open of every row instead of after a request per open.
const { accounts } = useAccounts()
const { settings: companySettings } = useCompanySettings()
// The AI proposal shown this session, kept so we can log a calibration sample
// (proposed vs actually booked) once the user confirms.
const [aiProposal, setAiProposal] = useState<AiProposalMeta | null>(null)
@@ -126,7 +130,7 @@ export default function QuickReviewDialog({
// company_settings.dimensions_enabled, same gate as BulkBookDialog. Seeded
// from the counterparty template's learned bag so the user sees what the
// booking will carry and can change it.
const [dimensionsEnabled, setDimensionsEnabled] = useState(false)
const dimensionsEnabled = companySettings?.dimensions_enabled === true
const [dims, setDims] = useState<Record<string, string>>(
() => ({ ...(counterpartyDefaultDimensions ?? {}) }),
)
@@ -141,22 +145,6 @@ export default function QuickReviewDialog({
}
}, [])
// Fetch accounts on mount
useEffect(() => {
async function fetchAccounts() {
try {
const res = await fetch('/api/bookkeeping/accounts')
const { data } = await res.json()
if (data) {
setAccounts(data)
}
} catch {
// Non-critical
}
}
fetchAccounts()
}, [])
// Reset local mirror whenever the underlying transaction changes (the parent
// reuses the dialog instance across rows).
useEffect(() => {
@@ -171,22 +159,6 @@ export default function QuickReviewDialog({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [transaction])
// Company settings gate the dimension affordance (dimensions_enabled).
// Fetched once per open; on failure the picker simply stays hidden.
useEffect(() => {
if (!open) return
let cancelled = false
fetch('/api/settings')
.then((r) => r.json())
.then(({ data }) => {
if (!cancelled) setDimensionsEnabled(data?.dimensions_enabled === true)
})
.catch(() => {
if (!cancelled) setDimensionsEnabled(false)
})
return () => { cancelled = true }
}, [open])
// Backfill the SEK conversion on demand. resolveSekAmount silently falls
// back to the raw foreign amount when amount_sek/exchange_rate are null,
// which means the user would see misleading "kr" values in the verifikation