diff --git a/DECISIONS.md b/DECISIONS.md index 03dc58ca..e6db45b9 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -984,5 +984,6 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-08-13] Radius ladder locked (convention 16): 4 tiers by role (pill toolbar controls / rounded-xl overlays / rounded-lg surfaces / rounded-sm leaves); rounded-md, bare rounded, rounded-2xl and rounded-[Npx] retired app-wide, hard-failed by check:guards off-ladder-radius. Before: 7 radii in circulation with no rule; one toolbar row on /transactions mixed 4 shape languages. [2026-08-13] Toolbar shape language: pills won over "inputs stay rectangles". Search in a page toolbar is a pill (ToolbarSearch, h-8) matching chips/pickers/buttons beside it; the same search inside a dialog or form keeps rounded-lg Input. Rationale: convention 8 already made pickers pill chips and convention 3 made buttons pills, so the rectangle search/segmented were the odd ones out; one row = one shape reads as trust. [2026-08-13] Dialogs promoted 8px -> 12px (rounded-xl): SettingsModal, slide-over and CommandPalette were already 12px, so dialog.tsx was the overlay-tier outlier, not the rule. +[2026-08-14] The onboarding branch question ("Var fanns bokföringen innan?") became its own journey step at the existing KLART station (done -> source, same station grammar as momsyn/moms under MOMSEN) instead of a sixth rail station: a 6-point rail crowds the 680px band's 150px label slots, "BOKFÖRINGEN INNAN" would sit next to the existing "BOKFÖRINGEN" station, and mode='add' (which never sees the branch question) would need an asymmetric rail. The done screen ends in a revealed Fortsätt that dispatches DONE_CONTINUE (mode='first' only, reducer-guarded). [2026-08-14] Migration /preview fetches ALL allowed fiscal years (dropped latestOnly): the connect step's "Hittade X konton och Y verifikationer" renders from /preview's sieStats, not /sie-data's generateImportPreview, so fixing only /sie-data would have left the founder-reported "0 verifikationer" (actual: 4153) on screen. Costs one SIE export per extra year at connect time, the same work /sie-data repeats right after; honest counts won over latency. [2026-08-14] /sie-data validation stays newest-file-only (not per-file, not on the merged parse): preserves exactly which datasets are accepted today, and validateSIEFile assumes single-file invariants (balance yearIndexes relative to ONE current year) that mergeParsedSIEFiles deliberately does not preserve. Older files' problems still surface per-file at import time. diff --git a/components/onboarding/journey/OnboardingJourney.tsx b/components/onboarding/journey/OnboardingJourney.tsx index 976959e5..ea88094f 100644 --- a/components/onboarding/journey/OnboardingJourney.tsx +++ b/components/onboarding/journey/OnboardingJourney.tsx @@ -180,12 +180,36 @@ export default function OnboardingJourney({ if (initialOrgNumber) submitOrg(initialOrgNumber) }, [initialOrgNumber, submitOrg]) + // One choice only: rapid clicks on different chips must not race two + // PATCHes (last-write-wins could persist the wrong path after navigation). + const onBranch = useCallback( + (choice: BranchChoice) => { + if (branchChosenRef.current) return + branchChosenRef.current = true + const dest = branchDestination(choice) + if (dest.path) { + // Fire-and-forget: the checklist path is a nicety, routing is + // the point. A lost PATCH just leaves the Hem checklist + // unpathed; it must never block or delay the navigation. + fetch('/api/onboarding/state', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path: dest.path }), + keepalive: true, + }).catch(() => logError('branch path persist failed', { choice })) + } + captureBranch(choice) + router.push(dest.href) + }, + [router], + ) + // A short thinking beat between questions. const prevStep = useRef(state.step) useEffect(() => { if (prevStep.current === state.step) return prevStep.current = state.step - if (state.step === 'done' || state.submitting) return + if (state.step === 'done' || state.step === 'source' || state.submitting) return setThinking(true) const timer = window.setTimeout(() => setThinking(false), 420) return () => window.clearTimeout(timer) @@ -257,7 +281,7 @@ export default function OnboardingJourney({ /* ── derived display ──────────────────────────────────────────── */ - const orbState: OrbState = state.step === 'done' + const orbState: OrbState = state.step === 'done' || state.step === 'source' ? 'check' : state.submitting ? narration === null @@ -662,29 +686,12 @@ export default function OnboardingJourney({ momsAnswer={momsAnswer} methodAnswer={methodAnswer} onOpen={() => router.push('/')} - onBranch={(choice) => { - // One choice only: rapid clicks on different chips must not - // race two PATCHes (last-write-wins could persist the wrong - // path after navigation). - if (branchChosenRef.current) return - branchChosenRef.current = true - const dest = branchDestination(choice) - if (dest.path) { - // Fire-and-forget: the checklist path is a nicety, routing is - // the point. A lost PATCH just leaves the Hem checklist - // unpathed; it must never block or delay the navigation. - fetch('/api/onboarding/state', { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ path: dest.path }), - keepalive: true, - }).catch(() => logError('branch path persist failed', { choice })) - } - captureBranch(choice) - router.push(dest.href) - }} + onContinue={() => dispatch({ type: 'DONE_CONTINUE' })} /> ) + + case 'source': + return } } @@ -762,7 +769,10 @@ export default function OnboardingJourney({