'use client' import { useCallback, useEffect, useState } from 'react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' import { Checkbox } from '@/components/ui/checkbox' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { ArrowRight, Loader2, Plus, Trash2 } from 'lucide-react' import { formatCurrency } from '@/lib/utils' import { useToast } from '@/components/ui/use-toast' import type { AccrualsProposal } from '@/lib/bokslut/accruals/types' import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message' interface AccrualsStepProps { periodId: string onBack: () => void onContinue: () => void } interface AutoState { vacation: { accept: boolean } } interface ManualEntry { id: string kind: 'audit_fee' | 'manual_prepaid_expense' | 'manual_accrued_expense' amount: string description: string expenseAccount: string prepaidAccount: string accruedAccount: string liabilityAccount: '2991' | '2992' } function makeId() { return Math.random().toString(36).slice(2, 10) } export function AccrualsStep({ periodId, onBack, onContinue }: AccrualsStepProps) { const { toast } = useToast() const [proposal, setProposal] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [auto, setAuto] = useState({ vacation: { accept: true } }) const [manual, setManual] = useState([]) const [posting, setPosting] = useState(false) useEffect(() => { let cancelled = false setLoading(true) setError(null) fetch(`/api/bookkeeping/fiscal-periods/${periodId}/accruals`) .then(async (res) => { const body = await res.json() if (cancelled) return if (!res.ok) { setError(getUserErrorMessage(body?.error) ?? 'Kunde inte ladda periodiseringar') return } setProposal(body.data as AccrualsProposal) }) .catch(() => { if (!cancelled) setError('Kunde inte ladda periodiseringar') }) .finally(() => { if (!cancelled) setLoading(false) }) return () => { cancelled = true } }, [periodId]) const addManual = useCallback((kind: ManualEntry['kind']) => { setManual((prev) => [ ...prev, { id: makeId(), kind, amount: '', description: '', expenseAccount: kind === 'audit_fee' ? '6420' : '', prepaidAccount: '', accruedAccount: '', liabilityAccount: '2992', }, ]) }, []) const removeManual = useCallback((id: string) => { setManual((prev) => prev.filter((m) => m.id !== id)) }, []) const updateManual = useCallback((id: string, patch: Partial) => { setManual((prev) => prev.map((m) => (m.id === id ? { ...m, ...patch } : m))) }, []) const handleCommit = useCallback(async () => { if (!proposal) return setPosting(true) try { const items: unknown[] = [] if (proposal.proposals.find((p) => p.kind === 'vacation_liability_change') && auto.vacation.accept) { items.push({ kind: 'vacation_liability_change' }) } for (const m of manual) { const amount = parseFloat(m.amount) if (!Number.isFinite(amount) || amount <= 0) continue if (m.kind === 'audit_fee') { items.push({ kind: 'audit_fee', amount, liability_account: m.liabilityAccount }) } else if (m.kind === 'manual_prepaid_expense') { if (!m.expenseAccount || !m.prepaidAccount || !m.description) continue items.push({ kind: 'manual_prepaid_expense', amount, expense_account: m.expenseAccount, prepaid_account: m.prepaidAccount, description: m.description, }) } else if (m.kind === 'manual_accrued_expense') { if (!m.expenseAccount || !m.accruedAccount || !m.description) continue items.push({ kind: 'manual_accrued_expense', amount, expense_account: m.expenseAccount, accrued_account: m.accruedAccount, description: m.description, }) } } if (items.length === 0) { onContinue() return } const res = await fetch(`/api/bookkeeping/fiscal-periods/${periodId}/accruals`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items }), }) const body = await res.json() if (!res.ok) { setError(getUserErrorMessage(body?.error) ?? 'Kunde inte bokföra periodiseringarna') return } const created = body.data?.created?.length ?? 0 toast({ title: `${created} periodisering${created === 1 ? '' : 'ar'} bokförd${ created === 1 ? '' : 'a' }`, description: 'Vänd dem första dagen i nästa räkenskapsår.', }) onContinue() } catch (err) { setError(err instanceof Error ? getUserErrorMessage(err) : 'Okänt fel') } finally { setPosting(false) } }, [proposal, auto, manual, periodId, onContinue, toast]) if (loading) { return ( ) } if (error && !proposal) { return ( {error} ) } if (!proposal) return null const vacation = proposal.proposals.find((p) => p.kind === 'vacation_liability_change') return (
Periodiseringar

Förutbetalda kostnader (17xx) och upplupna kostnader (29xx). Posteringarna ska vändas på första dagen av nästa räkenskapsår: datumet visas per verifikation. Automatisk omvändning är planerad till en kommande version.

{vacation && (
{vacation.label}

{vacation.description}

{vacation.reverses_on ? ( Vänds {vacation.reverses_on} ) : ( Rullas vidare (ingen vändning) )}

{formatCurrency(vacation.amount)}

setAuto({ vacation: { accept: Boolean(c) } })} />
)} Manuella periodiseringar

Lägg till revisionsarvode, hyra som löper över årsskiftet, förutbetalda försäkringar m.m.

{manual.length === 0 && (

Inga manuella periodiseringar tillagda än.

)} {manual.map((m) => ( updateManual(m.id, patch)} onRemove={() => removeManual(m.id)} /> ))}
{error && ( {error} )}
) } function ManualEntryEditor({ entry, onChange, onRemove, }: { entry: ManualEntry onChange: (patch: Partial) => void onRemove: () => void }) { return (

{entry.kind === 'audit_fee' && 'Revisions-/bokslutsarvode'} {entry.kind === 'manual_prepaid_expense' && 'Förutbetald kostnad'} {entry.kind === 'manual_accrued_expense' && 'Upplupen kostnad'}

onChange({ amount: e.target.value })} className="tabular-nums h-8" />
{entry.kind === 'audit_fee' && (
)} {entry.kind !== 'audit_fee' && ( <>
onChange({ expenseAccount: e.target.value })} placeholder="t.ex. 6310" className="tabular-nums h-8" />
{entry.kind === 'manual_prepaid_expense' && (
onChange({ prepaidAccount: e.target.value })} placeholder="t.ex. 1730" className="tabular-nums h-8" />
)} {entry.kind === 'manual_accrued_expense' && (
onChange({ accruedAccount: e.target.value })} placeholder="t.ex. 2990" className="tabular-nums h-8" />
)}
onChange({ description: e.target.value })} placeholder="t.ex. Försäkring 2026" className="h-8" />
)}
) }