// Period Hook - React hook for period context and validation import { useMemo, useCallback } from 'react'; import { usePeriodStore } from '@/stores/periodStore'; import { getPeriodForDate, getPreviousPeriod, getSamePeriodPreviousYear, getYearToDateRange, canPostToDate, validatePeriodClose, } from '@/lib/periods'; import type { AccountingPeriod, FiscalYear, PeriodStatus } from '@/types/periods'; import type { Transaction } from '@/types/accounting'; /** * Hook for accessing period context in components */ export function usePeriodContext() { const { currentFiscalYear, currentPeriod, selectedPeriod, selectedVATPeriod, comparisonPeriod, comparisonType, periods, fiscalYears, vatPeriods, isLoading, } = usePeriodStore(); const effectivePeriod = selectedPeriod || currentPeriod; return { // Current context currentFiscalYear, currentPeriod, selectedPeriod, effectivePeriod, selectedVATPeriod, comparisonPeriod, comparisonType, // Lists periods, fiscalYears, vatPeriods, // Loading isLoading, }; } /** * Hook for period selection and navigation */ export function usePeriodSelector() { const { periods, fiscalYears, selectedPeriod, setSelectedPeriod, setComparisonPeriod, clearComparison, } = usePeriodStore(); const selectPeriod = useCallback( (periodId: string) => { const period = periods.find((p) => p.id === periodId); if (period) { setSelectedPeriod(period); } }, [periods, setSelectedPeriod] ); const selectPreviousPeriod = useCallback(() => { if (!selectedPeriod) return; const previous = getPreviousPeriod(selectedPeriod, periods); if (previous) { setSelectedPeriod(previous); } }, [selectedPeriod, periods, setSelectedPeriod]); const selectNextPeriod = useCallback(() => { if (!selectedPeriod) return; const currentIndex = periods.findIndex((p) => p.id === selectedPeriod.id); if (currentIndex >= 0 && currentIndex < periods.length - 1) { setSelectedPeriod(periods[currentIndex + 1]); } }, [selectedPeriod, periods, setSelectedPeriod]); const enableComparison = useCallback( (type: 'previous-period' | 'previous-year' | 'custom', customPeriod?: AccountingPeriod) => { if (!selectedPeriod) return; let comparisonPeriodData: AccountingPeriod | undefined; if (type === 'previous-period') { comparisonPeriodData = getPreviousPeriod(selectedPeriod, periods); } else if (type === 'previous-year') { comparisonPeriodData = getSamePeriodPreviousYear(selectedPeriod, periods); } else if (type === 'custom' && customPeriod) { comparisonPeriodData = customPeriod; } if (comparisonPeriodData) { setComparisonPeriod(comparisonPeriodData, type); } }, [selectedPeriod, periods, setComparisonPeriod] ); const disableComparison = useCallback(() => { clearComparison(); }, [clearComparison]); // Get periods grouped by fiscal year const periodsByYear = useMemo(() => { const grouped: Record = {}; for (const period of periods) { if (!grouped[period.fiscalYearId]) { grouped[period.fiscalYearId] = []; } grouped[period.fiscalYearId].push(period); } return grouped; }, [periods]); // Get open periods only const openPeriods = useMemo( () => periods.filter((p) => p.status === 'open'), [periods] ); return { // State selectedPeriod, periods, fiscalYears, periodsByYear, openPeriods, // Actions selectPeriod, selectPreviousPeriod, selectNextPeriod, enableComparison, disableComparison, }; } /** * Hook for posting validation */ export function usePostingValidation() { const { periods, periodSettings } = usePeriodStore(); const validatePostingDate = useCallback( (date: string) => { if (!periodSettings) { // Default to strict validation if no settings return canPostToDate(date, periods, { preventPostingToClosedPeriods: true, preventPostingToFuturePeriods: true, }); } return canPostToDate(date, periods, { preventPostingToClosedPeriods: periodSettings.preventPostingToClosedPeriods, preventPostingToFuturePeriods: periodSettings.preventPostingToFuturePeriods, }); }, [periods, periodSettings] ); const getPeriodStatus = useCallback( (date: string): PeriodStatus | 'no-period' => { const period = getPeriodForDate(date, periods); return period?.status || 'no-period'; }, [periods] ); const isDatePostable = useCallback( (date: string): boolean => { return validatePostingDate(date).allowed; }, [validatePostingDate] ); return { validatePostingDate, getPeriodStatus, isDatePostable, }; } /** * Hook for period management (closing, locking, etc.) */ export function usePeriodManagement() { const { periods, closePeriod, reopenPeriod, lockPeriod, updatePeriod, } = usePeriodStore(); const canClosePeriod = useCallback( (periodId: string, transactions: Transaction[]): { canClose: boolean; errors: string[]; warnings: string[] } => { const period = periods.find((p) => p.id === periodId); if (!period) { return { canClose: false, errors: ['Periode ikke fundet'], warnings: [], }; } const validation = validatePeriodClose(period, transactions, { requireAllReconciled: true, }); return { canClose: validation.isValid, errors: validation.errors.map((e) => e.messageDanish), warnings: validation.warnings.map((w) => w.messageDanish), }; }, [periods] ); const closeAccountingPeriod = useCallback( (periodId: string, userId: string) => { closePeriod(periodId, userId); }, [closePeriod] ); const reopenAccountingPeriod = useCallback( (periodId: string, userId: string) => { reopenPeriod(periodId, userId); }, [reopenPeriod] ); const lockAccountingPeriod = useCallback( (periodId: string, userId: string) => { lockPeriod(periodId, userId); }, [lockPeriod] ); const getPeriodActions = useCallback( (periodId: string) => { const period = periods.find((p) => p.id === periodId); if (!period) return { canClose: false, canReopen: false, canLock: false }; return { canClose: period.status === 'open', canReopen: period.status === 'closed', canLock: period.status === 'closed', }; }, [periods] ); return { canClosePeriod, closeAccountingPeriod, reopenAccountingPeriod, lockAccountingPeriod, getPeriodActions, }; } /** * Hook for year-to-date calculations */ export function useYearToDate() { const { currentFiscalYear, selectedPeriod, periods } = usePeriodStore(); const ytdRange = useMemo(() => { if (!currentFiscalYear || !selectedPeriod) return null; return getYearToDateRange(selectedPeriod, currentFiscalYear); }, [currentFiscalYear, selectedPeriod]); const ytdPeriods = useMemo(() => { if (!currentFiscalYear || !selectedPeriod) return []; return periods.filter((p) => { if (p.fiscalYearId !== currentFiscalYear.id) return false; return p.periodNumber <= selectedPeriod.periodNumber; }); }, [currentFiscalYear, selectedPeriod, periods]); return { ytdRange, ytdPeriods, fiscalYear: currentFiscalYear, }; } /** * Combined hook for common period operations */ export function usePeriod() { const context = usePeriodContext(); const selector = usePeriodSelector(); const validation = usePostingValidation(); const management = usePeriodManagement(); const ytd = useYearToDate(); return { // Context ...context, // Selector selectPeriod: selector.selectPeriod, selectPreviousPeriod: selector.selectPreviousPeriod, selectNextPeriod: selector.selectNextPeriod, enableComparison: selector.enableComparison, disableComparison: selector.disableComparison, periodsByYear: selector.periodsByYear, openPeriods: selector.openPeriods, // Validation validatePostingDate: validation.validatePostingDate, getPeriodStatus: validation.getPeriodStatus, isDatePostable: validation.isDatePostable, // Management canClosePeriod: management.canClosePeriod, closeAccountingPeriod: management.closeAccountingPeriod, reopenAccountingPeriod: management.reopenAccountingPeriod, lockAccountingPeriod: management.lockAccountingPeriod, getPeriodActions: management.getPeriodActions, // YTD ytdRange: ytd.ytdRange, ytdPeriods: ytd.ytdPeriods, }; }