perf(reference-data): sweep the remaining raw reads onto the session cache, ratchet to 0 (#1941)

Final consumer migration of the responsiveness plan: the 35 files still
fetching fiscal periods, settings, accounts, cash accounts, dimensions or
templates on their own now read lib/reference-data, and every client
write site invalidates the shared cache instead of refetching locally.

Settings and registries: FiscalYearsManager, FiscalPeriodEditor (period
snapshotted once per company so a revalidation cannot reset dates being
edited), BookingTemplatesPanel, ChartOfAccounts, ChartOfAccountsManager,
EditAccountDialog, CorrectionEntryDialog, StrikeLinesDialog,
InvoicePaymentAccountsSettings; the dimensions registry (DimensionsManager,
DimensionCombobox, LineDimensionFields, DimensionFilter, bookkeeping/[id])
reads useDimensions and the ad-hoc fetchDimensions/fetchDimensionsCached
helpers are deleted.

Pages and pickers: CashAccountSelector (FyPicker-shaped restore, once per
company load), use-account-names, FiscalYearGapNotice,
OpeningBalancePeriodStep, BankFileConfirmStep, ImportReviewStep, the import
page (invalidates accounts + periods after a SIE execute), customers list,
invoices list + detail, pending, salary employee, asset dispose, year-end
and periodisering pages (invalidate periods after closing), reports
DimensionPnlView (its pivot picker read the wrong payload key and was
always empty; it now populates), SkatteverketPanel, TemplatePicker,
ArticleForm (vat_registered).

Invoice dialogs and extensions: SendInvoiceDialog, PaymentBookingDialog
(init reduced to the credit-note lookup + catalogue, proposal and voucher
preview fire on open when cached; a local getSession replaces the network
getUser for the fallback CC), InvoiceInboxWorkspace, TicWorkspace,
ArcimMigrationWorkspace (invalidates after each SIE import step),
enable-banking AccountPickerDialog.

raw-reference-fetch ratchet: 35 -> 0 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:56:37 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5
parent 4560ccbfc9
commit 3ee3565d6d
41 changed files with 636 additions and 977 deletions
@@ -1,6 +1,7 @@
'use client'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useAccounts, useCompanySettings, useFiscalPeriods } from '@/lib/reference-data/hooks'
import Link from 'next/link'
import {
Dialog,
@@ -33,7 +34,6 @@ import {
resolveFiscalYearStart,
resolveGapFillStart,
} from '../lib/date-suggestions'
import type { CompanySettings } from '@/types'
import type { StoredAccount } from '../types'
import {
BankSyncProgressDialog,
@@ -97,12 +97,41 @@ export function AccountPickerDialog({
const [lastBookedDate, setLastBookedDate] = useState<string | null>(null)
// Earliest completed SIE import coverage start: present = migrator flow.
const [sieCoverageStart, setSieCoverageStart] = useState<string | null>(null)
const [chartAccounts, setChartAccounts] = useState<ChartAccount[]>([])
const [chartError, setChartError] = useState(false)
// Reference data from the session cache (lib/reference-data), seeded by
// the dashboard layout: settings, the period containing today and the
// chart are known when the dialog opens, no requests of its own. A failed
// load keeps settingsLoaded false so the calendar-year fallback is never
// presented as the authoritative fiscal-year start (issue #917).
const {
settings: companySettings,
isLoading: settingsLoading,
error: settingsError,
} = useCompanySettings()
const { periods, isLoading: periodsLoading, error: periodsError } = useFiscalPeriods()
// Inactive accounts included: the old chart query did not filter on is_active.
const { accounts: allAccounts, error: chartLoadError } = useAccounts(false)
const settingsLoaded = !settingsLoading && !periodsLoading && !settingsError && !periodsError
const currentPeriodStart = useMemo(() => {
const today = new Date().toISOString().split('T')[0]
const containing = periods
.filter((p) => p.period_start <= today && today <= p.period_end)
.sort((a, b) => b.period_start.localeCompare(a.period_start))
return containing[0]?.period_start || null
}, [periods])
// 19xx accounts for the per-account ledger combobox. Class 19 = bank/cash
// on the BAS chart.
const chartAccounts = useMemo<ChartAccount[]>(
() =>
allAccounts
.filter((a) => a.account_number.startsWith('19'))
.sort((a, b) => a.account_number.localeCompare(b.account_number))
.map((a) => ({ account_number: a.account_number, account_name: a.account_name })),
[allAccounts],
)
// Surface the failure: without the 19xx chart the ledger picker is
// silently empty, which reads as "no bank accounts exist".
const chartError = Boolean(chartLoadError)
const [ledgerByUid, setLedgerByUid] = useState<Record<string, string>>({})
const [companySettings, setCompanySettings] = useState<Pick<CompanySettings, 'fiscal_year_start_month' | 'entity_type'> | null>(null)
const [currentPeriodStart, setCurrentPeriodStart] = useState<string | null>(null)
const [settingsLoaded, setSettingsLoaded] = useState(false)
const [lookbackMode, setLookbackMode] = useState<LookbackMode>('fiscal-year')
const [customSubMode, setCustomSubMode] = useState<CustomSubMode>('date')
@@ -127,9 +156,6 @@ export function AccountPickerDialog({
useEffect(() => {
if (open) {
// Re-arm the "settings loaded" gate each open so the fiscal-year label
// doesn't flash last-open's resolved date before this open's fetch lands.
setSettingsLoaded(false)
const initial = new Set<string>(
accounts.filter(a => a.enabled !== false).map(a => a.uid)
)
@@ -159,45 +185,11 @@ export function AccountPickerDialog({
}
}, [open, accounts])
// Load fiscal_year_start_month + entity_type so "Sedan räkenskapsårets början"
// resolves to the right date for non-calendar fiscal years, plus the actual
// fiscal_periods row containing today: the recurring setting cannot represent
// an extended or shortened first year, so the period row wins when it exists.
useEffect(() => {
if (!open || !company?.id) return
let cancelled = false
;(async () => {
const today = new Date().toISOString().split('T')[0]
const [settingsRes, periodRes] = await Promise.all([
supabase
.from('company_settings')
.select('fiscal_year_start_month, entity_type')
.eq('company_id', company.id)
.maybeSingle(),
supabase
.from('fiscal_periods')
.select('period_start')
.eq('company_id', company.id)
.lte('period_start', today)
.gte('period_end', today)
.order('period_start', { ascending: false })
.limit(1)
.maybeSingle(),
])
if (cancelled) return
if (settingsRes.error || periodRes.error) {
// A failed fetch must not present the calendar-year fallback as the
// authoritative fiscal-year start (issue #917). Leave settingsLoaded
// false so the date stays masked; if the user proceeds anyway the
// request falls back to the recurring-setting derivation.
return
}
setCompanySettings((settingsRes.data as { fiscal_year_start_month?: number; entity_type?: CompanySettings['entity_type'] } | null) as Pick<CompanySettings, 'fiscal_year_start_month' | 'entity_type'> | null)
setCurrentPeriodStart((periodRes.data as { period_start?: string } | null)?.period_start || null)
setSettingsLoaded(true)
})()
return () => { cancelled = true }
}, [open, company?.id, supabase])
// fiscal_year_start_month + entity_type make "Sedan räkenskapsårets början"
// resolve to the right date for non-calendar fiscal years, and the actual
// fiscal_periods row containing today wins when it exists: the recurring
// setting cannot represent an extended or shortened first year. Both are
// derived above from the session cache.
// Fetch the latest posted verifikat date so we can offer "day after the last
// booked entry" as a one-click escape from the default fiscal-year start.
@@ -303,31 +295,6 @@ export function AccountPickerDialog({
return () => { cancelled = true }
}, [open, isInitialSelection, company?.id, connectionId, supabase, accounts])
// Load 19xx accounts from the chart for the per-account ledger combobox.
// Class 19 = bank/cash on the BAS chart.
useEffect(() => {
if (!open || !company?.id) return
let cancelled = false
;(async () => {
const { data, error } = await supabase
.from('chart_of_accounts')
.select('account_number, account_name')
.eq('company_id', company.id)
.like('account_number', '19%')
.order('account_number', { ascending: true })
if (cancelled) return
if (error) {
// Surface the failure: without the 19xx chart the ledger picker is
// silently empty, which reads as "no bank accounts exist".
setChartError(true)
return
}
setChartError(false)
setChartAccounts((data as ChartAccount[] | null) || [])
})()
return () => { cancelled = true }
}, [open, company?.id, supabase])
const allSelected = accounts.length > 0 && selected.size === accounts.length
const noneSelected = selected.size === 0