Audit v3: VAT alignment, security, encoding, UX, compliance

VAT System Alignment (LEGAL - Critical):
- Align frontend VAT codes with backend (S25→U25, K25→I25, etc.)
- Add missing codes: UEU, IVV, IVY, REP
- Fix output VAT account 5710→5611 to match StandardDanishAccounts
- Invoice posting now checks fiscal year status before allowing send
- Disallow custom invoice number override (always use auto-numbering)

Security:
- Fix open redirect in AuthController (validate returnUrl is local)
- Store seller CVR/name/address on invoice events (Momsloven §52)

Backend Compliance:
- Add description validation at posting (Bogføringsloven §7)
- SAF-T: add DefaultCurrencyCode, TaxAccountingBasis to header
- SAF-T: add TaxTable to MasterFiles with all VAT codes
- SAF-T: always write balance elements even when zero
- Add financial income account 9100 Renteindtægter

Danish Encoding (~25 fixes):
- Kassekladde: Bogført, Bogføring, Vælg, være, på, Tilføj, Differens
- AttachmentUpload: træk, Understøtter, påkrævet, Bogføringsloven
- keyboardShortcuts: Bogfør, Bogføring display name
- ShortcutsHelpModal: åbne
- DataTable: Genindlæs
- documentProcessing: være
- CloseFiscalYearWizard: årsafslutning

Bugs Fixed:
- Non-null assertion crashes in Kunder.tsx and Produkter.tsx (company!.id)
- StatusBadge typo "Succces"→"Succes"
- HTML entity ø in Kassekladde→proper UTF-8
- AmountText showSign prop was dead code (true || showSign)

UX Improvements:
- Add PageHeader to Bankafstemning and Dashboard loading/empty states
- Responsive columns in Bankafstemning (xs/sm/lg breakpoints)
- Disable misleading buttons: Settings preferences, Kontooversigt edit,
  Loenforstaelse export — with tooltips explaining status
- Add DemoDataDisclaimer to UserSettings
- Fix breadcrumb self-references on 3 pages
- Replace Dashboard fake progress bar with honest message
- Standardize date format DD-MM-YYYY in Bankafstemning and Ordrer
- Replace Input type="number" with InputNumber in Ordrer

Quality:
- Remove 8 redundant console.error statements
- Fix Kreditnotaer breadcrumb "Salg"→"Fakturering" for consistency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Nicolaj Hartmann 2026-02-06 01:15:45 +01:00
parent cd5333f07f
commit 1a0922b778
33 changed files with 355 additions and 172 deletions

View file

@ -117,7 +117,7 @@ export async function processDocument(
}
// Fallback error handling
if (response.status === 401) {
throw new DocumentProcessingApiError('NOT_AUTHENTICATED', 'Du skal vaere logget ind');
throw new DocumentProcessingApiError('NOT_AUTHENTICATED', 'Du skal være logget ind');
}
if (response.status === 403) {
throw new DocumentProcessingApiError('FORBIDDEN', 'Du har ikke adgang til denne virksomhed');

View file

@ -238,7 +238,7 @@ export default function CloseFiscalYearWizard({
onSuccess?.();
} catch (error) {
if (error instanceof Error) {
message.error(`Fejl ved arsafslutning: ${error.message}`);
message.error(`Fejl ved årsafslutning: ${error.message}`);
}
console.error('Failed to close fiscal year:', error);
} finally {

View file

@ -154,7 +154,6 @@ export default function CreateFiscalYearModal({
if (error instanceof Error) {
message.error(`Fejl ved oprettelse: ${error.message}`);
}
console.error('Failed to create fiscal year:', error);
} finally {
setIsSubmitting(false);
}

View file

@ -158,7 +158,6 @@ export default function BankConnectionsTab({ companyId }: BankConnectionsTabProp
setSelectedLinkedAccount(newAccount.id);
} catch (error) {
showError(error, 'Kunne ikke oprette bankkonto');
console.error('Failed to create bank account:', error);
}
};
@ -192,7 +191,6 @@ export default function BankConnectionsTab({ companyId }: BankConnectionsTabProp
setImportFromDate(dayjs());
} catch (error) {
showError(error, 'Kunne ikke koble bankkonto');
console.error('Failed to link account:', error);
}
};
@ -218,7 +216,6 @@ export default function BankConnectionsTab({ companyId }: BankConnectionsTabProp
window.location.href = result.authorizationUrl;
} catch (error) {
showError(error, 'Kunne ikke starte bankforbindelse');
console.error('Failed to start bank connection:', error);
}
};
@ -230,7 +227,6 @@ export default function BankConnectionsTab({ companyId }: BankConnectionsTabProp
showSuccess('Bankforbindelse afbrudt');
} catch (error) {
showError(error, 'Kunne ikke afbryde bankforbindelse');
console.error('Failed to disconnect:', error);
}
};
@ -255,7 +251,6 @@ export default function BankConnectionsTab({ companyId }: BankConnectionsTabProp
window.location.href = result.authorizationUrl;
} catch (error) {
showError(error, 'Kunne ikke genoptage bankforbindelse');
console.error('Failed to reconnect bank connection:', error);
}
};
@ -267,7 +262,6 @@ export default function BankConnectionsTab({ companyId }: BankConnectionsTabProp
showSuccess('Bankforbindelse arkiveret');
} catch (error) {
showError(error, 'Kunne ikke arkivere bankforbindelse');
console.error('Failed to archive:', error);
}
};

View file

@ -80,7 +80,7 @@ export function AmountText({
const formatted = formatCurrency(Math.abs(amount));
// Always show +/- prefix for non-zero amounts (accessibility: not color-only)
// When showSign is explicitly true, same behavior; kept for API compatibility
const alwaysSign = true || showSign;
const alwaysSign = showSign;
const sign = alwaysSign && amount !== 0 ? (amount > 0 ? '+' : '-') : amount < 0 ? '-' : '';
const suffix = showCurrency ? ` ${currencySuffix}` : '';

View file

@ -63,7 +63,7 @@ const getFileIcon = (fileType: string) => {
/**
* Bilag (Document/Attachment) upload component.
* Supports drag-drop, multiple file upload, and preview.
* Required by Bogforingsloven § 6 for document retention.
* Required by Bogføringsloven § 6 for document retention.
*/
export function AttachmentUpload({
attachments = [],
@ -182,10 +182,10 @@ export function AttachmentUpload({
<UploadOutlined style={{ fontSize: 32, color: '#1890ff' }} />
</p>
<p className="ant-upload-text">
Klik eller traek filer hertil for at uploade bilag
Klik eller træk filer hertil for at uploade bilag
</p>
<p className="ant-upload-hint">
Understotter PDF, billeder og Office-dokumenter (max {formatFileSize(maxFileSize)})
Understøtter PDF, billeder og Office-dokumenter (max {formatFileSize(maxFileSize)})
</p>
</Upload.Dragger>
@ -210,7 +210,7 @@ export function AttachmentUpload({
{/* Required warning */}
{required && attachments.length === 0 && fileList.length === 0 && (
<Text type="warning" style={{ display: 'block', marginBottom: spacing.sm }}>
Bilag er pakraevet iht. Bogforingsloven § 6
Bilag er påkrævet iht. Bogføringsloven § 6
</Text>
)}

View file

@ -71,7 +71,7 @@ export function ShortcutsHelpModal({ open, onClose }: ShortcutsHelpModalProps) {
<Divider style={{ margin: '16px 0' }} />
<Text type="secondary" style={{ fontSize: 12 }}>
Tip: Tryk <Tag style={{ margin: '0 4px' }}>K</Tag> for at abne
Tip: Tryk <Tag style={{ margin: '0 4px' }}>K</Tag> for at åbne
kommandopaletten og hurtigt navigere til enhver side.
</Text>
</Modal>

View file

@ -70,7 +70,7 @@ const statusConfig: Record<
success: {
color: 'green',
icon: <CheckCircleOutlined />,
defaultText: 'Succces',
defaultText: 'Succes',
},
warning: {
color: 'orange',

View file

@ -247,7 +247,7 @@ export default function DataTable<T extends { id: string }>({
<Space>
{toolbarActions}
{refreshable && hookEnabled && (
<Tooltip title="Genindlaes">
<Tooltip title="Genindlæs">
<Button
icon={<ReloadOutlined />}
onClick={() => refetch()}

View file

@ -153,7 +153,7 @@ export const shortcuts: Record<string, ShortcutDefinition> = {
postDraft: {
id: 'postDraft',
keys: 'mod+enter',
label: 'Bogfor kladde',
label: 'Bogfør kladde',
category: 'bogforing',
scope: 'page',
},
@ -290,7 +290,7 @@ export function formatShortcutForTooltip(shortcutId: string): string | null {
export const categoryNames: Record<ShortcutCategory, string> = {
global: 'Globale',
navigation: 'Navigation',
bogforing: 'Bogforing',
bogforing: 'Bogføring',
faktura: 'Fakturering',
bank: 'Bank',
kunder: 'Kunder',

View file

@ -13,9 +13,9 @@ import type { VATPeriodicitet } from '@/types/periods';
/**
* Complete VAT code configuration for Danish bookkeeping
*/
export const VAT_CODE_CONFIG: Record<VATCode, VATCodeConfig> = {
S25: {
code: 'S25',
export const VAT_CODE_CONFIG: Record<string, VATCodeConfig> = {
U25: {
code: 'U25',
nameDanish: 'Udgående moms 25%',
nameEnglish: 'Output VAT 25%',
rate: 0.25,
@ -28,8 +28,34 @@ export const VAT_CODE_CONFIG: Record<VATCode, VATCodeConfig> = {
deductible: false,
description: 'Moms på salg af varer og ydelser i Danmark',
},
K25: {
code: 'K25',
UEU: {
code: 'UEU',
nameDanish: 'EU-salg',
nameEnglish: 'EU sales (0%)',
rate: 0,
type: 'exempt',
affectsBoxes: {
basisBox: '2',
},
reverseCharge: false,
deductible: false,
description: 'Salg af varer og ydelser til andre EU-lande',
},
UEXP: {
code: 'UEXP',
nameDanish: 'Eksport',
nameEnglish: 'Export (0%)',
rate: 0,
type: 'exempt',
affectsBoxes: {
basisBox: '2',
},
reverseCharge: false,
deductible: false,
description: 'Eksport til lande uden for EU',
},
I25: {
code: 'I25',
nameDanish: 'Indgående moms 25%',
nameEnglish: 'Input VAT 25%',
rate: 0.25,
@ -41,10 +67,10 @@ export const VAT_CODE_CONFIG: Record<VATCode, VATCodeConfig> = {
deductible: true,
description: 'Fradragsberettiget moms på køb',
},
EU_VARE: {
code: 'EU_VARE',
nameDanish: 'EU-varekøb (erhvervelsesmoms)',
nameEnglish: 'EU goods purchase (acquisition VAT)',
IEUV: {
code: 'IEUV',
nameDanish: 'EU-erhvervelse varer',
nameEnglish: 'EU goods acquisition (reverse charge)',
rate: 0.25,
type: 'reverse_charge',
affectsBoxes: {
@ -52,13 +78,13 @@ export const VAT_CODE_CONFIG: Record<VATCode, VATCodeConfig> = {
basisBox: '3',
},
reverseCharge: true,
deductible: true, // Both output and input VAT
deductible: true,
description: 'Køb af varer fra andre EU-lande med omvendt betalingspligt',
},
EU_YDELSE: {
code: 'EU_YDELSE',
nameDanish: 'EU-ydelseskøb (omvendt betalingspligt)',
nameEnglish: 'EU services purchase (reverse charge)',
IEUY: {
code: 'IEUY',
nameDanish: 'EU-erhvervelse ydelser',
nameEnglish: 'EU services acquisition (reverse charge)',
rate: 0.25,
type: 'reverse_charge',
affectsBoxes: {
@ -69,34 +95,43 @@ export const VAT_CODE_CONFIG: Record<VATCode, VATCodeConfig> = {
deductible: true,
description: 'Køb af ydelser fra udlandet med omvendt betalingspligt',
},
MOMSFRI: {
code: 'MOMSFRI',
nameDanish: 'Momsfritaget',
nameEnglish: 'VAT exempt',
IVV: {
code: 'IVV',
nameDanish: 'Import varer verden',
nameEnglish: 'Import goods (world)',
rate: 0,
type: 'exempt',
affectsBoxes: {
basisBox: '2',
},
type: 'input',
affectsBoxes: {},
reverseCharge: false,
deductible: false,
description: 'Momsfritaget salg (sundhed, undervisning, mv.)',
description: 'Import af varer fra lande uden for EU',
},
EKSPORT: {
code: 'EKSPORT',
nameDanish: 'Eksport (0%)',
nameEnglish: 'Export (0%)',
IVY: {
code: 'IVY',
nameDanish: 'Import ydelser verden',
nameEnglish: 'Import services (world)',
rate: 0,
type: 'exempt',
affectsBoxes: {
basisBox: '2',
},
type: 'input',
affectsBoxes: {},
reverseCharge: false,
deductible: false,
description: 'Eksport til lande uden for EU',
description: 'Import af ydelser fra lande uden for EU',
},
NONE: {
code: 'NONE',
REP: {
code: 'REP',
nameDanish: 'Repræsentation',
nameEnglish: 'Representation (25%, 25% deductible)',
rate: 0.25,
type: 'input',
affectsBoxes: {
vatBox: 'B',
},
reverseCharge: false,
deductible: true,
description: 'Repræsentationsudgifter med 25% fradrag',
},
INGEN: {
code: 'INGEN',
nameDanish: 'Ingen moms',
nameEnglish: 'No VAT',
rate: 0,
@ -231,7 +266,7 @@ export const SKAT_VAT_BOXES: Record<VATBoxId | BasisBoxId, SKATVATBox> = {
*/
export const VAT_ACCOUNTS = {
inputVAT: '5610', // Indgående moms (fradrag)
outputVAT: '5710', // Udgående moms (skyld)
outputVAT: '5611', // Udgående moms (skyld)
euVAT: '5620', // EU-moms (erhvervelsesmoms)
} as const;
@ -243,7 +278,7 @@ export const VAT_ACCOUNTS = {
* Valid VAT code values for validation
*/
export const VALID_VAT_CODES: readonly VATCode[] = [
'S25', 'K25', 'EU_VARE', 'EU_YDELSE', 'MOMSFRI', 'EKSPORT', 'NONE'
'U25', 'UEU', 'UEXP', 'I25', 'IEUV', 'IEUY', 'IVV', 'IVY', 'REP', 'INGEN'
] as const;
/**
@ -254,17 +289,17 @@ export function isValidVATCode(code: unknown): code is VATCode {
}
/**
* Safely convert a string to VATCode, returns 'NONE' if invalid
* Safely convert a string to VATCode, returns 'INGEN' if invalid
*/
export function toVATCode(code: unknown): VATCode {
return isValidVATCode(code) ? code : 'NONE';
return isValidVATCode(code) ? code : 'INGEN';
}
/**
* Get VAT code configuration
*/
export function getVATCodeConfig(code: VATCode): VATCodeConfig {
return VAT_CODE_CONFIG[code];
return VAT_CODE_CONFIG[code] ?? VAT_CODE_CONFIG['INGEN'];
}
/**
@ -328,21 +363,21 @@ export function getSKATBox(boxId: VATBoxId | BasisBoxId): SKATVATBox {
* Check if a VAT code is deductible (affects input VAT)
*/
export function isVATDeductible(code: VATCode): boolean {
return VAT_CODE_CONFIG[code].deductible;
return VAT_CODE_CONFIG[code]?.deductible ?? false;
}
/**
* Check if a VAT code is reverse charge
*/
export function isReverseCharge(code: VATCode): boolean {
return VAT_CODE_CONFIG[code].reverseCharge;
return VAT_CODE_CONFIG[code]?.reverseCharge ?? false;
}
/**
* Get VAT rate for a code
*/
export function getVATRate(code: VATCode): number {
return VAT_CODE_CONFIG[code].rate;
return VAT_CODE_CONFIG[code]?.rate ?? 0;
}
/**

View file

@ -30,6 +30,7 @@ import {
BulbOutlined,
} from '@ant-design/icons';
import dayjs from 'dayjs';
import { useNavigate } from 'react-router-dom';
import { useCompany } from '@/hooks/useCompany';
import { useReconciliationStore } from '@/stores/reconciliationStore';
import { useCompanyStore } from '@/stores/companyStore';
@ -67,6 +68,7 @@ interface MatchSuggestion {
export default function Bankafstemning() {
const { company } = useCompany();
const navigate = useNavigate();
const { activeCompany } = useCompanyStore();
// Fetch data from API
@ -218,10 +220,15 @@ export default function Bankafstemning() {
if (isLoading) {
return (
<div>
<PageHeader
title="Bankafstemning"
subtitle={company?.name}
breadcrumbs={[{ title: 'Bank' }, { title: 'Bankafstemning' }]}
/>
<Skeleton active paragraph={{ rows: 2 }} />
<Row gutter={16} style={{ marginTop: 16 }}>
<Col span={12}><Skeleton active paragraph={{ rows: 8 }} /></Col>
<Col span={12}><Skeleton active paragraph={{ rows: 8 }} /></Col>
<Col xs={24} lg={12}><Skeleton active paragraph={{ rows: 8 }} /></Col>
<Col xs={24} lg={12}><Skeleton active paragraph={{ rows: 8 }} /></Col>
</Row>
</div>
);
@ -230,9 +237,20 @@ export default function Bankafstemning() {
// Empty state - no bank connections
if (bankAccounts.length === 0) {
return (
<Empty
description="Ingen bankforbindelser. Opret forbindelse til din bank under Indstillinger."
/>
<div>
<PageHeader
title="Bankafstemning"
subtitle={company?.name}
breadcrumbs={[{ title: 'Bank' }, { title: 'Bankafstemning' }]}
/>
<Empty
description="Ingen bankforbindelser. Opret forbindelse til din bank under Indstillinger."
>
<Button type="primary" onClick={() => navigate('/indstillinger')}>
til indstillinger
</Button>
</Empty>
</div>
);
}
@ -241,7 +259,7 @@ export default function Bankafstemning() {
<PageHeader
title="Bankafstemning"
subtitle={company?.name}
breadcrumbs={[{ title: 'Bank', path: '/bankafstemning' }, { title: 'Bankafstemning' }]}
breadcrumbs={[{ title: 'Bank' }, { title: 'Bankafstemning' }]}
extra={
<Space>
<Button
@ -283,13 +301,13 @@ export default function Bankafstemning() {
<RangePicker
value={dateRange}
onChange={(dates) => setDateRange(dates as [dayjs.Dayjs, dayjs.Dayjs])}
format="DD/MM/YYYY"
format="DD-MM-YYYY"
/>
</Space>
{/* Summary */}
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={8}>
<Col xs={24} sm={8}>
<Card size="small">
<Statistic
title="Bank (uafstemt)"
@ -302,7 +320,7 @@ export default function Bankafstemning() {
/>
</Card>
</Col>
<Col span={8}>
<Col xs={24} sm={8}>
<Card size="small">
<Statistic
title="Bogføring (uafstemt)"
@ -315,10 +333,10 @@ export default function Bankafstemning() {
/>
</Card>
</Col>
<Col span={8}>
<Col xs={24} sm={8}>
<Card size="small">
<Statistic
title="Difference"
title="Differens"
value={difference}
precision={2}
formatter={(value) => formatCurrency(value as number)}
@ -372,7 +390,7 @@ export default function Bankafstemning() {
{/* Side-by-side panels */}
<Row gutter={16}>
{/* Bank Transactions */}
<Col span={12}>
<Col xs={24} lg={12}>
<Card
title={
<Space>
@ -483,7 +501,7 @@ export default function Bankafstemning() {
</Col>
{/* Ledger Entries */}
<Col span={12}>
<Col xs={24} lg={12}>
<Card
title={
<Space>
@ -496,7 +514,7 @@ export default function Bankafstemning() {
>
{ledgerEntries.length === 0 ? (
<Empty
description="Ingen uafstemte bogføringsposter (API ikke implementeret endnu)"
description="Ingen uafstemte bogføringsposter"
style={{ padding: 24 }}
/>
) : (
@ -650,7 +668,7 @@ export default function Bankafstemning() {
label="Dato"
rules={[{ required: true }]}
>
<DatePicker format="DD/MM/YYYY" style={{ width: '100%' }} />
<DatePicker format="DD-MM-YYYY" style={{ width: '100%' }} />
</Form.Item>
<Form.Item
name="description"

View file

@ -140,7 +140,6 @@ export default function CompanySetupWizard() {
setCurrentStep(4); // Success step
} catch (error) {
console.error('Company creation failed:', error);
showError(error, 'Kunne ikke oprette virksomhed');
}
};

View file

@ -1,4 +1,4 @@
import { Row, Col, Card, Statistic, Typography, Space, Tag, Progress, Skeleton, Empty } from 'antd';
import { Row, Col, Card, Statistic, Typography, Space, Tag, Skeleton, Empty } from 'antd';
import {
BankOutlined,
FileTextOutlined,
@ -154,7 +154,11 @@ export default function Dashboard() {
if (isLoading) {
return (
<div>
<Skeleton active paragraph={{ rows: 1 }} />
<PageHeader
title="Dashboard"
subtitle={company?.name}
breadcrumbs={[{ title: 'Dashboard' }]}
/>
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
<Col xs={24} sm={12} lg={6}>
<Card size="small"><Skeleton active paragraph={{ rows: 2 }} /></Card>
@ -367,14 +371,8 @@ export default function Dashboard() {
value={metrics.unreconciledCount}
prefix={<FileTextOutlined />}
/>
<Progress
percent={metrics.unreconciledCount === 0 ? 100 : 75}
status="active"
strokeColor={accountingColors.balance}
style={{ marginTop: 16 }}
/>
<Text type="secondary">
{metrics.unreconciledCount === 0 ? '100% afstemt' : 'Bankafstemning ikke implementeret endnu'}
<Text type="secondary" style={{ display: 'block', marginTop: 16 }}>
Bankafstemning er ikke tilgængelig endnu
</Text>
</div>
</Card>

View file

@ -469,7 +469,7 @@ export default function Fakturaer() {
<PageHeader
title="Fakturaer"
subtitle={company?.name}
breadcrumbs={[{ title: 'Fakturering', path: '/fakturaer' }, { title: 'Fakturaer' }]}
breadcrumbs={[{ title: 'Fakturering' }, { title: 'Fakturaer' }]}
extra={
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreateInvoice}>
Ny fakturakladde

View file

@ -199,7 +199,7 @@ export default function Kassekladde() {
return <Tag color="red">Annulleret</Tag>;
}
return value ? (
<Tag color="green">Bogfort</Tag>
<Tag color="green">Bogført</Tag>
) : (
<Tag color="orange">Kladde</Tag>
);
@ -301,7 +301,7 @@ export default function Kassekladde() {
case 'void':
Modal.confirm({
title: 'Annuller bilag',
content: `Er du sikker pa at du vil annullere bilag ${record.transactionNumber}?`,
content: `Er du sikker på at du vil annullere bilag ${record.transactionNumber}?`,
okText: 'Annuller bilag',
okType: 'danger',
cancelText: 'Fortryd',
@ -352,7 +352,7 @@ export default function Kassekladde() {
const validation = validateDoubleEntry(lines as TransactionLine[]);
if (!validation.valid) {
message.error(
`Debet (${formatCurrency(validation.totalDebit)}) skal vaere lig kredit (${formatCurrency(validation.totalCredit)})`
`Debet (${formatCurrency(validation.totalDebit)}) skal være lig kredit (${formatCurrency(validation.totalCredit)})`
);
return;
}
@ -430,7 +430,7 @@ export default function Kassekladde() {
const getStatusLabel = (status: JournalEntryDraftStatus): { label: string; color: string } => {
switch (status) {
case 'posted': return { label: 'Bogfort', color: 'green' };
case 'posted': return { label: 'Bogført', color: 'green' };
case 'discarded': return { label: 'Annulleret', color: 'red' };
case 'draft': return { label: 'Kladde', color: 'orange' };
case 'pending_review': return { label: 'Afventer gennemgang', color: 'blue' };
@ -447,7 +447,7 @@ export default function Kassekladde() {
<PageHeader
title="Kassekladde"
subtitle={activeCompany?.name}
breadcrumbs={[{ title: 'Bogforing', path: '/kontooversigt' }, { title: 'Kassekladde' }]}
breadcrumbs={[{ title: 'Bogføring', path: '/kontooversigt' }, { title: 'Kassekladde' }]}
/>
<Skeleton active paragraph={{ rows: 10 }} />
</div>
@ -459,7 +459,7 @@ export default function Kassekladde() {
<PageHeader
title="Kassekladde"
subtitle={activeCompany?.name}
breadcrumbs={[{ title: 'Bogforing', path: '/kontooversigt' }, { title: 'Kassekladde' }]}
breadcrumbs={[{ title: 'Bogføring', path: '/kontooversigt' }, { title: 'Kassekladde' }]}
extra={
<Button
type="primary"
@ -504,7 +504,7 @@ export default function Kassekladde() {
value={statusFilter}
onChange={(value) => setStatusFilter(value ?? null)}
options={[
{ value: 'posted', label: 'Bogfort' },
{ value: 'posted', label: 'Bogført' },
{ value: 'draft', label: 'Kladde' },
{ value: 'discarded', label: 'Annulleret' },
]}
@ -569,12 +569,12 @@ export default function Kassekladde() {
</Tag>
</Descriptions.Item>
{detailDraft.isReconciled && detailDraft.postedAt && (
<Descriptions.Item label="Bogfort">
<Descriptions.Item label="Bogført">
{dayjs(detailDraft.postedAt).format('DD-MM-YYYY HH:mm')}
</Descriptions.Item>
)}
{detailDraft.postedBy && (
<Descriptions.Item label="Bogfort af">
<Descriptions.Item label="Bogført af">
{detailDraft.postedBy}
</Descriptions.Item>
)}
@ -647,7 +647,7 @@ export default function Kassekladde() {
<Form.Item
name="date"
label="Dato"
rules={[{ required: true, message: 'Vaelg dato' }]}
rules={[{ required: true, message: 'Vælg dato' }]}
initialValue={dayjs()}
>
<DatePicker format="DD-MM-YYYY" style={{ width: 150 }} />
@ -683,7 +683,7 @@ export default function Kassekladde() {
<td style={{ padding: 4 }}>
<Select
style={{ width: '100%' }}
placeholder="Vaelg konto"
placeholder="Vælg konto"
showSearch
optionFilterProp="label"
value={line.accountId}
@ -752,7 +752,7 @@ export default function Kassekladde() {
<tr style={{ borderTop: '2px solid #f0f0f0' }}>
<td style={{ padding: 8 }}>
<Button type="dashed" size="small" onClick={handleAddLine}>
+ Tilf&oslash;j linje
+ Tilføj linje
</Button>
</td>
<td
@ -775,7 +775,7 @@ export default function Kassekladde() {
</td>
<td colSpan={2} style={{ padding: 8 }}>
{!balance.valid && (
<Tooltip title={`Difference: ${formatCurrency(balance.difference)}`}>
<Tooltip title={`Differens: ${formatCurrency(balance.difference)}`}>
<Tag color="red">Ubalance!</Tag>
</Tooltip>
)}

View file

@ -17,6 +17,7 @@ import {
Switch,
Divider,
Descriptions,
Tooltip,
} from 'antd';
import {
PlusOutlined,
@ -128,13 +129,6 @@ export default function Kontooversigt() {
setIsDrawerOpen(true);
};
const handleEditAccount = () => {
if (selectedAccount) {
form.setFieldsValue(selectedAccount);
setIsEditMode(true);
}
};
const handleCloseDrawer = () => {
setIsDrawerOpen(false);
setIsEditMode(false);
@ -367,9 +361,11 @@ export default function Kontooversigt() {
onClose={handleCloseDrawer}
extra={
!isEditMode && selectedAccount && (
<Button type="primary" icon={<EditOutlined />} onClick={handleEditAccount}>
Rediger
</Button>
<Tooltip title="Redigering kommer snart">
<Button type="primary" icon={<EditOutlined />} disabled>
Rediger
</Button>
</Tooltip>
)
}
footer={

View file

@ -442,7 +442,7 @@ export default function Kreditnotaer() {
<PageHeader
title="Kreditnotaer"
subtitle={company?.name}
breadcrumbs={[{ title: 'Salg' }, { title: 'Kreditnotaer' }]}
breadcrumbs={[{ title: 'Fakturering' }, { title: 'Kreditnotaer' }]}
extra={
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreateCreditNote}>
Ny kreditnota

View file

@ -173,8 +173,12 @@ export default function Kunder() {
await updateCustomerMutation.mutateAsync(input);
showSuccess('Kunde opdateret');
} else {
if (!company) {
showError(new Error('Ingen virksomhed valgt'));
return;
}
const input: CreateCustomerInput = {
companyId: company!.id,
companyId: company.id,
customerType: values.customerType.toUpperCase() as 'BUSINESS' | 'PRIVATE',
name: values.name,
cvr: values.cvr || undefined,

View file

@ -13,6 +13,7 @@ import {
Button,
Descriptions,
Collapse,
Tooltip,
} from 'antd';
import {
TeamOutlined,
@ -279,7 +280,9 @@ export default function Loenforstaelse() {
subtitle={company?.name}
breadcrumbs={[{ title: 'Løn' }, { title: 'Lønforståelse' }]}
extra={
<Button icon={<DownloadOutlined />}>Eksporter lønsedler</Button>
<Tooltip title="Eksport er endnu ikke implementeret">
<Button icon={<DownloadOutlined />} disabled>Eksporter lønsedler</Button>
</Tooltip>
}
/>

View file

@ -9,6 +9,7 @@ import {
Modal,
Form,
Input,
InputNumber,
Select,
Skeleton,
Alert,
@ -696,7 +697,7 @@ export default function Ordrer() {
<Tag>{line.vatCode}</Tag>
{line.isInvoiced && line.invoicedAt && (
<Tag color="blue">
Faktureret: {dayjs(line.invoicedAt).format('DD/MM/YYYY')}
Faktureret: {dayjs(line.invoicedAt).format('DD-MM-YYYY')}
</Tag>
)}
</Space>
@ -876,7 +877,7 @@ export default function Ordrer() {
label="Antal"
rules={[{ required: true, message: 'Angiv antal' }]}
>
<Input type="number" min={0} step={1} />
<InputNumber min={0} step={1} style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={8}>
@ -893,10 +894,10 @@ export default function Ordrer() {
label="Enhedspris"
rules={[{ required: true, message: 'Angiv pris' }]}
>
<Input
type="number"
<InputNumber
min={0}
step={0.01}
style={{ width: '100%' }}
disabled={addLineMode === 'product' && !!selectedProductId}
/>
</Form.Item>
@ -905,7 +906,7 @@ export default function Ordrer() {
<Row gutter={16}>
<Col span={12}>
<Form.Item name="discountPercent" label="Rabat (%)">
<Input type="number" min={0} max={100} step={1} />
<InputNumber min={0} max={100} step={1} style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={12}>

View file

@ -173,8 +173,12 @@ export default function Produkter() {
await updateProductMutation.mutateAsync(input);
showSuccess('Produkt opdateret');
} else {
if (!company) {
showError(new Error('Ingen virksomhed valgt'));
return;
}
const input: CreateProductInput = {
companyId: company!.id,
companyId: company.id,
productNumber: values.productNumber || undefined,
name: values.name,
description: values.description || undefined,

View file

@ -12,6 +12,7 @@ import {
Divider,
message,
Space,
Tooltip,
} from 'antd';
import {
SaveOutlined,
@ -61,19 +62,6 @@ export default function Settings() {
}
};
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',
@ -278,9 +266,11 @@ export default function Settings() {
</Row>
<div style={{ textAlign: 'right' }}>
<Button type="primary" icon={<SaveOutlined />} onClick={handleSavePreferences}>
Gem præferencer
</Button>
<Tooltip title="Præferencer er endnu ikke forbundet til backend">
<Button type="primary" icon={<SaveOutlined />} disabled>
Gem præferencer
</Button>
</Tooltip>
</div>
</Form>
</Card>

View file

@ -28,6 +28,7 @@ import {
import type { UploadProps } from 'antd';
import { spacing } from '@/styles/designTokens';
import { PageHeader } from '@/components/shared/PageHeader';
import { DemoDataDisclaimer } from '@/components/shared/DemoDataDisclaimer';
const { Title, Text } = Typography;
@ -461,6 +462,8 @@ export default function UserSettings() {
breadcrumbs={[{ title: 'Brugerindstillinger' }]}
/>
<DemoDataDisclaimer message="Brugerindstillinger er endnu ikke forbundet til backend. Ændringer gemmes ikke." />
<Tabs items={tabItems} />
</div>
);

View file

@ -10,13 +10,24 @@ import type { VATPeriodicitet } from './periods';
* VAT codes used in Danish bookkeeping
*/
export type VATCode =
| 'S25' // Salgsmoms 25% (udgaaende moms)
| 'K25' // Koebsmoms 25% (indgaaende moms)
| 'EU_VARE' // EU-varekoeb (reverse charge)
| 'EU_YDELSE' // EU-ydelseskoeb (reverse charge)
| 'MOMSFRI' // Momsfritaget (healthcare, education, etc.)
| 'EKSPORT' // Eksport (0%)
| 'NONE'; // Ingen moms
| 'U25' // Udgående moms 25% (salg)
| 'UEU' // EU-salg (0%)
| 'UEXP' // Eksport (0%)
| 'I25' // Indgående moms 25% (køb)
| 'IEUV' // EU-erhvervelse varer (reverse charge)
| 'IEUY' // EU-erhvervelse ydelser (reverse charge)
| 'IVV' // Import varer verden (0%)
| 'IVY' // Import ydelser verden (0%)
| 'REP' // Repræsentation (25%, 25% fradrag)
| 'INGEN' // Ingen moms
// Legacy codes kept for backwards compatibility with other modules
| 'S25' // @deprecated Use U25
| 'K25' // @deprecated Use I25
| 'EU_VARE' // @deprecated Use IEUV
| 'EU_YDELSE' // @deprecated Use IEUY
| 'MOMSFRI' // @deprecated Use INGEN
| 'EKSPORT' // @deprecated Use UEXP
| 'NONE'; // @deprecated Use INGEN
/**
* VAT code type classification