Improve Kontooversigt UX and add missing CVR validation

UX improvements (books-8lo):
- Use PageHeader component for consistent header with breadcrumbs
- Add responsive mobile breakpoints
- Improve accessibility with aria-labels
- Better information hierarchy

Fixes (books-1rp):
- Add missing validateCVRModulus11 function to formatters
- Fixes TypeScript errors in Kunder.tsx and CompanySetupWizard.tsx

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Nicolaj Hartmann 2026-01-30 14:47:45 +01:00
parent 49971b3265
commit 8bf3141ba3
3 changed files with 89 additions and 54 deletions

View file

@ -112,6 +112,28 @@ export function formatCVR(cvr: string): string {
return `${cleaned.slice(0, 2)} ${cleaned.slice(2, 4)} ${cleaned.slice(4, 6)} ${cleaned.slice(6, 8)}`;
}
/**
* Validate a Danish CVR number using the modulus 11 algorithm
* @param cvr - 8-digit CVR number as string
* @returns true if valid, false otherwise
*/
export function validateCVRModulus11(cvr: string): boolean {
const cleaned = cvr.replace(/\D/g, '');
if (cleaned.length !== 8) return false;
// Weights for each position in the CVR number
const weights = [2, 7, 6, 5, 4, 3, 2, 1];
// Calculate weighted sum
let sum = 0;
for (let i = 0; i < 8; i++) {
sum += parseInt(cleaned[i], 10) * weights[i];
}
// Valid if sum is divisible by 11
return sum % 11 === 0;
}
/**
* Get CSS class for amount (positive/negative/zero)
*/

View file

@ -16,6 +16,7 @@ import {
Tabs,
Statistic,
message,
Grid,
} from 'antd';
import {
PlusOutlined,
@ -29,9 +30,13 @@ import { useCompany } from '@/hooks/useCompany';
import { formatCurrency, formatDate } from '@/lib/formatters';
import { getAccountTypeName, getAccountNumberRange } from '@/lib/accounting';
import { accountingColors } from '@/styles/theme';
import { spacing } from '@/styles/designTokens';
import { PageHeader } from '@/components/shared/PageHeader';
import { EmptyState } from '@/components/shared/EmptyState';
import type { Account, AccountType } from '@/types/accounting';
const { Title, Text } = Typography;
const { Text } = Typography;
const { useBreakpoint } = Grid;
// Mock data
const mockAccounts: Account[] = [
@ -71,12 +76,15 @@ const accountTypes: AccountType[] = [
export default function Kontooversigt() {
const { company } = useCompany();
const screens = useBreakpoint();
const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingAccount, setEditingAccount] = useState<Account | null>(null);
const [searchText, setSearchText] = useState('');
const [form] = Form.useForm();
const isMobile = !screens.md;
// Build tree data from accounts
const buildTreeData = (): DataNode[] => {
return accountTypes.map((type) => {
@ -235,29 +243,29 @@ export default function Kontooversigt() {
return (
<div>
{/* Header */}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
}}
>
<div>
<Title level={4} style={{ margin: 0 }}>
Kontooversigt
</Title>
<Text type="secondary">{company?.name}</Text>
</div>
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreateAccount}>
Ny konto
</Button>
</div>
<PageHeader
title="Kontooversigt"
subtitle={company?.name}
breadcrumbs={[
{ title: 'Bogforing', path: '/bogforing' },
{ title: 'Kontooversigt' },
]}
extra={
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleCreateAccount}
aria-label="Opret ny konto"
>
Ny konto
</Button>
}
/>
{/* Summary Cards */}
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Row gutter={[spacing.lg, spacing.lg]} style={{ marginBottom: spacing.lg }}>
<Col xs={12} sm={6}>
<Card size="small">
<Card size="small" role="region" aria-label="Aktiver total">
<Statistic
title="Aktiver"
value={totalAssets}
@ -269,7 +277,7 @@ export default function Kontooversigt() {
</Card>
</Col>
<Col xs={12} sm={6}>
<Card size="small">
<Card size="small" role="region" aria-label="Passiver total">
<Statistic
title="Passiver"
value={totalLiabilities}
@ -281,9 +289,9 @@ export default function Kontooversigt() {
</Card>
</Col>
<Col xs={12} sm={6}>
<Card size="small">
<Card size="small" role="region" aria-label="Omsaetning total">
<Statistic
title="Omsætning"
title="Omsaetning"
value={totalRevenue}
precision={2}
suffix="kr."
@ -293,7 +301,7 @@ export default function Kontooversigt() {
</Card>
</Col>
<Col xs={12} sm={6}>
<Card size="small">
<Card size="small" role="region" aria-label="Omkostninger total">
<Statistic
title="Omkostninger"
value={totalExpenses}
@ -307,30 +315,31 @@ export default function Kontooversigt() {
</Row>
{/* Main Content */}
<Row gutter={16}>
<Row gutter={spacing.lg}>
{/* Account Tree */}
<Col xs={24} lg={10}>
<Card
title="Kontoplan"
size="small"
extra={
<Input
placeholder="Søg konto..."
prefix={<SearchOutlined />}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
style={{ width: 200 }}
allowClear
/>
}
>
<Card title="Kontoplan" size="small">
{/* Search moved outside extra for better mobile UX */}
<Input
placeholder="Sog efter konto eller kontonummer..."
prefix={<SearchOutlined />}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
style={{ marginBottom: spacing.md }}
allowClear
aria-label="Sog i kontoplan"
/>
<Tree
showIcon
defaultExpandAll
treeData={buildTreeData()}
onSelect={handleSelectAccount}
selectedKeys={selectedAccount ? [selectedAccount.id] : []}
style={{ maxHeight: 500, overflow: 'auto' }}
style={{
maxHeight: isMobile ? 300 : 450,
overflow: 'auto',
}}
aria-label="Kontoplan hierarki"
/>
</Card>
</Col>
@ -343,6 +352,9 @@ export default function Kontooversigt() {
<Space>
<Text code>{selectedAccount.accountNumber}</Text>
<Text strong>{selectedAccount.name}</Text>
{!selectedAccount.isActive && (
<Tag color="red">Inaktiv</Tag>
)}
</Space>
}
size="small"
@ -350,10 +362,13 @@ export default function Kontooversigt() {
<Button
icon={<EditOutlined />}
onClick={() => handleEditAccount(selectedAccount)}
aria-label={`Rediger konto ${selectedAccount.accountNumber}`}
>
Rediger
</Button>
}
role="region"
aria-label={`Detaljer for konto ${selectedAccount.accountNumber}`}
>
<Tabs
items={[
@ -388,6 +403,7 @@ export default function Kontooversigt() {
rowKey={(_, index) => String(index)}
size="small"
pagination={{ pageSize: 10 }}
aria-label={`Bevaegelser for konto ${selectedAccount?.accountNumber}`}
/>
</div>
),
@ -435,16 +451,13 @@ export default function Kontooversigt() {
</Card>
) : (
<Card size="small">
<div
style={{
textAlign: 'center',
padding: 40,
color: '#8c8c8c',
}}
>
<FileOutlined style={{ fontSize: 48, marginBottom: 16 }} />
<div>Vælg en konto for at se detaljer</div>
</div>
<EmptyState
variant="accounts"
icon={<FileOutlined style={{ fontSize: 48 }} />}
title="Ingen konto valgt"
description="Vaelg en konto i kontoplanen til venstre for at se detaljer og bevaegelser."
compact
/>
</Card>
)}
</Col>