feat: BAS data overhaul, currency revaluation, expenses, UI polish, and cleanup

- Update BAS account catalog with comprehensive SRU codes and K2 flags
- Add currency revaluation service with tests and API route
- Add expenses page and account deletion API
- Enhance booking templates with new patterns and improved tests
- Improve transaction categorization with template picker and description matching
- Polish dashboard, onboarding, import, and transaction UIs
- Refactor year-end service for multi-step closing
- Move SRU generator to ne-bilaga, remove standalone SRU export
- Remove unused dev docs, mock data, and extension hooks
- Add invoice delivery note sequences migration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-03-04 14:19:56 +01:00
co-authored by Claude Opus 4.6
parent a9bf24ce0b
commit 66a4027f1e
104 changed files with 4964 additions and 5501 deletions
+101 -176
View File
@@ -4,7 +4,6 @@ import { useState, useEffect } from 'react'
import Link from 'next/link'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { cn, formatCurrency } from '@/lib/utils'
import {
calculateEFTax,
@@ -16,18 +15,14 @@ import { UpcomingDeadlinesWidget } from '@/components/deadlines/UpcomingDeadline
import { TaxTodoWidget } from '@/components/deadlines/TaxTodoWidget'
import NewUserChecklist from '@/components/onboarding/NewUserChecklist'
import {
TrendingUp,
TrendingDown,
Receipt,
ArrowLeftRight,
ChevronDown,
ChevronUp,
ArrowRight,
Camera,
Users,
Landmark,
CheckCircle2,
ClipboardList,
FileWarning,
} from 'lucide-react'
import { getExtensionDefinition } from '@/lib/extensions/sectors'
@@ -113,17 +108,14 @@ export default function DashboardContent({ firstName, settings, summary, onboard
<Link key="overdue" href="/invoices?status=unpaid" className="group">
<Card className="h-full border-l-2 border-l-destructive hover:bg-muted/20 transition-colors">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Receipt className="h-4 w-4 text-destructive flex-shrink-0" />
<div>
<p className="font-medium text-sm">Förfallna fakturor</p>
<p className="text-xs text-muted-foreground mt-0.5">
{summary.overdueInvoicesCount} st
</p>
</div>
<div className="flex items-center gap-3">
<Receipt className="h-4 w-4 text-destructive flex-shrink-0" />
<div>
<p className="font-medium text-sm">Förfallna fakturor</p>
<p className="text-xs text-muted-foreground mt-0.5">
{summary.overdueInvoicesCount} st
</p>
</div>
<ArrowRight className="h-3.5 w-3.5 text-muted-foreground/50 group-hover:text-muted-foreground group-hover:translate-x-0.5 transition-all" />
</div>
</CardContent>
</Card>
@@ -136,17 +128,14 @@ export default function DashboardContent({ firstName, settings, summary, onboard
<Link key="unpaid" href="/invoices?status=unpaid" className="group">
<Card className="h-full border-l-2 border-l-warning hover:bg-muted/20 transition-colors">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Receipt className="h-4 w-4 text-warning-foreground flex-shrink-0" />
<div>
<p className="font-medium text-sm">Obetalda fakturor</p>
<p className="text-xs text-muted-foreground mt-0.5">
{summary.unpaidInvoicesCount - summary.overdueInvoicesCount} st · {formatCurrency(summary.unpaidInvoicesTotal)}
</p>
</div>
<div className="flex items-center gap-3">
<Receipt className="h-4 w-4 text-warning-foreground flex-shrink-0" />
<div>
<p className="font-medium text-sm">Obetalda fakturor</p>
<p className="text-xs text-muted-foreground mt-0.5">
{summary.unpaidInvoicesCount - summary.overdueInvoicesCount} st · {formatCurrency(summary.unpaidInvoicesTotal)}
</p>
</div>
<ArrowRight className="h-3.5 w-3.5 text-muted-foreground/50 group-hover:text-muted-foreground group-hover:translate-x-0.5 transition-all" />
</div>
</CardContent>
</Card>
@@ -159,17 +148,14 @@ export default function DashboardContent({ firstName, settings, summary, onboard
<Link key="transactions" href="/transactions" className="group">
<Card className="h-full border-l-2 border-l-warning hover:bg-muted/20 transition-colors">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<ArrowLeftRight className="h-4 w-4 text-warning-foreground flex-shrink-0" />
<div>
<p className="font-medium text-sm">Transaktioner</p>
<p className="text-xs text-muted-foreground mt-0.5">
{summary.uncategorizedCount} obokförda
</p>
</div>
<div className="flex items-center gap-3">
<ArrowLeftRight className="h-4 w-4 text-warning-foreground flex-shrink-0" />
<div>
<p className="font-medium text-sm">Transaktioner</p>
<p className="text-xs text-muted-foreground mt-0.5">
{summary.uncategorizedCount} obokförda
</p>
</div>
<ArrowRight className="h-3.5 w-3.5 text-muted-foreground/50 group-hover:text-muted-foreground group-hover:translate-x-0.5 transition-all" />
</div>
</CardContent>
</Card>
@@ -182,19 +168,16 @@ export default function DashboardContent({ firstName, settings, summary, onboard
<Link key="receipts" href="/receipts" className="group">
<Card className="h-full border-l-2 border-l-primary hover:bg-muted/20 transition-colors">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Camera className="h-4 w-4 text-primary flex-shrink-0" />
<div>
<p className="font-medium text-sm">Kvitton</p>
<p className="text-xs text-muted-foreground mt-0.5">
{summary.receiptQueue.pending_review_count > 0
? `${summary.receiptQueue.pending_review_count} att granska`
: `${summary.receiptQueue.unmatched_receipts_count} omatchade`}
</p>
</div>
<div className="flex items-center gap-3">
<Camera className="h-4 w-4 text-primary flex-shrink-0" />
<div>
<p className="font-medium text-sm">Kvitton</p>
<p className="text-xs text-muted-foreground mt-0.5">
{summary.receiptQueue.pending_review_count > 0
? `${summary.receiptQueue.pending_review_count} att granska`
: `${summary.receiptQueue.unmatched_receipts_count} omatchade`}
</p>
</div>
<ArrowRight className="h-3.5 w-3.5 text-muted-foreground/50 group-hover:text-muted-foreground group-hover:translate-x-0.5 transition-all" />
</div>
</CardContent>
</Card>
@@ -207,17 +190,14 @@ export default function DashboardContent({ firstName, settings, summary, onboard
<Link key="missing-underlag" href="/bookkeeping?missingUnderlag=true" className="group">
<Card className="h-full border-l-2 border-l-warning hover:bg-muted/20 transition-colors">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<FileWarning className="h-4 w-4 text-warning-foreground flex-shrink-0" />
<div>
<p className="font-medium text-sm">Saknade underlag</p>
<p className="text-xs text-muted-foreground mt-0.5">
{summary.missingUnderlagCount} verifikationer utan underlag
</p>
</div>
<div className="flex items-center gap-3">
<FileWarning className="h-4 w-4 text-warning-foreground flex-shrink-0" />
<div>
<p className="font-medium text-sm">Saknade underlag</p>
<p className="text-xs text-muted-foreground mt-0.5">
{summary.missingUnderlagCount} verifikationer utan underlag
</p>
</div>
<ArrowRight className="h-3.5 w-3.5 text-muted-foreground/50 group-hover:text-muted-foreground group-hover:translate-x-0.5 transition-all" />
</div>
</CardContent>
</Card>
@@ -231,17 +211,14 @@ export default function DashboardContent({ firstName, settings, summary, onboard
<Link key="bank-expiry" href="/settings?tab=banking" className="group">
<Card className="h-full border-l-2 border-l-warning hover:bg-muted/20 transition-colors">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Landmark className="h-4 w-4 text-warning-foreground flex-shrink-0" />
<div>
<p className="font-medium text-sm">Banksamtycke löper ut</p>
<p className="text-xs text-muted-foreground mt-0.5">
{conn.bank_name} — {conn.days_left} {conn.days_left === 1 ? 'dag' : 'dagar'} kvar
</p>
</div>
<div className="flex items-center gap-3">
<Landmark className="h-4 w-4 text-warning-foreground flex-shrink-0" />
<div>
<p className="font-medium text-sm">Banksamtycke löper ut</p>
<p className="text-xs text-muted-foreground mt-0.5">
{conn.bank_name} — {conn.days_left} {conn.days_left === 1 ? 'dag' : 'dagar'} kvar
</p>
</div>
<ArrowRight className="h-3.5 w-3.5 text-muted-foreground/50 group-hover:text-muted-foreground group-hover:translate-x-0.5 transition-all" />
</div>
</CardContent>
</Card>
@@ -277,61 +254,33 @@ export default function DashboardContent({ firstName, settings, summary, onboard
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">
{(() => {
const hour = new Date().getHours()
if (hour < 12) return 'Godmorgon'
if (hour < 5) return 'God natt'
if (hour < 10) return 'Godmorgon'
if (hour < 14) return 'Hej'
if (hour < 18) return 'God eftermiddag'
return 'God kväll'
})()}{firstName ? `, ${firstName}` : ''}
</h1>
<p className="text-muted-foreground mt-1">
{summary.overdueInvoicesCount > 0
? `${summary.overdueInvoicesCount} förfallna fakturor kräver åtgärd`
: summary.deadlines.filter(d => !d.is_completed && new Date(d.due_date) <= new Date()).length > 0
? `${summary.deadlines.filter(d => !d.is_completed && new Date(d.due_date) <= new Date()).length} passerade deadlines`
: 'Allt är som det ska'}
{(() => {
if (summary.overdueInvoicesCount > 0)
return `${summary.overdueInvoicesCount} förfallna fakturor kräver åtgärd`
const passedDeadlines = summary.deadlines.filter(d => !d.is_completed && new Date(d.due_date) <= new Date()).length
if (passedDeadlines > 0)
return `${passedDeadlines} passerade deadlines`
if (summary.uncategorizedCount > 0)
return `${summary.uncategorizedCount} obokförda transaktioner`
if (summary.receiptQueue && summary.receiptQueue.pending_review_count > 0)
return `${summary.receiptQueue.pending_review_count} kvitton att granska`
if (summary.missingUnderlagCount > 0)
return `${summary.missingUnderlagCount} verifikationer saknar underlag`
if (summary.unpaidInvoicesCount > 0)
return `${summary.unpaidInvoicesCount} obetalda fakturor`
return 'Allt är som det ska'
})()}
</p>
</header>
{/* Status pills */}
{(() => {
const passedDeadlines = summary.deadlines.filter(d => !d.is_completed && new Date(d.due_date) <= new Date())
const todoItems: { label: string; href: string; count: number; variant: 'destructive' | 'warning' | 'default' }[] = []
if (passedDeadlines.length > 0) {
todoItems.push({ label: 'passerade deadlines', href: '/deadlines', count: passedDeadlines.length, variant: 'destructive' })
}
if (summary.overdueInvoicesCount > 0) {
todoItems.push({ label: 'förfallna fakturor', href: '/invoices?status=unpaid', count: summary.overdueInvoicesCount, variant: 'destructive' })
}
if (summary.uncategorizedCount > 0) {
todoItems.push({ label: 'obokförda', href: '/transactions', count: summary.uncategorizedCount, variant: 'warning' })
}
if (summary.receiptQueue && summary.receiptQueue.pending_review_count > 0) {
todoItems.push({ label: 'kvitton att granska', href: '/receipts', count: summary.receiptQueue.pending_review_count, variant: 'default' })
}
if (summary.missingUnderlagCount > 0) {
todoItems.push({ label: 'saknade underlag', href: '/bookkeeping?missingUnderlag=true', count: summary.missingUnderlagCount, variant: 'warning' })
}
if (todoItems.length === 0) return null
return (
<section className="mb-10">
<div className="flex flex-wrap gap-1.5">
{todoItems.map((item) => (
<Link key={item.href} href={item.href}>
<Badge
variant={item.variant === 'destructive' ? 'destructive' : item.variant === 'warning' ? 'outline' : 'secondary'}
className="px-2.5 py-1 text-xs cursor-pointer hover:opacity-80 transition-opacity"
>
{item.count} {item.label}
</Badge>
</Link>
))}
</div>
</section>
)
})()}
{/* New user checklist */}
{onboardingProgress && (
<section className="mb-10">
@@ -358,10 +307,7 @@ export default function DashboardContent({ firstName, settings, summary, onboard
{/* Card 1: Resultat */}
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-2">
<TrendingUp className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-xs text-muted-foreground">Resultat</span>
</div>
<p className="text-xs text-muted-foreground mb-2">Resultat</p>
<p className={cn(
'font-display text-xl font-medium tabular-nums leading-tight',
summary.mtd.net >= 0 ? 'text-success' : 'text-destructive'
@@ -379,10 +325,7 @@ export default function DashboardContent({ firstName, settings, summary, onboard
<Link href="/invoices?status=unpaid">
<Card className="h-full hover:border-primary/50 transition-colors">
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-2">
<Receipt className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-xs text-muted-foreground">Att få betalt</span>
</div>
<p className="text-xs text-muted-foreground mb-2">Att få betalt</p>
<p className="font-display text-xl font-medium tabular-nums leading-tight">
{summary.unpaidInvoicesCount}
<span className="text-sm ml-0.5 text-muted-foreground font-normal">st</span>
@@ -398,10 +341,7 @@ export default function DashboardContent({ firstName, settings, summary, onboard
{summary.bankBalance !== null ? (
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-2">
<Landmark className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-xs text-muted-foreground">Banksaldo</span>
</div>
<p className="text-xs text-muted-foreground mb-2">Banksaldo</p>
<p className="font-display text-xl font-medium tabular-nums leading-tight">
{formatLargeNumber(summary.bankBalance)}
<span className="text-sm ml-0.5 text-muted-foreground font-normal">kr</span>
@@ -412,10 +352,7 @@ export default function DashboardContent({ firstName, settings, summary, onboard
<Link href="/import">
<Card className="h-full hover:border-primary/50 transition-colors">
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-2">
<Landmark className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-xs text-muted-foreground">Banksaldo</span>
</div>
<p className="text-xs text-muted-foreground mb-2">Banksaldo</p>
<p className="text-sm font-medium text-primary">Koppla bank</p>
<p className="text-[11px] text-muted-foreground mt-1">Importera transaktioner</p>
</CardContent>
@@ -426,10 +363,7 @@ export default function DashboardContent({ firstName, settings, summary, onboard
{/* Card 4: Att göra */}
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-2">
<ClipboardList className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-xs text-muted-foreground">Att göra</span>
</div>
<p className="text-xs text-muted-foreground mb-2">Att göra</p>
{todoCount > 0 ? (
<>
<p className="font-display text-xl font-medium tabular-nums leading-tight text-warning-foreground">
@@ -469,17 +403,14 @@ export default function DashboardContent({ firstName, settings, summary, onboard
? 'border-primary/20 bg-primary/[0.03] hover:bg-primary/[0.06]'
: 'border-border/40 hover:bg-muted/30'
)}>
<div className={cn(
'p-2 rounded-lg',
action.accent ? 'bg-primary/8' : 'bg-muted/50'
)}>
<Icon className={cn(
'h-4 w-4',
action.accent ? 'text-primary' : 'text-muted-foreground'
)} />
</div>
<div className="min-w-0">
<p className="text-sm font-medium truncate">{action.label}</p>
<p className="text-sm font-medium truncate flex items-center gap-1.5">
<Icon className={cn(
'h-3.5 w-3.5 flex-shrink-0',
action.accent ? 'text-primary' : 'text-muted-foreground'
)} />
{action.label}
</p>
<p className="text-xs text-muted-foreground truncate hidden md:block">{action.desc}</p>
</div>
</div>
@@ -493,11 +424,11 @@ export default function DashboardContent({ firstName, settings, summary, onboard
return (
<Link key={action.key} href={action.href} className="group">
<div className="flex items-center gap-3 px-4 py-3 rounded-xl border border-border/40 hover:bg-muted/30 transition-colors duration-150">
<div className="p-2 rounded-lg bg-muted/50">
<Icon className="h-4 w-4 text-muted-foreground" />
</div>
<div className="min-w-0">
<p className="text-sm font-medium truncate">{action.label}</p>
<p className="text-sm font-medium truncate flex items-center gap-1.5">
<Icon className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
{action.label}
</p>
<p className="text-xs text-muted-foreground truncate hidden md:block">{action.description}</p>
</div>
</div>
@@ -511,11 +442,11 @@ export default function DashboardContent({ firstName, settings, summary, onboard
className="group text-left"
>
<div className="flex items-center gap-3 px-4 py-3 rounded-xl border border-border/40 hover:bg-muted/30 transition-colors duration-150">
<div className="p-2 rounded-lg bg-muted/50">
<Icon className="h-4 w-4 text-muted-foreground" />
</div>
<div className="min-w-0">
<p className="text-sm font-medium truncate">{action.label}</p>
<p className="text-sm font-medium truncate flex items-center gap-1.5">
<Icon className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
{action.label}
</p>
<p className="text-xs text-muted-foreground truncate hidden md:block">{action.description}</p>
</div>
</div>
@@ -525,21 +456,7 @@ export default function DashboardContent({ firstName, settings, summary, onboard
</div>
</section>
{/* Upcoming deadlines — always visible */}
{summary.deadlines && summary.deadlines.length > 0 && (
<section className="mb-10">
<UpcomingDeadlinesWidget deadlines={summary.deadlines} maxItems={8} />
</section>
)}
{/* Tax todo widget — visible when there are incomplete tax deadlines */}
{summary.deadlines?.some(d => d.deadline_type === 'tax' && !d.is_completed) && (
<section className="mb-10">
<TaxTodoWidget deadlines={summary.deadlines} />
</section>
)}
{/* Alerts section — always visible */}
{/* Alerts section */}
{alertItems.length > 0 && (
<section id="alerts-section" className="mb-10">
<h2 className="font-display text-lg font-medium mb-4">Att hantera</h2>
@@ -558,6 +475,21 @@ export default function DashboardContent({ firstName, settings, summary, onboard
</section>
)}
{/* Upcoming deadlines — always visible */}
{summary.deadlines && summary.deadlines.length > 0 && (
<section className="mb-10">
<UpcomingDeadlinesWidget deadlines={summary.deadlines} maxItems={8} />
</section>
)}
{/* Tax todo widget — visible when there are incomplete tax deadlines */}
{summary.deadlines?.some(d => d.deadline_type === 'tax' && !d.is_completed) && (
<section className="mb-10">
<TaxTodoWidget deadlines={summary.deadlines} />
</section>
)}
{/* Collapsible details section */}
<button
onClick={() => setShowMore(!showMore)}
@@ -607,7 +539,6 @@ export default function DashboardContent({ firstName, settings, summary, onboard
{' '}saknas i resultatet
</p>
</div>
<ArrowRight className="h-3.5 w-3.5 text-muted-foreground/50 group-hover:text-muted-foreground flex-shrink-0 mt-0.5 transition-colors" />
</div>
</Link>
</section>
@@ -619,10 +550,7 @@ export default function DashboardContent({ firstName, settings, summary, onboard
<div className="grid md:grid-cols-2 gap-3">
<Card>
<CardContent className="p-5">
<div className="flex items-center gap-2 mb-3">
<TrendingUp className="h-3.5 w-3.5 text-success" />
<span className="text-sm text-muted-foreground">Intäkter</span>
</div>
<p className="text-sm text-muted-foreground mb-3">Intäkter</p>
<div>
<p className="font-display text-2xl font-medium tabular-nums leading-tight">
{formatLargeNumber(summary.mtd.income)}
@@ -641,10 +569,7 @@ export default function DashboardContent({ firstName, settings, summary, onboard
<Card>
<CardContent className="p-5">
<div className="flex items-center gap-2 mb-3">
<TrendingDown className="h-3.5 w-3.5 text-destructive" />
<span className="text-sm text-muted-foreground">Kostnader</span>
</div>
<p className="text-sm text-muted-foreground mb-3">Kostnader</p>
<div>
<p className="font-display text-2xl font-medium tabular-nums leading-tight">
{formatLargeNumber(summary.mtd.expenses)}
+4 -153
View File
@@ -1,6 +1,6 @@
'use client'
import { useState, useCallback, useEffect } from 'react'
import { useState } from 'react'
import Link from 'next/link'
import { usePathname, useRouter } from 'next/navigation'
import { createClient } from '@/lib/supabase/client'
@@ -23,16 +23,13 @@ import {
ChevronDown,
Building2,
FileInput,
Store,
Wallet,
} from 'lucide-react'
import { getExtensionDefinition } from '@/lib/extensions/sectors'
import { resolveIcon } from '@/lib/extensions/icon-resolver'
import type { EntityType } from '@/types'
interface DashboardNavProps {
companyName: string
entityType: EntityType
enabledExtensions?: { sector_slug: string; extension_slug: string }[]
uncategorizedTransactionCount?: number
}
@@ -51,6 +48,7 @@ const navItems: NavItem[] = [
{ href: '/deadlines', label: 'Deadlines', icon: Calendar, group: 'main' },
{ href: '/invoices', label: 'Fakturor', icon: Receipt, group: 'finans' },
{ href: '/customers', label: 'Kunder', icon: Users, group: 'finans' },
{ href: '/expenses', label: 'Utgifter', icon: Wallet, group: 'finans' },
// Temporarily hidden pending module rework (see feedback #49)
{ href: '/suppliers', label: 'Leverantörer', icon: Building2, group: 'finans', hidden: true },
{ href: '/supplier-invoices', label: 'Leverantörsfakturor', icon: FileInput, group: 'finans', hidden: true },
@@ -68,7 +66,7 @@ const groupLabels: Record<string, string> = {
övrigt: 'Övrigt',
}
export default function DashboardNav({ companyName, entityType, enabledExtensions, uncategorizedTransactionCount = 0 }: DashboardNavProps) {
export default function DashboardNav({ companyName, entityType, uncategorizedTransactionCount = 0 }: DashboardNavProps) {
const pathname = usePathname()
const router = useRouter()
const supabase = createClient()
@@ -77,46 +75,8 @@ export default function DashboardNav({ companyName, entityType, enabledExtension
const isOnOvrigtPage = ['/help', '/settings'].some(p => pathname.startsWith(p))
const [manualOvrigtExpanded, setManualOvrigtExpanded] = useState(false)
const isOvrigtExpanded = isOnOvrigtPage || manualOvrigtExpanded
// Auto-expand Tillägg when on an extension page or marketplace, or when manually toggled (persisted)
const isOnExtensionPage = pathname.startsWith('/e/') || pathname.startsWith('/extensions')
const [manualTillaggExpanded, setManualTillaggExpanded] = useState(false)
const isTillaggExpanded = isOnExtensionPage || manualTillaggExpanded
// Restore persisted state after hydration to avoid SSR mismatch
useEffect(() => {
const stored = localStorage.getItem('tillagg-expanded') === 'true'
if (stored) setManualTillaggExpanded(true)
}, [])
const [liveExtensions, setLiveExtensions] = useState(enabledExtensions ?? [])
const fetchExtensions = useCallback(async () => {
try {
const res = await fetch('/api/extensions/toggles')
if (res.ok) {
const { data } = await res.json()
if (data) setLiveExtensions(data)
}
} catch {
// keep current state on error
}
}, [])
// Fetch extensions on mount if Tillägg starts expanded
useEffect(() => {
if (isTillaggExpanded) fetchExtensions()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const toggleTillagg = () => {
const next = !isTillaggExpanded
setManualTillaggExpanded(next)
localStorage.setItem('tillagg-expanded', String(next))
if (next) fetchExtensions()
}
const openMobileMenu = () => {
setIsMobileMenuOpen(true)
fetchExtensions()
}
const handleLogout = async () => {
@@ -233,68 +193,6 @@ export default function DashboardNav({ companyName, entityType, enabledExtension
</div>
</div>
{/* Tillägg - collapsible, with marketplace link */}
<div className="mb-4">
<button
onClick={toggleTillagg}
className="w-full flex items-center justify-between px-3 mb-1.5 text-[10px] font-semibold text-muted-foreground/70 uppercase tracking-[0.08em] hover:text-muted-foreground transition-colors"
>
<span>Tillägg</span>
<ChevronDown className={cn(
"h-3 w-3 transition-transform duration-200",
isTillaggExpanded && "rotate-180"
)} />
</button>
{isTillaggExpanded && (
<div className="space-y-px animate-fade-in">
{liveExtensions.length > 0 ? liveExtensions.map((toggle) => {
const def = getExtensionDefinition(toggle.sector_slug, toggle.extension_slug)
if (!def) return null
const ExtIcon = resolveIcon(def.icon)
const href = `/e/${toggle.sector_slug}/${toggle.extension_slug}`
const active = isActive(href)
return (
<Link
key={`${toggle.sector_slug}/${toggle.extension_slug}`}
href={href}
className={cn(
'group flex items-center px-3 py-[7px] text-[13px] transition-colors duration-150 rounded-lg',
active
? 'bg-primary/8 text-foreground font-medium'
: 'text-muted-foreground hover:text-foreground hover:bg-muted/40'
)}
>
<ExtIcon className={cn(
"mr-2.5 h-[15px] w-[15px] flex-shrink-0",
active ? "text-primary" : "text-muted-foreground/70 group-hover:text-muted-foreground"
)} />
{def.name}
</Link>
)
}) : (
<p className="px-3 py-2 text-[12px] text-muted-foreground/60">
Inga tillägg aktiverade
</p>
)}
<Link
href="/extensions"
className={cn(
'group flex items-center px-3 py-[7px] text-[13px] transition-colors duration-150 rounded-lg',
isActive('/extensions')
? 'bg-primary/8 text-foreground font-medium'
: 'text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted/40'
)}
>
<Store className={cn(
"mr-2.5 h-[15px] w-[15px] flex-shrink-0",
isActive('/extensions') ? "text-primary" : "text-muted-foreground/50 group-hover:text-muted-foreground"
)} />
Utforska fler...
</Link>
</div>
)}
</div>
{/* Övrigt group - collapsible */}
<div className="mb-4">
<button
@@ -488,53 +386,6 @@ export default function DashboardNav({ companyName, entityType, enabledExtension
})}
</div>
{/* Tillägg */}
<div className="mb-4">
<p className="px-3 py-2 text-xs font-medium text-muted-foreground uppercase tracking-wider">
Tillägg
</p>
{liveExtensions.length > 0 ? liveExtensions.map((toggle) => {
const def = getExtensionDefinition(toggle.sector_slug, toggle.extension_slug)
if (!def) return null
const ExtIcon = resolveIcon(def.icon)
const href = `/e/${toggle.sector_slug}/${toggle.extension_slug}`
const active = isActive(href)
return (
<Link
key={`${toggle.sector_slug}/${toggle.extension_slug}`}
href={href}
onClick={closeMobileMenu}
className={cn(
'flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors',
active
? 'bg-primary/10 text-primary font-medium'
: 'text-muted-foreground hover:bg-secondary/50 hover:text-foreground'
)}
>
<ExtIcon className="h-5 w-5" />
{def.name}
</Link>
)
}) : (
<p className="px-3 py-2 text-xs text-muted-foreground/60">
Inga tillägg aktiverade
</p>
)}
<Link
href="/extensions"
onClick={closeMobileMenu}
className={cn(
'flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors',
isActive('/extensions')
? 'bg-primary/10 text-primary font-medium'
: 'text-muted-foreground/60 hover:bg-secondary/50 hover:text-foreground'
)}
>
<Store className="h-5 w-5" />
Utforska fler...
</Link>
</div>
{/* Other section */}
<div className="mb-4">
<p className="px-3 py-2 text-xs font-medium text-muted-foreground uppercase tracking-wider">