Backend Security & Data Integrity: - Block negative debit/credit amounts that bypass balance validation - Require document date at posting (was optional, bypassing fiscal year checks) - Fix event sourcing anti-pattern: timestamps now stored in events, not UtcNow in Apply - Add [Authorize] to BankingController OAuth callback - Add company access check on attachment downloads - Validate CVR in CompanyAggregate.Create and CompanyAggregate.Update - Require company CVR for invoice creation (Momsloven §52) - Delete leftover WeatherForecastController - Fix duplicate migration number 007 (renamed to 007b) - Remove dead code in VatCalculationService (identical if/else branches) Accounting Compliance: - Add missing VAT accounts to StandardDanishAccounts (5610, 5611, 5620) - Populate SAF-T TaxInformation on transaction lines (was always null) - Add AuditFileCountry and TaxRegistrationNumber to SAF-T header Critical Frontend Bugs: - Fix Dashboard <a href> causing full page reloads (now uses React Router Link) - Wire Kassekladde filters to actual data (account, status, date range) - Pre-populate form when editing existing Kassekladde drafts - Add detail drawer for "Vis detaljer" action (was just a toast) - Toggle advanced filters with "Flere filtre" button - CloseFiscalYearWizard now actually posts closing entries via mutations - "Create next year" checkbox now creates the next fiscal year Danish Character Encoding (~50 fixes): - Fix ø/æ/å across Momsindberetning, DocumentUploadModal, Bankafstemning, Kontooversigt, CloseFiscalYearWizard, vatCodes, periodStore, periods, accounting, types/periods Dead Buttons & UX: - Disable Momsindberetning PDF/Export buttons with tooltips - FiscalYearSelector "Administrer" now navigates to Settings - Settings bank tab now uses real BankConnectionsTab component - Bankafstemning save button disabled with development tooltip - Replace hardcoded account options with real API data (Bankafstemning, Fakturaer) - Header help button shows info message, notification bell shows popover Consistency & Quality: - Remove 7 console.log statements from production code - Adopt PageHeader on 6 remaining pages (Kreditnotaer, Settings, Admin, etc.) - Standardize loading states to Skeleton pattern (5 pages) - Replace deprecated bodyStyle prop on Ant Design Cards - Standardize date format to DD-MM-YYYY - Fix sidebar width mismatch in designTokens - Fix Kontooversigt breadcrumb pointing to non-existent route Accessibility: - Add aria-label to sidebar navigation - Add +/- prefix to AmountText for color-blind users - Fix CompanySwitcher permanent skeleton when no companies Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
347 lines
10 KiB
TypeScript
347 lines
10 KiB
TypeScript
import {
|
|
Typography,
|
|
Card,
|
|
Row,
|
|
Col,
|
|
Form,
|
|
Input,
|
|
Select,
|
|
Button,
|
|
Tabs,
|
|
Switch,
|
|
Divider,
|
|
message,
|
|
Space,
|
|
} from 'antd';
|
|
import {
|
|
SaveOutlined,
|
|
BuildOutlined,
|
|
UserOutlined,
|
|
BankOutlined,
|
|
SettingOutlined,
|
|
} from '@ant-design/icons';
|
|
import { useCompany } from '@/hooks/useCompany';
|
|
import { useUpdateCompany } from '@/api/mutations/companyMutations';
|
|
import { DemoDataDisclaimer } from '@/components/shared/DemoDataDisclaimer';
|
|
import { PageHeader } from '@/components/shared/PageHeader';
|
|
import BankConnectionsTab from '@/components/settings/BankConnectionsTab';
|
|
|
|
const { Title, Text } = Typography;
|
|
|
|
export default function Settings() {
|
|
const { company } = useCompany();
|
|
const [companyForm] = Form.useForm();
|
|
const [preferencesForm] = Form.useForm();
|
|
const updateCompanyMutation = useUpdateCompany();
|
|
|
|
const handleSaveCompany = async () => {
|
|
try {
|
|
const values = await companyForm.validateFields();
|
|
|
|
if (!company?.id) {
|
|
message.error('Ingen virksomhed valgt');
|
|
return;
|
|
}
|
|
|
|
await updateCompanyMutation.mutateAsync({
|
|
id: company.id,
|
|
input: {
|
|
name: values.name,
|
|
cvr: values.cvr,
|
|
address: values.address,
|
|
city: values.city,
|
|
postalCode: values.postalCode,
|
|
},
|
|
});
|
|
message.success('Virksomhedsoplysninger gemt');
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
message.error(`Fejl ved gemning: ${error.message}`);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleSavePreferences = async () => {
|
|
try {
|
|
await preferencesForm.validateFields();
|
|
// TODO: Backend does not yet have a preferences mutation.
|
|
// Preferences like VAT period, auto-reconcile, etc. need a dedicated backend endpoint.
|
|
message.info('Præferencer er endnu ikke forbundet til backend');
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
message.error(`Fejl ved gemning: ${error.message}`);
|
|
}
|
|
}
|
|
};
|
|
|
|
const tabItems = [
|
|
{
|
|
key: 'company',
|
|
label: (
|
|
<span>
|
|
<BuildOutlined /> Virksomhed
|
|
</span>
|
|
),
|
|
children: (
|
|
<Card>
|
|
<Form
|
|
form={companyForm}
|
|
layout="vertical"
|
|
initialValues={{
|
|
name: company?.name,
|
|
cvr: company?.cvr,
|
|
address: company?.address,
|
|
city: company?.city,
|
|
postalCode: company?.postalCode,
|
|
fiscalYearStart: company?.fiscalYearStart || 1,
|
|
currency: company?.currency || 'DKK',
|
|
}}
|
|
>
|
|
<Row gutter={16}>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
name="name"
|
|
label="Virksomhedsnavn"
|
|
rules={[{ required: true, message: 'Indtast virksomhedsnavn' }]}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
</Col>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
name="cvr"
|
|
label="CVR-nummer"
|
|
rules={[
|
|
{ required: true, message: 'Indtast CVR-nummer' },
|
|
{ pattern: /^\d{8}$/, message: 'CVR skal være 8 cifre' },
|
|
]}
|
|
>
|
|
<Input maxLength={8} />
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Divider>Adresse</Divider>
|
|
|
|
<Row gutter={16}>
|
|
<Col span={24}>
|
|
<Form.Item name="address" label="Adresse">
|
|
<Input />
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
<Row gutter={16}>
|
|
<Col span={8}>
|
|
<Form.Item name="postalCode" label="Postnummer">
|
|
<Input maxLength={4} />
|
|
</Form.Item>
|
|
</Col>
|
|
<Col span={16}>
|
|
<Form.Item name="city" label="By">
|
|
<Input />
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Divider>Regnskab</Divider>
|
|
|
|
<Row gutter={16}>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
name="fiscalYearStart"
|
|
label="Regnskabsår starter"
|
|
tooltip="Hvilken måned starter jeres regnskabsår?"
|
|
>
|
|
<Select
|
|
options={[
|
|
{ value: 1, label: 'Januar' },
|
|
{ value: 2, label: 'Februar' },
|
|
{ value: 3, label: 'Marts' },
|
|
{ value: 4, label: 'April' },
|
|
{ value: 5, label: 'Maj' },
|
|
{ value: 6, label: 'Juni' },
|
|
{ value: 7, label: 'Juli' },
|
|
{ value: 8, label: 'August' },
|
|
{ value: 9, label: 'September' },
|
|
{ value: 10, label: 'Oktober' },
|
|
{ value: 11, label: 'November' },
|
|
{ value: 12, label: 'December' },
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
</Col>
|
|
<Col span={12}>
|
|
<Form.Item name="currency" label="Valuta">
|
|
<Select
|
|
options={[
|
|
{ value: 'DKK', label: 'DKK - Danske kroner' },
|
|
{ value: 'EUR', label: 'EUR - Euro' },
|
|
{ value: 'USD', label: 'USD - US Dollar' },
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
|
|
<div style={{ textAlign: 'right' }}>
|
|
<Button type="primary" icon={<SaveOutlined />} onClick={handleSaveCompany}>
|
|
Gem ændringer
|
|
</Button>
|
|
</div>
|
|
</Form>
|
|
</Card>
|
|
),
|
|
},
|
|
{
|
|
key: 'preferences',
|
|
label: (
|
|
<span>
|
|
<SettingOutlined /> Præferencer
|
|
</span>
|
|
),
|
|
children: (
|
|
<Card>
|
|
<Form
|
|
form={preferencesForm}
|
|
layout="vertical"
|
|
initialValues={{
|
|
vatPeriod: 'quarterly',
|
|
autoReconcile: true,
|
|
emailNotifications: true,
|
|
defaultPageSize: 20,
|
|
}}
|
|
>
|
|
<Title level={5}>Moms</Title>
|
|
<Row gutter={16}>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
name="vatPeriod"
|
|
label="Momsperiode"
|
|
tooltip="Hvor ofte indberetter I moms?"
|
|
>
|
|
<Select
|
|
options={[
|
|
{ value: 'monthly', label: 'Månedlig' },
|
|
{ value: 'quarterly', label: 'Kvartalsvis' },
|
|
{ value: 'half-yearly', label: 'Halvårlig' },
|
|
{ value: 'yearly', label: 'Årlig' },
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Divider />
|
|
|
|
<Title level={5}>Automatisering</Title>
|
|
<Form.Item
|
|
name="autoReconcile"
|
|
label="Automatisk afstemningsforslag"
|
|
valuePropName="checked"
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
<Text type="secondary" style={{ display: 'block', marginTop: -16, marginBottom: 16 }}>
|
|
Systemet vil automatisk foreslå matches mellem bank og bogføring
|
|
</Text>
|
|
|
|
<Divider />
|
|
|
|
<Title level={5}>Notifikationer</Title>
|
|
<Form.Item
|
|
name="emailNotifications"
|
|
label="Email-notifikationer"
|
|
valuePropName="checked"
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
<Text type="secondary" style={{ display: 'block', marginTop: -16, marginBottom: 16 }}>
|
|
Modtag påmindelser om frister og vigtige handlinger
|
|
</Text>
|
|
|
|
<Divider />
|
|
|
|
<Title level={5}>Visning</Title>
|
|
<Row gutter={16}>
|
|
<Col span={12}>
|
|
<Form.Item name="defaultPageSize" label="Standard antal rækker i tabeller">
|
|
<Select
|
|
options={[
|
|
{ value: 10, label: '10 rækker' },
|
|
{ value: 20, label: '20 rækker' },
|
|
{ value: 50, label: '50 rækker' },
|
|
{ value: 100, label: '100 rækker' },
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
|
|
<div style={{ textAlign: 'right' }}>
|
|
<Button type="primary" icon={<SaveOutlined />} onClick={handleSavePreferences}>
|
|
Gem præferencer
|
|
</Button>
|
|
</div>
|
|
</Form>
|
|
</Card>
|
|
),
|
|
},
|
|
{
|
|
key: 'bankAccounts',
|
|
label: (
|
|
<span>
|
|
<BankOutlined /> Bankkonti
|
|
</span>
|
|
),
|
|
children: (
|
|
<BankConnectionsTab companyId={company?.id} />
|
|
),
|
|
},
|
|
{
|
|
key: 'users',
|
|
label: (
|
|
<span>
|
|
<UserOutlined /> Brugere
|
|
</span>
|
|
),
|
|
children: (
|
|
<Card>
|
|
<DemoDataDisclaimer message="Brugerstyring er under udvikling" />
|
|
<Space direction="vertical" style={{ width: '100%' }}>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
}}
|
|
>
|
|
<Title level={5} style={{ margin: 0 }}>
|
|
Brugere med adgang
|
|
</Title>
|
|
<Button type="primary" disabled>Inviter bruger</Button>
|
|
</div>
|
|
|
|
<Divider />
|
|
|
|
<Text type="secondary">
|
|
Brugere med adgang til denne virksomhed vil blive vist her,
|
|
når funktionen er implementeret.
|
|
</Text>
|
|
</Space>
|
|
</Card>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div>
|
|
<PageHeader
|
|
title="Indstillinger"
|
|
subtitle={company?.name}
|
|
breadcrumbs={[{ title: 'Indstillinger' }]}
|
|
/>
|
|
|
|
<Tabs items={tabItems} />
|
|
</div>
|
|
);
|
|
}
|