perf(bookkeeping): fiscal-year pickers and cash accounts read the session cache (#1934)

First consumer migration onto lib/reference-data. FyPicker and
FiscalYearSelector (14 consumer surfaces, 47 fiscal-period fetch sites
before this series) now read useFiscalPeriods(); with the layout seed the
restore of the persisted scope runs in the first effect tick and onReady
fires on mount instead of after a round trip. Their restore rules are
extracted into a pure resolveInitialFiscalScope() (lib/reference-data/
fiscal-scope.ts) so the two pickers cannot drift apart again, and the
restore runs once per company load, not on every background revalidation.

- /reports: the static catalog renders immediately; only the "no fiscal
  year" empty state waits for the picker (previously six skeleton bars
  until /api/bookkeeping/fiscal-periods resolved).
- JournalEntryList (/bookkeeping): resolves its initial scope from the
  cached list instead of its own fetch; the saved-scope shortcut still
  unblocks the entries fetch first when nothing is cached, and resolution
  is guarded to once per company so a revalidation can never snap a
  deep-link "all years" visit back to the stored year.
- /transactions: the account chooser reads useCashAccounts({ enabledOnly })
  (seeded) instead of fetching /api/cash-accounts on every visit; the bank
  sync button invalidates that entry after a sync.
- STORAGE_KEY_PREFIX / ALL_YEARS_VALUE move to a dependency-free
  fiscal-year-storage.ts (re-exported from FiscalYearSelector) so lib/ code
  can import them without a React component.

raw-reference-fetch ratchet: 55 -> 51 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:24:24 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5
parent b436d47b64
commit 9a56b7aff9
10 changed files with 282 additions and 195 deletions
+6 -10
View File
@@ -3,7 +3,6 @@
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { useTranslations } from 'next-intl'
import { Skeleton } from '@/components/ui/skeleton'
import { PageHeader } from '@/components/ui/page-header'
import { HelpPopover } from '@/components/ui/help-popover'
import { EmptyState } from '@/components/ui/empty-state'
@@ -25,7 +24,10 @@ import { getReport } from '@/lib/reports/catalog'
export default function ReportsPage() {
const router = useRouter()
const [selectedPeriod, setSelectedPeriod] = useState('')
const [isLoadingInit, setIsLoadingInit] = useState(true)
// The catalog itself is static; only the "no fiscal year" empty state has
// to wait for the picker to finish restoring its scope (one effect tick
// when the periods are seeded, see FyPicker).
const [fyReady, setFyReady] = useState(false)
const { company } = useCompany()
const { settings } = useCompanySettings()
const t = useTranslations('reports')
@@ -62,18 +64,12 @@ export default function ReportsPage() {
onChange={(id) => setSelectedPeriod(id || '')}
includeAllOption={false}
hideFuturePeriods
onReady={() => setIsLoadingInit(false)}
onReady={() => setFyReady(true)}
/>
}
/>
{isLoadingInit ? (
<div className="space-y-3">
{[1, 2, 3, 4, 5, 6].map((i) => (
<Skeleton key={i} className="h-9 w-full" />
))}
</div>
) : !selectedPeriod ? (
{fyReady && !selectedPeriod ? (
<EmptyState
title="Inget räkenskapsår valt"
description="Skapa ett räkenskapsår för att kunna se rapporter."
+6 -20
View File
@@ -68,12 +68,13 @@ import {
MATCHABLE_SUPPLIER_INVOICE_STATUSES,
} from '@/lib/invoices/matchable-statuses'
import { useCompany } from '@/contexts/CompanyContext'
import { useCashAccounts } from '@/lib/reference-data/hooks'
import { fetchAllRows } from '@/lib/supabase/fetch-all'
import { useRealtimeSupabase } from '@/lib/hooks/use-realtime-supabase'
import { getErrorMessage } from '@/lib/errors/get-error-message'
import { cn, formatCurrency, formatDate } from '@/lib/utils'
import { roundOre } from '@/lib/money'
import type { TransactionCategory, CreateTransactionInput, Invoice, Customer, SupplierInvoice, Supplier, VatTreatment, EntityType, LinePatternEntry, BookingTemplateLibrary, CashAccount } from '@/types'
import type { TransactionCategory, CreateTransactionInput, Invoice, Customer, SupplierInvoice, Supplier, VatTreatment, EntityType, LinePatternEntry, BookingTemplateLibrary } from '@/types'
import type { SuggestedTemplate } from '@/lib/transactions/category-suggestions'
import { isImportedTransaction } from '@/lib/transactions/origin'
import { computeJeUnderlagStatus, type JeUnderlagStatus } from '@/lib/transactions/underlag-status'
@@ -534,7 +535,10 @@ export default function TransactionsPage() {
}, [companyId])
// Registered cash accounts (cash_accounts): the account chooser's rows,
// with PSD2 balances when the bank reports them.
const [cashAccounts, setCashAccounts] = useState<CashAccount[]>([])
// Registered, enabled cash accounts for the account chooser: session-cached
// and seeded by the dashboard layout (lib/reference-data), so the chooser
// renders populated on the first paint. Bank sync invalidates the entry.
const { cashAccounts } = useCashAccounts({ enabledOnly: true })
const { toast } = useToast()
const { dialogProps: confirmDialogProps, confirm } = useDestructiveConfirm()
@@ -882,24 +886,6 @@ export default function TransactionsPage() {
const PAGE_SIZE = 200
// Account chooser rows: the registered, enabled cash accounts. One fetch
// per company; balances refresh with the page (bank sync triggers a
// router.refresh via the sync toast flow).
useEffect(() => {
if (!companyId) return
let cancelled = false
fetch('/api/cash-accounts?enabled_only=true')
.then((res) => (res.ok ? res.json() : { data: [] }))
.then((json: { data?: CashAccount[] }) => {
if (!cancelled) setCashAccounts(json.data ?? [])
})
.catch(() => {
if (!cancelled) setCashAccounts([])
})
return () => {
cancelled = true
}
}, [companyId])
// Same sequence-guard pattern as fetchGenerationRef: only the newest
// skattekonto fetch may write rows. A company switch bumps the sequence so a