books/frontend/src/hooks/usePeriod.ts
Nicolaj Hartmann 66f6fa138d Initial commit: Books accounting system with EventFlow CQRS
Backend (.NET 10):
- EventFlow CQRS/Event Sourcing with PostgreSQL
- GraphQL.NET API with mutations and queries
- Custom ReadModelSqlGenerator for snake_case PostgreSQL columns
- Hangfire for background job processing
- Integration tests with isolated test databases

Frontend (React/Vite):
- Initial project structure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 02:52:30 +01:00

345 lines
8.7 KiB
TypeScript

// 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<string, AccountingPeriod[]> = {};
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,
};
}