18c20e68e6
* fix(scoping): skatteverket per company + true pristine gates + scoped dismissals Skatteverket connections become per (user, company): the token table carried BOTH UNIQUE(user_id) and UNIQUE(company_id) (two stacked half migrations), so one connection leaked "connected" onto every company the user belongs to, sync ran the token against the wrong orgnr (behorighet 403), and reconnecting from another company silently moved the row and went dark on the first company's crons. Token reads/writes are now scoped by company through the whole chain (token-store, api-client refresh coalescing, skvRequest and its 21 call sites, resolve-auth, crons, MCP), /skattekonto/saldo answers 401 NOT_CONNECTED for companies without their own row (which is what the page's startkort keys on), and the dashboard connect-nudge counts only the active company's row. Bookkeeping's pristine start card now keys on all-years emptiness via a count probe instead of "no active filters": the default fiscal-year selection counted as a filter, which made the card unreachable on brand-new companies (it showed "inga traffar" instead). Two browser-global localStorage keys become company-scoped with legacy fallbacks: the inbox onboarding dismissal (dismissing on one company hid the card everywhere) and the periodisering auto-detect toggle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(scoping): dedupe cron work per (user, company) + guard the ledger probe CodeRabbit findings on #1610: the skattekonto sync cron still deduped token rows by user_id alone, which would drop every company but one for multi-company operators (the exact scenario the PR fixes); and the all-years ledger probe could leave a stale false behind on a failed refetch, letting the pristine card render unconfirmed. The probe now resets to unknown in flight and carries the fetch generation guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
118 lines
4.3 KiB
TypeScript
118 lines
4.3 KiB
TypeScript
'use client'
|
|
|
|
import { useCallback, useMemo, useSyncExternalStore } from 'react'
|
|
import Link from 'next/link'
|
|
import { ExternalLink } from 'lucide-react'
|
|
import { Switch } from '@/components/ui/switch'
|
|
import { useCompanyOptional } from '@/contexts/CompanyContext'
|
|
import {
|
|
SettingsRow,
|
|
SettingsRowEnd,
|
|
} from '@/components/settings/SettingsRows'
|
|
|
|
/**
|
|
* Per-company toggle for the periodisering wizard's auto-detection step.
|
|
*
|
|
* Backed by localStorage (key: `periodisering_autodetect_enabled:<companyId>`,
|
|
* with the old unscoped key as a read fallback so existing choices survive:
|
|
* the unscoped key silently applied one company's choice to every company in
|
|
* the browser) because
|
|
* the company_settings table does not yet have a dedicated column for this
|
|
* preference, and the task description explicitly allows the persistence to
|
|
* be UI-local. A future migration can promote this to a real
|
|
* `company_settings.periodisering_autodetect_enabled boolean` column and
|
|
* the wizard's auto-detect step will read either source.
|
|
*
|
|
* Default: enabled. The wizard's auto-detect step renders regardless: the
|
|
* toggle merely controls whether the GET response includes `autoDetected`
|
|
* on subsequent fetches. (Today the API always returns it; the wizard step
|
|
* can early-out based on this setting locally.)
|
|
*/
|
|
const STORAGE_KEY = 'periodisering_autodetect_enabled'
|
|
|
|
function storageKeyFor(companyId: string | null): string {
|
|
return companyId ? `${STORAGE_KEY}:${companyId}` : STORAGE_KEY
|
|
}
|
|
|
|
function readStored(companyId: string | null): boolean {
|
|
if (typeof window === 'undefined') return true
|
|
try {
|
|
const stored =
|
|
window.localStorage.getItem(storageKeyFor(companyId)) ??
|
|
window.localStorage.getItem(STORAGE_KEY)
|
|
return stored === null ? true : stored !== 'false'
|
|
} catch {
|
|
return true
|
|
}
|
|
}
|
|
|
|
/** Subscribe to localStorage changes from OTHER tabs. Same-tab updates are
|
|
* picked up via the explicit re-render after `setItem`: see
|
|
* `notifyChange` below. */
|
|
function subscribe(callback: () => void): () => void {
|
|
if (typeof window === 'undefined') return () => {}
|
|
const handler = (e: StorageEvent) => {
|
|
if (e.key === null || e.key === STORAGE_KEY || e.key.startsWith(`${STORAGE_KEY}:`)) callback()
|
|
}
|
|
const customHandler = () => callback()
|
|
window.addEventListener('storage', handler)
|
|
window.addEventListener('gnubok-periodisering-toggle', customHandler)
|
|
return () => {
|
|
window.removeEventListener('storage', handler)
|
|
window.removeEventListener('gnubok-periodisering-toggle', customHandler)
|
|
}
|
|
}
|
|
|
|
/** Fire a same-tab notification so useSyncExternalStore re-subscribers
|
|
* see the change without a manual setState. */
|
|
function notifyChange() {
|
|
if (typeof window === 'undefined') return
|
|
window.dispatchEvent(new Event('gnubok-periodisering-toggle'))
|
|
}
|
|
|
|
export function PeriodiseringAutoDetectToggle() {
|
|
const companyId = useCompanyOptional()?.company?.id ?? null
|
|
const getSnapshot = useMemo(() => () => readStored(companyId), [companyId])
|
|
const enabled = useSyncExternalStore(
|
|
subscribe,
|
|
getSnapshot,
|
|
// Server snapshot: default to enabled. Matches the client default so
|
|
// hydration is identical.
|
|
() => true,
|
|
)
|
|
|
|
const handleChange = useCallback((value: boolean) => {
|
|
try {
|
|
window.localStorage.setItem(storageKeyFor(companyId), String(value))
|
|
} catch {
|
|
// No-op; if storage is blocked the toggle simply won't persist.
|
|
}
|
|
notifyChange()
|
|
}, [companyId])
|
|
|
|
return (
|
|
<SettingsRow
|
|
label="Periodisering"
|
|
help="Skannar fakturor i bokslutet efter datumintervall som sträcker sig in i nästa räkenskapsår och föreslår periodiseringar i bokslut-wizarden."
|
|
>
|
|
<Switch
|
|
id="periodisering-autodetect"
|
|
checked={enabled}
|
|
onCheckedChange={handleChange}
|
|
/>
|
|
<label htmlFor="periodisering-autodetect" className="cursor-pointer text-sm">
|
|
Aktivera automatisk periodiseringsdetektering
|
|
</label>
|
|
<SettingsRowEnd>
|
|
<Link
|
|
href="/bookkeeping/year-end/periodisering"
|
|
className="inline-flex items-center gap-1.5 text-xs text-muted-foreground transition-colors hover:text-foreground"
|
|
>
|
|
<ExternalLink className="h-3.5 w-3.5" />
|
|
Öppna periodiserings-wizarden
|
|
</Link>
|
|
</SettingsRowEnd>
|
|
</SettingsRow>
|
|
)
|
|
}
|