diff --git a/components/dashboard/DashboardContent.tsx b/components/dashboard/DashboardContent.tsx index 1996fe9a..ea4e23fc 100644 --- a/components/dashboard/DashboardContent.tsx +++ b/components/dashboard/DashboardContent.tsx @@ -24,6 +24,8 @@ import { resolveIcon } from '@/lib/extensions/icon-resolver' import type { QuickActionDefinition } from '@/lib/extensions/types' import type { CompanySettings, Deadline, ReceiptQueueSummary, OnboardingProgress } from '@/types' +const SETUP_FRESH_START_KEY = 'erp_setup_fresh_start' + interface DashboardContentProps { firstName?: string | null settings: CompanySettings | null @@ -71,6 +73,31 @@ export default function DashboardContent({ firstName, settings, summary, onboard return () => window.removeEventListener('extension-toggle-changed', handler) }, []) + // Setup gate — blocks dashboard until user imports data or chooses fresh start + const needsSetup = onboardingProgress && !onboardingProgress.hasBankConnected && !onboardingProgress.hasSIEImport + const [setupGateActive, setSetupGateActive] = useState(!!needsSetup) + + useEffect(() => { + if (!needsSetup) { + setSetupGateActive(false) + return + } + const freshStart = localStorage.getItem(SETUP_FRESH_START_KEY) === 'true' + const oldDismissed = localStorage.getItem('erp_checklist_dismissed') === 'true' + if (freshStart || oldDismissed) setSetupGateActive(false) + }, [needsSetup]) + + if (setupGateActive) { + return ( + { + localStorage.setItem(SETUP_FRESH_START_KEY, 'true') + setSetupGateActive(false) + }} + /> + ) + } + const formatLargeNumber = (amount: number) => { return new Intl.NumberFormat('sv-SE', { style: 'decimal', @@ -264,18 +291,6 @@ export default function DashboardContent({ firstName, settings, summary, onboard - {/* New user checklist */} - {onboardingProgress && ( -
- -
- )} - {/* 4 Key Summary Cards */}
diff --git a/components/onboarding/NewUserChecklist.tsx b/components/onboarding/NewUserChecklist.tsx index a9da8179..20a1b766 100644 --- a/components/onboarding/NewUserChecklist.tsx +++ b/components/onboarding/NewUserChecklist.tsx @@ -1,298 +1,160 @@ 'use client' -import { useState, useEffect } from 'react' import Link from 'next/link' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { Button } from '@/components/ui/button' -import { Progress } from '@/components/ui/progress' import { - Check, - Circle, - PartyPopper, - Rocket, - X, - FileUp, ArrowRight, - UserPlus, - Receipt, - ArrowLeftRight, + FileText, + Landmark, + ArrowRightLeft, + ShieldCheck, } from 'lucide-react' import { cn } from '@/lib/utils' - -interface ChecklistItem { - id: string - label: string - description: string - href: string - completed: boolean - icon: React.ElementType -} +import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' interface NewUserChecklistProps { - hasCustomers: boolean - hasInvoices: boolean - hasBankConnected: boolean - hasSIEImport: boolean - onDismiss?: () => void + onFreshStart: () => void className?: string } -const CHECKLIST_DISMISSED_KEY = 'erp_checklist_dismissed' -const FRESH_START_KEY = 'erp_onboarding_fresh' - export default function NewUserChecklist({ - hasCustomers, - hasInvoices, - hasBankConnected, - onDismiss, + onFreshStart, className, }: NewUserChecklistProps) { - const [isDismissed, setIsDismissed] = useState(true) - const [isAnimatingOut, setIsAnimatingOut] = useState(false) - const [showChecklist, setShowChecklist] = useState(false) + const hasMigration = ENABLED_EXTENSION_IDS.has('arcim-migration') + const hasBanking = ENABLED_EXTENSION_IDS.has('enable-banking') - useEffect(() => { - const dismissed = localStorage.getItem(CHECKLIST_DISMISSED_KEY) - setIsDismissed(dismissed === 'true') + return ( +
+
+ {/* Header */} +
+

+ Välkommen till gnubok +

+

+ Börja med att flytta din bokföring, sedan kopplar du banken. + Ingenting ändras i ditt nuvarande system. +

+
- const freshStart = localStorage.getItem(FRESH_START_KEY) - if (freshStart === 'true') { - setShowChecklist(true) - } - }, []) - - const handleFreshStart = () => { - setShowChecklist(true) - localStorage.setItem(FRESH_START_KEY, 'true') - } - - const handleDismiss = () => { - setIsAnimatingOut(true) - setTimeout(() => { - localStorage.setItem(CHECKLIST_DISMISSED_KEY, 'true') - setIsDismissed(true) - onDismiss?.() - }, 300) - } - - const items: ChecklistItem[] = [ - { - id: 'customer', - label: 'Lägg till din första kund', - description: 'Spara kunduppgifter för enkel fakturering', - href: '/customers/new', - completed: hasCustomers, - icon: UserPlus, - }, - { - id: 'invoice', - label: 'Skicka din första faktura', - description: 'Skapa en professionell faktura på 60 sekunder', - href: '/invoices/new', - completed: hasInvoices, - icon: Receipt, - }, - { - id: 'bank', - label: 'Importera transaktioner', - description: 'Koppla bank eller ladda upp kontoutdrag', - href: '/import', - completed: hasBankConnected, - icon: ArrowLeftRight, - }, - ] - - const completedCount = items.filter((item) => item.completed).length - const progress = (completedCount / items.length) * 100 - const allCompleted = completedCount === items.length - - if (isDismissed) { - return null - } - - // Celebration state when all checklist items done - if (showChecklist && allCompleted && !isAnimatingOut) { - return ( - - -
-
- -
-
-

Snyggt jobbat!

-

- Du har slutfört alla steg. Nu är du redo att köra! -

-
- + {/* Step 1: Migrate bookkeeping */} +
+
+ + 1 + +

+ Flytta din bokföring +

- - - ) - } - // Path selection — shown initially - if (!showChecklist) { - return ( - - -
-
- Välkommen till gnubok -

- Hur vill du komma igång? -

-
- -
-
- -
- {/* Migration path — routes directly to import/migration wizard */} +
+ {hasMigration && ( + +
+
+ +
+
+

+ Migrera från Fortnox, Bokio eller Visma +

+

+ Flytta bokföring, kunder, leverantörer och fakturor automatiskt. +

+
+ +
+ + )} + -
-
- +
+
+
-

- Migrera från annat system +

+ Importera SIE-fil

-

- Flytta bokföring, kunder, leverantörer och fakturor från Fortnox, Visma, Bokio, Björn Lundén eller Briox +

+ Exportera en SIE4-fil från ditt nuvarande bokföringsprogram och ladda upp den här.

- +
- - {/* Fresh start path */} - -
- - - ) - } - - // Fresh start checklist - return ( - - -
-
- Kom igång -

- {completedCount} av {items.length} steg klara -

-
-
- -
- -
- -
    - {items.map((item) => { - const Icon = item.icon - return ( -
  • - {item.completed ? ( -
    -
    - -
    - - {item.label} - -
    - ) : ( - -
    - -
    -
    -

    - - {item.label} -

    -

    - {item.description} -

    -
    - - )} -
  • - ) - })} -
-
-
+ + {/* Step 2: Connect bank */} +
+
+ + 2 + +

+ Koppla din bank +

+
+ +
+ +
+
+ +
+
+

+ Anslut ditt bankkonto +

+

+ {hasBanking + ? 'Koppla via PSD2 — transaktioner synkas automatiskt varje dag.' + : 'Importera kontoutdrag från din bank — CSV, OFX och de flesta svenska banker.'} +

+
+ +
+ +
+
+ + {/* Escape hatch */} +
+
+
+ eller +
+
+ +
+ +
+ +
+ +

+ Din data är krypterad och lagras säkert i Sverige +

+
+
+
+
) } diff --git a/lib/bookkeeping/engine.ts b/lib/bookkeeping/engine.ts index 85b079fd..c2befac6 100644 --- a/lib/bookkeeping/engine.ts +++ b/lib/bookkeeping/engine.ts @@ -379,6 +379,7 @@ export async function reverseEntry( if (linesError) { await supabase.from('journal_entries').update({ status: 'cancelled' }).eq('id', reversalEntry.id) + await supabase.from('journal_entry_lines').delete().eq('journal_entry_id', reversalEntry.id) throw new Error(`Failed to create reversal lines: ${linesError.message}`) } @@ -390,6 +391,7 @@ export async function reverseEntry( if (postError) { await supabase.from('journal_entries').update({ status: 'cancelled' }).eq('id', reversalEntry.id) + await supabase.from('journal_entry_lines').delete().eq('journal_entry_id', reversalEntry.id) throw new Error(`Failed to post reversal entry: ${postError.message}`) } @@ -408,6 +410,7 @@ export async function reverseEntry( // Another concurrent reversal already changed the status — mark the orphaned // reversal as cancelled so it's excluded from reports but remains traceable. await supabase.from('journal_entries').update({ status: 'cancelled' }).eq('id', reversalEntry.id) + await supabase.from('journal_entry_lines').delete().eq('journal_entry_id', reversalEntry.id) throw new Error('Entry was already reversed by a concurrent operation') }