/** * Shared display helpers for the unified Banking workspace * (`pages/accounting/banking` + `components/accounting/banking/*`). * Display-only — API payloads stay ISO / raw. */ const amountFormatter = new Intl.NumberFormat('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) /** `1234.5` → `1.234,50`; '' for null/undefined/''. */ export const formatAmount = (value: any): string => { if (value === null || value === undefined || value === '') return '' const num = Number(value) return isNaN(num) ? String(value) : amountFormatter.format(num) } /** `DE02513900000095588301` → `DE02 5139 0000 0095 5883 01`. */ export const formatIban = (iban: any): string => { return String(iban || '').replace(/\s+/g, '').replace(/(.{4})/g, '$1 ').trim() } /** Truncate long free text (remittance info, messages) for table cells. */ export const truncateText = (text: any, max = 120): string => { const str = String(text || '') return str.length > max ? str.substring(0, max) + '…' : str } /** * Laravel-proxied routes return HTTP 200 with `{ success:false, message }` for * application-level refusals, and `{ status: 4xx/5xx }` for transport errors. */ export const okLaravel = (res: any): boolean => { return !!res && res.success !== false && Number(res.status || 200) < 400 } /** iDempiere-backed routes follow the app-wide `Number(res.status) === 200` convention. */ export const okIdempiere = (res: any): boolean => { return !!res && Number(res.status) === 200 } export type PillTone = 'neutral' | 'info' | 'success' | 'warning' | 'danger' /** EBICS connection lifecycle (Laravel `bank_connections.status`). */ export const connectionStatusMeta: Record = { created: { key: 'banking_status_created', fallback: 'Angelegt', tone: 'neutral' }, keys_sent: { key: 'banking_status_keys_sent', fallback: 'Schlüssel gesendet', tone: 'info' }, letter_pending: { key: 'banking_status_letter_pending', fallback: 'INI-Brief ausstehend', tone: 'info' }, bank_keys_verified: { key: 'banking_status_bank_keys_verified', fallback: 'Bankschlüssel geprüft', tone: 'warning' }, active: { key: 'banking_status_active', fallback: 'Aktiv', tone: 'success' }, suspended: { key: 'banking_status_suspended', fallback: 'Pausiert', tone: 'danger' } } /** Staged statement / transaction `import_status`. */ export const importStatusMeta: Record = { pending: { key: 'banking_import_status_pending', fallback: 'Ausstehend', tone: 'info' }, imported: { key: 'banking_import_status_imported', fallback: 'Importiert', tone: 'success' }, failed: { key: 'banking_import_status_failed', fallback: 'Fehlgeschlagen', tone: 'danger' }, skipped: { key: 'banking_import_status_skipped', fallback: 'Übersprungen', tone: 'neutral' } } /** Preview verdicts from both the EBICS dry-run and the iDempiere import plan. */ export const verdictMeta: Record = { would_create: { key: 'banking_verdict_would_create', fallback: 'Wird angelegt', tone: 'info' }, would_stage: { key: 'banking_verdict_would_stage', fallback: 'Wird bereitgestellt', tone: 'info' }, already_imported: { key: 'banking_verdict_already_imported', fallback: 'Bereits importiert', tone: 'neutral' }, already_in_idempiere: { key: 'banking_verdict_already_in_idempiere', fallback: 'Bereits in iDempiere', tone: 'neutral' }, skip_imported: { key: 'banking_verdict_skip_imported', fallback: 'Übersprungen (importiert)', tone: 'neutral' }, skip_non_eur: { key: 'banking_verdict_skip_non_eur', fallback: 'Übersprungen (nicht EUR)', tone: 'warning' }, already_staged: { key: 'banking_verdict_already_staged', fallback: 'Bereits bereitgestellt', tone: 'neutral' }, staged: { key: 'banking_verdict_staged', fallback: 'Bereitgestellt', tone: 'success' }, bank_account_missing: { key: 'banking_verdict_bank_account_missing', fallback: 'Bankkonto fehlt', tone: 'danger' }, no_pending_lines: { key: 'banking_verdict_no_pending_lines', fallback: 'Keine offenen Umsätze', tone: 'warning' } } /** Resolve `{ label, tone }` for a status value against one of the meta maps. */ export const resolveMeta = ( map: Record, value: any, t: any ): { label: string; tone: PillTone } => { const meta = map[String(value || '')] if (!meta) return { label: String(value || '—'), tone: 'neutral' } return { label: t?.[meta.key] || meta.fallback, tone: meta.tone } } /** * Newest-first ordering for every list on the Banking page (statements, * transactions, lines, fetch/import results). Compares ISO-ish date strings * (`YYYY-MM-DD…`) descending, then an optional numeric tie-breaker descending * (line no / id) so a statement's own lines also read latest → oldest. * Returns a NEW array — Laravel keeps processing imports oldest-first (balance * chain); only the display is flipped. */ export const newestFirst = (rows: T[] | null | undefined, dateKey: string, tieKey = ''): T[] => { return [...(rows || [])].sort((a: any, b: any) => { const byDate = String(b?.[dateKey] ?? '').localeCompare(String(a?.[dateKey] ?? '')) if (byDate) return byDate return tieKey ? Number(b?.[tieKey] ?? 0) - Number(a?.[tieKey] ?? 0) : 0 }) }