books/frontend/src/pages/Settings.tsx
Nicolaj Hartmann 709d0a4739 Audit v2: fix security, data integrity, compliance, bugs, encoding, UX
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>
2026-02-06 00:18:19 +01:00

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>
);
}