// CloseFiscalYearWizard - Multi-step wizard for year-end closing (årsafslutning) import { useState, useEffect, useMemo } from 'react'; import { Modal, Steps, Button, Alert, Typography, Table, Statistic, Row, Col, Card, Select, Divider, Checkbox, Result, Tag, Form, } from 'antd'; import { CheckCircleOutlined, WarningOutlined, LockOutlined, ArrowRightOutlined, ExclamationCircleOutlined, } from '@ant-design/icons'; import { usePeriodStore } from '@/stores/periodStore'; import { calculateClosingSummary, validateFiscalYearClose, generateClosingEntries, calculateClosingBalances, type FiscalYearClosingSummary, type GeneratedClosingEntry, } from '@/lib/fiscalYear'; import { formatCurrency } from '@/lib/formatters'; import type { FiscalYear } from '@/types/periods'; import type { Account, Transaction } from '@/types/accounting'; import { useCloseFiscalYear, useCreateFiscalYear } from '@/api/mutations/fiscalYearMutations'; import { useCreateJournalEntryDraft, useUpdateJournalEntryDraft } from '@/api/mutations/draftMutations'; import { message } from 'antd'; import dayjs from 'dayjs'; const { Text, Title, Paragraph } = Typography; interface CloseFiscalYearWizardProps { open: boolean; fiscalYear: FiscalYear; accounts: Account[]; transactions: Transaction[]; onClose: () => void; onSuccess?: () => void; } type WizardStep = 'validation' | 'summary' | 'transfer' | 'confirm' | 'complete'; // Default "Overført resultat" account (equity) const DEFAULT_RESULT_ACCOUNT = { id: 'acc-3900', accountNumber: '3900', name: 'Overført resultat', }; export default function CloseFiscalYearWizard({ open, fiscalYear, accounts, transactions, onClose, onSuccess, }: CloseFiscalYearWizardProps) { const [currentStep, setCurrentStep] = useState('validation'); const [isSubmitting, setIsSubmitting] = useState(false); const [resultAccountId, setResultAccountId] = useState(DEFAULT_RESULT_ACCOUNT.id); const [createNextYear, setCreateNextYear] = useState(true); const [closeOpenPeriods, setCloseOpenPeriods] = useState(true); const [confirmLock, setConfirmLock] = useState(false); const { periods, closeFiscalYear, lockFiscalYear, closePeriod, lockPeriod, } = usePeriodStore(); const closeFiscalYearMutation = useCloseFiscalYear(); const createFiscalYearMutation = useCreateFiscalYear(); const createDraftMutation = useCreateJournalEntryDraft(); const updateDraftMutation = useUpdateJournalEntryDraft(); // Reset wizard when opened useEffect(() => { if (open) { setCurrentStep('validation'); setIsSubmitting(false); setConfirmLock(false); } }, [open]); // Calculate closing balances const closingBalances = useMemo( () => calculateClosingBalances(fiscalYear, accounts, transactions), [fiscalYear, accounts, transactions] ); // Calculate summary const summary: FiscalYearClosingSummary = useMemo( () => calculateClosingSummary(fiscalYear, periods, closingBalances, transactions), [fiscalYear, periods, closingBalances, transactions] ); // Validate fiscal year close const validation = useMemo( () => validateFiscalYearClose(fiscalYear, periods), [fiscalYear, periods] ); // Generate closing entries preview const closingEntries: GeneratedClosingEntry[] = useMemo(() => { const resultAccount = accounts.find((a) => a.id === resultAccountId) || { id: resultAccountId, accountNumber: DEFAULT_RESULT_ACCOUNT.accountNumber, name: DEFAULT_RESULT_ACCOUNT.name, }; return generateClosingEntries( fiscalYear, closingBalances, resultAccount.id, resultAccount.accountNumber, resultAccount.name ); }, [fiscalYear, closingBalances, resultAccountId, accounts]); // Equity accounts for result transfer const equityAccounts = useMemo( () => accounts.filter((a) => a.type === 'equity'), [accounts] ); // Year periods const yearPeriods = useMemo( () => periods.filter((p) => p.fiscalYearId === fiscalYear.id), [periods, fiscalYear.id] ); const openPeriodsInYear = yearPeriods.filter((p) => p.status === 'open'); const handleNext = () => { const steps: WizardStep[] = ['validation', 'summary', 'transfer', 'confirm', 'complete']; const currentIndex = steps.indexOf(currentStep); if (currentIndex < steps.length - 1) { setCurrentStep(steps[currentIndex + 1]); } }; const handlePrevious = () => { const steps: WizardStep[] = ['validation', 'summary', 'transfer', 'confirm', 'complete']; const currentIndex = steps.indexOf(currentStep); if (currentIndex > 0) { setCurrentStep(steps[currentIndex - 1]); } }; const handleSubmit = async () => { setIsSubmitting(true); try { // 1. Post closing entries as journal entry drafts for (const entry of closingEntries) { // Create a draft for each closing entry const draft = await createDraftMutation.mutateAsync({ companyId: fiscalYear.companyId, name: entry.descriptionDanish, documentDate: fiscalYear.endDate, description: entry.descriptionDanish, fiscalYearId: fiscalYear.id, }); // Add lines to the draft if (entry.lines.length > 0) { await updateDraftMutation.mutateAsync({ id: draft.id, lines: entry.lines.map((line, idx) => ({ lineNumber: idx + 1, accountId: line.accountId, debitAmount: line.debit, creditAmount: line.credit, description: entry.descriptionDanish, })), }); } } // 2. Close open periods if requested (local store) if (closeOpenPeriods) { for (const period of openPeriodsInYear) { closePeriod(period.id, 'system'); } } // 3. Lock all periods in the year (local store) for (const period of yearPeriods) { lockPeriod(period.id, 'system'); } // 4. Call backend mutation to close the fiscal year await closeFiscalYearMutation.mutateAsync(fiscalYear.id); // 5. Also update local store closeFiscalYear(fiscalYear.id, 'system'); lockFiscalYear(fiscalYear.id, 'system'); // 6. Create next fiscal year if requested if (createNextYear) { const nextStartDate = dayjs(fiscalYear.endDate).add(1, 'day'); const nextEndDate = nextStartDate.add(12, 'month').subtract(1, 'day'); const nextStartYear = nextStartDate.year(); const nextEndYear = nextEndDate.year(); const nextName = nextStartYear === nextEndYear ? `${nextStartYear}` : `${nextStartYear}/${nextEndYear}`; await createFiscalYearMutation.mutateAsync({ companyId: fiscalYear.companyId, name: nextName, startDate: nextStartDate.format('YYYY-MM-DD'), endDate: nextEndDate.format('YYYY-MM-DD'), }); } // 7. Move to complete step setCurrentStep('complete'); onSuccess?.(); } catch (error) { if (error instanceof Error) { message.error(`Fejl ved årsafslutning: ${error.message}`); } console.error('Failed to close fiscal year:', error); } finally { setIsSubmitting(false); } }; const getStepNumber = (step: WizardStep): number => { const steps: WizardStep[] = ['validation', 'summary', 'transfer', 'confirm', 'complete']; return steps.indexOf(step); }; const renderValidationStep = () => (
Validering af regnskabsår {fiscalYear.name} Før årsafslutning kontrolleres regnskabsåret for eventuelle problemer. {validation.errors.length > 0 && ( {validation.errors.map((err, idx) => (
  • {err.messageDanish}
  • ))} } style={{ marginBottom: 16 }} /> )} {validation.warnings.length > 0 && ( } message="Advarsler" description={
      {validation.warnings.map((warn, idx) => (
    • {warn.messageDanish}
    • ))}
    } style={{ marginBottom: 16 }} /> )} {summary.unreconciledCount > 0 && ( } message={`${summary.unreconciledCount} transaktioner er ikke afstemt`} description="Det anbefales at afstemme alle transaktioner før årsafslutning." style={{ marginBottom: 16 }} /> )} {openPeriodsInYear.length > 0 && ( setCloseOpenPeriods(e.target.checked)} > Luk automatisk {openPeriodsInYear.length} åbne periode(r) ved årsafslutning )} {validation.isValid && validation.warnings.length === 0 && ( } message="Regnskabsåret er klar til afslutning" description="Ingen fejl eller advarsler fundet." /> )}
    ); const renderSummaryStep = () => (
    Resultatoversigt for {fiscalYear.name} formatCurrency(Number(val))} valueStyle={{ color: '#3f8600' }} /> formatCurrency(Number(val))} valueStyle={{ color: '#cf1322' }} /> formatCurrency(Number(val))} valueStyle={{ color: summary.netResult >= 0 ? '#3f8600' : '#cf1322' }} prefix={summary.netResult >= 0 ? '+' : ''} /> Årsresultatet på {formatCurrency(summary.netResult)} vil blive overført til egenkapitalen ved årsafslutning.
    ); const renderTransferStep = () => (
    Resultatoverførsel Vælg hvilken egenkapitalkonto årsresultatet skal overføres til.