'use client' import { useState, useEffect } from 'react' import { useRouter } from 'next/navigation' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Badge } from '@/components/ui/badge' import AccountCombobox from '@/components/bookkeeping/AccountCombobox' import CorrectionPreview from '@/components/bookkeeping/CorrectionPreview' import { useToast } from '@/components/ui/use-toast' import { getErrorMessage } from '@/lib/errors/get-error-message' import { Plus, Trash2 } from 'lucide-react' import { formatDate } from '@/lib/utils' import { formatVoucher } from '@/lib/bookkeeping/voucher-series-resolver' import type { JournalEntry, JournalEntryLine, BASAccount } from '@/types' interface CorrectionLine { account_number: string debit_amount: string credit_amount: string line_description: string } interface Props { entry: JournalEntry open: boolean onOpenChange: (open: boolean) => void onCorrected: () => void } export default function CorrectionEntryDialog({ entry, open, onOpenChange, onCorrected }: Props) { const { toast } = useToast() const router = useRouter() const [accounts, setAccounts] = useState([]) const [lines, setLines] = useState([]) const [isSubmitting, setIsSubmitting] = useState(false) const originalLines = ((entry.lines || []) as JournalEntryLine[]) .slice() .sort((a, b) => a.sort_order - b.sort_order) useEffect(() => { if (open) { // Pre-fill with original entry's lines setLines( originalLines.map((l) => ({ account_number: l.account_number, debit_amount: Number(l.debit_amount) > 0 ? String(Number(l.debit_amount)) : '', credit_amount: Number(l.credit_amount) > 0 ? String(Number(l.credit_amount)) : '', line_description: l.line_description || '', })) ) fetchAccounts() } }, [open, entry.id]) // eslint-disable-line react-hooks/exhaustive-deps async function fetchAccounts() { try { const res = await fetch('/api/bookkeeping/accounts') const { data } = await res.json() setAccounts(data || []) } catch { // Accounts will be empty — user can still type account numbers manually } } const updateLine = (index: number, field: keyof CorrectionLine, value: string) => { setLines((prev) => prev.map((l, i) => (i === index ? { ...l, [field]: value } : l))) } const addLine = () => { setLines((prev) => [...prev, { account_number: '', debit_amount: '', credit_amount: '', line_description: '' }]) } const removeLine = (index: number) => { setLines((prev) => prev.filter((_, i) => i !== index)) } const totalDebit = lines.reduce((sum, l) => sum + (parseFloat(l.debit_amount) || 0), 0) const totalCredit = lines.reduce((sum, l) => sum + (parseFloat(l.credit_amount) || 0), 0) const roundedDebit = Math.round(totalDebit * 100) / 100 const roundedCredit = Math.round(totalCredit * 100) / 100 const isBalanced = roundedDebit === roundedCredit && roundedDebit > 0 const hasValidLines = lines.length >= 2 && lines.every((l) => l.account_number.length === 4) async function handleSubmit() { if (!isBalanced || !hasValidLines) return setIsSubmitting(true) try { const apiLines = lines.map((l) => ({ account_number: l.account_number, debit_amount: parseFloat(l.debit_amount) || 0, credit_amount: parseFloat(l.credit_amount) || 0, line_description: l.line_description || undefined, })) const res = await fetch(`/api/bookkeeping/journal-entries/${entry.id}/correct`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ lines: apiLines }), }) const result = await res.json() if (!res.ok) { const error = new Error('Failed to create correction') as Error & { body?: unknown; status?: number } error.body = result error.status = res.status throw error } const correctedId = result.data?.corrected?.id toast({ title: 'Ändringsverifikation skapad', description: 'Storno och rättelse har bokförts.', action: correctedId ? ( ) : undefined, }) onOpenChange(false) onCorrected() } catch (err) { const anyErr = err as { body?: unknown; status?: number } toast({ title: 'Kunde inte spara ändringsverifikation', description: getErrorMessage(anyErr.body ?? err, { context: 'journal_entry', statusCode: anyErr.status }), variant: 'destructive', }) } finally { setIsSubmitting(false) } } return ( Skapa ändringsverifikation {/* Storno explanation */}

Hur fungerar en ändringsverifikation?

En bokförd verifikation kan inte ändras direkt. Istället skapas automatiskt:

  1. En stornoverifikation som nollställer den ursprungliga
  2. En ny verifikation med dina rättade uppgifter

Rättelsen bokförs i samma räkenskapsperiod som originalet — du hittar den under originalets räkenskapsår.

{/* Original entry metadata — lines live inside CorrectionPreview below */}
{formatVoucher(entry)} {formatDate(entry.entry_date)} Original

{entry.description}

{/* Live diff: original | storno | correction | förändring */} {/* Corrected lines (editable) */}

Rättade rader

Det här är hela den nya verifikationen — alla konton som ska finnas kvar måste stå kvar. Tar du bort ett konto nollställs det (stornon återför det). Vill du bara återföra hela verifikatet utan att ersätta det, använd Återför (storno) istället.

{lines.map((line, index) => (
updateLine(index, 'account_number', v)} />
updateLine(index, 'line_description', e.target.value)} placeholder="Beskrivning" className="h-8" />
updateLine(index, 'debit_amount', e.target.value)} placeholder="Debet" className="h-8 text-right" min={0} step="0.01" /> updateLine(index, 'credit_amount', e.target.value)} placeholder="Kredit" className="h-8 text-right" min={0} step="0.01" />
))}
{/* Balance summary */}
Debet: {roundedDebit.toLocaleString('sv-SE', { minimumFractionDigits: 2 })}
Kredit: {roundedCredit.toLocaleString('sv-SE', { minimumFractionDigits: 2 })}
{!isBalanced && roundedDebit + roundedCredit > 0 && (

Debet och kredit måste vara lika och större än 0.

)}
) }