ba94f60d06
Add modal review/confirmation dialogs before submitting invoices, supplier invoices, and journal entries. Since journal entries are legally immutable once posted, users now see a full summary with an amber warning before confirming. Add AccountNumber component with rich tooltips showing account name, class, type, and plain-language Swedish explanation for ~45 key BAS accounts across all report views. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
211 lines
7.9 KiB
TypeScript
211 lines
7.9 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { createClient } from '@/lib/supabase/client'
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import { ChevronDown, ChevronRight, Search } from 'lucide-react'
|
|
import { AccountNumber } from '@/components/ui/account-number'
|
|
import type { BASAccount } from '@/types'
|
|
|
|
const CLASS_LABELS: Record<number, string> = {
|
|
1: 'Tillgångar',
|
|
2: 'Eget kapital och skulder',
|
|
3: 'Intäkter',
|
|
4: 'Varor och material',
|
|
5: 'Övriga externa kostnader',
|
|
6: 'Övriga externa kostnader',
|
|
7: 'Personal och avskrivningar',
|
|
8: 'Finansiella poster',
|
|
}
|
|
|
|
export default function ChartOfAccounts() {
|
|
const [accounts, setAccounts] = useState<BASAccount[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [expandedClasses, setExpandedClasses] = useState<Set<number>>(new Set())
|
|
const [searchQuery, setSearchQuery] = useState('')
|
|
const [editingSRU, setEditingSRU] = useState<string | null>(null)
|
|
const [sruValue, setSruValue] = useState('')
|
|
|
|
useEffect(() => {
|
|
fetchAccounts()
|
|
}, [])
|
|
|
|
async function fetchAccounts() {
|
|
const res = await fetch('/api/bookkeeping/accounts')
|
|
const { data } = await res.json()
|
|
setAccounts(data || [])
|
|
setLoading(false)
|
|
}
|
|
|
|
async function updateSRUCode(accountId: string, newSruCode: string) {
|
|
const supabase = createClient()
|
|
const trimmed = newSruCode.trim() || null
|
|
await supabase
|
|
.from('chart_of_accounts')
|
|
.update({ sru_code: trimmed })
|
|
.eq('id', accountId)
|
|
|
|
setAccounts((prev) =>
|
|
prev.map((a) => (a.id === accountId ? { ...a, sru_code: trimmed } : a))
|
|
)
|
|
setEditingSRU(null)
|
|
}
|
|
|
|
const toggleClass = (cls: number) => {
|
|
const next = new Set(expandedClasses)
|
|
if (next.has(cls)) {
|
|
next.delete(cls)
|
|
} else {
|
|
next.add(cls)
|
|
}
|
|
setExpandedClasses(next)
|
|
}
|
|
|
|
const filteredAccounts = accounts.filter((a) => {
|
|
if (!searchQuery) return true
|
|
const q = searchQuery.toLowerCase()
|
|
return (
|
|
a.account_number.includes(q) ||
|
|
a.account_name.toLowerCase().includes(q)
|
|
)
|
|
})
|
|
|
|
const groupedByClass = filteredAccounts.reduce(
|
|
(acc, account) => {
|
|
const cls = account.account_class
|
|
if (!acc[cls]) acc[cls] = []
|
|
acc[cls].push(account)
|
|
return acc
|
|
},
|
|
{} as Record<number, BASAccount[]>
|
|
)
|
|
|
|
if (loading) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="p-8 text-center text-muted-foreground">
|
|
Laddar kontoplan...
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder="Sök konto (nummer eller namn)..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="pl-9"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
{Object.entries(groupedByClass)
|
|
.sort(([a], [b]) => Number(a) - Number(b))
|
|
.map(([cls, classAccounts]) => {
|
|
const classNum = Number(cls)
|
|
const isExpanded = expandedClasses.has(classNum) || !!searchQuery
|
|
|
|
return (
|
|
<Card key={cls}>
|
|
<button
|
|
onClick={() => toggleClass(classNum)}
|
|
className="w-full flex items-center justify-between p-4 hover:bg-muted/50 transition-colors"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
{isExpanded ? (
|
|
<ChevronDown className="h-4 w-4" />
|
|
) : (
|
|
<ChevronRight className="h-4 w-4" />
|
|
)}
|
|
<span className="font-semibold">
|
|
Klass {cls}: {CLASS_LABELS[classNum] || ''}
|
|
</span>
|
|
<Badge variant="secondary">{classAccounts.length} konton</Badge>
|
|
</div>
|
|
</button>
|
|
|
|
{isExpanded && (
|
|
<CardContent className="pt-0 pb-4">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b text-left text-muted-foreground">
|
|
<th className="py-2 w-24">Konto</th>
|
|
<th className="py-2">Namn</th>
|
|
<th className="py-2 w-20 text-center">SRU</th>
|
|
<th className="py-2 w-24 text-center">Typ</th>
|
|
<th className="py-2 w-24 text-center">Normal</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{classAccounts.map((account) => (
|
|
<tr
|
|
key={account.id}
|
|
className={`border-b last:border-0 ${
|
|
!account.is_active ? 'opacity-50' : ''
|
|
}`}
|
|
>
|
|
<td className="py-2"><AccountNumber number={account.account_number} name={account.account_name} /></td>
|
|
<td className="py-2">{account.account_name}</td>
|
|
<td className="py-2 text-center">
|
|
{editingSRU === account.id ? (
|
|
<Input
|
|
value={sruValue}
|
|
onChange={(e) => setSruValue(e.target.value)}
|
|
onBlur={() => updateSRUCode(account.id, sruValue)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') updateSRUCode(account.id, sruValue)
|
|
if (e.key === 'Escape') setEditingSRU(null)
|
|
}}
|
|
className="h-7 w-16 text-xs text-center px-1"
|
|
autoFocus
|
|
/>
|
|
) : (
|
|
<button
|
|
className="text-xs font-mono text-muted-foreground hover:text-foreground cursor-pointer min-w-[2rem]"
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
setEditingSRU(account.id)
|
|
setSruValue(account.sru_code || '')
|
|
}}
|
|
>
|
|
{account.sru_code || '—'}
|
|
</button>
|
|
)}
|
|
</td>
|
|
<td className="py-2 text-center">
|
|
<Badge variant="outline" className="text-xs">
|
|
{account.account_type === 'asset'
|
|
? 'Tillgång'
|
|
: account.account_type === 'liability'
|
|
? 'Skuld'
|
|
: account.account_type === 'equity'
|
|
? 'EK'
|
|
: account.account_type === 'revenue'
|
|
? 'Intäkt'
|
|
: 'Kostnad'}
|
|
</Badge>
|
|
</td>
|
|
<td className="py-2 text-center text-xs text-muted-foreground">
|
|
{account.normal_balance === 'debit' ? 'Debet' : 'Kredit'}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</CardContent>
|
|
)}
|
|
</Card>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|