diff --git a/components/extensions/general/ArcimMigrationWorkspace.tsx b/components/extensions/general/ArcimMigrationWorkspace.tsx index 31637fd7..cce5d617 100644 --- a/components/extensions/general/ArcimMigrationWorkspace.tsx +++ b/components/extensions/general/ArcimMigrationWorkspace.tsx @@ -2,17 +2,16 @@ import { useState, useCallback, useEffect, useReducer, useRef } from 'react' import { useTranslations } from 'next-intl' -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Progress } from '@/components/ui/progress' -import { Button, buttonVariants } from '@/components/ui/button' +import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Switch } from '@/components/ui/switch' import { useToast } from '@/components/ui/use-toast' import { cn } from '@/lib/utils' import { ConfirmationDialog } from '@/components/ui/confirmation-dialog' +import { AttnLine } from '@/components/ui/attn-line' import Link from 'next/link' -import { FallbackPrompt } from '@/components/ui/fallback-prompt' import { getBranding } from '@/lib/branding/service' import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message' @@ -20,25 +19,13 @@ const branding = getBranding() import { ArrowLeft, ArrowRight, - Loader2, - AlertCircle, - CheckCircle, - Building2, - Users, - Truck, - FileText, - Database, - ExternalLink, - Info, - RotateCcw, - RefreshCw, - AlertTriangle, - ChevronDown, + Check, ChevronRight, - Calendar, + ExternalLink, + Loader2, + RefreshCw, + RotateCcw, XCircle, - BookOpen, - Paperclip, } from 'lucide-react' import type { WorkspaceComponentProps } from '@/lib/extensions/workspace-registry' import { @@ -251,6 +238,7 @@ interface MigrationResults { } import AccountMappingStep from '@/components/import/AccountMappingStep' import ArcimMigrationTheater from '@/components/extensions/general/ArcimMigrationTheater' +import TheaterCanvas from '@/components/import/TheaterCanvas' import type { TheaterModel } from '@/lib/import/theater-model' import type { AccountMapping, ImportResult, ParsedSIEFile } from '@/lib/import/types' import type { BASAccount } from '@/types' @@ -271,11 +259,6 @@ const STEP_LABELS: Record = { result: 'Resultat', } -const MONTH_NAMES = [ - 'Januari', 'Februari', 'Mars', 'April', 'Maj', 'Juni', - 'Juli', 'Augusti', 'September', 'Oktober', 'November', 'December', -] - interface MigrationOptions { importCompanyInfo: boolean importSIEData: boolean @@ -353,6 +336,94 @@ interface SIEData { basAccounts: BASAccount[] } +// ── Shared step chrome ─────────────────────────────────────────── +// Living Paper: step content sits directly on the page. The serif headline +// is the step's one display element; sections are kickers over hairline +// rows; attention is one ochre sentence (AttnLine); the SIE escape hatch is +// a quiet underlined link, never a boxed prompt. + +function StepHeading({ title, lede }: { title: string; lede?: string }) { + return ( +
+

{title}

+ {lede &&

{lede}

} +
+ ) +} + +function SectionKicker({ children }: { children: React.ReactNode }) { + return ( +

+ {children} +

+ ) +} + +function SieFallbackLine({ message, label = 'Ladda upp SIE-fil' }: { message: string; label?: string }) { + return ( +

+ {message}{' '} + + {label} + +

+ ) +} + +function SpinnerLine({ children }: { children: React.ReactNode }) { + return ( +
+
+ ) +} + +/** + * The quiet step indicator that replaces the boxed progress card: the step + * labels as an uppercase tracking kicker row (done steps muted with a small + * check, the current step in foreground ink) over a hairline thread whose + * ink segment is the progress. No card, no fat bar. + */ +function StepRail({ steps, currentIndex }: { steps: WizardStep[]; currentIndex: number }) { + const progressPercent = ((currentIndex + 1) / steps.length) * 100 + return ( + + ) +} + // ── Provider selection step ────────────────────────────────────── interface ConnectionStatus { @@ -416,97 +487,66 @@ function ProviderStep({ const showSieRequiredBanner = !isLoadingStatus && !hasSieImport && !allSieViaApi return ( -
- {/* SIE-required banner (not relevant for Fortnox/Briox: they fetch SIE via API) */} +
+ 0 ? 'Anslut ytterligare system' : 'Välj ditt nuvarande bokföringssystem'} + lede="Vi hämtar bokföringsdata via SIE och kunder, leverantörer och fakturor via API:et." + /> + + {/* SIE-required attention (not relevant for Fortnox/Briox: they fetch + SIE via API): one ochre sentence with the action embedded, never a + banner. */} {showSieRequiredBanner && ( -
- -
-

SIE-import krävs först

-

- Bokio och Visma hämtar endast kunder, leverantörer och fakturor via API:et. Bokföringsdata (kontoplan, verifikationer och balanser) måste importeras via SIE-fil först. Gäller inte Fortnox, Briox, Björn Lundén och WINT: där hämtar vi bokföringen direkt via API:et. -

- - - Ladda upp SIE-fil - - -
-
+ + Bokio och Visma hämtar endast kunder, leverantörer och fakturor via API:et: importera + bokföringsdatan (kontoplan, verifikationer och balanser) via SIE-fil först. Gäller inte + Fortnox, Briox, Björn Lundén och WINT, där hämtas bokföringen direkt via API:et. + )} - {/* Existing connections */} + {/* Existing connections: quiet hairline rows, no cards. Being connected + is the normal state here, so it reads as muted text, not a chip. */} {activeConsents.length > 0 && ( - - - Aktiva anslutningar - - Du har redan anslutna leverantörer. Synka igen för att hämta ny data. - - - +
+ Aktiva anslutningar +
{activeConsents.map((consent) => { const providerInfo = ARCIM_PROVIDERS.find(p => p.id === consent.provider) const completedImports = connectionStatus?.sieImports.filter(i => i.status === 'completed') ?? [] const lastImport = completedImports[0] return ( -
-
- {providerInfo?.name -
-
-

{providerInfo?.name ?? consent.provider}

- - - Ansluten - -
-
- {consent.companyName && ( -

{consent.companyName}

- )} - {lastImport ? ( -

- Senaste import: {new Date(lastImport.imported_at ?? lastImport.created_at).toLocaleDateString('sv-SE')} - {lastImport.transactions_count != null && `, ${lastImport.transactions_count} verifikationer`} -

- ) : ( -

- Ansluten {consent.createdAt ? new Date(consent.createdAt).toLocaleDateString('sv-SE') : ''} -

- )} - {(connectionStatus?.entityCounts.customers ?? 0) > 0 && ( -

- {connectionStatus?.entityCounts.customers} kunder, {connectionStatus?.entityCounts.suppliers} leverantörer, {connectionStatus?.entityCounts.invoices} fakturor -

- )} -
-
- +
+ {providerInfo?.name +
+

{providerInfo?.name ?? consent.provider}

+

+ {consent.companyName && <>{consent.companyName} · } + {lastImport ? ( + <> + Senaste import {new Date(lastImport.imported_at ?? lastImport.created_at).toLocaleDateString('sv-SE')} + {lastImport.transactions_count != null && ( + , {lastImport.transactions_count} verifikationer + )} + + ) : ( + <>Ansluten {consent.createdAt ? new Date(consent.createdAt).toLocaleDateString('sv-SE') : ''} + )} +

+ {(connectionStatus?.entityCounts.customers ?? 0) > 0 && ( +

+ {connectionStatus?.entityCounts.customers} kunder, {connectionStatus?.entityCounts.suppliers} leverantörer, {connectionStatus?.entityCounts.invoices} fakturor +

+ )}
-
+
) })} - - +
+
)} - {/* Provider selection */} - - - {activeConsents.length > 0 ? 'Anslut ytterligare system' : 'Välj ditt nuvarande bokföringssystem'} - - Vi hämtar bokföringsdata via SIE och kunder, leverantörer och fakturor via API:et. - - - - {isLoadingStatus ? ( -
- -
- ) : ( -
- {ARCIM_PROVIDERS.map((provider) => { - const comingSoon = COMING_SOON_PROVIDERS.has(provider.id) - const alreadyConnected = activeConsents.some(c => c.provider === provider.id) - // Providers without SIE-over-API only expose entity data - // (customers, suppliers, invoices): the ledger must arrive via - // SIE upload first. Gate the connection entry until a completed - // SIE import exists so users don't authenticate into a flow that - // can't import anything yet. The /migrate route enforces this - // server-side regardless; this is just the matching UX. - const needsSieFirst = !hasSieImport && !provider.sieViaApi - const isDisabled = comingSoon || alreadyConnected || needsSieFirst - return ( - - ) - })} -
- )} -
-
+ {/* Provider selection: quiet list rows on the page, hairline-divided. */} + {isLoadingStatus ? ( +
+ +
+ ) : ( +
+ {ARCIM_PROVIDERS.map((provider) => { + const comingSoon = COMING_SOON_PROVIDERS.has(provider.id) + const alreadyConnected = activeConsents.some(c => c.provider === provider.id) + // Providers without SIE-over-API only expose entity data + // (customers, suppliers, invoices): the ledger must arrive via + // SIE upload first. Gate the connection entry until a completed + // SIE import exists so users don't authenticate into a flow that + // can't import anything yet. The /migrate route enforces this + // server-side regardless; this is just the matching UX. + const needsSieFirst = !hasSieImport && !provider.sieViaApi + const isDisabled = comingSoon || alreadyConnected || needsSieFirst + return ( + + ) + })} +
+ )}
) } @@ -671,152 +710,134 @@ function ConnectStep({ : !!(apiToken && (!needsCompanyId || companyId)) return ( -
- - - Anslut till {providerName} - - {authType === 'token' - ? tokenDescription - : `Logga in i ${providerName} för att ge ${branding.appName.toLowerCase()} tillgång att läsa din bokföringsdata.` - } - - - - {isLoading && ( -
- -

Förbereder anslutning...

-
- )} +
+ - {error && ( - <> -
- -
-

Anslutning misslyckades

-

{error}

- {provider === 'fortnox' && ( -

- Obs: Fortnox kräver ett aktivt integrationstillägg (tillkostnadsbelagd tilläggstjänst) för att kunna använda integrationer. Kontrollera att detta är aktiverat i ditt Fortnox-konto. -

- )} -
-
- - - )} + {isLoading && Förbereder anslutning...} - {/* OAuth flow */} - {authType === 'oauth' && authUrl && !isLoading && ( -
+ {error && ( +
+
+

Anslutning misslyckades

+

{error}

+ {provider === 'fortnox' && (

- Klicka nedan för att logga in i {providerName}. - Fönstret stängs automatiskt när du är klar. + Obs: Fortnox kräver ett aktivt integrationstillägg (tillkostnadsbelagd tilläggstjänst) för att kunna använda integrationer. Kontrollera att detta är aktiverat i ditt Fortnox-konto.

-
+ +
+ )} + + {/* OAuth flow */} + {authType === 'oauth' && authUrl && !isLoading && ( +
+

+ Klicka nedan för att logga in i {providerName}. + Fönstret stängs automatiskt när du är klar. +

+ +
+ )} + + {/* Token-based flow */} + {authType === 'token' && consentId && !isLoading && ( +
+

+ {tokenHelpText} +

+ {/* WINT is a login form: e-mail reads above password (CSS order; + the button keeps its place). Other token providers keep + token-first order. */} +
+ {needsApiToken && ( +
+ + - Logga in i {providerName} - - -
- )} - - {/* Token-based flow */} - {authType === 'token' && consentId && !isLoading && ( -
-

- {tokenHelpText} -

- {/* WINT is a login form: e-mail reads above password (CSS order; - the button keeps its place). Other token providers keep - token-first order. */} -
- {needsApiToken && ( -
- - setApiToken(e.target.value)} - /> -
- )} - {needsCompanyId && ( -
- - setCompanyId(e.target.value)} - /> -
- )} - + value={apiToken} + onChange={(e) => setApiToken(e.target.value)} + />
-
- )} - - + )} + {needsCompanyId && ( +
+ + setCompanyId(e.target.value)} + /> +
+ )} + +
+
+ )} -
+
- )} -
-
- {/* License-missing keeps the SIE fallback visible: re-auth loops - until the customer re-orders the Fortnox Integration license, - so a manual SIE import is the reliable escape hatch. */} - {(!authExpired || licenseMissing) && ( - - )} - - )} + {isLoading && ( +
+ Hämtar bokföringsdata... +
+ )} - {/* SIE stats summary */} - {preview?.sieAvailable && preview.sieStats && ( -
- -
-

- Hittade {preview.sieStats.accountCount} konton och {preview.sieStats.transactionCount} verifikationer -

-

- {preview.sieStats.fiscalYears.length === 1 - ? `Räkenskapsår ${preview.sieStats.fiscalYears[0]}` - : `${preview.sieStats.fiscalYears.length} räkenskapsår: ${preview.sieStats.fiscalYears.join(', ')}` - } -

-
-
- )} + {/* SIE stats: one quiet statline, the same grammar as the import + reveal, instead of a boxed summary. */} + {preview?.sieAvailable && preview.sieStats && ( +

+ {preview.sieStats.accountCount.toLocaleString('sv-SE')} konton + {' · '} + {preview.sieStats.transactionCount.toLocaleString('sv-SE')} verifikationer + {' · '} + {preview.sieStats.fiscalYears.length === 1 + ? `räkenskapsåret ${preview.sieStats.fiscalYears[0]}` + : `${preview.sieStats.fiscalYears.length} räkenskapsår: ${preview.sieStats.fiscalYears.join(', ')}`} +

+ )} - {preview && !preview.sieAvailable && !isLoading && preview.hasSieData && ( -
- -
-

SIE-data redan importerad

-

- Bokföringsdata har redan importerats via SIE-fil. Du kan fortsätta med att importera kunder, leverantörer och fakturor. -

-
-
- )} + {preview && !preview.sieAvailable && !isLoading && preview.hasSieData && ( +

+ Bokföringsdatan är redan importerad via SIE-fil. Du kan fortsätta med att importera + kunder, leverantörer och fakturor. +

+ )} +
- {preview && !preview.sieAvailable && !isLoading && !preview.hasSieData && ( -
- -
-

SIE-import krävs

-

- Bokföringsdata (kontoplan, verifikationer och balanser) måste importeras via SIE-fil innan kunder, leverantörer och fakturor kan hämtas. Exportera en SIE-fil från {ARCIM_PROVIDERS.find(p => p.id === preview.consent.provider)?.name ?? 'ditt bokföringssystem'} och ladda upp den i {branding.appName.toLowerCase()}. -

- - - Gå till SIE-importen - - -
-
+ {error && ( +
+
+

Kunde inte hämta bokföringsdata

+

{error}

+
+ {authExpired && ( + )} - - + {/* License-missing keeps the SIE fallback visible: re-auth loops + until the customer re-orders the Fortnox Integration license, + so a manual SIE import is the reliable escape hatch. */} + {(!authExpired || licenseMissing) && ( + + )} +
+ )} -
+ {preview && !preview.sieAvailable && !isLoading && !preview.hasSieData && ( +
+
+

SIE-import krävs

+

+ Bokföringsdata (kontoplan, verifikationer och balanser) måste importeras via SIE-fil innan kunder, leverantörer och fakturor kan hämtas. Exportera en SIE-fil från {ARCIM_PROVIDERS.find(p => p.id === preview.consent.provider)?.name ?? 'ditt bokföringssystem'} och ladda upp den i {branding.appName.toLowerCase()}. +

+
+ +
+ )} + +
+
+
+

Kunde inte ladda SIE-data

+

{error}

+ {errorDetails && errorDetails.length > 0 && ( +
    + {errorDetails.slice(0, 8).map((detail, i) => ( +
  • {detail}
  • + ))} + {errorDetails.length > 8 && ( +
  • … och {errorDetails.length - 8} fel till
  • + )} +
+ )} +
+ +
+ +
) } @@ -1091,142 +1060,112 @@ function OptionsStep({ if (options.importSupplierInvoices) selectedItems.push('Leverantörsfakturor') return ( -
- - - Vad vill du importera? - - Bokföringsdata importeras via SIE-fil. Kunder, leverantörer och fakturor hämtas via API:et. - - - - } - label="Företagsinformation" - description="Namn, organisationsnummer, adress" - checked={options.importCompanyInfo} - onChange={() => toggleOption('importCompanyInfo')} - /> +
+ - {sieAvailable && ( - <> - {/* Years whose provider export failed: must be visible before - the user proceeds, otherwise an IB/UB gap slips through. */} - {failedYears.length > 0 && ( -
-
- -
-

- {failedYears.length === 1 - ? `Räkenskapsår ${failedYears[0].year} kunde inte hämtas` - : `Räkenskapsår ${failedYears.map(f => f.year).join(', ')} kunde inte hämtas`} -

-

- Exporten från källsystemet misslyckades för{' '} - {failedYears.length === 1 ? 'det här räkenskapsåret' : 'dessa räkenskapsår'}. - Om du fortsätter importeras övriga år, men ingående och utgående balanser - kan sakna kontinuitet mellan åren. Försök igen senare eller ladda upp en - SIE-fil för {failedYears.length === 1 ? 'det saknade året' : 'de saknade åren'} manuellt. -

-
-
-
- )} - } - label="Bokföringsdata (SIE)" - description={ - replacedFileCount > 0 && newFileCount > 0 - ? `${newFileCount} nya och ${replacedFileCount} uppdaterade räkenskapsår` - : replacedFileCount > 0 - ? `${replacedFileCount} räkenskapsår med uppdaterad data: tidigare import ersätts` - : newFileCount > 0 - ? `${newFileCount} ny(a) räkenskapsår att importera` - : 'Kontoplan, ingående balanser och verifikationer' - } - checked={options.importSIEData} - onChange={() => toggleOption('importSIEData')} - /> - {/* Per-file import status */} - {fileStatuses.length > 0 && ( -
- {fileStatuses.map((fs) => ( -
- {fs.previousImport ? ( - <> - - - Räkenskapsår {fs.fiscalYear}: ersätter tidigare import - {fs.previousImport.importedAt - ? ` från ${new Date(fs.previousImport.importedAt).toLocaleDateString('sv-SE')}` - : ''} - - - ) : ( - <> - - Räkenskapsår {fs.fiscalYear}: ny data att importera - - )} -
- ))} -
- )} - {options.importSIEData && ( -
-
- -
-
-

Verifikationsserie

-

Serie för importerade verifikationer

-
- onChange({ ...options, voucherSeries: e.target.value.toUpperCase() || 'B' })} - maxLength={2} - /> -
- )} - - )} + {/* Years whose provider export failed: must be visible before the user + proceeds, otherwise an IB/UB gap slips through. One ochre sentence. */} + {sieAvailable && failedYears.length > 0 && ( + + {failedYears.length === 1 + ? `Räkenskapsår ${failedYears[0].year} kunde inte hämtas från källsystemet: om du fortsätter importeras övriga år, men ingående och utgående balanser kan sakna kontinuitet. Försök igen senare eller ladda upp en SIE-fil för det saknade året manuellt.` + : `Räkenskapsår ${failedYears.map(f => f.year).join(', ')} kunde inte hämtas från källsystemet: om du fortsätter importeras övriga år, men ingående och utgående balanser kan sakna kontinuitet. Försök igen senare eller ladda upp SIE-filer för de saknade åren manuellt.`} + + )} - } - label="Kunder" - description="Kund-register med kontaktuppgifter" - checked={options.importCustomers} - onChange={() => toggleOption('importCustomers')} - /> - } - label="Leverantörer" - description="Leverantör-register med bankuppgifter" - checked={options.importSuppliers} - onChange={() => toggleOption('importSuppliers')} - /> - } - label="Kundfakturor" - description="Alla kundfakturor (betalda och obetalda)" - checked={options.importSalesInvoices} - onChange={() => toggleOption('importSalesInvoices')} - /> - } - label="Leverantörsfakturor" - description={provider === 'fortnox' - ? 'Endast obetalda leverantörsfakturor hämtas. Historiska betalda fakturor finns kvar i Fortnox.' - : 'Alla leverantörsfakturor (betalda och obetalda)'} - checked={options.importSupplierInvoices} - onChange={() => toggleOption('importSupplierInvoices')} - /> - - + {/* Clean hairline rows with the toggle on the right: no bordered box + per row, no nested boxes. */} +
+ toggleOption('importCompanyInfo')} + /> -
+ {sieAvailable && ( +
+ 0 && newFileCount > 0 + ? `${newFileCount} nya och ${replacedFileCount} uppdaterade räkenskapsår` + : replacedFileCount > 0 + ? `${replacedFileCount} räkenskapsår med uppdaterad data: tidigare import ersätts` + : newFileCount > 0 + ? `${newFileCount} ny(a) räkenskapsår att importera` + : 'Kontoplan, ingående balanser och verifikationer' + } + checked={options.importSIEData} + onChange={() => toggleOption('importSIEData')} + /> + {/* Per-file import status: quiet muted lines. */} + {fileStatuses.length > 0 && ( +
+ {fileStatuses.map((fs) => ( +

+ {fs.previousImport + ? `Räkenskapsår ${fs.fiscalYear}: ersätter tidigare import${ + fs.previousImport.importedAt + ? ` från ${new Date(fs.previousImport.importedAt).toLocaleDateString('sv-SE')}` + : '' + }` + : `Räkenskapsår ${fs.fiscalYear}: ny data att importera`} +

+ ))} +
+ )} + {/* Verifikationsserie: one aligned row, not a nested box. */} + {options.importSIEData && ( +
+
+

Verifikationsserie

+

Serie för importerade verifikationer

+
+ onChange({ ...options, voucherSeries: e.target.value.toUpperCase() || 'B' })} + maxLength={2} + /> +
+ )} +
+ )} + + toggleOption('importCustomers')} + /> + toggleOption('importSuppliers')} + /> + toggleOption('importSalesInvoices')} + /> + toggleOption('importSupplierInvoices')} + /> +
+ +
- - {/* Expanded details */} - {expanded && ( -
- {/* Errors: shown prominently */} - {result.errors.length > 0 && ( -
-
- -
-

- {result.errors.length === 1 ? '1 fel vid import' : `${result.errors.length} fel vid import`} -

- {result.errors.map((e, i) => ( -

{e}

- ))} -
-
-
- )} - - {/* Opening balance adjustment */} - {d?.openingBalance && ( -
-
- -
-

Ingående balanser justerade

-

- {d.openingBalance.explanation === 'unallocated_result' && ( - <> - Differens på {Math.abs(d.openingBalance.imbalance).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} SEK bokförd - på konto {d.openingBalance.bookedToAccount}. Detta beror troligen på att föregående - års resultat inte allokerats till eget kapital i källsystemet, vanligt vid byte - av bokföringsprogram. - - )} - {d.openingBalance.explanation === 'excluded_accounts' && ( - <> - Exkluderade systemkonton (t.ex. Fortnox 0099) hade ingående saldon. Differensen - ({Math.abs(d.openingBalance.imbalance).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} SEK) - bokförd på konto {d.openingBalance.bookedToAccount}. - - )} - {d.openingBalance.explanation === 'rounding' && ( - <> - Avrundningsdifferens ({Math.abs(d.openingBalance.imbalance).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} SEK) - bokförd på konto {d.openingBalance.bookedToAccount}. - - )} - {!d.openingBalance.explanation && ( - <> - Differens på {Math.abs(d.openingBalance.imbalance).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} SEK bokförd - på konto {d.openingBalance.bookedToAccount}. - - )} -

-
-
-
- )} - - {/* Skipped vouchers breakdown */} - {d?.skippedVouchers && d.skippedVouchers.total > 0 && ( -
-
- -
-

- {d.skippedVouchers.total} verifikationer hoppades över -

-

- Ofullständiga verifikationer i källsystemet som inte kan importeras. - Saldon har justerats automatiskt via omföringsverifikation. -

-
- {d.skippedVouchers.unbalanced > 0 && ( -
- Obalanserade - {d.skippedVouchers.unbalanced} -
- )} - {d.skippedVouchers.unmapped > 0 && ( -
- Ej mappade konton - {d.skippedVouchers.unmapped} -
- )} - {d.skippedVouchers.singleLine > 0 && ( -
- Enradsverifikationer - {d.skippedVouchers.singleLine} -
- )} - {d.skippedVouchers.empty > 0 && ( -
- Tomma - {d.skippedVouchers.empty} -
- )} -
-
-
-
- )} - - {/* Migration adjustment info */} - {d?.migrationAdjustment?.created && ( -
-
- -
-

Omföringsverifikation skapad

-

- {d.migrationAdjustment.accountsAdjusted} konton justerade för att saldon ska matcha - källsystemet. Verifikationen kompenserar för hoppade verifikationer så att dina - balansräkning och resultaträkning stämmer. -

-
-
-
- )} - - {/* Untransferred prior-year results — omföring av årets resultat saknas */} - {d?.untransferredResults && d.untransferredResults.length > 0 && ( -
-
- -
-

- Årets resultat är inte omfört till eget kapital -

-

- Följande räkenskapsår saknar omföring av årets resultat. Senare års - balansräkning visar en differens på beloppet tills omföringen bokförs - (konto 8999 mot eget kapital, t.ex. 2099) i respektive år. -

-
- {d.untransferredResults.map((u) => ( -
- {u.period_name} - - {u.pl_net.toLocaleString('sv-SE', { minimumFractionDigits: 2 })} SEK - -
- ))} -
-
-
-
- )} - - {/* Remaining warnings — previously dropped entirely in this flow. - Strings covered by structured cards above are filtered out. */} - {(() => { - const remainingWarnings = result.warnings.filter( - (w) => - !(d?.skippedVouchers && d.skippedVouchers.total > 0 && w.includes('hoppades över')) && - !(d?.untransferredResults && d.untransferredResults.length > 0 && w.includes('förts om till eget kapital')) - ) - if (remainingWarnings.length === 0) return null - return ( -
-
- -
-

- {remainingWarnings.length === 1 ? '1 varning' : `${remainingWarnings.length} varningar`} -

- {remainingWarnings.map((w, i) => ( -

{w}

- ))} -
-
-
- ) - })()} - - {/* Retry info (only shown if retries happened) */} - {d && d.retriedBatches > 0 && ( -

- {d.retriedBatches} {d.retriedBatches === 1 ? 'batch' : 'batcher'} behövde omförsök - {d.failedBatches > 0 && ( - - {' · '}{d.failedBatches} misslyckades trots omförsök - - )} -

- )} +
+
+ {fyLabel} + + {result.journalEntriesCreated.toLocaleString('sv-SE')} verifikationer + {result.replacedPriorImport && result.replacedPriorImport.deletedEntries > 0 && ( + <> · ersatte {result.replacedPriorImport.deletedEntries.toLocaleString('sv-SE')} tidigare importerade + )} + + + {status.label} + +
+ {result.errors.length > 0 && ( +
+ {result.errors.map((e, i) => ( +

{e}

+ ))}
)} + {warningSentences.map((w, i) => ( + {w} + ))} + {infoLines.map((l, i) => ( +

{l}

+ ))} + {d && d.failedBatches > 0 && ( +

+ {d.retriedBatches} {d.retriedBatches === 1 ? 'batch' : 'batcher'} behövde omförsök, {d.failedBatches} misslyckades trots omförsök. +

+ )}
) } @@ -1641,12 +1429,7 @@ function DocumentImportFollowUp({ if (state.phase === 'hidden' || state.phase === 'dismissed') return null - const title = ( - - - {t('ext_arcim_documents_title')} - - ) + const title = {t('ext_arcim_documents_title')} if ( state.phase === 'discovering' || @@ -1661,65 +1444,52 @@ function DocumentImportFollowUp({ : t('ext_arcim_documents_reconnecting') return ( - - {title} - -
-
-
-
+
+ {title} + {label} +
) } if (state.phase === 'offered') { return ( - - - {title} - - {t('ext_arcim_documents_prompt', { count: state.found })} - - - +
+ {title} +

+ {t('ext_arcim_documents_prompt', { count: state.found })} +

+
- - +
+
) } if (state.phase === 'empty') { return ( - - - {title} - {t('ext_arcim_documents_empty')} - - - - - +
+ {title} +

{t('ext_arcim_documents_empty')}

+ +
) } if (state.phase === 'complete') { if (!state.result) { return ( - - - {title} - {t('ext_arcim_documents_result_description')} - - +
+ {title} +

{t('ext_arcim_documents_result_description')}

+
) } @@ -1748,92 +1518,91 @@ function DocumentImportFollowUp({ ] return ( - - - {title} - {t('ext_arcim_documents_result_description')} - - -
- {outcomes.map(({ label, value, valueClassName }) => ( -
-
{label}
-
- {value} -
-
- ))} -
- {unmatched > 0 && ( -

- {t('ext_arcim_documents_unmatched_help')} -

- )} - {failed > 0 && ( -
-

- {t('ext_arcim_documents_partial_failure')} -

- +
+ {title} +

{t('ext_arcim_documents_result_description')}

+
+ {outcomes.map(({ label, value, valueClassName }) => ( +
+
{label}
+
+ {value} +
- )} - - + ))} +
+ {unmatched > 0 && ( +

+ {t('ext_arcim_documents_unmatched_help')} +

+ )} + {failed > 0 && ( +
+

+ {t('ext_arcim_documents_partial_failure')} +

+ +
+ )} +
) } const reconnectRequired = state.problem?.reconnectRequired === true const discoveryFailed = state.phase === 'discovery-error' return ( - - - {title} - - {state.problem?.message - ? state.problem.message - : reconnectRequired - ? t('ext_arcim_documents_scope_error') - : discoveryFailed - ? t('ext_arcim_documents_discovery_error') - : t('ext_arcim_documents_import_error')} - - - - {state.problem?.requestId && ( -

- {t('ext_arcim_documents_error_reference', { - requestId: state.problem.requestId, - })} -

+
+ {title} +

+ {state.problem?.message + ? state.problem.message + : reconnectRequired + ? t('ext_arcim_documents_scope_error') + : discoveryFailed + ? t('ext_arcim_documents_discovery_error') + : t('ext_arcim_documents_import_error')} +

+ {state.problem?.requestId && ( +

+ {t('ext_arcim_documents_error_reference', { + requestId: state.problem.requestId, + })} +

+ )} + - - + {reconnectRequired + ? t('ext_arcim_documents_reconnect_action') + : discoveryFailed + ? t('ext_arcim_documents_retry_discovery') + : t('ext_arcim_documents_retry_import')} + +
) } +const NEXT_STEPS: { title: string; sub: string }[] = [ + { title: 'Granska importerade verifikationer', sub: 'Kontrollera att bokföringen ser korrekt ut i huvudboken' }, + { title: 'Stäm av balansräkningen', sub: 'Jämför ingående balanser och saldon mot ditt tidigare system' }, + { title: 'Kontrollera kunder och leverantörer', sub: 'Verifiera kontaktuppgifter, organisationsnummer och bankinfo' }, +] + function ResultStep({ results, sieResults, error, documentImportState, + theaterModel, onDone, onRetry, onDiscoverDocuments, @@ -1845,6 +1614,7 @@ function ResultStep({ sieResults: ImportResult[] error: string | null documentImportState: ArcimDocumentImportState + theaterModel: TheaterModel | null onDone: () => void onRetry: () => void onDiscoverDocuments: () => void @@ -1854,24 +1624,15 @@ function ResultStep({ }) { if (error) { return ( -
- - -
- -
-

Migreringen misslyckades

-

{error}

-
-
-
-
- -
+
+
+

+ Migreringen misslyckades +

+

{error}

+
+ +