feat(import): the import theater during SIE execute (#1471)

* feat(onboarding): branch question on the journey done screen

Second slice of the approved activation concept: the moment the company
exists, the done screen asks "Var fanns bokföringen innan?" with
provider chips (real logos), SIE file, and new-business options, plus a
quiet look-around escape. Choices persist initial_setup_path
(fire-and-forget) and deep-link into the existing flows: providers jump
straight to the migration wizard's connect step (sieViaApi providers
only; Visma/Bokio land on the provider list where the SIE-first gate
lives), the SIE chip opens the upload step, new business lands on Hem
with step one checked off.

mode='add' keeps the plain "Öppna Accounted" button: the concept's own
guard, and it avoids writing the path onto the previous company if
setActiveCompany silently failed. Routing lives in a pure helper with
tests; anonymous onboarding_branch_chosen funnel event follows the
guarded capture pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(onboarding): review triage: single-choice latch, preselect reset

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: restore package-lock.json to main (worktree npm install mutated it)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(import): the import theater: a knowledge graph draws itself during SIE execute

Third slice of the activation concept. While the SIE import commits
server-side (one opaque call, up to ~5 min), the client parses the same
file locally (the parser is browser-clean) and a canvas constellation
builds itself: company hub, fiscal years as tree rings, account-class
anchors, top accounts and recognized counterparties, with paced
narration lines alongside. The final line holds with the elapsed counter
until the server answers, so the theater never outruns the truth.

- lib/import/theater-model.ts: pure aggregation of ParsedSIEFile into a
  capped display model (14 accounts, 12 counterparties, >=2 sightings,
  internal accounting texts skipped, counterparty attached to its
  counter account rather than the bank leg). Tested with fixture-string
  SIE per the sie-parser test pattern.
- components/import/ImportTheater.tsx: ink-on-paper canvas + narration,
  tokens read per frame (theme/palette reactive, JourneyOrb idiom),
  reduced motion renders the settled graph and all lines instantly.
- Wizard: client parse kicks off at execute start via dynamic import,
  capped at 8 MB; any failure silently leaves the existing spinner
  takeover, which also remains for oversized files.

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>
This commit is contained in:
Jakob Wennberg
2026-08-09 19:58:33 +02:00
committed by GitHub
co-authored by Claude Fable 5 Jakob Wennberg
parent 622b144a3d
commit d4ef4f8bc4
7 changed files with 788 additions and 15 deletions
+30 -13
View File
@@ -27,7 +27,9 @@ import { useUnsavedChanges } from '@/lib/hooks/use-unsaved-changes'
import { useCanWrite } from '@/lib/hooks/use-can-write'
import { createClient } from '@/lib/supabase/client'
import { useCompany } from '@/contexts/CompanyContext'
import ImportTheater from '@/components/import/ImportTheater'
import type { ImportPreview, AccountMapping } from '@/lib/import/types'
import type { TheaterModel } from '@/lib/import/theater-model'
const SERIES_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
@@ -37,6 +39,9 @@ interface ImportReviewStepProps {
onExecute: (options: ImportExecuteOptions) => Promise<void>
onBack: () => void
isLoading: boolean
/** Client-parsed graph model for the import theater; null falls back to
* the plain spinner takeover (parse failed, oversized file, or pending). */
theaterModel?: TheaterModel | null
}
export interface ImportExecuteOptions {
@@ -54,6 +59,7 @@ export default function ImportReviewStep({
onExecute,
onBack,
isLoading,
theaterModel = null,
}: ImportReviewStepProps) {
const { canWrite } = useCanWrite()
const { company } = useCompany()
@@ -168,27 +174,38 @@ export default function ImportReviewStep({
m.sourceName.trim() !== m.targetName?.trim()
).length
// Full-screen loading takeover during import execution
// Full-screen loading takeover during import execution. With a client-parsed
// model the theater plays (the graph draws itself while the server writes);
// without one, the plain spinner takeover remains the fallback.
if (isLoading) {
return (
<div className="space-y-6">
<Card>
<CardContent className="pt-8 pb-8">
<div className="flex flex-col items-center text-center space-y-6">
<Loader2 className="h-10 w-10 text-primary animate-spin" />
<div className="space-y-1">
<p className="font-medium text-lg">Importerar bokföring...</p>
<p className="text-sm text-muted-foreground">
{preview.voucherCount} verifikationer bearbetas
{theaterModel ? (
<div className="space-y-6">
<ImportTheater model={theaterModel} preview={preview} elapsed={elapsed} />
<p className="text-center text-sm text-muted-foreground">
Stäng inte sidan. Importen kan ta upp till några minuter beroende på antalet verifikationer.
</p>
</div>
<div className="text-2xl font-display tabular-nums text-muted-foreground">
{elapsed}s
) : (
<div className="flex flex-col items-center text-center space-y-6">
<Loader2 className="h-10 w-10 text-primary animate-spin" />
<div className="space-y-1">
<p className="font-medium text-lg">Importerar bokföring...</p>
<p className="text-sm text-muted-foreground">
{preview.voucherCount} verifikationer bearbetas
</p>
</div>
<div className="text-2xl font-display tabular-nums text-muted-foreground">
{elapsed}s
</div>
<p className="text-sm text-muted-foreground max-w-sm">
Stäng inte sidan. Importen kan ta upp till några minuter beroende på antalet verifikationer.
</p>
</div>
<p className="text-sm text-muted-foreground max-w-sm">
Stäng inte sidan. Importen kan ta upp till några minuter beroende på antalet verifikationer.
</p>
</div>
)}
</CardContent>
</Card>
</div>