'use client' import { useState, useEffect } from 'react' import Link from 'next/link' import { useTranslations } from 'next-intl' import { Card, CardContent } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { cn, formatCurrency } from '@/lib/utils' import { UpcomingDeadlinesWidget } from '@/components/deadlines/UpcomingDeadlinesWidget' import { TaxTodoWidget } from '@/components/deadlines/TaxTodoWidget' import NewUserChecklist from '@/components/onboarding/NewUserChecklist' import AttGoraSection from '@/components/dashboard/AttGoraSection' import { Receipt, ArrowLeftRight, ChevronRight, CheckCircle2, Clock, ArrowRight, MessageCircle, } from 'lucide-react' import type { Deadline, OnboardingProgress } from '@/types' import type { SuggestedMatch, WorklistCounts } from '@/lib/worklist/types' const setupFreshStartKey = (companyId: string) => `erp_setup_fresh_start:${companyId}` interface DashboardContentProps { companyId: string summary: { ytd: { income: number; expenses: number; net: number } mtd: { income: number; expenses: number; net: number } unpaidInvoicesCount: number unpaidInvoicesTotal: number unpaidVatTotal: number overdueInvoicesCount: number bankBalance: number | null expiringBankConnections?: { id: string; bank_name: string; days_left: number }[] deadlines: Deadline[] staleUncategorizedCount: number } /** Unified pending-work counts from lib/worklist — same source as the sidebar badges. */ worklist: WorklistCounts /** High-confidence transaction↔invoice matches for inline one-click confirm. */ suggestedMatches: SuggestedMatch[] onboardingProgress?: OnboardingProgress /** * False until the company has a verified agent_profile. When false the hero * slot shows a build-assistant prompt instead of the next-best-action card, * so existing/migrated users are nudged to build the assistant without a * full-screen onboarding takeover. */ agentBuilt?: boolean } export default function DashboardContent({ companyId, summary, worklist, suggestedMatches, onboardingProgress, agentBuilt = true }: DashboardContentProps) { const t = useTranslations('dashboard') // The setup gate exists to nudge brand-new users into a data-import step // before they hit the dashboard. Once the assistant is built we treat the // user as past that phase — they've already committed to using the tool — // and let the dashboard render normally. This also keeps the sandbox // (which ships with a pre-built assistant + seeded data but no bank // connection / SIE import) from showing a checklist that re-links to // /onboarding/agent. const needsSetup = !agentBuilt && onboardingProgress && !onboardingProgress.hasBankConnected && !onboardingProgress.hasSIEImport const [setupGateActive, setSetupGateActive] = useState(!!needsSetup) useEffect(() => { if (!needsSetup) { setSetupGateActive(false) return } const scopedKey = setupFreshStartKey(companyId) const freshStart = localStorage.getItem(scopedKey) === 'true' const legacyFreshStart = localStorage.getItem('erp_setup_fresh_start') === 'true' const legacyDismissed = localStorage.getItem('erp_checklist_dismissed') === 'true' if (freshStart || legacyFreshStart || legacyDismissed) { if (!freshStart) { localStorage.setItem(scopedKey, 'true') } setSetupGateActive(false) } }, [needsSetup, companyId]) if (setupGateActive) { return ( { localStorage.setItem(setupFreshStartKey(companyId), 'true') setSetupGateActive(false) }} /> ) } const formatLargeNumber = (amount: number) => { return new Intl.NumberFormat('sv-SE', { style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: 0, }).format(amount) } // One number, one source: the worklist total plus expiring bank connections // (dashboard-only, not a lib/worklist category). Must match AttGoraSection's // header so the tile and the section never disagree. const todoCount = worklist.total + (summary.expiringBankConnections?.length ?? 0) // Pick the single most-urgent next action so the launchpad surfaces one // unambiguous CTA. Order matches the friction we actually want to remove // first: stale → overdue → uncategorized → unpaid → all clear. const nextBestAction = (() => { if (summary.staleUncategorizedCount > 0) { return { href: '/transactions', title: 'Gamla transaktioner väntar', body: `${summary.staleUncategorizedCount} transaktion${summary.staleUncategorizedCount === 1 ? '' : 'er'} äldre än 14 dagar saknar bokföring.`, cta: 'Bokför nu', tone: 'destructive' as const, icon: Clock, } } if (summary.overdueInvoicesCount > 0) { return { href: '/invoices?status=unpaid', title: 'Förfallna fakturor', body: `${summary.overdueInvoicesCount} st · ${formatCurrency(summary.unpaidInvoicesTotal)}`, cta: 'Gå till fakturor', tone: 'destructive' as const, icon: Receipt, } } if (worklist.counts.book_transaction > 0) { const n = worklist.counts.book_transaction return { href: '/transactions', title: 'Transaktioner att bokföra', body: `${n} obokförd${n === 1 ? '' : 'a'} transaktion${n === 1 ? '' : 'er'}.`, cta: 'Bokför nu', tone: 'primary' as const, icon: ArrowLeftRight, } } if (summary.unpaidInvoicesCount > 0) { return { href: '/invoices?status=unpaid', title: 'Obetalda fakturor', body: `${summary.unpaidInvoicesCount} st · ${formatCurrency(summary.unpaidInvoicesTotal)}`, cta: 'Visa fakturor', tone: 'primary' as const, icon: Receipt, } } return { href: '/invoices/new', title: 'Allt är ikapp', body: 'Inga obokförda transaktioner och inga obetalda fakturor. Skicka nästa faktura?', cta: 'Skapa faktura', tone: 'neutral' as const, icon: CheckCircle2, } })() return (
{!agentBuilt ? ( /* Build-assistant hero — shown until the company has a verified agent_profile. Takes the hero slot so existing/migrated users get a clear prompt instead of a full-screen onboarding takeover. */

Bygg din bokföringsassistent

Beta

Några frågor om din verksamhet kalibrerar en assistent som föreslår bokföring åt dig.

Kom igång
) : ( /* Next best action — single hero card, shown for every agent-built company regardless of nav density so there is always one clear CTA */

{nextBestAction.title}

{nextBestAction.body}

{nextBestAction.cta}
)} {/* Key metrics — 4 compact cards */}

{t('result')}

= 0 ? 'text-success' : 'text-destructive' )}> {formatLargeNumber(summary.mtd.net)} kr

{formatCurrency(summary.ytd.net)} {t('this_year_short')}

{t('to_be_paid')}

{summary.unpaidInvoicesCount} {t('units') && {t('units')}}

{formatCurrency(summary.unpaidInvoicesTotal)}

{summary.bankBalance !== null ? (

{t('bank_balance')}

{formatLargeNumber(summary.bankBalance)} kr

) : (

{t('bank_balance')}

{t('connect_bank')}

)}

{t('todo')}

{todoCount > 0 ? (

{todoCount} {t('units') && {t('units')}}

) : (

{t('all_done')}

)}
{/* Att göra — the unified worklist. One section, every actionable item, same counts as the sidebar badges (lib/worklist). */} {/* Result — revenue / expenses (always visible) */}

{t('revenue')}

{formatLargeNumber(summary.mtd.income)} kr

{t('this_month')}

{t('this_year_block')}

{formatCurrency(summary.ytd.income)}

{t('expenses')}

{formatLargeNumber(summary.mtd.expenses)} kr

{t('this_month')}

{t('this_year_block')}

{formatCurrency(summary.ytd.expenses)}

{/* Upcoming deadlines */} {summary.deadlines && summary.deadlines.length > 0 && (
)} {/* Tax todo */} {summary.deadlines?.some(d => d.deadline_type === 'tax' && !d.is_completed) && (
)}
) }