'use client' import { useState, useEffect, useCallback, useMemo } from 'react' import { Card, CardContent } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Switch } from '@/components/ui/switch' import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' import { useToast } from '@/components/ui/use-toast' import { AccountNumber } from '@/components/ui/account-number' import { AddAccountDialog } from './AddAccountDialog' import { EditAccountDialog } from './EditAccountDialog' import { Search, ChevronDown, ChevronRight, Plus, Pencil, Trash2, Loader2, CheckCircle2, BookOpen, } from 'lucide-react' import type { BASAccount } from '@/types' import type { BASReferenceAccount } from '@/lib/bookkeeping/bas-reference' // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- interface ReferenceAccount extends BASReferenceAccount { is_activated: boolean is_active: boolean is_system_account: boolean } // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const CLASS_LABELS: Record = { 1: 'Tillgångar', 2: 'Eget kapital och skulder', 3: 'Rörelseintäkter', 4: 'Varuinköp och material', 5: 'Övriga externa kostnader', 6: 'Övriga externa kostnader', 7: 'Personalkostnader och avskrivningar', 8: 'Finansiella poster och resultat', } const TYPE_LABELS: Record = { asset: 'Tillgång', liability: 'Skuld', equity: 'EK', revenue: 'Intakt', expense: 'Kostnad', untaxed_reserves: 'Ob. reserver', } // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- export default function ChartOfAccountsManager() { const { toast } = useToast() // View state const [view, setView] = useState<'my-accounts' | 'bas-catalog'>('my-accounts') const [searchQuery, setSearchQuery] = useState('') const [expandedClasses, setExpandedClasses] = useState>(new Set()) const [hideK2Excluded, setHideK2Excluded] = useState(null) // Data state const [accounts, setAccounts] = useState([]) const [referenceAccounts, setReferenceAccounts] = useState([]) const [loading, setLoading] = useState(true) // Dialog state const [addDialogOpen, setAddDialogOpen] = useState(false) const [editAccount, setEditAccount] = useState(null) // Action states const [togglingAccount, setTogglingAccount] = useState(null) const [deletingAccount, setDeletingAccount] = useState(null) const [activatingAccounts, setActivatingAccounts] = useState>(new Set()) // ------------------------------------------- // Data fetching // ------------------------------------------- const fetchAccounts = useCallback(async () => { const res = await fetch('/api/bookkeeping/accounts') const { data } = await res.json() setAccounts(data || []) }, []) const fetchReference = useCallback(async () => { const res = await fetch('/api/bookkeeping/accounts/reference') const { data } = await res.json() setReferenceAccounts(data || []) }, []) useEffect(() => { async function load() { setLoading(true) await Promise.all([fetchAccounts(), fetchReference()]) // Set K2 filter default based on company settings (plan_type) if (hideK2Excluded === null) { try { const res = await fetch('/api/settings') if (res.ok) { const { data } = await res.json() // Default to hiding K2-excluded accounts if the company uses K2 (plan_type === 'k1') setHideK2Excluded(data?.plan_type === 'k1') } else { setHideK2Excluded(false) } } catch { setHideK2Excluded(false) } } setLoading(false) } load() }, [fetchAccounts, fetchReference, hideK2Excluded]) const refreshAll = useCallback(async () => { await Promise.all([fetchAccounts(), fetchReference()]) }, [fetchAccounts, fetchReference]) // ------------------------------------------- // Actions // ------------------------------------------- async function toggleActive(account: BASAccount) { setTogglingAccount(account.account_number) try { const res = await fetch(`/api/bookkeeping/accounts/${account.account_number}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ is_active: !account.is_active }), }) if (!res.ok) throw new Error('Kunde inte uppdatera kontot') await refreshAll() } catch { toast({ title: 'Fel', description: 'Kunde inte uppdatera kontot', variant: 'destructive' }) } finally { setTogglingAccount(null) } } async function deleteAccount(account: BASAccount) { setDeletingAccount(account.account_number) try { const res = await fetch(`/api/bookkeeping/accounts/${account.account_number}`, { method: 'DELETE', }) if (!res.ok) { const data = await res.json() throw new Error(data.error || 'Kunde inte ta bort kontot') } toast({ title: 'Konto borttaget', description: `${account.account_number} ${account.account_name}` }) await refreshAll() } catch (err) { toast({ title: 'Fel', description: err instanceof Error ? err.message : 'Kunde inte ta bort kontot', variant: 'destructive', }) } finally { setDeletingAccount(null) } } async function activateBASAccount(accountNumber: string) { setActivatingAccounts((prev) => new Set(prev).add(accountNumber)) try { const res = await fetch('/api/bookkeeping/accounts/activate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ account_numbers: [accountNumber] }), }) if (!res.ok) throw new Error('Kunde inte aktivera kontot') const { activated } = await res.json() if (activated > 0) { toast({ title: 'Konto aktiverat', description: `Konto ${accountNumber} har lagts till i din kontoplan` }) } await refreshAll() } catch { toast({ title: 'Fel', description: 'Kunde inte aktivera kontot', variant: 'destructive' }) } finally { setActivatingAccounts((prev) => { const next = new Set(prev) next.delete(accountNumber) return next }) } } // ------------------------------------------- // Toggle class expansion // ------------------------------------------- function toggleClass(cls: number) { setExpandedClasses((prev) => { const next = new Set(prev) if (next.has(cls)) { next.delete(cls) } else { next.add(cls) } return next }) } // ------------------------------------------- // Filtered & grouped data // ------------------------------------------- const filteredAccounts = useMemo(() => { if (!searchQuery) return accounts const q = searchQuery.toLowerCase() return accounts.filter( (a) => a.account_number.includes(q) || a.account_name.toLowerCase().includes(q) ) }, [accounts, searchQuery]) const groupedAccounts = useMemo(() => { const grouped: Record = {} for (const a of filteredAccounts) { const cls = a.account_class if (!grouped[cls]) grouped[cls] = [] grouped[cls].push(a) } return grouped }, [filteredAccounts]) const filteredReference = useMemo(() => { let filtered = referenceAccounts if (hideK2Excluded) { filtered = filtered.filter((a) => !a.k2_excluded) } if (searchQuery) { const q = searchQuery.toLowerCase() filtered = filtered.filter( (a) => a.account_number.includes(q) || a.account_name.toLowerCase().includes(q) ) } return filtered }, [referenceAccounts, searchQuery, hideK2Excluded]) const groupedReference = useMemo(() => { const grouped: Record = {} for (const a of filteredReference) { const cls = a.account_class if (!grouped[cls]) grouped[cls] = [] grouped[cls].push(a) } return grouped }, [filteredReference]) // ------------------------------------------- // Render // ------------------------------------------- if (loading) { return ( Laddar kontoplan... ) } return (
{/* Header controls */}
{ setView(v as 'my-accounts' | 'bas-catalog') setExpandedClasses(new Set()) }} > Mina konton {accounts.length} BAS-katalog {view === 'my-accounts' && ( )} {view === 'bas-catalog' && ( )}
{/* Search */}
setSearchQuery(e.target.value)} className="pl-9" />
{/* My Accounts view */} {view === 'my-accounts' && (
{Object.entries(groupedAccounts) .sort(([a], [b]) => Number(a) - Number(b)) .map(([cls, classAccounts]) => { const classNum = Number(cls) const isExpanded = expandedClasses.has(classNum) || !!searchQuery const activeCount = classAccounts.filter((a) => a.is_active).length return ( {isExpanded && ( {classAccounts.map((account) => ( ))}
Konto Namn SRU Typ Aktiv
{account.account_name} {account.is_system_account && ( System )} {account.sru_code || '\u2014'} {TYPE_LABELS[account.account_type] || account.account_type} toggleActive(account)} disabled={togglingAccount === account.account_number} className="scale-75" />
{!account.is_system_account && ( )}
)}
) })} {filteredAccounts.length === 0 && ( {searchQuery ? 'Inga konton matchar sökningen' : 'Inga konton i kontoplanen'} )}
)} {/* BAS Catalog view */} {view === 'bas-catalog' && (
{Object.entries(groupedReference) .sort(([a], [b]) => Number(a) - Number(b)) .map(([cls, classAccounts]) => { const classNum = Number(cls) const isExpanded = expandedClasses.has(classNum) || !!searchQuery const activatedCount = classAccounts.filter((a) => a.is_activated).length return ( {isExpanded && ( {classAccounts.map((account) => ( ))}
Konto Namn SRU Typ Status
{account.account_name} {account.description && (

{account.description}

)}
{account.sru_code || '\u2014'} {TYPE_LABELS[account.account_type] || account.account_type} {account.is_activated ? ( Aktiverat ) : ( )}
)}
) })} {filteredReference.length === 0 && ( Inga konton matchar sökningen )}
)} {/* Dialogs */} {editAccount && ( { if (!open) setEditAccount(null) }} account={editAccount} onSaved={refreshAll} /> )}
) }