books/frontend/src/components/shared/AmountText.tsx
Nicolaj Hartmann 1a0922b778 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>
2026-02-06 01:15:45 +01:00

215 lines
5.9 KiB
TypeScript

import { Typography, Tooltip } from 'antd';
import { ArrowUpOutlined, ArrowDownOutlined, MinusOutlined } from '@ant-design/icons';
import { formatCurrency } from '@/lib/formatters';
import { accountingColors } from '@/styles/theme';
import { typography, getAmountColor } from '@/styles/designTokens';
const { Text } = Typography;
export type AmountType = 'debit' | 'credit' | 'neutral' | 'auto';
export interface AmountTextProps {
/** The amount to display */
amount: number;
/** Override automatic color detection */
type?: AmountType;
/** Whether to show the sign (+/-) */
showSign?: boolean;
/** Whether to show currency suffix */
showCurrency?: boolean;
/** Currency suffix text */
currencySuffix?: string;
/** Text size variant */
size?: 'small' | 'default' | 'large';
/** Whether to use tabular (monospace) numbers */
tabular?: boolean;
/** Whether to show tooltip with full precision */
showTooltip?: boolean;
/** Show icon indicator for accessibility (not color-only) */
showIcon?: boolean;
/** Additional CSS class */
className?: string;
/** Inline style overrides */
style?: React.CSSProperties;
}
/**
* A consistent component for displaying monetary amounts.
* Automatically colors based on value (positive = credit/green, negative = debit/red).
*
* @example
* <AmountText amount={1234.56} />
* <AmountText amount={-500} type="debit" showSign />
* <AmountText amount={0} type="neutral" />
*/
export function AmountText({
amount,
type = 'auto',
showSign = false,
showCurrency = true,
currencySuffix = 'kr.',
size = 'default',
tabular = true,
showTooltip = false,
showIcon = false,
className,
style,
}: AmountTextProps) {
const getColor = (): string => {
if (type === 'auto') {
return getAmountColor(amount);
}
if (type === 'neutral') {
return accountingColors.neutral;
}
return accountingColors[type];
};
const getFontSize = (): number => {
switch (size) {
case 'small':
return typography.caption.fontSize;
case 'large':
return typography.h3.fontSize;
default:
return typography.body.fontSize;
}
};
const formatAmount = (): string => {
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 = showSign;
const sign = alwaysSign && amount !== 0 ? (amount > 0 ? '+' : '-') : amount < 0 ? '-' : '';
const suffix = showCurrency ? ` ${currencySuffix}` : '';
return `${sign}${formatted}${suffix}`;
};
// Determine icon for accessibility (not relying on color alone)
const getIcon = () => {
if (!showIcon) return null;
const effectiveType = type === 'auto'
? (amount > 0 ? 'credit' : amount < 0 ? 'debit' : 'neutral')
: type;
const iconStyle = { marginRight: 4, fontSize: getFontSize() - 2 };
switch (effectiveType) {
case 'credit':
return <ArrowUpOutlined style={iconStyle} aria-label="Kredit" />;
case 'debit':
return <ArrowDownOutlined style={iconStyle} aria-label="Debet" />;
default:
return <MinusOutlined style={iconStyle} aria-label="Nul" />;
}
};
const textStyle: React.CSSProperties = {
color: getColor(),
fontSize: getFontSize(),
fontVariantNumeric: tabular ? 'tabular-nums' : undefined,
...style,
};
const content = (
<Text className={className} style={textStyle}>
{getIcon()}
{formatAmount()}
</Text>
);
if (showTooltip) {
return (
<Tooltip title={`${amount.toLocaleString('da-DK', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ${currencySuffix}`}>
{content}
</Tooltip>
);
}
return content;
}
/**
* Displays a balance with label
*/
export interface BalanceDisplayProps {
label: string;
amount: number;
showChange?: boolean;
change?: number;
size?: 'small' | 'default' | 'large';
}
export function BalanceDisplay({ label, amount, size = 'default' }: BalanceDisplayProps) {
return (
<div>
<Text type="secondary" style={{ fontSize: typography.caption.fontSize }}>
{label}
</Text>
<div>
<AmountText amount={amount} size={size} />
</div>
</div>
);
}
/**
* Displays debit and credit columns for double-entry bookkeeping
*/
export interface DoubleEntryAmountProps {
debit?: number;
credit?: number;
showZero?: boolean;
}
export function DoubleEntryAmount({ debit, credit, showZero = false }: DoubleEntryAmountProps) {
const showDebit = debit !== undefined && (debit !== 0 || showZero);
const showCredit = credit !== undefined && (credit !== 0 || showZero);
return (
<>
<span style={{ display: 'inline-block', minWidth: 100, textAlign: 'right' }}>
{showDebit && <AmountText amount={debit!} type="debit" showCurrency={false} />}
</span>
<span style={{ display: 'inline-block', minWidth: 100, textAlign: 'right', marginLeft: 16 }}>
{showCredit && <AmountText amount={credit!} type="credit" showCurrency={false} />}
</span>
</>
);
}
/**
* Displays a running balance with optional delta indicator
*/
export interface RunningBalanceProps {
balance: number;
previousBalance?: number;
}
export function RunningBalance({ balance, previousBalance }: RunningBalanceProps) {
const delta = previousBalance !== undefined ? balance - previousBalance : undefined;
return (
<span className="tabular-nums">
<AmountText amount={balance} type="auto" />
{delta !== undefined && delta !== 0 && (
<Text
type="secondary"
style={{
fontSize: typography.small.fontSize,
marginLeft: 4,
color: delta > 0 ? accountingColors.credit : accountingColors.debit,
}}
>
({delta > 0 ? '+' : ''}
{formatCurrency(delta)})
</Text>
)}
</span>
);
}
export default AmountText;