books/frontend/src/components/shared/AmountText.tsx

216 lines
5.9 KiB
TypeScript
Raw Normal View History

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));
Audit v2: fix security, data integrity, compliance, bugs, encoding, UX Backend Security & Data Integrity: - Block negative debit/credit amounts that bypass balance validation - Require document date at posting (was optional, bypassing fiscal year checks) - Fix event sourcing anti-pattern: timestamps now stored in events, not UtcNow in Apply - Add [Authorize] to BankingController OAuth callback - Add company access check on attachment downloads - Validate CVR in CompanyAggregate.Create and CompanyAggregate.Update - Require company CVR for invoice creation (Momsloven §52) - Delete leftover WeatherForecastController - Fix duplicate migration number 007 (renamed to 007b) - Remove dead code in VatCalculationService (identical if/else branches) Accounting Compliance: - Add missing VAT accounts to StandardDanishAccounts (5610, 5611, 5620) - Populate SAF-T TaxInformation on transaction lines (was always null) - Add AuditFileCountry and TaxRegistrationNumber to SAF-T header Critical Frontend Bugs: - Fix Dashboard <a href> causing full page reloads (now uses React Router Link) - Wire Kassekladde filters to actual data (account, status, date range) - Pre-populate form when editing existing Kassekladde drafts - Add detail drawer for "Vis detaljer" action (was just a toast) - Toggle advanced filters with "Flere filtre" button - CloseFiscalYearWizard now actually posts closing entries via mutations - "Create next year" checkbox now creates the next fiscal year Danish Character Encoding (~50 fixes): - Fix ø/æ/å across Momsindberetning, DocumentUploadModal, Bankafstemning, Kontooversigt, CloseFiscalYearWizard, vatCodes, periodStore, periods, accounting, types/periods Dead Buttons & UX: - Disable Momsindberetning PDF/Export buttons with tooltips - FiscalYearSelector "Administrer" now navigates to Settings - Settings bank tab now uses real BankConnectionsTab component - Bankafstemning save button disabled with development tooltip - Replace hardcoded account options with real API data (Bankafstemning, Fakturaer) - Header help button shows info message, notification bell shows popover Consistency & Quality: - Remove 7 console.log statements from production code - Adopt PageHeader on 6 remaining pages (Kreditnotaer, Settings, Admin, etc.) - Standardize loading states to Skeleton pattern (5 pages) - Replace deprecated bodyStyle prop on Ant Design Cards - Standardize date format to DD-MM-YYYY - Fix sidebar width mismatch in designTokens - Fix Kontooversigt breadcrumb pointing to non-existent route Accessibility: - Add aria-label to sidebar navigation - Add +/- prefix to AmountText for color-blind users - Fix CompanySwitcher permanent skeleton when no companies Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 00:18:19 +01:00
// Always show +/- prefix for non-zero amounts (accessibility: not color-only)
// When showSign is explicitly true, same behavior; kept for API compatibility
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 &oslash; 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
const alwaysSign = showSign;
Audit v2: fix security, data integrity, compliance, bugs, encoding, UX Backend Security & Data Integrity: - Block negative debit/credit amounts that bypass balance validation - Require document date at posting (was optional, bypassing fiscal year checks) - Fix event sourcing anti-pattern: timestamps now stored in events, not UtcNow in Apply - Add [Authorize] to BankingController OAuth callback - Add company access check on attachment downloads - Validate CVR in CompanyAggregate.Create and CompanyAggregate.Update - Require company CVR for invoice creation (Momsloven §52) - Delete leftover WeatherForecastController - Fix duplicate migration number 007 (renamed to 007b) - Remove dead code in VatCalculationService (identical if/else branches) Accounting Compliance: - Add missing VAT accounts to StandardDanishAccounts (5610, 5611, 5620) - Populate SAF-T TaxInformation on transaction lines (was always null) - Add AuditFileCountry and TaxRegistrationNumber to SAF-T header Critical Frontend Bugs: - Fix Dashboard <a href> causing full page reloads (now uses React Router Link) - Wire Kassekladde filters to actual data (account, status, date range) - Pre-populate form when editing existing Kassekladde drafts - Add detail drawer for "Vis detaljer" action (was just a toast) - Toggle advanced filters with "Flere filtre" button - CloseFiscalYearWizard now actually posts closing entries via mutations - "Create next year" checkbox now creates the next fiscal year Danish Character Encoding (~50 fixes): - Fix ø/æ/å across Momsindberetning, DocumentUploadModal, Bankafstemning, Kontooversigt, CloseFiscalYearWizard, vatCodes, periodStore, periods, accounting, types/periods Dead Buttons & UX: - Disable Momsindberetning PDF/Export buttons with tooltips - FiscalYearSelector "Administrer" now navigates to Settings - Settings bank tab now uses real BankConnectionsTab component - Bankafstemning save button disabled with development tooltip - Replace hardcoded account options with real API data (Bankafstemning, Fakturaer) - Header help button shows info message, notification bell shows popover Consistency & Quality: - Remove 7 console.log statements from production code - Adopt PageHeader on 6 remaining pages (Kreditnotaer, Settings, Admin, etc.) - Standardize loading states to Skeleton pattern (5 pages) - Replace deprecated bodyStyle prop on Ant Design Cards - Standardize date format to DD-MM-YYYY - Fix sidebar width mismatch in designTokens - Fix Kontooversigt breadcrumb pointing to non-existent route Accessibility: - Add aria-label to sidebar navigation - Add +/- prefix to AmountText for color-blind users - Fix CompanySwitcher permanent skeleton when no companies Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 00:18:19 +01:00
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;