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>
215 lines
5.9 KiB
TypeScript
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;
|