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:
parent
49971b3265
commit
8bf3141ba3
3 changed files with 89 additions and 54 deletions
|
|
@ -1,11 +1,11 @@
|
||||||
{"id":"books-1rp","title":"http://localhost:3000/kunder","status":"open","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:30:29.369137+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:30:29.369137+01:00"}
|
{"id":"books-1rp","title":"http://localhost:3000/kunder","status":"in_progress","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:30:29.369137+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:45:00.573047+01:00"}
|
||||||
{"id":"books-5tg","title":"opret et backend job med hangfir\ne som sikrer, at bankkonto altid stemmer med den pågældende konto","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:24:01.505911+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:37:08.293897+01:00","closed_at":"2026-01-30T14:37:08.293897+01:00","close_reason":"Closed"}
|
{"id":"books-5tg","title":"opret et backend job med hangfir\ne som sikrer, at bankkonto altid stemmer med den pågældende konto","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:24:01.505911+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:37:08.293897+01:00","closed_at":"2026-01-30T14:37:08.293897+01:00","close_reason":"Closed"}
|
||||||
{"id":"books-8ea","title":"fjern brugers navn fra højre hjørne ved profile ikonet","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:20:16.406033+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:22:59.64468+01:00","closed_at":"2026-01-30T14:22:59.64468+01:00","close_reason":"Closed"}
|
{"id":"books-8ea","title":"fjern brugers navn fra højre hjørne ved profile ikonet","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:20:16.406033+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:22:59.64468+01:00","closed_at":"2026-01-30T14:22:59.64468+01:00","close_reason":"Closed"}
|
||||||
{"id":"books-8lo","title":"revisit the laytoug and desig nfor kontooversigten.","status":"open","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:25:06.620288+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:25:06.620288+01:00"}
|
{"id":"books-8lo","title":"revisit the laytoug and desig nfor kontooversigten.","status":"in_progress","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:25:06.620288+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:45:00.448995+01:00"}
|
||||||
{"id":"books-bj6","title":"Test automatisk pickup","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:04:40.572496+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:05:44.401903+01:00","closed_at":"2026-01-30T14:05:44.401903+01:00","close_reason":"completed"}
|
{"id":"books-bj6","title":"Test automatisk pickup","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:04:40.572496+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:05:44.401903+01:00","closed_at":"2026-01-30T14:05:44.401903+01:00","close_reason":"completed"}
|
||||||
{"id":"books-byl","title":"opret giv et shortlink på frontenden til backend på /hangfire","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:24:34.946139+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:40:44.62014+01:00","closed_at":"2026-01-30T14:40:44.62014+01:00","close_reason":"Closed"}
|
{"id":"books-byl","title":"opret giv et shortlink på frontenden til backend på /hangfire","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:24:34.946139+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:40:44.62014+01:00","closed_at":"2026-01-30T14:40:44.62014+01:00","close_reason":"Closed"}
|
||||||
{"id":"books-cdf","title":"opret","status":"open","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:23:39.411558+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:23:39.411558+01:00"}
|
{"id":"books-cdf","title":"opret","status":"open","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:23:39.411558+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:23:39.411558+01:00"}
|
||||||
{"id":"books-ced","title":"brug smb om regnskab + fropntend designer til at sikrer at alt er godt for både balance og kontooversigt","status":"open","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:25:46.484629+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:25:46.484629+01:00"}
|
{"id":"books-ced","title":"brug smb om regnskab + fropntend designer til at sikrer at alt er godt for både balance og kontooversigt","status":"in_progress","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:25:46.484629+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:45:00.511206+01:00"}
|
||||||
{"id":"books-h6e","title":"fjern hurtig bogføring og den visning der høre dertil","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:14:50.436314+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:18:09.911294+01:00","closed_at":"2026-01-30T14:18:09.911294+01:00","close_reason":"Closed"}
|
{"id":"books-h6e","title":"fjern hurtig bogføring og den visning der høre dertil","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:14:50.436314+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:18:09.911294+01:00","closed_at":"2026-01-30T14:18:09.911294+01:00","close_reason":"Closed"}
|
||||||
{"id":"books-hzt","title":"fix bug med tilføj brugere står forkert med encoded tegn","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:21:34.556319+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:28:31.320973+01:00","closed_at":"2026-01-30T14:28:31.320973+01:00","close_reason":"Closed"}
|
{"id":"books-hzt","title":"fix bug med tilføj brugere står forkert med encoded tegn","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:21:34.556319+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:28:31.320973+01:00","closed_at":"2026-01-30T14:28:31.320973+01:00","close_reason":"Closed"}
|
||||||
{"id":"books-sbm","title":"ændre navnet i venstre side til Books","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:11:13.017202+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:12:14.16594+01:00","closed_at":"2026-01-30T14:12:14.16594+01:00","close_reason":"Closed"}
|
{"id":"books-sbm","title":"ændre navnet i venstre side til Books","status":"closed","priority":2,"issue_type":"task","owner":"nhh@softwarehuset.com","created_at":"2026-01-30T14:11:13.017202+01:00","created_by":"Nicolaj Hartmann","updated_at":"2026-01-30T14:12:14.16594+01:00","closed_at":"2026-01-30T14:12:14.16594+01:00","close_reason":"Closed"}
|
||||||
|
|
|
||||||
|
|
@ -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)}`;
|
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)
|
* Get CSS class for amount (positive/negative/zero)
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
Tabs,
|
Tabs,
|
||||||
Statistic,
|
Statistic,
|
||||||
message,
|
message,
|
||||||
|
Grid,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
|
|
@ -29,9 +30,13 @@ import { useCompany } from '@/hooks/useCompany';
|
||||||
import { formatCurrency, formatDate } from '@/lib/formatters';
|
import { formatCurrency, formatDate } from '@/lib/formatters';
|
||||||
import { getAccountTypeName, getAccountNumberRange } from '@/lib/accounting';
|
import { getAccountTypeName, getAccountNumberRange } from '@/lib/accounting';
|
||||||
import { accountingColors } from '@/styles/theme';
|
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';
|
import type { Account, AccountType } from '@/types/accounting';
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
const { useBreakpoint } = Grid;
|
||||||
|
|
||||||
// Mock data
|
// Mock data
|
||||||
const mockAccounts: Account[] = [
|
const mockAccounts: Account[] = [
|
||||||
|
|
@ -71,12 +76,15 @@ const accountTypes: AccountType[] = [
|
||||||
|
|
||||||
export default function Kontooversigt() {
|
export default function Kontooversigt() {
|
||||||
const { company } = useCompany();
|
const { company } = useCompany();
|
||||||
|
const screens = useBreakpoint();
|
||||||
const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
|
const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [editingAccount, setEditingAccount] = useState<Account | null>(null);
|
const [editingAccount, setEditingAccount] = useState<Account | null>(null);
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const isMobile = !screens.md;
|
||||||
|
|
||||||
// Build tree data from accounts
|
// Build tree data from accounts
|
||||||
const buildTreeData = (): DataNode[] => {
|
const buildTreeData = (): DataNode[] => {
|
||||||
return accountTypes.map((type) => {
|
return accountTypes.map((type) => {
|
||||||
|
|
@ -235,29 +243,29 @@ export default function Kontooversigt() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div
|
<PageHeader
|
||||||
style={{
|
title="Kontooversigt"
|
||||||
display: 'flex',
|
subtitle={company?.name}
|
||||||
justifyContent: 'space-between',
|
breadcrumbs={[
|
||||||
alignItems: 'center',
|
{ title: 'Bogforing', path: '/bogforing' },
|
||||||
marginBottom: 16,
|
{ title: 'Kontooversigt' },
|
||||||
}}
|
]}
|
||||||
|
extra={
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={handleCreateAccount}
|
||||||
|
aria-label="Opret ny konto"
|
||||||
>
|
>
|
||||||
<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
|
Ny konto
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Summary Cards */}
|
{/* Summary Cards */}
|
||||||
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
<Row gutter={[spacing.lg, spacing.lg]} style={{ marginBottom: spacing.lg }}>
|
||||||
<Col xs={12} sm={6}>
|
<Col xs={12} sm={6}>
|
||||||
<Card size="small">
|
<Card size="small" role="region" aria-label="Aktiver total">
|
||||||
<Statistic
|
<Statistic
|
||||||
title="Aktiver"
|
title="Aktiver"
|
||||||
value={totalAssets}
|
value={totalAssets}
|
||||||
|
|
@ -269,7 +277,7 @@ export default function Kontooversigt() {
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={6}>
|
<Col xs={12} sm={6}>
|
||||||
<Card size="small">
|
<Card size="small" role="region" aria-label="Passiver total">
|
||||||
<Statistic
|
<Statistic
|
||||||
title="Passiver"
|
title="Passiver"
|
||||||
value={totalLiabilities}
|
value={totalLiabilities}
|
||||||
|
|
@ -281,9 +289,9 @@ export default function Kontooversigt() {
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={6}>
|
<Col xs={12} sm={6}>
|
||||||
<Card size="small">
|
<Card size="small" role="region" aria-label="Omsaetning total">
|
||||||
<Statistic
|
<Statistic
|
||||||
title="Omsætning"
|
title="Omsaetning"
|
||||||
value={totalRevenue}
|
value={totalRevenue}
|
||||||
precision={2}
|
precision={2}
|
||||||
suffix="kr."
|
suffix="kr."
|
||||||
|
|
@ -293,7 +301,7 @@ export default function Kontooversigt() {
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={6}>
|
<Col xs={12} sm={6}>
|
||||||
<Card size="small">
|
<Card size="small" role="region" aria-label="Omkostninger total">
|
||||||
<Statistic
|
<Statistic
|
||||||
title="Omkostninger"
|
title="Omkostninger"
|
||||||
value={totalExpenses}
|
value={totalExpenses}
|
||||||
|
|
@ -307,30 +315,31 @@ export default function Kontooversigt() {
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<Row gutter={16}>
|
<Row gutter={spacing.lg}>
|
||||||
{/* Account Tree */}
|
{/* Account Tree */}
|
||||||
<Col xs={24} lg={10}>
|
<Col xs={24} lg={10}>
|
||||||
<Card
|
<Card title="Kontoplan" size="small">
|
||||||
title="Kontoplan"
|
{/* Search moved outside extra for better mobile UX */}
|
||||||
size="small"
|
|
||||||
extra={
|
|
||||||
<Input
|
<Input
|
||||||
placeholder="Søg konto..."
|
placeholder="Sog efter konto eller kontonummer..."
|
||||||
prefix={<SearchOutlined />}
|
prefix={<SearchOutlined />}
|
||||||
value={searchText}
|
value={searchText}
|
||||||
onChange={(e) => setSearchText(e.target.value)}
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
style={{ width: 200 }}
|
style={{ marginBottom: spacing.md }}
|
||||||
allowClear
|
allowClear
|
||||||
|
aria-label="Sog i kontoplan"
|
||||||
/>
|
/>
|
||||||
}
|
|
||||||
>
|
|
||||||
<Tree
|
<Tree
|
||||||
showIcon
|
showIcon
|
||||||
defaultExpandAll
|
defaultExpandAll
|
||||||
treeData={buildTreeData()}
|
treeData={buildTreeData()}
|
||||||
onSelect={handleSelectAccount}
|
onSelect={handleSelectAccount}
|
||||||
selectedKeys={selectedAccount ? [selectedAccount.id] : []}
|
selectedKeys={selectedAccount ? [selectedAccount.id] : []}
|
||||||
style={{ maxHeight: 500, overflow: 'auto' }}
|
style={{
|
||||||
|
maxHeight: isMobile ? 300 : 450,
|
||||||
|
overflow: 'auto',
|
||||||
|
}}
|
||||||
|
aria-label="Kontoplan hierarki"
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
@ -343,6 +352,9 @@ export default function Kontooversigt() {
|
||||||
<Space>
|
<Space>
|
||||||
<Text code>{selectedAccount.accountNumber}</Text>
|
<Text code>{selectedAccount.accountNumber}</Text>
|
||||||
<Text strong>{selectedAccount.name}</Text>
|
<Text strong>{selectedAccount.name}</Text>
|
||||||
|
{!selectedAccount.isActive && (
|
||||||
|
<Tag color="red">Inaktiv</Tag>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
size="small"
|
size="small"
|
||||||
|
|
@ -350,10 +362,13 @@ export default function Kontooversigt() {
|
||||||
<Button
|
<Button
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined />}
|
||||||
onClick={() => handleEditAccount(selectedAccount)}
|
onClick={() => handleEditAccount(selectedAccount)}
|
||||||
|
aria-label={`Rediger konto ${selectedAccount.accountNumber}`}
|
||||||
>
|
>
|
||||||
Rediger
|
Rediger
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
role="region"
|
||||||
|
aria-label={`Detaljer for konto ${selectedAccount.accountNumber}`}
|
||||||
>
|
>
|
||||||
<Tabs
|
<Tabs
|
||||||
items={[
|
items={[
|
||||||
|
|
@ -388,6 +403,7 @@ export default function Kontooversigt() {
|
||||||
rowKey={(_, index) => String(index)}
|
rowKey={(_, index) => String(index)}
|
||||||
size="small"
|
size="small"
|
||||||
pagination={{ pageSize: 10 }}
|
pagination={{ pageSize: 10 }}
|
||||||
|
aria-label={`Bevaegelser for konto ${selectedAccount?.accountNumber}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|
@ -435,16 +451,13 @@ export default function Kontooversigt() {
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<Card size="small">
|
<Card size="small">
|
||||||
<div
|
<EmptyState
|
||||||
style={{
|
variant="accounts"
|
||||||
textAlign: 'center',
|
icon={<FileOutlined style={{ fontSize: 48 }} />}
|
||||||
padding: 40,
|
title="Ingen konto valgt"
|
||||||
color: '#8c8c8c',
|
description="Vaelg en konto i kontoplanen til venstre for at se detaljer og bevaegelser."
|
||||||
}}
|
compact
|
||||||
>
|
/>
|
||||||
<FileOutlined style={{ fontSize: 48, marginBottom: 16 }} />
|
|
||||||
<div>Vælg en konto for at se detaljer</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue