2026-01-30 22:42:00 +01:00
|
|
|
// FiscalYearSelector - Dropdown for selecting active fiscal year (regnskabsar)
|
2026-01-18 02:52:30 +01:00
|
|
|
|
|
|
|
|
import { useState, useEffect } from 'react';
|
2026-01-30 22:42:00 +01:00
|
|
|
import { Select, Space, Typography, Tag, Divider, Button, Skeleton } from 'antd';
|
2026-01-18 02:52:30 +01:00
|
|
|
import {
|
|
|
|
|
CalendarOutlined,
|
|
|
|
|
PlusOutlined,
|
|
|
|
|
SettingOutlined,
|
|
|
|
|
CheckCircleOutlined,
|
|
|
|
|
MinusCircleOutlined,
|
|
|
|
|
LockOutlined,
|
|
|
|
|
} from '@ant-design/icons';
|
|
|
|
|
import { usePeriodStore } from '@/stores/periodStore';
|
2026-01-30 22:42:00 +01:00
|
|
|
import { useCompanyStore } from '@/stores/companyStore';
|
|
|
|
|
import { useFiscalYears } from '@/api/queries/fiscalYearQueries';
|
2026-01-18 02:52:30 +01:00
|
|
|
import type { FiscalYear } from '@/types/periods';
|
|
|
|
|
import { formatDateShort } from '@/lib/formatters';
|
|
|
|
|
import CreateFiscalYearModal from '@/components/modals/CreateFiscalYearModal';
|
|
|
|
|
|
|
|
|
|
const { Text } = Typography;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Status badge configuration
|
|
|
|
|
*/
|
|
|
|
|
const STATUS_CONFIG: Record<FiscalYear['status'], {
|
|
|
|
|
color: string;
|
|
|
|
|
icon: React.ReactNode;
|
|
|
|
|
label: string;
|
|
|
|
|
}> = {
|
|
|
|
|
open: {
|
|
|
|
|
color: 'success',
|
|
|
|
|
icon: <CheckCircleOutlined />,
|
2026-01-30 22:42:00 +01:00
|
|
|
label: 'Aben',
|
2026-01-18 02:52:30 +01:00
|
|
|
},
|
|
|
|
|
closed: {
|
|
|
|
|
color: 'warning',
|
|
|
|
|
icon: <MinusCircleOutlined />,
|
|
|
|
|
label: 'Lukket',
|
|
|
|
|
},
|
|
|
|
|
locked: {
|
|
|
|
|
color: 'error',
|
|
|
|
|
icon: <LockOutlined />,
|
2026-01-30 22:42:00 +01:00
|
|
|
label: 'Last',
|
2026-01-18 02:52:30 +01:00
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
interface FiscalYearSelectorProps {
|
|
|
|
|
onCreateNew?: () => void;
|
|
|
|
|
onManage?: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function FiscalYearSelector({ onCreateNew, onManage }: FiscalYearSelectorProps) {
|
2026-01-30 22:42:00 +01:00
|
|
|
const { activeCompany } = useCompanyStore();
|
2026-01-18 02:52:30 +01:00
|
|
|
const {
|
|
|
|
|
fiscalYears,
|
|
|
|
|
currentFiscalYear,
|
|
|
|
|
setFiscalYears,
|
|
|
|
|
setCurrentFiscalYear,
|
|
|
|
|
} = usePeriodStore();
|
|
|
|
|
|
2026-01-30 22:42:00 +01:00
|
|
|
const { data: fiscalYearsData = [], isLoading } = useFiscalYears(activeCompany?.id);
|
|
|
|
|
|
2026-01-18 02:52:30 +01:00
|
|
|
const [createModalOpen, setCreateModalOpen] = useState(false);
|
|
|
|
|
|
2026-01-30 22:42:00 +01:00
|
|
|
// Sync fiscal years with store when data changes
|
2026-01-18 02:52:30 +01:00
|
|
|
useEffect(() => {
|
2026-01-30 22:42:00 +01:00
|
|
|
if (fiscalYearsData.length > 0) {
|
|
|
|
|
setFiscalYears(fiscalYearsData);
|
2026-01-18 02:52:30 +01:00
|
|
|
}
|
2026-01-30 22:42:00 +01:00
|
|
|
}, [fiscalYearsData, setFiscalYears]);
|
2026-01-18 02:52:30 +01:00
|
|
|
|
|
|
|
|
// Set default fiscal year if none selected
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (fiscalYears.length > 0 && !currentFiscalYear) {
|
|
|
|
|
// Default to most recent open year, or first year
|
|
|
|
|
const openYear = fiscalYears.find(y => y.status === 'open');
|
|
|
|
|
setCurrentFiscalYear(openYear || fiscalYears[0]);
|
|
|
|
|
}
|
|
|
|
|
}, [fiscalYears, currentFiscalYear, setCurrentFiscalYear]);
|
|
|
|
|
|
|
|
|
|
const handleFiscalYearChange = (yearId: string) => {
|
|
|
|
|
const year = fiscalYears.find((y) => y.id === yearId);
|
|
|
|
|
if (year) {
|
|
|
|
|
setCurrentFiscalYear(year);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCreateNew = () => {
|
|
|
|
|
if (onCreateNew) {
|
|
|
|
|
onCreateNew();
|
|
|
|
|
} else {
|
|
|
|
|
setCreateModalOpen(true);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCloseCreateModal = () => {
|
|
|
|
|
setCreateModalOpen(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCreateSuccess = (newYear: FiscalYear) => {
|
|
|
|
|
setCurrentFiscalYear(newYear);
|
|
|
|
|
setCreateModalOpen(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleManage = () => {
|
|
|
|
|
if (onManage) {
|
|
|
|
|
onManage();
|
|
|
|
|
} else {
|
|
|
|
|
// Navigate to settings page
|
|
|
|
|
console.log('Navigate to fiscal year settings');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-30 22:42:00 +01:00
|
|
|
if (isLoading) {
|
|
|
|
|
return <Skeleton.Input style={{ width: 200 }} active />;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 02:52:30 +01:00
|
|
|
// Sort fiscal years by start date descending (newest first)
|
|
|
|
|
const sortedYears = [...fiscalYears].sort(
|
|
|
|
|
(a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Space>
|
|
|
|
|
<CalendarOutlined style={{ fontSize: 16, color: '#8c8c8c' }} />
|
|
|
|
|
<Select
|
|
|
|
|
value={currentFiscalYear?.id}
|
|
|
|
|
onChange={handleFiscalYearChange}
|
|
|
|
|
style={{ minWidth: 200 }}
|
|
|
|
|
optionLabelProp="label"
|
|
|
|
|
popupMatchSelectWidth={false}
|
|
|
|
|
dropdownRender={(menu) => (
|
|
|
|
|
<>
|
|
|
|
|
{menu}
|
|
|
|
|
<Divider style={{ margin: '8px 0' }} />
|
|
|
|
|
<Space style={{ padding: '0 8px 8px' }}>
|
|
|
|
|
<Button
|
|
|
|
|
type="text"
|
|
|
|
|
icon={<PlusOutlined />}
|
|
|
|
|
onClick={handleCreateNew}
|
|
|
|
|
size="small"
|
|
|
|
|
>
|
2026-01-30 22:42:00 +01:00
|
|
|
Opret nyt regnskabsar
|
2026-01-18 02:52:30 +01:00
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="text"
|
|
|
|
|
icon={<SettingOutlined />}
|
|
|
|
|
onClick={handleManage}
|
|
|
|
|
size="small"
|
|
|
|
|
>
|
|
|
|
|
Administrer
|
|
|
|
|
</Button>
|
|
|
|
|
</Space>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
options={sortedYears.map((year) => ({
|
|
|
|
|
value: year.id,
|
2026-01-30 22:42:00 +01:00
|
|
|
label: `Regnskabsar ${year.name}`,
|
2026-01-18 02:52:30 +01:00
|
|
|
year,
|
|
|
|
|
}))}
|
|
|
|
|
optionRender={(option) => {
|
|
|
|
|
const year = option.data.year;
|
|
|
|
|
// Type guard - ensure year exists and has required properties
|
|
|
|
|
if (!year || typeof year !== 'object' || !('status' in year)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const fiscalYear = year as FiscalYear;
|
|
|
|
|
const statusConfig = STATUS_CONFIG[fiscalYear.status];
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Space
|
|
|
|
|
direction="vertical"
|
|
|
|
|
size={0}
|
|
|
|
|
style={{ padding: '4px 0', width: '100%' }}
|
|
|
|
|
>
|
|
|
|
|
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
|
|
|
|
<Text strong>{fiscalYear.name}</Text>
|
|
|
|
|
<Tag
|
|
|
|
|
color={statusConfig.color}
|
|
|
|
|
icon={statusConfig.icon}
|
|
|
|
|
style={{ marginLeft: 8 }}
|
|
|
|
|
>
|
|
|
|
|
{statusConfig.label}
|
|
|
|
|
</Tag>
|
|
|
|
|
</Space>
|
|
|
|
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
|
|
|
{formatDateShort(fiscalYear.startDate)} - {formatDateShort(fiscalYear.endDate)}
|
|
|
|
|
</Text>
|
|
|
|
|
</Space>
|
|
|
|
|
);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
{currentFiscalYear && (
|
|
|
|
|
<Tag
|
|
|
|
|
color={STATUS_CONFIG[currentFiscalYear.status].color}
|
|
|
|
|
icon={STATUS_CONFIG[currentFiscalYear.status].icon}
|
|
|
|
|
>
|
|
|
|
|
{STATUS_CONFIG[currentFiscalYear.status].label}
|
|
|
|
|
</Tag>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Create Fiscal Year Modal */}
|
|
|
|
|
<CreateFiscalYearModal
|
|
|
|
|
open={createModalOpen}
|
|
|
|
|
onClose={handleCloseCreateModal}
|
|
|
|
|
onSuccess={handleCreateSuccess}
|
|
|
|
|
/>
|
|
|
|
|
</Space>
|
|
|
|
|
);
|
|
|
|
|
}
|