'use client' import { useState, useEffect, useCallback } from 'react' import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Label } from '@/components/ui/label' import { AlertCircle, Loader2, CheckCircle2 } from 'lucide-react' import type { FiscalPeriod } from '@/types' interface EditableRow { id: string account_number: string account_name: string debit_amount: number credit_amount: number } interface OpeningBalancePeriodStepProps { rows: EditableRow[] /** `replace` is true when the selected period already has opening balances * (the existing IB verifikat will be stornoed and replaced). */ onExecute: (fiscalPeriodId: string, replace: boolean) => void onBack: () => void isLoading: boolean error: string | null } export default function OpeningBalancePeriodStep({ rows, onExecute, onBack, isLoading, error, }: OpeningBalancePeriodStepProps) { const [periods, setPeriods] = useState([]) const [selectedPeriodId, setSelectedPeriodId] = useState('') const [loadingPeriods, setLoadingPeriods] = useState(true) // Compute totals let totalDebit = 0 let totalCredit = 0 for (const row of rows) { totalDebit = Math.round((totalDebit + row.debit_amount) * 100) / 100 totalCredit = Math.round((totalCredit + row.credit_amount) * 100) / 100 } // Compare in whole öre, mirroring the engine's validateBalance: a float // epsilon like (< 0.01) would misclassify exact 1-öre imbalances as // balanced, since e.g. 0.03 - 0.02 evaluates to just under 0.01. const balanceDiff = Math.round((totalDebit - totalCredit) * 100) / 100 const isBalanced = Math.round((totalDebit - totalCredit) * 100) === 0 useEffect(() => { async function fetchPeriods() { setLoadingPeriods(true) try { const res = await fetch('/api/bookkeeping/fiscal-periods') if (res.ok) { const data = await res.json() const allPeriods: FiscalPeriod[] = data.data || [] setPeriods(allPeriods) // Auto-select first open period without OB const openPeriod = allPeriods.find( (p) => !p.is_closed && !p.locked_at && !p.opening_balances_set, ) if (openPeriod) { setSelectedPeriodId(openPeriod.id) } } } catch { // Silent, user can still select period } finally { setLoadingPeriods(false) } } fetchPeriods() }, []) const selectedPeriod = periods.find((p) => p.id === selectedPeriodId) const periodHasOB = !!selectedPeriod?.opening_balances_set const periodIsClosed = selectedPeriod?.is_closed const periodIsLocked = !!selectedPeriod?.locked_at // A period that already has IB can still be corrected, as long as it is open // and unlocked: the existing IB verifikat is stornoed and replaced. const canExecute = !!selectedPeriodId && !periodIsClosed && !periodIsLocked && isBalanced && !isLoading const handleExecute = useCallback(() => { if (canExecute) { onExecute(selectedPeriodId, periodHasOB) } }, [canExecute, selectedPeriodId, periodHasOB, onExecute]) return ( Välj räkenskapsperiod Välj vilken räkenskapsperiod de ingående balanserna ska bokföras på. {/* Period selector */}
{loadingPeriods ? (
Hämtar perioder...
) : periods.length === 0 ? (

Inga räkenskapsperioder hittades. Skapa en räkenskapsperiod under Bokföring först.

) : ( )}
{/* Replace notice: selecting a period that already has IB corrects it */} {periodHasOB && !periodIsClosed && !periodIsLocked && (

Denna period har redan ingående balanser. Om du fortsätter makuleras (stornas) den befintliga IB-verifikationen och en ny bokförs med beloppen nedan.

)} {/* Summary */}

Sammanfattning

Antal konton: {rows.length} Total debet: {totalDebit.toLocaleString('sv-SE', { minimumFractionDigits: 2 })} SEK Total kredit: {totalCredit.toLocaleString('sv-SE', { minimumFractionDigits: 2 })} SEK
{isBalanced ? (
Balanserar
) : (
Balanserar inte (differens{' '} {balanceDiff.toLocaleString('sv-SE', { minimumFractionDigits: 2 })} {' '} SEK)
)}
{/* Error */} {error && (

{error}

)} {/* Actions */}
) }