import { STORE_KEY, CURRENCIES, COMPANY_PUBLIC_ADDRESS, createDemoIban, createDemoBic, FX_SPREAD, CRYPTO_SPREAD, createDemoState, loadDemoState, saveDemoState, moneyToCents, createCustomer, openAccount, createInternalTransfer, convertBetweenAccounts, createExternalPayment, approvePayment, createInterbankTransfer, approveInterbankTransfer, verifyDemoReference, createSupportTicket, updateSupportTicket, recordCashOperation, issueCard, setCardStatus, setCardControls, loadPrepaidCard, simulateCardPurchase, openSavingsDeposit, matureSavingsDeposit, createLoanApplication, approveLoanApplication, disburseLoan, makeLoanRepayment, quoteExchange, executeExchangeTrade, simulateAtmWithdrawal, createSwiftMessage, approveSwiftMessage, updateCaseStatus, updateCustomerKyc, submitDemoKycRequest, exportAsCsv, DEMO_BRAND, createIncomingPayment, cancelPayment, createCryptoTransfer, createCryptoReceipt, approveCryptoTransfer, cancelCryptoTransfer, createComplaint, updateComplaint, accountBalanceEur, submitDemoOnboardingApplication, updateDemoOnboardingApplication, getCustomer, getAccount, totalCustomerDepositsCents, isBalanced, addAudit, runBackgroundSimulationTick } from './core.mjs'; import { supportedLanguages, translate } from './i18n.mjs'; import { ECB_DAILY_URL, CBK_FOREX_URL, ECB_FALLBACK_DATE, ECB_FALLBACK_RATES_PER_EUR, CBK_FALLBACK_DATE, CBK_FALLBACK_KES_PER_EUR, parseEcbDailyReference, parseCbkDailyReference } from './fx.mjs'; import { localAssistantReply } from './assistant.mjs'; import { INTEROP_IDEAS, INTEROP_FEATURES, INTEROP_SYSTEMS, interopLocalizedText, ensureInteropLabState, activeInteropSystem, selectInteropSystem, toggleInteropConnector, refreshInteropConnector, runInteropFeature, resetInteropLab } from './interop-lab.mjs'; const $ = (selector, root = document) => root.querySelector(selector); const $$ = (selector, root = document) => [...root.querySelectorAll(selector)]; const main = $('#mainContent'); const nav = $('#primaryNav'); const modalRoot = $('#modalRoot'); const toastRegion = $('#toastRegion'); const languageSelect = $('#languageSelect'); const STAFF_LEVELS = [ { id: 'platform_admin', level: 1, label: 'rolePlatformAdmin', scope: 'roleScopePlatform', operator: 'Luka Babić' }, { id: 'group_admin', level: 2, label: 'roleGroupAdmin', scope: 'roleScopeGroup', operator: 'Ivana Radić' }, { id: 'country_manager', level: 3, label: 'roleCountryManager', scope: 'roleScopeCountry', operator: 'David Novak' }, { id: 'branch_manager', level: 4, label: 'roleBranchManager', scope: 'roleScopeBranch', operator: 'Maja Kovač' }, { id: 'checker', level: 5, label: 'roleChecker', scope: 'roleScopeReview', operator: 'Ivan Novak' }, { id: 'operations_maker', level: 6, label: 'roleMaker', scope: 'roleScopeOperations', operator: 'Maja Kovač' }, { id: 'auditor', level: 7, label: 'roleAuditor', scope: 'roleScopeAudit', operator: 'Ana Kralj' } ]; let state = loadDemoState(); ensureInteropLabState(state); const requestedLanguage = new URLSearchParams(location.search).get('lang'); const languageFromEntry = supportedLanguages.some(language => language.code === requestedLanguage) ? requestedLanguage : null; if (languageFromEntry) state.settings.language = languageFromEntry; let currentView = document.body.dataset.startView || 'overview'; let portalCustomerId = state.accounts.find(account => account.status === 'active' && state.customers.find(customer => customer.id === account.customerId)?.kyc === 'verified')?.customerId || state.customers[0]?.id; let paymentTab = 'orders'; let amlTab = 'cases'; let supportTab = 'tickets'; let exchangeTab = 'fx'; let interopTab = 'workbench'; let interopCategory = 'all'; let exchangeSide = 'buy'; let exchangeCustomerId = null; let currentSearchPopover; let pendingConfirmation = null; let ecbFetchPromise = null; let cbkFetchPromise = null; let optionalMarketFeed = { status: 'idle', reference: null, message: '' }; let optionalMarketFeedPromise = null; const fxReferenceData = { ecb: { status: 'bundled-sample', rateDate: ECB_FALLBACK_DATE, fetchedAt: null, source: 'Bundled ECB reference sample', rates: { EUR: 1, ...ECB_FALLBACK_RATES_PER_EUR } }, cbk: { status: 'bundled-sample', rateDate: CBK_FALLBACK_DATE, fetchedAt: null, source: 'Bundled CBK indicative-rate sample', kesPerEur: CBK_FALLBACK_KES_PER_EUR } }; const assistantChats = new Map(); let referenceCheckResult = null; let browserSimulationTimer = null; let backgroundTickUnsubscribe = null; let backgroundDesktopSupported = Boolean(window.adriaDemo?.setBackgroundPreferences); const filterState = { customers: { q: '', kyc: 'all', risk: 'all', page: 1 }, accounts: { q: '', status: 'all', currency: 'all', page: 1 }, payments: { q: '', status: 'all', page: 1 }, transactions: { q: '', type: 'all', page: 1 }, cards: { q: '', status: 'all', page: 1 }, savings: { q: '', status: 'all', page: 1 }, loans: { q: '', status: 'all', page: 1 }, cases: { q: '', severity: 'all', status: 'open', page: 1 }, complaints: { q: '', status: 'all', page: 1 }, branches: { page: 1 }, onboarding: { q: '', status: 'all', page: 1 }, ledger: { page: 1 } }; const PAGE_SIZE = 12; const navGroups = [ { label: 'bankOperations', items: [ ['overview', 'overview', 'overview'], ['customers', 'customers', 'users'], ['accounts', 'accounts', 'account'], ['payments', 'payments', 'payments'], ['cards', 'cards', 'card'], ['savings', 'savings', 'savings'], ['loans', 'loans', 'loan'], ['branches', 'branchesCash', 'branch'], ['atms', 'atmNetwork', 'atm'] ] }, { label: 'customerChannels', items: [['portal', 'customerPortal', 'portal'], ['onboarding', 'onlineOnboarding', 'verify'], ['support', 'callCentre', 'support']] }, { label: 'controls', items: [['aml', 'amlKyc', 'shield'], ['complaints', 'complaints', 'support'], ['swift', 'swiftLab', 'swift'], ['exchange', 'exchange', 'exchange'], ['interop', 'interoperability', 'globe'], ['institutions', 'institutions', 'bank'], ['checker', 'localChecks', 'verify']] }, { label: 'finance', items: [['ledger', 'ledger', 'ledger'], ['reports', 'reports', 'reports'], ['settings', 'settings', 'settings']] } ]; function visibleNavGroups() { if (document.body.dataset.surface === 'client') return [{ label: 'customerChannels', items: [['portal', 'customerPortal', 'portal'], ['complaints', 'complaints', 'support'], ['exchange', 'exchange', 'exchange'], ['support', 'callCentre', 'support']] }]; return navGroups; } const iconPaths = { overview: '', users: '', account: '', payments: '', card: '', savings: '', loan: '', branch: '', atm: '', shield: '', swift: '', exchange: '', ledger: '', reports: '', settings: '', bell: '', search: '', bank: '', portal: '', globe: '', spark: '', download: '', arrow: '', clock: '', shieldCheck: '', plus: '', support: '', verify: '', cash: '', close: '', dots: '' }; function icon(name, cls = '') { return ``; } function t(key) { return translate(state.settings.language || 'hr', key); } function currentStaffRole() { return STAFF_LEVELS.find(role => role.id === state.settings.role) || STAFF_LEVELS[5]; } function canWrite() { return currentStaffRole().level < 7; } function canApprove() { return currentStaffRole().level <= 5; } function canReviewCompliance() { return currentStaffRole().level <= 5; } function canAdminister() { return currentStaffRole().level <= 2; } function esc(value) { return String(value ?? '').replace(/[&<>"']/g, character => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character]); } function locale() { return supportedLanguages.find(item => item.code === state.settings.language)?.locale || 'hr-HR'; } function currency(valueCents, currencyCode = 'EUR', options = {}) { const amount = Number(valueCents || 0) / 100; try { return new Intl.NumberFormat(locale(), { style: 'currency', currency: currencyCode, maximumFractionDigits: options.digits ?? 2, minimumFractionDigits: options.minimum ?? 0 }).format(amount); } catch { return `${amount.toFixed(2)} ${currencyCode}`; } } function currencyMajor(amount, currencyCode = 'EUR', maximumFractionDigits = 2) { try { return new Intl.NumberFormat(locale(), { style: 'currency', currency: currencyCode, maximumFractionDigits, minimumFractionDigits: 0 }).format(Number(amount) || 0); } catch { return `${Number(amount || 0).toFixed(2)} ${currencyCode}`; } } function compactCurrency(cents) { const amount = (Number(cents) || 0) / 100; try { return new Intl.NumberFormat(locale(), { style: 'currency', currency: 'EUR', notation: 'compact', maximumFractionDigits: 1 }).format(amount); } catch { return `€${Math.round(amount).toLocaleString()}`; } } function fmtDate(value, withTime = false) { if (!value || Number.isNaN(new Date(value).valueOf())) return '—'; try { return new Intl.DateTimeFormat(locale(), withTime ? { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' } : { day: '2-digit', month: 'short', year: 'numeric' }).format(new Date(value)); } catch { return new Date(value).toLocaleDateString(); } } function fmtTime(value) { if (!value || Number.isNaN(new Date(value).valueOf())) return '—'; return new Intl.DateTimeFormat(locale(), { hour: '2-digit', minute: '2-digit' }).format(new Date(value)); } function initials(name) { return String(name || 'AB').trim().split(/\s+/).slice(0, 2).map(word => word[0] || '').join('').toLocaleUpperCase(locale()); } function customerById(id) { return state.customers.find(item => item.id === id); } function accountById(id) { return state.accounts.find(item => item.id === id); } function labelStatus(status) { const key = ({ settled_simulation: 'settledSimulation', awaiting_approval: 'awaitingApproval', approved_simulation: 'settledSimulation', approved_undisbursed: 'approvedUndisbursed', repaid_simulation: 'repaidSimulation', matured_simulation: 'closed', in_review: 'investigating', in_progress: 'inProgress', cash_in: 'posted', active: 'active', synthetic_directory_match: 'directoryMatch', synthetic_account_match: 'accountMatch', synthetic_card_match: 'cardMatch', submitted_demo: 'submittedDemo', reviewed_demo: 'reviewedDemo', needs_info_demo: 'needsInfoDemo', closed_demo: 'closedDemo', format_only: 'formatOnly', format_checksum_only: 'formatChecksumOnly', luhn_format_only: 'luhnFormatOnly', checksum_invalid: 'checksumInvalid', format_invalid: 'formatInvalid', length_invalid: 'lengthInvalid', country_unsupported: 'countryUnsupported', not_found: 'notFound' })[status] || status; return t(key) === key ? String(status || '—').replaceAll('_', ' ') : t(key); } function badge(status, extra = '') { const normalized = String(status || 'neutral').toLowerCase(); const type = ['verified', 'active', 'online', 'posted', 'settled_simulation', 'approved_simulation', 'performing', 'repaid_simulation', 'closed', 'reviewed_demo'].includes(normalized) ? 'success' : ['pending', 'expired', 'awaiting_approval', 'approved_undisbursed', 'investigating', 'warning', 'low', 'maintenance', 'submitted_demo', 'needs_info_demo'].includes(normalized) ? 'warning' : ['high', 'blocked', 'restricted', 'escalated', 'cancelled', 'arrears', 'checksum_invalid', 'format_invalid', 'length_invalid', 'country_unsupported'].includes(normalized) ? 'danger' : ['medium', 'edd', 'crypto', 'format_only', 'format_checksum_only', 'luhn_format_only'].includes(normalized) ? 'info' : 'neutral'; return `${esc(labelStatus(status))}`; } function pageHeading(titleKey, subtitleKey, actions = '') { const title = t(titleKey); $('#crumbTitle').textContent = title; const printAction = ``; return `
${esc(t('safeLearning'))}

${esc(title)}

${esc(t(subtitleKey))}

${DEMO_BRAND}
${actions}${printAction}
`; } function renderWorkspaceContextBar() { if (document.body.dataset.surface === 'client') return ''; const system = activeInteropSystem(state); return ``; } function renderInteropSystemSwitcher() { const active = activeInteropSystem(state); const cards = INTEROP_SYSTEMS.map(system => { const isActive = system.id === active.id; return `
${esc(system.flag)}${esc(isActive ? t('interopSystemActive') : t('interopFictionalSystem'))}

${esc(system.name)}

${esc(system.city)} · ${esc(system.country)}

${esc(t('interopSystemCurrency'))}${esc(system.currency)}${esc(t('interopSystemTimezone'))}${esc(system.timezone)}

${esc(languageForSystemNote(system))}

${button('interop-system-select', isActive ? t('interopSystemActive') : t('interopSystemSwitch'), isActive ? 'small secondary' : 'small primary', isActive ? 'check' : 'exchange', `data-id="${esc(system.id)}" ${isActive ? 'disabled aria-current="true"' : ''}`)}
`; }).join(''); return `
${esc(t('interopSystemProfile'))}

${esc(t('interopSystemTitle'))}

${esc(t('interopSystemSub'))}

${esc(t('localOnly'))}
${cards}
i${esc(t('interopSystemBoundary'))}
`; } function languageForSystemNote(system) { return state.settings.language === 'hr' ? system.noteHr : system.noteEn; } function button(action, label, style = '', iconName = '', attrs = '') { const staffConsole = document.body.dataset.surface !== 'client' && currentView !== 'portal'; const writeActions = ['new-customer', 'new-account', 'new-account-for', 'transfer', 'new-payment', 'incoming-payment', 'cancel-payment', 'crypto-send', 'crypto-receive', 'cancel-crypto', 'new-complaint', 'cash-operation', 'issue-card', 'new-deposit', 'new-loan', 'repay-loan', 'atm-withdrawal', 'new-swift', 'mature-savings', 'card-status', 'card-controls', 'card-load', 'card-purchase', 'card-cancel', 'reconcile-atms', 'new-interbank', 'new-support-ticket', 'support-status', 'account-conversion', 'toggle-background-simulation', 'onboarding-status', 'interop-connector-toggle', 'interop-connector-refresh', 'interop-feature-run', 'interop-system-select', 'interop-reset']; const approvalActions = ['approve-loan', 'disburse-loan', 'approve-payment', 'approve-swift', 'approve-interbank', 'approve-crypto']; const restricted = staffConsole && ((writeActions.includes(action) && !canWrite()) || (approvalActions.includes(action) && !canApprove()) || (action === 'reset-demo' && !canAdminister())); const permissionAttrs = restricted ? `disabled title="${esc(!canWrite() ? t('roleReadOnly') : !canApprove() ? t('roleCheckerOnly') : t('roleAdminOnly'))}"` : ''; return ``; } function panel(title, body, options = {}) { const description = options.description ? `

${esc(options.description)}

` : ''; const actions = options.actions ? `
${options.actions}
` : ''; return `

${esc(title)}

${description}
${actions}
${body}
`; } function emptyState(text = t('noRecords')) { return `
${esc(text)}
`; } function pager(listName, page, count, pageSize = PAGE_SIZE) { const pages = Math.max(1, Math.ceil(count / pageSize)); return `
${count ? `${(page - 1) * pageSize + 1}–${Math.min(page * pageSize, count)} / ${count}` : `0 / 0`} ${esc(t('records'))}
${page} / ${pages}
`; } function paged(items, listName, size = PAGE_SIZE) { const filter = filterState[listName]; const pages = Math.max(1, Math.ceil(items.length / size)); if (filter.page > pages) filter.page = pages; const from = (filter.page - 1) * size; return { items: items.slice(from, from + size), footer: pager(listName, filter.page, items.length, size) }; } function tableShell(headers, rows, footer = '', noRows = t('noRecords')) { const body = rows.length ? rows.join('') : `${emptyState(noRows)}`; return `
${headers.map(value => ``).join('')}${body}
${esc(value)}
${footer}`; } function formatClient(customer, action = 'customer-open') { if (!customer) return '—'; return ``; } function amountWithDirection(tx) { const plus = tx.direction === 'in'; return `${plus ? '+' : '−'}${currency(tx.amountCents, tx.currency || 'EUR')}`; } function txLabel(tx) { const map = { internal: state.settings.language === 'hr' ? 'Interni prijenos' : 'Internal transfer', cash_in: t('depositCash'), cash_out: t('withdrawCash'), atm: state.settings.language === 'hr' ? 'Podizanje na bankomatu' : 'ATM withdrawal', savings_open: state.settings.language === 'hr' ? 'Oročena štednja' : 'Term savings placement', savings_mature: state.settings.language === 'hr' ? 'Dospijeće štednje' : 'Savings maturity', exchange: state.settings.language === 'hr' ? 'Zamjena valute / imovine' : 'FX / asset exchange', sepa_out: state.settings.language === 'hr' ? 'SEPA plaćanje' : 'SEPA payment', swift_out: state.settings.language === 'hr' ? 'Međunarodno plaćanje' : 'International payment', card_purchase: state.settings.language === 'hr' ? 'Kartična kupnja' : 'Card purchase', sepa_in: state.settings.language === 'hr' ? 'SEPA priljev' : 'SEPA credit transfer', loan_disbursement: t('loanDisbursement'), loan_repayment: t('loanRepayment'), salary: state.settings.language === 'hr' ? 'Isplata plaće' : 'Salary payment', utility: state.settings.language === 'hr' ? 'Režijski račun' : 'Utility bill', subscription: state.settings.language === 'hr' ? 'Pretplata' : 'Subscription', merchant: state.settings.language === 'hr' ? 'Namira trgovca' : 'Merchant settlement', fee: state.settings.language === 'hr' ? 'Naknada usluge' : 'Service fee', refund: state.settings.language === 'hr' ? 'Povrat kartične transakcije' : 'Card refund', standing: state.settings.language === 'hr' ? 'Trajni nalog' : 'Standing order', crypto_in: state.settings.language === 'hr' ? 'Primitak kriptoimovine · simulacija' : 'Crypto receipt · simulation', crypto_out: state.settings.language === 'hr' ? 'Slanje kriptoimovine · simulacija' : 'Crypto send · simulation' }; return map[tx.type] || tx.label || tx.type; } function selectOptions(items, valueKey, labelFn, selected = '') { return items.map(item => ``).join(''); } function activeEurAccounts() { return state.accounts.filter(account => account.currency === 'EUR' && account.status === 'active'); } function customerAccounts(customerId) { return state.accounts.filter(account => account.customerId === customerId); } function accountLabel(account) { const customer = customerById(account.customerId); return `${account.id} · ${customer?.name || account.customerId} · ${account.currency} ${currency(account.balanceCents, account.currency)}`; } function prettyIban(value) { const normalized = String(value || '').replaceAll(' ', ''); return normalized ? normalized.match(/.{1,4}/g).join(' ') : 'Domestic demo ID · no IBAN'; } function prettyRoutingIdentifier(value) { const normalized = String(value || '').trim(); return /^DEMO-[A-Z]{2}-/.test(normalized) ? normalized : prettyIban(normalized); } function accountRouteSummary(account) { return `${prettyRoutingIdentifier(account.iban || account.accountNumber)} · ${account.bic || createDemoBic(account.countryCode || 'HR')} · DEMO only`; } function openPrintDocument(title, body) { const root = $('#printRoot'); if (!root) { window.print(); return; } root.innerHTML = ``; root.setAttribute('aria-hidden', 'false'); document.body.classList.add('printing-document'); const clear = () => { root.innerHTML = ''; root.setAttribute('aria-hidden', 'true'); document.body.classList.remove('printing-document'); }; window.addEventListener('afterprint', clear, { once: true }); setTimeout(() => { try { window.print(); } catch { clear(); } }, 60); setTimeout(clear, 60000); } function printStatement(customerId = portalCustomerId, accountId = '') { const customer = customerById(customerId); if (!customer) return; const accounts = customerAccounts(customerId).filter(account => !accountId || account.id === accountId); const transactions = state.transactions.filter(tx => tx.customerId === customerId && (!accountId || tx.accountId === accountId)).sort((a, b) => new Date(b.date) - new Date(a.date)); const accountRows = accounts.map(account => `${esc(account.id)} · ${esc(account.label)}${esc(account.currency)}${esc(accountRouteSummary(account))}${esc(currency(account.balanceCents, account.currency))}`).join(''); const transactionRows = transactions.map(tx => `${esc(fmtDate(tx.date, true))}${esc(tx.id)} · ${esc(txLabel(tx))}${esc(tx.counterparty || tx.channel || '—')}${esc(tx.reference || '—')}${esc(labelStatus(tx.status))}${tx.direction === 'in' ? '+' : '−'}${esc(currency(tx.amountCents, tx.currency || 'EUR'))}`).join(''); const title = t('accountStatement'); openPrintDocument(title, `

${esc(title)}

${esc(t('myAccounts'))}

${accountRows}
${esc(t('account'))}${esc(t('currency'))}${esc(t('demoIbanOrDomesticId'))} / BIC · demo${esc(t('balance'))}

${esc(t('transactionHistory'))} · ${transactions.length}

${transactionRows || ``}
${esc(t('date'))}${esc(t('transaction'))}${esc(t('counterparty'))}${esc(t('reference'))}${esc(t('status'))}${esc(t('amount'))}
${esc(t('noTransactions'))}
`); } function printTransaction(transactionId) { const tx = state.transactions.find(item => item.id === transactionId); if (!tx) return; const customer = customerById(tx.customerId); const account = accountById(tx.accountId); openPrintDocument(t('transactionReceipt'), `

${esc(t('transactionReceipt'))}

${esc(t('client'))}${esc(customer?.name || tx.customerId)} · ${esc(customer?.id || '')}
${esc(t('account'))}${esc(account?.id || tx.accountId || '—')} · ${esc(account?.currency || tx.currency || 'EUR')}
${esc(t('amount'))}${esc(amountWithDirection(tx).replace(/<[^>]*>/g, ''))}
${esc(t('counterparty'))}${esc(tx.counterparty || '—')}
${esc(t('reference'))}${esc(tx.reference || '—')}
${esc(t('date'))}${esc(fmtDate(tx.date, true))}
${esc(t('status'))}${esc(labelStatus(tx.status))}
${esc(t('channel'))}${esc(tx.channel || 'Local demo')}
`); } function printPayment(paymentId) { const payment = state.payments.find(item => item.id === paymentId); if (!payment) return; const incoming = payment.direction === 'in'; openPrintDocument(incoming ? t('incomingPayment') : t('paymentInstruction'), `

${esc(incoming ? t('incomingPayment') : t('paymentInstruction'))}

${esc(t('client'))}${esc(customerById(payment.customerId)?.name || payment.customerId)}
${esc(incoming ? t('sender') : t('beneficiary'))}${esc(incoming ? payment.sender || 'Fictional remitter' : payment.beneficiary)}
${esc(t('country'))}${esc(incoming ? payment.senderCountry || '—' : payment.beneficiaryCountry || '—')}
${esc(t('demoIbanOrDomesticId'))} · not routable${esc(prettyRoutingIdentifier(incoming ? payment.senderIban : payment.beneficiaryIban))}
BIC · demo only${esc(incoming ? payment.senderBic || '—' : payment.beneficiaryBic || '—')}
${esc(t('amount'))}${esc(currency(payment.amountCents, payment.currency))}
${esc(t('reference'))}${esc(payment.reference)}
${esc(t('date'))}${esc(fmtDate(payment.createdAt, true))}
`); } function printSwift(messageId) { const message = state.swiftMessages.find(item => item.id === messageId); if (!message) return; openPrintDocument(t('swiftLab'), `

${esc(t('swiftLab'))} · ${esc(message.type)}

${esc(t('beneficiary'))}${esc(message.beneficiary)}
${esc(t('country'))}${esc(message.beneficiaryCountry || '—')}
BIC · demo only${esc(message.bic)}
${esc(t('demoIbanOrDomesticId'))} · not routable${esc(prettyRoutingIdentifier(message.beneficiaryIban))}
${esc(t('amount'))}${esc(currency(message.amountCents, message.currency))}
${esc(t('reference'))}${esc(message.reference)}

Local-format preview · not transmitted

`); } function updateDemoRoutingDisplay(form) { const country = form?.elements?.country?.value || 'HR'; const serial = Number(form?.elements?.accountSequence?.value || 1); const iban = form?.elements?.iban || form?.elements?.senderIban; const bic = form?.elements?.bic || form?.elements?.senderBic; if (iban) iban.value = prettyRoutingIdentifier(createDemoIban(country, serial) || `DEMO-${country}-${String(serial).padStart(10, '0')}`); const routeField = iban?.closest('.form-field'); if (routeField?.querySelector('label')) routeField.querySelector('label').textContent = t('demoIbanOrDomesticId'); if (bic) bic.value = createDemoBic(country); } function updateStaticText() { document.documentElement.lang = state.settings.language || 'hr'; document.documentElement.dir = state.settings.language === 'ar' ? 'rtl' : 'ltr'; languageSelect.value = state.settings.language || 'hr'; $$('[data-i18n]').forEach(node => { node.textContent = t(node.dataset.i18n); }); $$('[data-i18n-placeholder]').forEach(node => { node.placeholder = t(node.dataset.i18nPlaceholder); }); const staffRole = currentStaffRole(); if ($('#operatorName')) $('#operatorName').textContent = state.settings.operator || staffRole.operator; if ($('#operatorRole')) $('#operatorRole').textContent = `L${staffRole.level} · ${t(staffRole.label)}`; if ($('.operator-profile .avatar')) $('.operator-profile .avatar').textContent = initials(state.settings.operator || staffRole.operator); if ($('.top-avatar')) $('.top-avatar').textContent = initials(state.settings.operator || staffRole.operator); const now = new Date(); const todayStamp = $('#todayStamp'); if (todayStamp) todayStamp.textContent = new Intl.DateTimeFormat(locale(), { weekday: 'short', day: 'numeric', month: 'short' }).format(now); if ($('#globalSearch')) $('#globalSearch').placeholder = t('searchPlaceholder'); } function renderNavigation() { const highPriorityCount = state.cases.filter(item => item.status !== 'closed').length; const groups = visibleNavGroups(); nav.innerHTML = groups.map((group, groupIndex) => `${group.items.map(([view, labelKey, iconName]) => { const badgeText = view === 'aml' && highPriorityCount ? `${Math.min(99, highPriorityCount)}` : ''; return ``; }).join('')}${groupIndex < groups.length - 1 ? '' : ''}`).join(''); } function buildChart() { const now = new Date(); const points = []; for (let day = 13; day >= 0; day--) { const date = new Date(now); date.setDate(now.getDate() - day); const bucket = state.transactions.filter(tx => { const txDate = new Date(tx.date); return txDate.toDateString() === date.toDateString() && tx.status === 'posted'; }); const volume = bucket.reduce((sum, tx) => sum + tx.amountCents, 0) / 100; points.push({ date, volume }); } const max = Math.max(1800, ...points.map(item => item.volume)); const coords = points.map((item, index) => ({ x: 39 + index * 50, y: 131 - (item.volume / max) * 93 })); const line = coords.map(point => `${point.x},${point.y}`).join(' '); const area = `M ${coords[0].x} 139 L ${line.replaceAll(' ', ' L ')} L ${coords.at(-1).x} 139 Z`; const labels = [0, 3, 6, 9, 13].map(index => { const label = new Intl.DateTimeFormat(locale(), { day: 'numeric', month: 'short' }).format(points[index].date); return `${esc(label)}`; }).join(''); const grid = [35, 66, 97, 128].map(y => ``).join(''); const last = coords.at(-1); return `
${grid}${labels}
`; } function renderOverview() { const now = new Date(); const dayStart = new Date(now); dayStart.setHours(0, 0, 0, 0); const todayTransactions = state.transactions.filter(tx => new Date(tx.date) >= dayStart && tx.status === 'posted'); const todayCount = todayTransactions.length + state.payments.filter(payment => new Date(payment.createdAt) >= dayStart).length; const posted30 = state.transactions.filter(tx => new Date(tx.date) >= new Date(now.getTime() - 30 * 86400000) && tx.status === 'posted'); const volume30 = posted30.reduce((sum, tx) => sum + tx.amountCents, 0); const deposits = totalCustomerDepositsCents(state); const liquidityEurCents = state.bankAccounts.reduce((sum, account) => { const rate = CURRENCIES[account.currency]?.eurRate; return Number.isFinite(rate) && rate > 0 ? sum + Math.round(account.balanceCents * rate) : sum; }, 0); const liquidityCents = liquidityEurCents; const pendingKyc = state.customers.filter(customer => ['pending', 'expired', 'edd'].includes(customer.kyc)).length; const openCases = state.cases.filter(item => item.status !== 'closed').length; const activeCustomers = state.customers.filter(customer => customer.kyc === 'verified').length; const companyLocation = state.meta.companyLocation || { city: 'Boulder', region: 'Colorado' }; const title = pageHeading('overview', 'dashSub', `${esc(t('demoOnly'))}${esc(companyLocation.city)}, ${esc(companyLocation.region)} · listed address${button('new-customer', t('newCustomer'), 'primary', 'plus')}`); const metrics = `
${esc(t('totalDeposits'))}${icon('savings')}
${esc(compactCurrency(deposits))}
+2.8%${esc(t('sinceYesterday'))}
${esc(t('paymentsToday'))}${icon('payments')}
${todayCount}${esc(t('activity').toLowerCase())}
${esc(t('localSimulation'))}
${esc(t('activeCustomers'))}${icon('users')}
${activeCustomers}/ ${state.customers.length}
${esc(t('kycPending'))}: ${pendingKyc}
${esc(t('liquidity'))}${icon('bank')}
${esc(compactCurrency(liquidityCents))}
${esc(t('availableToMove'))}
`; const activityRows = state.transactions.slice(0, 7).map(tx => { const customer = customerById(tx.customerId); return `
${icon(tx.direction === 'in' ? 'arrow' : 'payments')}
${esc(txLabel(tx))}${esc(customer?.name || tx.customerId)} · ${esc(tx.counterparty || tx.channel)}
${esc(amountWithDirection(tx))}${esc(fmtTime(tx.date))}
`; }).join(''); const trend = `
${esc(compactCurrency(volume30))}+8.4%

${esc(t('monthlyVolume'))} · ${esc(t('sampleValue').toLowerCase())}

${buildChart()}`; const bankRows = state.bankAccounts.map((account, index) => { const iconClass = index === 0 ? 'reserve' : ''; const label = index === 0 ? t('centralReserve') : index === 1 ? t('mainAccount') : account.label; return ``; }).join(''); const branchPills = state.branches.map(branch => `${esc(branch.code)} · ${esc(branch.city)}`).join(''); const lower = `
${panel(t('mainAccount'), `
${esc(t('localSimulation'))} · EUR equivalent${esc(t('cleared'))}
${esc(currency(liquidityCents))}${esc(t('liquidityAvailable'))}
${branchPills}
`, { description: t('cashPositions'), actions: `` })} ${panel(t('quickActions'), `
${icon('shieldCheck')} ${esc(t('openCases'))}${openCases} ${esc(t('openCase').toLowerCase())}

${esc(t('amlSub'))}

`, { description: t('localSimulation') })}
`; return `${title}${metrics}
${panel(t('monthlyVolume'), trend, { description: t('thisMonth'), actions: `
` })}${panel(t('recentActivity'), `
${activityRows || emptyState(t('noTransactions'))}
`, { description: `${todayCount} ${t('processedToday').toLowerCase()}`, actions: `` })}
${lower}
${renderAssistant('staff')}${panel(t('staffGuide'), renderProcedureGuide('staff'), { description: t('staffGuideSub') })}
`; } function assistantChat(audience, customerId = '') { const key = audience === 'staff' ? 'staff' : `customer:${customerId}`; if (!assistantChats.has(key)) assistantChats.set(key, []); return assistantChats.get(key); } function renderAssistant(audience, customer = null) { const messages = assistantChat(audience, customer?.id || ''); const promptList = audience === 'staff' ? [['assistantPromptAml', 'How should I review an AML alert?'], ['assistantPromptRates', 'Explain the ECB reference rates'], ['assistantPromptSwift', 'How do I prepare a SWIFT draft?']] : [['assistantPromptBalance', 'Show my balances'], ['assistantPromptFx', 'How do I convert currency?'], ['assistantPromptCard', 'How can I lock a card?']]; const transcript = messages.length ? messages.slice(-6).map(message => `
${message.role === 'user' ? esc(t('you')) : esc(t('localCopilot'))}

${esc(message.text)}

`).join('') : `
${esc(t('assistantWelcome'))}
`; const body = `
${transcript}
${promptList.map(([labelKey, question]) => ``).join('')}

${esc(t('assistantDisclaimer'))}

`; return panel(t('assistantTitle'), body, { description: audience === 'staff' ? t('staffAssistantSub') : t('customerAssistantSub'), actions: 'LOCAL · RULE-BASED' }); } function renderProcedureGuide(audience) { const steps = audience === 'staff' ? [['1 · Prepare', 'Use only fictional records; check role and maker/checker state.'], ['2 · Review', 'Use the synthetic AML/KYC queue; do not imply real screening.'], ['3 · Approve', 'Use the local demo checker; inspect the audit entry and ledger.'], ['4 · Explain', 'Tell the customer nothing was sent to a payment network.']] : [['1 · Open', 'Choose a fictional profile and request a demo currency account.'], ['2 · Verify', 'Submit a synthetic KYC rehearsal; do not enter real ID details.'], ['3 · Manage', 'Try a local transfer, FX conversion, savings, card or credit flow.'], ['4 · Review', 'Check the activity history; no real funds move.']]; return `
${steps.map(([title, text]) => `
${esc(title)}

${esc(text)}

`).join('')}
`; } function onboardingNextStep(status) { const key = ({ submitted_demo: 'onboardingNextSubmitted', needs_info_demo: 'onboardingNextNeedsInfo', reviewed_demo: 'onboardingNextReviewed', closed_demo: 'onboardingNextClosed' })[status] || 'onboardingNextSubmitted'; return t(key); } function renderPortal() { const customer = customerById(portalCustomerId) || state.customers[0]; if (!customer) return emptyState(); const accounts = customerAccounts(customer.id); const eurBalance = accounts.filter(account => account.status === 'active').reduce((sum, account) => { const eur = accountBalanceEur(state, account); return eur === null ? sum : sum + Math.round(eur); }, 0); const cards = state.cards.filter(card => card.customerId === customer.id); const loans = (state.loans || []).filter(loan => loan.customerId === customer.id); const savingsDeposits = (state.deposits || []).filter(deposit => deposit.customerId === customer.id); const transactions = state.transactions.filter(tx => tx.customerId === customer.id).slice(0, 6); const pendingPayments = state.payments.filter(payment => payment.customerId === customer.id && payment.direction === 'out' && payment.status === 'awaiting_approval').slice(0, 4); const customerCrypto = state.wallets.filter(wallet => wallet.customerId === customer.id && wallet.units > 0); const customerComplaints = (state.complaints || []).filter(complaint => complaint.customerId === customer.id).slice(0, 3); const customerOptions = state.customers.slice().sort((a, b) => a.name.localeCompare(b.name, locale())).map(item => ``).join(''); const accountRows = accounts.length ? accounts.map(account => ``).join('') : emptyState(t('noAccounts')); const cardRows = cards.length ? cards.slice(0, 4).map(card => { const statusAction = ['active', 'locked'].includes(card.status) ? `` : badge(card.status); const loadAction = card.fundingType === 'prepaid' && card.status === 'active' ? `` : ''; const purchaseAction = card.status === 'active' ? `` : ''; const balance = card.fundingType === 'prepaid' ? ` · ${esc(t('prepaidBalance'))}: ${esc(currency(card.prepaidBalanceCents || 0))}` : ''; return `
${icon('card')}
${esc(card.product)} · ${esc(t('cardTokenSuffix'))} ${esc(card.last4)}${esc(t('status'))}: ${esc(labelStatus(card.status))} · ${esc(t('virtualTokenOnly'))}${balance}
${statusAction}${loadAction}${purchaseAction}
`; }).join('') : emptyState(t('noRecords')); const loanRows = loans.length ? loans.map(loan => `
${icon('loan')}
${esc(loan.product)} · ${esc(currency(loan.outstandingCents))}${esc(loan.id)} · ${esc(t('nextPayment'))}: ${esc(loan.nextPaymentAt ? fmtDate(loan.nextPaymentAt) : '—')}
${badge(loan.status)}
`).join('') : emptyState(t('noRecords')); const savingsRows = savingsDeposits.length ? savingsDeposits.map(deposit => `
${icon('savings')}
${esc(currency(deposit.principalCents))} · ${deposit.termMonths} ${esc(t('term').toLowerCase())}${esc(deposit.id)} · ${esc(t('maturity'))}: ${esc(fmtDate(deposit.maturityAt))} · ${(deposit.annualRateBps / 100).toFixed(2)}% indicative
${badge(deposit.status)}
`).join('') : emptyState(t('noRecords')); const activityRows = transactions.map(tx => `
${icon(tx.direction === 'in' ? 'arrow' : 'payments')}
${esc(txLabel(tx))}${esc(tx.counterparty || tx.channel)} · ${esc(fmtDate(tx.date, true))}
${esc(amountWithDirection(tx))}
`).join(''); const hasCurrencyPair = new Set(accounts.filter(account => account.status === 'active' && !CURRENCIES[account.currency]?.crypto).map(account => account.currency)).size > 1; const actionButtons = `
`; const customerTickets = (state.supportTickets || []).filter(ticket => ticket.customerId === customer.id).slice(0, 3); const ticketRows = customerTickets.length ? customerTickets.map(ticket => `
${icon('support')}
${esc(ticket.subject)}${esc(ticket.id)} · ${esc(fmtDate(ticket.createdAt, true))}
${badge(ticket.status)}
`).join('') : emptyState(t('noRecords')); const demoApplications = (state.onboardingApplications || []).filter(application => application.customerId === customer.id).slice(0, 4); const demoApplicationRows = demoApplications.length ? demoApplications.map(application => `
${icon('verify')}
${esc(application.id)} · ${esc(t(application.reviewPath === 'enhanced_review_demo' ? 'onboardingEnhancedPath' : 'onboardingStandardPath'))}${esc(fmtDate(application.submittedAt, true))} · ${esc(onboardingNextStep(application.status))}
${badge(application.status)}
`).join('') : emptyState(t('noOnboardingApplications')); const onboardingPanel = panel(t('onlineOnboarding'), demoApplicationRows, { description: t('onboardingBoundary'), actions: `` }); const pendingPaymentRows = pendingPayments.length ? pendingPayments.map(payment => `
${icon('payments')}
${esc(payment.beneficiary)} · ${esc(currency(payment.amountCents, payment.currency))}${esc(payment.id)} · ${esc(t('awaitingApproval'))} · not sent
`).join('') : emptyState(t('noPendingPayments')); const cryptoRows = customerCrypto.length ? customerCrypto.slice(0, 4).map(wallet => `
${esc(wallet.asset.slice(0, 2))}
${esc(wallet.asset)} · ${new Intl.NumberFormat(locale(), { maximumFractionDigits: CURRENCIES[wallet.asset]?.decimals || 2 }).format(wallet.units)}${esc(t('digitalAssetNotice'))}
`).join('') : emptyState(t('noRecords')); const complaintRows = customerComplaints.length ? customerComplaints.map(complaint => `
${icon('support')}
${esc(complaint.subject)}${esc(complaint.id)} · ${esc(complaint.customerVisibleResponse || t('complaintLocalOnly'))}
${badge(complaint.status)}
`).join('') : emptyState(t('noRecords')); const standalonePortalLink = document.body.dataset.surface === 'client' ? '' : `${esc(t('openClientPortal'))} ↗`; return `${pageHeading('customerPortal', 'portalSub', `${standalonePortalLink}${esc(t('demoOnly'))}`)}
ADRIA BANK · ${esc(t('customerPortal'))}

${esc(t('welcomeBack'))}, ${esc(customer.name.split(' ')[0])}

${esc(customer.city)}, ${esc(customer.country)} · ${esc(customer.id)} · ${badge(customer.kyc)}

${esc(t('availableBalance'))} · ${esc(t('sampleValue'))}${esc(compactCurrency(eurBalance))}${accounts.filter(account => account.status === 'active').length} active multi-currency demo account(s) · ${esc(t('digitalFirstGoal'))}
${icon('shieldCheck')}${esc(t('userPortalNotice'))} ${esc(t('noCustomerLogin'))}
${actionButtons}
${esc(t('myAccounts'))}${accounts.length}${esc(t('allSynthetic'))}
${esc(t('myCards'))}${cards.filter(card => card.status === 'active').length}${esc(t('active'))}
${esc(t('myCredit'))}${loans.filter(loan => ['performing','arrears'].includes(loan.status)).length}${esc(t('loanBook'))}
${esc(t('myActivity'))}${state.transactions.filter(tx => tx.customerId === customer.id).length}${esc(t('localSimulation'))}
${panel(t('myAccounts'), accountRows, { description: t('accountsSub') })}${panel(t('myCredit'), loanRows, { description: t('loansSub') })}${panel(t('savings'), savingsRows, { description: t('savingsSub') })}
${panel(t('myCards'), cardRows, { description: t('cardsSub') })}${panel(t('myActivity'), `
${activityRows || emptyState(t('noTransactions'))}
`, { description: `${transactions.length} ${t('records')}` })}
${onboardingPanel}${panel(t('contactSupport'), ticketRows, { description: t('supportHistory') })}${panel(t('complaints'), complaintRows, { description: t('complaintDemoDisclosure'), actions: `` })}${panel(t('wallets'), cryptoRows, { description: t('digitalAssetNotice'), actions: `` })}${panel(t('pendingPayments'), pendingPaymentRows, { description: t('stopBeforeApproval') })}${panel(t('customerGuide'), renderProcedureGuide('customer'), { description: t('digitalFirstGoal') })}
${state.settings.backgroundSimulationEnabled !== false ? `
i${esc(t('backgroundSafety'))} · ${esc(t('backgroundActive'))}
` : ''}${renderAssistant('customer', customer)}
!${esc(t('userPortalNotice'))}
`; } function renderCustomers() { const f = filterState.customers; const query = f.q.toLocaleLowerCase(locale()); const filtered = state.customers.filter(customer => { const search = `${customer.id} ${customer.name} ${customer.city} ${customer.country} ${customer.email}`.toLocaleLowerCase(locale()); return (!query || search.includes(query)) && (f.kyc === 'all' || customer.kyc === f.kyc) && (f.risk === 'all' || customer.risk === f.risk); }).sort((a, b) => a.name.localeCompare(b.name, locale())); const page = paged(filtered, 'customers'); const rows = page.items.map(customer => `${formatClient(customer)}${esc(customer.city)}, ${esc(customer.country)}${esc(customer.segment)}${badge(customer.risk)}${badge(customer.kyc)}${customerAccounts(customer.id).length}${esc(fmtDate(customer.joinedAt))}`); const content = tableShell([t('client'), t('city'), t('segment'), t('risk'), t('kyc'), t('accounts'), t('date'), t('actions')], rows, page.footer); const verified = state.customers.filter(customer => customer.kyc === 'verified').length; const kycFollow = state.customers.length - verified; return `${pageHeading('customers', 'customersSub', `${button('new-customer', t('newCustomer'), 'primary', 'plus')}${button('new-account', t('newAccount'), '', 'account')}`)}
${esc(t('totalClients'))}${state.customers.length}${esc(t('allSynthetic'))}
${esc(t('verified'))} KYC${verified}${Math.round(verified / Math.max(state.customers.length, 1) * 100)}%
${esc(t('kycPending'))}${kycFollow}${esc(t('nextReview'))}
${esc(t('openCases'))}${state.cases.filter(item => item.status !== 'closed').length}${esc(t('illustrative'))}
${filtered.length} ${esc(t('results'))}
${content}
`; } function renderAccounts() { const f = filterState.accounts; const query = f.q.toLocaleLowerCase(locale()); const filtered = state.accounts.filter(account => { const customer = customerById(account.customerId); const text = `${account.id} ${account.accountNumber || ''} ${account.iban} ${account.label} ${customer?.name || ''} ${account.currency}`.toLocaleLowerCase(locale()); return (!query || text.includes(query)) && (f.status === 'all' || account.status === f.status) && (f.currency === 'all' || account.currency === f.currency); }); const page = paged(filtered, 'accounts'); const rows = page.items.map(account => { const customer = customerById(account.customerId); return `${formatClient(customer)}${esc(account.id)}${esc(account.accountNumber || account.label)}${esc(prettyRoutingIdentifier(account.iban || account.accountNumber))}${esc(account.identifierKind === 'invalid_demo_iban' ? 'deliberately invalid checksum' : 'domestic demo ID')}${esc(account.bic || createDemoBic(account.countryCode))}${esc(account.currency)}${CURRENCIES[account.currency]?.noEurReference ? `${esc(t('noReferenceRate'))}` : ''}${esc(currency(account.balanceCents, account.currency))}${badge(account.status)}${esc(fmtDate(account.openedAt))}`; }); const balances = state.accounts.reduce((sum, account) => { const eur = accountBalanceEur(state, account); return eur === null ? sum : sum + Math.round(eur); }, 0); const nativeOnlyCount = state.accounts.filter(account => account.status === 'active' && accountBalanceEur(state, account) === null).length; return `${pageHeading('accounts', 'accountsSub', `${button('new-account', t('newAccount'), 'primary', 'plus')}${button('transfer', t('internalTransfer'), '', 'exchange')}`)}
${esc(t('totalAccounts'))}${state.accounts.length}${esc(t('active'))}: ${state.accounts.filter(item => item.status === 'active').length}
${esc(t('totalDeposits'))}${esc(compactCurrency(totalCustomerDepositsCents(state)))}${esc(t('allSynthetic'))}
${esc(t('available'))} · EUR${esc(compactCurrency(balances))}${esc(t('localSimulation'))}
${esc(t('nativeOnlyPositions'))}${nativeOnlyCount}${esc(t('noReferenceRate'))}
${esc(t('accountHelp'))}
${tableShell([t('client'), t('account'), t('demoIbanOrDomesticId'), 'BIC · DEMO', t('currency'), t('balance'), t('status'), t('date'), t('actions')], rows, page.footer)}
`; } function renderPayments() { const f = paymentTab === 'orders' ? filterState.payments : filterState.transactions; const viewAction = `
`; let body; if (paymentTab === 'orders') { const query = f.q.toLocaleLowerCase(locale()); const filtered = state.payments.filter(payment => { const customer = customerById(payment.customerId); const text = `${payment.id} ${payment.beneficiary} ${payment.sender || ''} ${payment.beneficiaryIban} ${payment.reference} ${customer?.name || ''}`.toLocaleLowerCase(locale()); return (!query || text.includes(query)) && (f.status === 'all' || payment.status === f.status); }); const page = paged(filtered, 'payments'); const rows = page.items.map(payment => { const customer = customerById(payment.customerId); const incoming = payment.direction === 'in'; const counterparty = incoming ? payment.sender || payment.beneficiary : payment.beneficiary; const route = incoming ? payment.senderIban : payment.beneficiaryIban; const country = incoming ? payment.senderCountry : payment.beneficiaryCountry; const approveAction = payment.status === 'awaiting_approval' && canApprove() ? `` : ''; const stopAction = !incoming && payment.status === 'awaiting_approval' && canWrite() ? `` : ''; return `${esc(payment.id)}${esc(payment.reference)}${esc(incoming ? t('incoming') : t('outgoing'))}${formatClient(customer)}${esc(counterparty)}${esc(country || '—')} · ${esc(prettyRoutingIdentifier(route))}${esc(incoming ? payment.senderBic || '—' : payment.beneficiaryBic || '—')} · DEMO BIC${esc(payment.rail)}${esc(currency(payment.amountCents, payment.currency))}${badge(payment.status)}${esc(fmtDate(payment.createdAt))}${approveAction}${stopAction}`; }); body = `
${state.payments.filter(item => item.status === 'awaiting_approval').length} ${esc(t('approvals').toLowerCase())}
${tableShell([t('reference'), t('direction'), t('client'), t('beneficiary'), t('rail'), t('amount'), t('status'), t('date'), t('actions')], rows, page.footer)}
i${esc(t('approvalNotice'))} ${esc(t('incomingPaymentDisclosure'))}
`; } else { const query = f.q.toLocaleLowerCase(locale()); const filtered = state.transactions.filter(tx => { const customer = customerById(tx.customerId); const text = `${tx.id} ${txLabel(tx)} ${tx.counterparty} ${tx.reference} ${customer?.name || ''} ${tx.channel}`.toLocaleLowerCase(locale()); return (!query || text.includes(query)) && (f.type === 'all' || tx.type === f.type); }); const page = paged(filtered, 'transactions'); const rows = page.items.map(tx => `${esc(tx.id)}${esc(tx.reference || '—')}${formatClient(customerById(tx.customerId))}${esc(txLabel(tx))}${esc(tx.channel || '—')}${esc(tx.counterparty || '—')}${amountWithDirection(tx)}${badge(tx.status)}${esc(fmtDate(tx.date, true))}`); const typeOptions = [...new Set(state.transactions.map(tx => tx.type))]; body = `
${button('download-csv', t('downloadCsv'), 'small', 'download')}
${tableShell([t('transaction'), t('client'), t('category'), t('counterparty'), t('amount'), t('status'), t('date'), t('actions')], rows, page.footer)}`; } const settled = state.payments.filter(payment => ['settled_simulation','approved_simulation'].includes(payment.status)).length; const incomingCount = state.payments.filter(payment => payment.direction === 'in').length; const awaiting = state.payments.filter(payment => payment.status === 'awaiting_approval').length; return `${pageHeading('payments', 'paymentsSub', `${button('transfer', t('internalTransfer'), '', 'exchange')}${button('new-payment', t('newPayment'), '', 'plus')}${button('incoming-payment', t('receivePayment'), 'primary', 'arrow')}`)}
${esc(t('payments'))}${state.payments.length}${esc(t('allSynthetic'))}
${esc(t('receivePayment'))}${incomingCount}${esc(t('noExternalFunds'))}
${esc(t('awaitingApproval'))}${awaiting}${esc(t('approvalRequired'))}
${esc(t('settledSimulation'))}${settled}${esc(t('localSimulation'))}
${viewAction}
${esc(t('controlsNotWired'))}
${body}
`; } function renderCards() { const f = filterState.cards; const query = f.q.toLocaleLowerCase(locale()); const filtered = state.cards.filter(card => { const customer = customerById(card.customerId); return (!query || `${card.id} ${card.product} ${customer?.name || ''} ${card.last4}`.toLocaleLowerCase(locale()).includes(query)) && (f.status === 'all' || card.status === f.status); }); const page = paged(filtered, 'cards'); const rows = page.items.map(card => { const customer = customerById(card.customerId); const activeOrLocked = ['active', 'locked'].includes(card.status); const nextStatus = card.status === 'active' ? 'locked' : 'active'; const statusAction = activeOrLocked ? `` : ''; const controlsAction = activeOrLocked ? `` : ''; const loadAction = card.fundingType === 'prepaid' && card.status === 'active' ? `` : ''; const purchaseAction = card.status === 'active' ? `` : ''; const cancelAction = activeOrLocked ? `` : ''; const controls = card.controls || {}; const actionCell = canWrite() ? `
${statusAction}${controlsAction}${loadAction}${purchaseAction}${cancelAction}
` : `${esc(t('roleReadOnly'))}`; const limits = `${currency(card.dailyLimitCents)} / ${currency(card.monthlyLimitCents || 0)}`; const flags = `${t('onlinePayments')}: ${t(controls.onlineEnabled === false ? 'disabled' : 'enabled')} · ${t('internationalPayments')}: ${t(controls.internationalEnabled ? 'enabled' : 'disabled')} · ${t('recurringPayments')}: ${t(controls.recurringEnabled === false ? 'disabled' : 'enabled')}`; return `${formatClient(customer)}${esc(card.product)}${esc(card.formFactor === 'virtual' ? t('virtualTokenOnly') : card.formFactor)}DEMO · ${esc(card.last4)}${esc(accountById(card.accountId)?.id || '—')}${esc(limits)}${card.fundingType === 'prepaid' ? `${esc(currency(card.prepaidBalanceCents || 0))}` : '—'}${badge(card.status)}${esc(flags)}${esc(fmtDate(card.issuedAt))}${actionCell}`; }); const active = state.cards.filter(card => card.status === 'active').length; const locked = state.cards.filter(card => card.status === 'locked').length; const prepaidValue = state.cards.reduce((sum, card) => sum + (card.fundingType === 'prepaid' ? card.prepaidBalanceCents || 0 : 0), 0); const prepaidCount = state.cards.filter(card => card.fundingType === 'prepaid').length; const stats = `
${esc(t('cardProducts'))}${state.cards.length}${esc(t('virtualTokenOnly'))}
${esc(t('active'))}${active}${esc(t('statusChanged'))}
${esc(t('locked'))}${locked}${esc(t('localCardControls'))}
${esc(t('prepaidBalance'))} · ${prepaidCount}${esc(currency(prepaidValue))}${esc(t('localSimulation'))}
`; const cardPreview = `
ADRIA BANK · ${esc(t('virtualToken'))}DEMO
${esc(t('demoToken'))} · 4817
${esc(t('syntheticData'))}${esc(t('noRealCardDetails'))}
adriabank
${esc(t('demoOnly'))}${esc(t('virtualTokenOnly'))}
`; return `${pageHeading('cards', 'cardsSub', `${button('issue-card', t('newCard'), 'primary', 'plus')}${button('card-purchase', t('simulateCardPurchase'), '', 'card')}`)}${stats}
${esc(t('virtualTokenOnly'))}
${tableShell([t('client'), t('product'), t('cardTokenSuffix'), t('account'), `${t('cardDailyLimit')} / ${t('cardMonthlyLimit')}`, t('prepaidBalance'), t('status'), t('cardUsageControls'), t('date'), t('actions')], rows, page.footer)}
${panel(t('productCard'), cardPreview, { description: t('localSimulation') })}
i${esc(t('cardWorkflowBoundary'))}
`; } function renderSavings() { const f = filterState.savings; const query = f.q.toLocaleLowerCase(locale()); const filtered = state.deposits.filter(deposit => { const customer = customerById(deposit.customerId); return (!query || `${deposit.id} ${customer?.name || ''}`.toLocaleLowerCase(locale()).includes(query)) && (f.status === 'all' || deposit.status === f.status); }); const page = paged(filtered, 'savings'); const rows = page.items.map(deposit => { const customer = customerById(deposit.customerId); const interest = Math.round(deposit.principalCents * deposit.annualRateBps / 10000 * deposit.termMonths / 12); const matureButton = deposit.status === 'active' && canWrite() ? `` : ''; return `${esc(deposit.id)}${formatClient(customer)}${esc(currency(deposit.principalCents))}${deposit.termMonths} ${esc(t('months3').split(' ')[1] || 'mo.')}${(deposit.annualRateBps / 100).toFixed(2)}%${esc(currency(interest))}${esc(fmtDate(deposit.maturityAt))}${badge(deposit.status)}${matureButton}`; }); const activeTotal = state.deposits.filter(item => item.status === 'active').reduce((sum, item) => sum + item.principalCents, 0); const next = state.deposits.filter(item => item.status === 'active').sort((a,b) => new Date(a.maturityAt) - new Date(b.maturityAt)).slice(0, 4); return `${pageHeading('savings', 'savingsSub', button('new-deposit', t('newDeposit'), 'primary', 'plus'))}
${esc(t('termDeposits'))}${state.deposits.filter(item => item.status === 'active').length}${esc(t('active'))}
${esc(t('totalDeposits'))}${esc(compactCurrency(activeTotal))}${esc(t('allSynthetic'))}
${esc(t('maturitySoon'))}${next.length}${esc(t('sevenDays'))}
${esc(t('rate'))}1.45–2.25%${esc(t('sampleValue'))}
${tableShell([t('reference'), t('client'), t('amount'), t('term'), t('rate'), 'Indicative interest', t('maturity'), t('status'), t('actions')], rows, page.footer)}
${panel(t('maturitySoon'), next.length ? `
${next.map(item => `
${esc(customerById(item.customerId)?.name || item.customerId)} · ${esc(currency(item.principalCents))}${esc(item.id)} · ${item.termMonths} ${esc(t('term').toLowerCase())}
${esc(fmtDate(item.maturityAt))}
`).join('')}
` : emptyState(t('noRecords')), { description: t('illustrative') })}
!Indicative demo rate only. No customer contract, disclosure or deposit guarantee is created by this sandbox.
`; } function renderLoans() { const f = filterState.loans; const query = f.q.toLocaleLowerCase(locale()); const loanRows = state.loans || []; const filtered = loanRows.filter(loan => { const customer = customerById(loan.customerId); const text = `${loan.id} ${loan.product} ${loan.purpose} ${customer?.name || ''} ${loan.customerId}`.toLocaleLowerCase(locale()); return (!query || text.includes(query)) && (f.status === 'all' || loan.status === f.status); }); const page = paged(filtered, 'loans'); const rows = page.items.map(loan => { const customer = customerById(loan.customerId); let action = ''; if (loan.status === 'awaiting_approval' && canApprove()) action = ``; else if (loan.status === 'approved_undisbursed' && canApprove()) action = ``; else if (['performing', 'arrears'].includes(loan.status) && canWrite()) action = ``; return `${esc(loan.id)}${esc(loan.product)}${formatClient(customer)}${esc(currency(loan.principalCents))}${esc(currency(loan.outstandingCents))}${(loan.annualRateBps / 100).toFixed(2)}%${loan.nextPaymentAt ? esc(fmtDate(loan.nextPaymentAt)) : '—'}${badge(loan.status)}${action}`; }); const funded = loanRows.filter(loan => ['performing', 'arrears'].includes(loan.status)); const bookCents = funded.reduce((sum, loan) => sum + loan.outstandingCents, 0); const pendingCount = loanRows.filter(loan => ['awaiting_approval', 'approved_undisbursed'].includes(loan.status)).length; const arrearsCents = funded.filter(loan => loan.status === 'arrears').reduce((sum, loan) => sum + loan.outstandingCents, 0); const statusOptions = ['awaiting_approval', 'approved_undisbursed', 'performing', 'arrears', 'repaid_simulation']; const sidePanel = `
${esc(t('fundedLoans'))}${funded.length}
${esc(t('awaitingApproval'))}${pendingCount}
${esc(t('arrears'))}${esc(currency(arrearsCents))}
${esc(t('settlementAccount'))}EUR · local demo
${esc(t('loanDisclosure'))}
`; return `${pageHeading('loans', 'loansSub', button('new-loan', t('newLoan'), 'primary', 'plus'))}
${esc(t('loanBook'))}${esc(compactCurrency(bookCents))}${esc(t('syntheticBalance'))}
${esc(t('fundedLoans'))}${funded.length}${esc(t('performing'))} · ${funded.filter(loan => loan.status === 'performing').length}
${esc(t('loanApplications'))}${loanRows.filter(loan => loan.status === 'awaiting_approval').length}${esc(t('awaitingApproval'))}
${esc(t('arrears'))}${esc(compactCurrency(arrearsCents))}${funded.filter(loan => loan.status === 'arrears').length} ${esc(t('loanCases'))}
${esc(t('localSimulation'))}
${tableShell([t('reference'), t('client'), t('principal'), t('outstanding'), t('rate'), t('nextPayment'), t('status'), t('actions')], rows, page.footer)}
${panel(t('creditPortfolio'), sidePanel, { description: t('indicativeCredit') })}
i${esc(t('loanAccountingNote'))}
`; } function renderBranches() { const totalCash = state.branches.reduce((sum, branch) => sum + branch.cashCents, 0); const activeAtms = state.atms.filter(atm => atm.status === 'online').length; const branchCards = state.branches.map(branch => { const atms = state.atms.filter(atm => atm.branchId === branch.id); const availableCash = branch.cashCents; return `
${icon('branch')}${esc(t('demoOnly'))}

${esc(branch.name)}

${esc(branch.code)} · ${esc(branch.city)}, ${esc(branch.country || 'Croatia')} · ${esc(branch.currency || 'EUR')} / ${esc((branch.language || 'hr').toUpperCase())}

${esc(t('cash'))} · demo${esc(currency(availableCash))}
${esc(t('teller'))}s${branch.tellers} desks
${esc(t('manager'))}${esc(branch.manager)}
${esc(t('machine'))}${atms.length} ${esc(t('linked').toLowerCase())}
`; }).join(''); const marketCards = (state.plannedMarkets || []).map(market => { const targetLanguage = supportedLanguages.find(language => language.code === market.language); const languageName = targetLanguage?.label.replace(/^[A-Z]{2}\s+/, '') || market.language; return `
${esc(market.flag)}${esc(t('planningOnly'))}

${esc(market.city)}

${esc(market.country)} · ${esc(market.market)}

${esc(t('marketPhase'))}${esc(market.phase)}
${esc(t('regulator'))}${esc(market.regulator)}${esc(t('openOfficialSource'))} ↗
${esc(market.currency || '—')} · ${esc(t('targetLanguage'))}: ${esc(market.language.toUpperCase())} · ${esc(languageName)}${esc(t('notLicensed'))}
`; }).join(''); const companyLocation = state.meta.companyLocation || { ...COMPANY_PUBLIC_ADDRESS }; const headquartersCard = `
🇺🇸${esc(t('publicCompanyAddress'))}

${esc(companyLocation.city)}, ${esc(companyLocation.region)}

${esc(companyLocation.street)} · ${esc(companyLocation.city)}, ${esc(companyLocation.region)} ${esc(companyLocation.postalCode)}

${esc(t('companyAddressListedBy'))}GNK DINAMO Ltd. website · FINA Info.BIZ
${esc(t('companyAddressNotVerified'))}${esc(t('companyLegalSeatUnverified'))}${esc(t('companyAffiliationNote'))}
`; const bankPos = `
${state.branches.map(branch => `
${esc(branch.name)}${esc(currency(branch.cashCents))}
`).join('')}
`; return `${pageHeading('branchesCash', 'branchesSub', `${button('cash-operation', t('cashOperation'), 'primary', 'cash')}${button('transfer', t('internalTransfer'), '', 'exchange')}`)}
${esc(t('simulatedBranches'))}${state.branches.length}${esc(t('syntheticBranch'))}
${esc(t('branchCash'))}${esc(compactCurrency(totalCash))}${esc(t('sampleValue'))}
${esc(t('machine'))}${state.atms.length}${activeAtms} ${esc(t('online').toLowerCase())} · demo
${esc(t('teller'))} desks${state.branches.reduce((sum, branch) => sum + branch.tellers, 0)}${esc(t('controlsNotWired'))}
${branchCards}
${panel(t('cashCoverage'), bankPos, { description: t('branchCashNote') })}
iCash counter actions post to the synthetic demo ledger only. Teller identity, dual control, counterfeit detection and end-of-day cash balancing are not implemented controls.
ADRIA BANK · ${esc(t('planningOnly'))}

${esc(t('globalExpansion'))}

${esc(t('marketStudy'))}

${(state.plannedMarkets || []).length} ${esc(t('proposedMarkets'))}
!${esc(t('marketNotice'))}
${headquartersCard}${marketCards}
`; } function renderAtms() { const totalCash = state.atms.reduce((sum, atm) => sum + atm.cashCents, 0); const atms = state.atms.map(atm => { const branch = state.branches.find(item => item.id === atm.branchId); const statusClass = atm.status === 'online' ? 'success' : atm.status === 'low' ? 'warning' : atm.status === 'maintenance' ? 'neutral' : 'danger'; const action = atm.status === 'online' ? `` : ''; return `
${icon('atm')}
${esc(atm.name)}${esc(atm.location)} · ${esc(branch?.code || '')}
${esc(currency(atm.cashCents))}${esc(t(atm.status === 'low' ? 'warning' : atm.status))}
${action}
`; }).join(''); return `${pageHeading('atmNetwork', 'atmSub', button('atm-withdrawal', t('simulateWithdrawal'), 'primary', 'cash'))}
${esc(t('machine'))}${state.atms.length}${esc(t('allSynthetic'))}
${esc(t('online'))}${state.atms.filter(atm => atm.status === 'online').length}${esc(t('statusChanged'))}
${esc(t('atmCash'))}${esc(compactCurrency(totalCash))}${esc(t('sampleValue'))}
${esc(t('warning'))}${state.atms.filter(atm => atm.status === 'low').length}${esc(t('maintenance'))}: ${state.atms.filter(atm => atm.status === 'maintenance').length}
${atms}
${panel(t('reconciliation'), `
${esc(t('cash'))} · fleet${esc(currency(totalCash))}
${esc(t('online'))}${state.atms.filter(atm => atm.status === 'online').length} / ${state.atms.length}
${esc(t('lastCheck'))}${esc(fmtTime(new Date().toISOString()))}
`, { description: t('controlsNotWired') })}
!ATM switch, HSM, card processor, cash cassette sensors and physical dispensing are not connected. This action is a ledger rehearsal only.
`; } function renderAml() { const openCases = state.cases.filter(item => item.status !== 'closed'); const highCount = openCases.filter(item => item.severity === 'high').length; const kycDue = state.customers.filter(customer => customer.kyc !== 'verified'); const tabBar = `
`; let body; if (amlTab === 'cases') { const f = filterState.cases; const query = f.q.toLocaleLowerCase(locale()); const filtered = state.cases.filter(item => { const customer = customerById(item.customerId); const text = `${item.id} ${item.title} ${customer?.name || ''} ${item.transactionId || ''}`.toLocaleLowerCase(locale()); return item.status !== 'closed' && (!query || text.includes(query)) && (f.severity === 'all' || item.severity === f.severity) && (f.status === 'all' || item.status === f.status); }); const page = paged(filtered, 'cases'); const rows = page.items.map(item => { const customer = customerById(item.customerId); const tx = state.transactions.find(entry => entry.id === item.transactionId); return `${esc(item.id)}${esc(fmtDate(item.createdAt, true))}${formatClient(customer)}${esc(item.title)}${esc(tx ? `${txLabel(tx)} · ${currency(tx.amountCents)}` : 'Synthetic scenario')}${badge(item.severity)}
${item.score}
${badge(item.status)}${esc(item.assignedTo || '—')}`; }); body = `
${tabBar}
${tableShell([t('reference'), t('client'), t('review'), t('priority'), t('score'), t('status'), t('assigned'), t('actions')], rows, page.footer)}`; } else { const f = filterState.customers; const filtered = kycDue.filter(customer => !f.q || `${customer.id} ${customer.name} ${customer.city}`.toLocaleLowerCase(locale()).includes(f.q.toLocaleLowerCase(locale()))).sort((a,b) => a.kyc.localeCompare(b.kyc)); const page = paged(filtered, 'customers'); const rows = page.items.map(customer => `${formatClient(customer)}${esc(customer.city)}, ${esc(customer.country)}${badge(customer.risk)}${badge(customer.kyc)}${esc(customer.kycRequest?.sourceOfFunds || customer.sourceOfFunds)}${customer.kind === 'business' ? esc(customer.beneficialOwner || t('noData')) : '—'}`); body = `
${esc(t('pending'))}: ${kycDue.filter(customer => customer.kyc === 'pending').length}
${tabBar}
${tableShell([t('client'), t('city'), t('risk'), t('kyc'), t('sourceFunds'), t('beneficialOwner'), t('actions')], rows, page.footer)}`; } const rules = `
${['amlRule1','amlRule2','amlRule3','amlRule4','amlRule5','amlRule6'].map((key, i) => `
${i + 1}. ${esc(t(key))}${esc(t('illustrative'))} · AMLR (EU) 2024/1624${badge(i < 2 ? 'simulated' : 'notConnected')}
`).join('')}
`; return `${pageHeading('amlKyc', 'amlSub', `${button('view-compliance', t('compliance'), '', 'shield')}`)}
${esc(t('openCases'))}${openCases.length}${esc(t('illustrative'))}
${esc(t('high'))} priority${highCount}${esc(t('review'))}
KYC ${esc(t('pending'))}${kycDue.length}${esc(t('nextReview'))}
${esc(t('screening'))}${esc(t('notConnected'))}${esc(t('controlsNotWired'))}
${tabBar}
${esc(t('demoOnly'))}
${body}
${panel(t('compliance'), rules, { description: t('complianceBody') })}
!Alerts are synthetic test cases. This is not transaction monitoring, sanctions screening, PEP screening, a suspicious-transaction reporting system or an MLRO workflow.
EU AML packageAMLR application date: 10 Jul 2027
`; } function renderSwift() { const waiting = state.swiftMessages.filter(message => message.status === 'awaiting_approval').length; const nostro = state.bankAccounts.filter(account => account.glCode === '1200').reduce((sum, account) => { const rate = CURRENCIES[account.currency]?.eurRate; return Number.isFinite(rate) && rate > 0 ? sum + account.balanceCents * rate : sum; }, 0); const rows = state.swiftMessages.map(message => { const customer = customerById(message.customerId); const approve = message.status === 'awaiting_approval' && canApprove() ? `` : ''; return `${esc(message.id)}${esc(message.type)} · ${esc(message.direction)}${formatClient(customer)}${esc(message.beneficiary)}${esc(message.bic || '—')}${esc(currency(message.amountCents, message.currency))}${badge(message.status)}${esc(fmtDate(message.createdAt, true))}${approve}`; }); const mainBankAccount = state.bankAccounts.find(account => account.id === 'BK-002'); const list = `
${esc(t('mainAccount'))}${esc(currency(mainBankAccount?.balanceCents || 0))}${esc(mainBankAccount?.ref || '')} · DEMO IBAN
${esc(t('nostro'))}${esc(currency(Math.round(nostro * 100)))}${state.bankAccounts.filter(account => account.glCode === '1200').length} sandbox positions
${esc(t('awaitingApproval'))}${waiting}${esc(t('controlsNotWired'))}
`; return `${pageHeading('swiftLab', 'swiftSub', button('new-swift', t('newSwift'), 'primary', 'plus'))}
!${esc(t('swiftDisclaimer'))}. MT103 / ISO 20022 examples below are local rehearsal data and are never transmitted.
${panel(t('mainAccount'), list, { description: t('swiftFlow') })}${panel(t('swiftLab'), `
${esc(t('swiftMain'))}${esc(mainBankAccount?.ref || '')}
${esc(t('swiftNostro'))} · EUR${esc(state.bankAccounts.find(item => item.id === 'BK-003')?.ref || '')}
${esc(t('swiftNostro'))} · USD${esc(state.bankAccounts.find(item => item.id === 'BK-004')?.ref || '')}
${esc(t('approvalRequired'))}
`, { description: t('localSimulation') })}

${esc(t('message'))} queue

${state.swiftMessages.length}
${esc(t('demoOnly'))}
${tableShell([t('reference'), t('client'), t('beneficiary'), t('amount'), t('status'), t('date'), t('actions')], rows)}
${panel('Message path', `
1 · ${esc(t('message'))}${esc(t('simulated'))}
2 · ${esc(t('screening'))} / sanctions${esc(t('notConnected'))}
3 · ${esc(t('approvals'))} · dual control${esc(t('required'))}
4 · Network transmission${esc(t('notConnected'))}
`, { description: t('controlsNotWired') })}${panel('ISO 20022 / SWIFT', `

This lab helps rehearse message fields, beneficiary validation, approvals and reconciliation concepts. A production integration needs authorised bank participation, connectivity, screening, reconciliation, security controls, business continuity and scheme-specific testing.

${esc(t('localOnly'))}MT103 format preview · pacs.008 sample records
`)}
`; } function fiatAssets() { return Object.keys(CURRENCIES).filter(code => code !== 'EUR' && !CURRENCIES[code].crypto && Number.isFinite(CURRENCIES[code].eurRate) && CURRENCIES[code].eurRate > 0 && CURRENCIES[code].exchangeEnabled !== false); } function accountCurrencies() { return Object.keys(CURRENCIES).filter(code => code !== 'EUR' && !CURRENCIES[code].crypto); } function digitalAssets() { return Object.keys(CURRENCIES).filter(code => CURRENCIES[code].crypto); } function assetName(asset) { const key = asset.toLowerCase(); return t(key) === key ? CURRENCIES[asset]?.name || asset : t(key); } function rateBoard(assets) { return `
${assets.map(asset => { const item = CURRENCIES[asset]; const fiatRate = item.crypto ? item.eurRate : 1 / item.eurRate; const rateDate = item.referenceDate || (item.referenceSource === 'CBK' ? CBK_FALLBACK_DATE : ECB_FALLBACK_DATE); const feedStatus = item.referenceSource === 'CBK' ? fxReferenceData.cbk.status : fxReferenceData.ecb.status; const rateStatus = item.referenceStatus || (String(feedStatus).startsWith('bundled-sample') ? feedStatus : 'bundled-sample'); const feed = item.crypto ? 'Offline illustrative sample' : item.referenceSource === 'CBK' ? `CBK daily indicative · ${rateDate} · ${rateStatus}` : item.referenceSource?.includes('peg') ? `ECB daily + USD/AED peg · ${rateDate} · ${rateStatus}` : `ECB daily · ${rateDate} · ${rateStatus}`; return `
${esc(asset.slice(0, 2))}${esc(assetName(asset))} ${esc(asset)}
${item.crypto ? esc(currencyMajor(item.eurRate, 'EUR', 2)) : `1 EUR = ${new Intl.NumberFormat(locale(), { maximumFractionDigits: ['JPY','KRW','ISK'].includes(asset) ? 2 : 4 }).format(fiatRate)} ${esc(asset)}`}${esc(feed)} · reference only, not a transaction quote
`; }).join('')}
`; } function renderOptionalMarketFeed() { let statusText = t('marketFeedIdle'); let resultMarkup = ''; if (optionalMarketFeed.status === 'loading') statusText = t('marketFeedLoading'); if (optionalMarketFeed.status === 'unconfigured') statusText = t('marketFeedNoKey'); if (optionalMarketFeed.status === 'unavailable') statusText = t('marketFeedUnavailable'); if (optionalMarketFeed.status === 'ready' && optionalMarketFeed.reference) { const reference = optionalMarketFeed.reference; statusText = t('marketFeedReturned'); resultMarkup = `
1 EUR = ${new Intl.NumberFormat(locale(), { minimumFractionDigits: 4, maximumFractionDigits: 6 }).format(reference.rate)} USD${esc(t('marketFeedAsOf'))}: ${esc(fmtDate(reference.timestamp, true))} · ${esc(reference.provider || 'Twelve Data')}
`; } const disabled = optionalMarketFeed.status === 'loading' ? 'disabled' : ''; return `

${esc(t('marketFeedTitle'))}

${esc(t('marketFeedPair'))}

OPTIONAL · READ-ONLY
${esc(statusText)}
${resultMarkup}
${esc(t('marketFeedProviderDocs'))} ↗
i${esc(t('marketFeedBoundary'))} ${esc(t('marketFeedSetup'))}
`; } async function refreshOptionalMarketFeed() { if (optionalMarketFeedPromise) return optionalMarketFeedPromise; optionalMarketFeed = { status: 'loading', reference: null, message: '' }; if (currentView === 'exchange') renderView(); optionalMarketFeedPromise = (async () => { try { let result; if (typeof window.adriaDemo?.getOptionalMarketReference === 'function') { result = await window.adriaDemo.getOptionalMarketReference(); } else { const response = await fetch('/api/market/eur-usd', { cache: 'no-store', credentials: 'same-origin' }); result = await response.json(); } optionalMarketFeed = result?.ok ? { status: 'ready', reference: result, message: '' } : { status: result?.configured === false ? 'unconfigured' : 'unavailable', reference: null, message: result?.message || '' }; } catch { optionalMarketFeed = { status: 'unavailable', reference: null, message: '' }; } finally { optionalMarketFeedPromise = null; if (currentView === 'exchange') renderView(); } return optionalMarketFeed; })(); return optionalMarketFeedPromise; } function applyEcbReference(reference) { fxReferenceData.ecb = { status: 'daily-reference', rateDate: reference.date, fetchedAt: new Date().toISOString(), source: reference.source || ECB_DAILY_URL, rates: reference.rates }; for (const [code, item] of Object.entries(CURRENCIES)) { if (item.ecbReference && Number.isFinite(reference.rates[code]) && reference.rates[code] > 0) { item.eurRate = 1 / reference.rates[code]; item.referenceDate = reference.date; item.referenceStatus = 'daily-reference'; } } if (Number.isFinite(reference.rates.USD) && reference.rates.USD > 0) { CURRENCIES.AED.eurRate = 1 / (reference.rates.USD * 3.6725); CURRENCIES.AED.referenceDate = reference.date; CURRENCIES.AED.referenceStatus = 'daily-reference · USD/AED peg assumption'; } } function applyCbkReference(reference) { fxReferenceData.cbk = { status: 'daily-indicative', rateDate: reference.date, fetchedAt: new Date().toISOString(), source: reference.source || CBK_FOREX_URL, kesPerEur: reference.kesPerEur }; CURRENCIES.KES.eurRate = 1 / reference.kesPerEur; CURRENCIES.KES.referenceDate = reference.date; CURRENCIES.KES.referenceStatus = 'daily-indicative'; } async function readFxReference(kind) { const electronMethod = kind === 'ecb' ? window.adriaDemo?.getEcbDailyReference : window.adriaDemo?.getCbkDailyReference; if (typeof electronMethod === 'function') { const result = await electronMethod(); if (!result?.ok) throw new Error(result?.error || `${kind.toUpperCase()} reference feed is unavailable.`); return kind === 'ecb' ? parseEcbDailyReference(result.xml) : parseCbkDailyReference(result.html); } const response = await fetch(`/api/fx/${kind}`, { cache: 'no-store', credentials: 'same-origin' }); if (!response.ok) throw new Error(`${kind.toUpperCase()} reference endpoint returned ${response.status}.`); const text = await response.text(); return kind === 'ecb' ? parseEcbDailyReference(text) : parseCbkDailyReference(text); } function refreshOneFxSource(kind) { if (kind === 'ecb') { if (!ecbFetchPromise) ecbFetchPromise = readFxReference('ecb').then(applyEcbReference).catch(error => { if (fxReferenceData.ecb.status === 'bundled-sample') fxReferenceData.ecb.status = 'bundled-sample · feed unavailable'; throw error; }).finally(() => { ecbFetchPromise = null; }); return ecbFetchPromise; } if (!cbkFetchPromise) cbkFetchPromise = readFxReference('cbk').then(applyCbkReference).catch(error => { if (fxReferenceData.cbk.status === 'bundled-sample') fxReferenceData.cbk.status = 'bundled-sample · feed unavailable'; throw error; }).finally(() => { cbkFetchPromise = null; }); return cbkFetchPromise; } async function refreshFxReferences(notify = false) { const results = await Promise.allSettled([refreshOneFxSource('ecb'), refreshOneFxSource('cbk')]); const success = results.some(result => result.status === 'fulfilled'); if (notify) toast(success ? t('fxRefreshComplete') : t('fxRefreshUnavailable'), success ? 'success' : 'warning'); if (currentView === 'exchange' && !modalRoot.childElementCount) renderView(); return success; } function conversionReferenceDetails(fromCode, toCode) { const codes = [fromCode, toCode]; const parts = []; if (codes.includes('KES')) parts.push(`CBK indicative ${fxReferenceData.cbk.rateDate}`); if (codes.some(code => CURRENCIES[code]?.ecbReference || code === 'AED')) parts.push(`ECB daily ${fxReferenceData.ecb.rateDate}`); return { referenceDate: [...new Set(parts.map(part => part.split(' ').at(-1)))].join(' / '), source: [...new Set(parts)].join(' + ') || 'Bundled indicative demo sample' }; } function walletValue(wallet) { const item = CURRENCIES[wallet.asset]; return item && Number.isFinite(item.eurRate) && item.eurRate > 0 ? wallet.units * item.eurRate : null; } function renderExchange() { const assets = exchangeTab === 'fx' ? fiatAssets() : digitalAssets(); const scopeCustomerId = document.body.dataset.surface === 'client' ? portalCustomerId : exchangeCustomerId; const accountIds = state.accounts.filter(account => account.currency === 'EUR' && account.status === 'active' && customerById(account.customerId)?.kyc === 'verified' && (!scopeCustomerId || account.customerId === scopeCustomerId)); const customerWallets = state.wallets.filter(wallet => wallet.units > 0.00000001 && (!scopeCustomerId || wallet.customerId === scopeCustomerId)); const totalWalletEur = customerWallets.reduce((sum, wallet) => sum + (walletValue(wallet) ?? 0), 0); const inputLabel = exchangeSide === 'buy' ? t('amountInputBuy') : t('amountInputSell'); const sideButtons = `
`; const assetOptions = assets.map(asset => ``).join(''); const accountOptions = accountIds.map(account => ``).join(''); const walletRows = customerWallets.length ? customerWallets.slice(0, 9).map(wallet => `
${esc(wallet.asset.slice(0, 2))}
${esc(assetName(wallet.asset))} · ${esc(wallet.asset)}${new Intl.NumberFormat(locale(), { maximumFractionDigits: CURRENCIES[wallet.asset]?.decimals || 2 }).format(wallet.units)} ${esc(wallet.asset)} · ${esc(customerById(wallet.customerId)?.name || wallet.customerId)}
${walletValue(wallet) === null ? 'No reference' : esc(currencyMajor(walletValue(wallet)))}${esc(t('sampleValue'))}
`).join('') : emptyState('No demo wallet positions yet. Complete a simulated buy to create one.'); const visibleTrades = state.exchangeTrades.filter(trade => !scopeCustomerId || trade.customerId === scopeCustomerId); const trades = visibleTrades.slice(0, 8).map(trade => `${esc(trade.id)}${esc(fmtDate(trade.createdAt, true))}${formatClient(customerById(trade.customerId))}${esc(t(trade.side))} ${esc(trade.asset)}${new Intl.NumberFormat(locale(), { maximumFractionDigits: CURRENCIES[trade.asset]?.decimals || 2 }).format(trade.units)} ${esc(trade.asset)}${esc(currency(trade.eurCents))}${esc(currency(trade.spreadCents))}${badge(trade.status)}`); const exchangeScope = scopeCustomerId ? `${esc(t('customerPortal'))}: ${esc(customerById(scopeCustomerId)?.name || scopeCustomerId)}` : ''; const pageIntro = pageHeading('exchange', 'exchangeSub', `${exchangeScope}${esc(t('demoOnly'))}${button('account-conversion', t('convertAccounts'), '', 'exchange')}${icon('globe')}Separate desk ↗`); const form = `

${esc(t('createTrade'))}

${esc(t('localSimulation'))} · ${esc(t('approvalNotice'))}

${sideButtons}
${esc(exchangeSide === 'buy' ? t('expectedOutput') : t('amountInputSell'))}
${esc(t('indicativeRate'))}—
${esc(t('fee'))}—
${esc(t('expectedOutput'))}—
${esc(exchangeTab === 'crypto' ? t('digitalAssetNotice') : t('fiatNotice'))}
`; const scopedWalletTransfers = (state.walletTransfers || []).filter(transfer => !scopeCustomerId || transfer.customerId === scopeCustomerId); const transferRows = scopedWalletTransfers.slice(0, 12).map(transfer => { const incoming = transfer.direction === 'in'; const label = incoming ? t('cryptoIncoming') : t('cryptoOutgoing'); const address = incoming ? transfer.source : transfer.destination; const canCancel = transfer.status === 'awaiting_approval' && (document.body.dataset.surface === 'client' ? transfer.customerId === portalCustomerId : canWrite()); const actions = `${transfer.status === 'awaiting_approval' && canApprove() ? `` : ''}${canCancel ? `` : ''}`; return `${esc(transfer.id)}${esc(fmtDate(transfer.createdAt, true))}${document.body.dataset.surface === 'client' ? '' : formatClient(customerById(transfer.customerId))}${esc(label)}${new Intl.NumberFormat(locale(), { maximumFractionDigits: CURRENCIES[transfer.asset]?.decimals || 8 }).format(transfer.units)} ${esc(transfer.asset)}${esc(address || '')}${esc(transfer.network)}${badge(transfer.status)}${actions || '—'}`; }); const walletQueue = panel(t('walletQueue'), tableShell([t('reference'), ...(document.body.dataset.surface === 'client' ? [] : [t('client')]), t('amount'), t('channel'), t('status'), t('actions')], transferRows, '', t('noWalletTransfers')), { description: t('digitalAssetNotice'), actions: `` }); const wallets = panel(t('portfolio'), `
${esc(t('sampleValue'))} · ${esc(t('localSimulation'))}${customerWallets.length} positions
${esc(currencyMajor(totalWalletEur))}${esc(t('eurValue'))} · ${esc(t('notLivePrice'))}
`, { description: t('controlsNotWired'), actions: `` }); const rates = panel(t('quoteTable'), rateBoard(assets), { description: t('quoteDisclosure'), actions: `${esc(t('notLivePrice'))}` }); const history = panel(t('recentTrades'), tableShell([t('reference'), t('client'), t('direction'), t('amount'), 'EUR', t('fee'), t('status')], trades), { description: `${visibleTrades.length} ${t('records')}` }); return `${pageIntro}
ADRIA EXCHANGE · SEPARATE DESK

${esc(t('exchange'))}

Dedicated treasury workflow · fiat FX + a strictly offline digital-asset rehearsal ledger

${esc(t('notConnected'))}
${rates}
${renderOptionalMarketFeed()}
${form}
${wallets}${walletQueue}${history}
${esc(t('quoteDisclosure'))}FIAT and crypto virtual balances are local sample records and do not leave this application.
`; } function renderLedger() { const assetAccounts = state.glAccounts.filter(account => account.group === 'asset'); const liabilityAccounts = state.glAccounts.filter(account => account.group === 'liability'); const assets = assetAccounts.reduce((sum, account) => sum + account.balanceCents, 0); const liabilities = liabilityAccounts.reduce((sum, account) => sum + account.balanceCents, 0); const equity = state.glAccounts.filter(account => ['equity','income'].includes(account.group)).reduce((sum, account) => sum + account.balanceCents, 0) - state.glAccounts.filter(account => account.group === 'expense').reduce((sum, account) => sum + account.balanceCents, 0); const f = filterState.ledger; const page = paged(state.ledgerJournals, 'ledger'); const rows = page.items.map(journal => `${esc(journal.id)}${esc(journal.ref || '—')}${esc(journal.description)}${esc(fmtDate(journal.at, true))}${journal.lines.length}${esc(currency(journal.debitCents))}${esc(currency(journal.creditCents))}${badge(journal.debitCents === journal.creditCents ? 'verified' : 'high')}`); const glRows = state.glAccounts.map(account => `${esc(account.code)}${esc(account.name)}${esc(account.group)}${esc(account.normal === 'debit' ? t('debit') : t('credit'))}${esc(currency(account.balanceCents))}`); const bankRows = state.bankAccounts.map(account => `${esc(account.label)}${esc(account.bank)}${esc(account.ref)}${esc(account.currency)}${esc(currency(account.balanceCents, account.currency))}${badge('simulated')}`); return `${pageHeading('ledger', 'ledgerSub', `${button('export-json', t('exportJson'), '', 'download')}${button('download-csv', t('downloadCsv'), 'primary', 'download')}`)}
${esc(t('totalAssets'))}${esc(compactCurrency(assets))}${esc(t('sampleValue'))}
${esc(t('customerLiabilities'))}${esc(compactCurrency(liabilities))}${esc(t('customerDeposits'))}
${esc(t('capital'))} + P&L${esc(compactCurrency(equity))}${esc(t('openingBalance'))}
${esc(t('journal'))}${state.ledgerJournals.length}${isBalanced(state) ? '✓ ' + t('cleared') : t('review')}
iInitial positions are a static teaching scenario. Demo journals enforce equal debit and credit totals; this is not a chart of accounts, IFRS/CRR return, statutory ledger or reconciliation to real bank statements.

${esc(t('journal'))}

${isBalanced(state) ? esc(t('cleared')) : esc(t('review'))}
${state.ledgerJournals.length} ${esc(t('records'))}
${tableShell([t('code'), t('reference'), t('description'), 'Lines', t('debit'), t('credit'), t('status'), t('actions')], rows, page.footer)}

Chart of accounts

Synthetic opening ledger and local simulation postings

${tableShell([t('code'), t('account'), t('category'), 'Normal balance', t('balance')], glRows)}

${esc(t('mainAccount'))}

${esc(t('controlsNotWired'))}

${tableShell([t('account'), t('bank'), t('reference'), t('currency'), t('balance'), t('status')], bankRows)}
`; } const regulatorySources = [ { title: 'Delaware Office of the State Bank Commissioner', url: 'https://banking.delaware.gov/', tag: 'USA · STATE BANKING' }, { title: 'FDIC · deposit insurance and bank data', url: 'https://www.fdic.gov/', tag: 'USA · BANK DATA' }, { title: 'HNB · authorisation and supervision of credit institutions', url: 'https://www.hnb.hr/en/core-functions/supervision/basics-and-objectives', tag: 'CROATIA · LICENSING' }, { title: 'ECB · daily euro foreign exchange reference rates', url: 'https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html', tag: 'DAILY INDICATIVE DATA' }, { title: 'Central Bank of Kenya · indicative foreign exchange rates', url: 'https://www.centralbank.go.ke/forex/', tag: 'KENYA · INDICATIVE DATA' }, { title: 'Central Bank of the UAE · licensing', url: 'https://centralbank.ae/en/licensing/', tag: 'UAE · LICENSING' }, { title: 'Japan Financial Services Agency', url: 'https://www.fsa.go.jp/en/', tag: 'JAPAN · REGULATOR' }, { title: 'Monetary Authority of Singapore', url: 'https://www.mas.gov.sg/', tag: 'SINGAPORE · REGULATOR' }, { title: 'FINMA · Swiss Financial Market Supervisory Authority', url: 'https://www.finma.ch/en/', tag: 'SWITZERLAND · REGULATOR' }, { title: 'BaFin · German Federal Financial Supervisory Authority', url: 'https://www.bafin.de/EN/Homepage/homepage_node.html', tag: 'GERMANY · REGULATOR' }, { title: 'UK Prudential Regulation Authority', url: 'https://www.bankofengland.co.uk/prudential-regulation', tag: 'UK · PRUDENTIAL' }, { title: 'UK Financial Conduct Authority', url: 'https://www.fca.org.uk/', tag: 'UK · CONDUCT' }, { title: 'EU Capital Requirements Regulation (CRR) and Directive (CRD)', url: 'https://eur-lex.europa.eu/eli/reg/2013/575/oj/eng', tag: 'PRUDENTIAL' }, { title: 'Croatia · Consumer Credit Act NN 86/2026 · effective 20 November 2026', url: 'https://narodne-novine.nn.hr/eli/sluzbeni/2026/86/1071', tag: 'CREDIT' }, { title: 'EU Consumer Credit Directive 2023/2225 · applies from 20 November 2026', url: 'https://eur-lex.europa.eu/EN/legal-content/summary/consumer-credit-agreements-2023.html', tag: 'CONSUMER CREDIT' }, { title: 'HNB · Loans and prior authorisation to grant credit', url: 'https://www.hnb.hr/en/about-us/consumer-protection/key-information/loans', tag: 'CREDIT AUTHORISATION' }, { title: 'EU AML Regulation 2024/1624 (AMLR) · applies 10 July 2027', url: 'https://eur-lex.europa.eu/eli/reg/2024/1624/oj/eng', tag: 'AML/CFT' }, { title: 'EBA · remote customer onboarding guidelines', url: 'https://www.eba.europa.eu/publications-and-media/press-releases/eba-publishes-guidelines-remote-customer-onboarding', tag: 'KYC' }, { title: 'Croatian Ministry of Finance · FIU / suspicious-transaction reporting', url: 'https://mfin.gov.hr/istaknute-teme/ured-za-sprjecavanje-pranja-novca/143', tag: 'FIU' }, { title: 'EU Transfer of Funds Regulation 2023/1113 · crypto Travel Rule', url: 'https://eur-lex.europa.eu/eli/reg/2023/1113/oj/eng', tag: 'TRAVEL RULE' }, { title: 'MiCA Regulation 2023/1114 · crypto-asset services', url: 'https://eur-lex.europa.eu/eli/reg/2023/1114/oj/eng', tag: 'CRYPTO' }, { title: 'Hanfa · MiCA crypto authorisation in Croatia', url: 'https://www.hanfa.hr/news/provision-of-crypto-asset-services-from-1-july-2026-crypto-licence-and-registration-in-hanfa-registers-mandatory-for-providers-consumers-urged-to-exercise-caution/', tag: 'CROATIA' }, { title: 'DORA Regulation 2022/2554 · applies 17 January 2025', url: 'https://eur-lex.europa.eu/eli/reg/2022/2554/oj/eng', tag: 'ICT RISK' }, { title: 'EU instant payments Regulation 2024/886 · payee verification', url: 'https://eur-lex.europa.eu/eli/reg/2024/886/oj/eng', tag: 'PAYMENTS' }, { title: 'Croatian Deposit Insurance Agency · insured amount / €100,000 limit', url: 'https://www.haod.hr/sustav-osiguranja-depozita/osigurani-iznos', tag: 'DEPOSIT GUARANTEE' }, { title: 'GDPR · Regulation (EU) 2016/679', url: 'https://eur-lex.europa.eu/eli/reg/2016/679/oj/eng', tag: 'PRIVACY' }, { title: 'PSD2 · payment services and strong customer authentication', url: 'https://eur-lex.europa.eu/eli/dir/2015/2366/oj/eng', tag: 'PAYMENTS' } ]; function renderReports() { const monthly = state.transactions.filter(tx => new Date(tx.date) > new Date(Date.now() - 30 * 86400000)); const riskCounts = ['low','medium','high'].map(risk => state.customers.filter(customer => customer.risk === risk).length); const channelCounts = ['Card','SEPA','Branch','ATM','Mobile'].map(channel => state.transactions.filter(tx => tx.channel.includes(channel)).length); const maxChannel = Math.max(1, ...channelCounts); const sourceRows = regulatorySources.map(source => `${esc(source.tag)}${esc(source.title)} ↗${esc(t('official'))}`); const riskSum = Math.max(1, riskCounts.reduce((sum, value) => sum + value, 0)); const riskPanel = `
${esc(t('low'))} ${riskCounts[0]}${esc(t('medium'))} ${riskCounts[1]}${esc(t('high'))} ${riskCounts[2]}
`; const channelPanel = `
${['Card','SEPA','Branch','ATM','Mobile'].map((channel, i) => `
${esc(channel)}${channelCounts[i]}
`).join('')}
`; return `${pageHeading('reports', 'reportsSub', `${button('download-csv', t('downloadCsv'), '', 'download')}${button('export-json', t('exportJson'), 'primary', 'download')}`)}
${esc(t('totalMovement'))} · 30d${esc(compactCurrency(monthly.reduce((sum, tx) => sum + tx.amountCents, 0)))}${monthly.length} ${esc(t('transaction').toLowerCase())}
${esc(t('totalClients'))}${state.customers.length}${esc(t('allSynthetic'))}
${esc(t('openCases'))}${state.cases.filter(item => item.status !== 'closed').length}${esc(t('illustrative'))}
${esc(t('journal'))} controls${isBalanced(state) ? 'PASS' : 'CHECK'}${state.ledgerJournals.length} balanced demo postings
${panel(t('customersRisk'), riskPanel, { description: t('sampleValue') })}${panel(t('paymentMix'), channelPanel, { description: t('monthlyVolume') })}

${esc(t('compliance'))} · ${esc(t('official'))}

Primary source reading list checked for this demo brief · 10 Oct 2026

${esc(t('notConnected'))}
!Not a complete legal inventory. National implementation, supervisory guidance, product perimeter, group structure and rule changes require Croatian banking, AML, payments, crypto, privacy, tax, consumer and prudential counsel plus direct regulator engagement.
${tableShell([t('category'), t('description'), t('status')], sourceRows)}
`; } function renderInstitutions() { const banks = state.demoBanks || []; const transfers = state.interbankTransfers || []; const bankRows = banks.map(bank => `${esc(bank.name)}${esc(bank.id)}${esc(bank.country)} · ${esc(bank.countryCode)}${esc(bank.currency)}${esc(bank.bic)} · test-shaped${esc(t('regulatorLink'))} ↗${esc(t('fictionalNotConnected'))}`); const transferRows = transfers.map(transfer => { const action = transfer.status === 'awaiting_approval' && canApprove() ? `` : transfer.status === 'awaiting_approval' ? `${esc(t('awaitingApproval'))}` : badge(transfer.status); return `${esc(transfer.id)}${esc(fmtDate(transfer.createdAt, true))}${esc(customerById(transfer.customerId)?.name || transfer.customerId)}${esc(transfer.fromAccountId)}${esc(transfer.beneficiary)}${esc(transfer.toBank)} · ${esc(transfer.beneficiaryAccount)}${esc(currency(transfer.amountCents, transfer.currency))}${action}`; }); const beneficiaryRows = (state.interbankAccounts || []).slice(0, 10).map(account => { const bank = banks.find(item => item.id === account.bankId); return `${esc(account.id)}${esc(bank?.name || account.bankId)}${esc(account.accountName)}${esc(account.accountNumber)}${esc(account.currency)}${esc(t('syntheticRecord'))}`; }); return `${pageHeading('institutions', 'institutionsSub', `${button('new-interbank', t('interbankTransfer'), 'primary', 'exchange')}${button('local-checker', t('localChecks'), '', 'verify')}`)}
${esc(t('fictionalBanks'))}${banks.length}${esc(t('fictionalNotConnected'))}
${esc(t('demoBeneficiaries'))}${(state.interbankAccounts || []).length}${esc(t('syntheticRecord'))}
${esc(t('interbankQueue'))}${transfers.filter(item => item.status === 'awaiting_approval').length}${esc(t('localCheckerOnly'))}
${esc(t('proposedMarkets'))}${(state.plannedMarkets || []).length}${esc(t('planningOnly'))}
!${esc(t('interbankDemoDisclosure'))}
${renderInteropSystemSwitcher()}

${esc(t('demoBankDirectory'))}

${esc(t('directorySub'))}

${esc(t('fictionalNotConnected'))}
${tableShell([t('institution'), t('country'), t('currency'), t('bic'), t('regulator'), t('connectivity')], bankRows)}

${esc(t('interbankQueue'))}

${esc(t('localCheckerOnly'))}

${transfers.length} ${esc(t('records'))}
${tableShell([t('reference'), t('client'), t('beneficiary'), t('amount'), t('actions')], transferRows)}
${panel(t('fictionalBeneficiaryDirectory'), tableShell([t('reference'), t('institution'), t('beneficiary'), t('accountNumber'), t('currency'), t('status')], beneficiaryRows), { description: t('syntheticDirectoryOnly') })}${panel(t('internationalMarkets'), `
${(state.plannedMarkets || []).map(market => `
${esc(market.flag)} ${esc(market.city)} · ${esc(market.currency)}${esc(market.regulator)} · ${esc(market.language.toUpperCase())}
${esc(t('regulatorLink'))} ↗
`).join('')}
`, { description: t('planningOnly') })}
`; } function renderChecker() { const result = referenceCheckResult ? `
${esc(referenceCheckResult.label)}${badge(referenceCheckResult.status)}

${esc(referenceCheckResult.detail)}

${esc(referenceCheckResult.note)}
` : `
${icon('verify')}

${esc(t('checkerNoResult'))}

`; const pending = (state.interbankTransfers || []).filter(transfer => transfer.status === 'awaiting_approval'); const pendingRows = pending.map(transfer => `${esc(transfer.id)}${esc(transfer.toBank)}${esc(transfer.beneficiary)}${esc(currency(transfer.amountCents, transfer.currency))}${canApprove() ? `` : `${esc(t('checkerRequired'))}`}`); const checkForm = `
${esc(t('referencePrivacy'))}
`; return `${pageHeading('localChecks', 'localChecksSub', `${button('clear-check-result', t('clear'), '', 'close')}${button('refresh-fx', t('refreshReference'), '', 'exchange')}`)}
!${esc(t('checkerDisclaimer'))}

${esc(t('localReferenceChecker'))}

${esc(t('localCheckerOnly'))}

${esc(t('runsLocally'))}
${checkForm}${result}
${panel(t('interbankQueue'), tableShell([t('reference'), t('institution'), t('beneficiary'), t('amount'), t('actions')], pendingRows), { description: t('makerCheckerSub') })}${panel(t('checkSteps'), `
1 · ${esc(t('chooseDemoRecord'))}

${esc(t('checkerStepOne'))}

2 · ${esc(t('inspectResult'))}

${esc(t('checkerStepTwo'))}

3 · ${esc(t('recordOutcome'))}

${esc(t('checkerStepThree'))}

`, { description: t('trainingOnly') })}
`; } function renderSupport() { const allTickets = state.supportTickets || []; const customerOnly = document.body.dataset.surface === 'client'; const tickets = customerOnly ? allTickets.filter(ticket => ticket.customerId === portalCustomerId) : allTickets; const rows = tickets.map(ticket => { const customer = customerById(ticket.customerId); const nextStatus = ticket.status === 'open' ? 'in_progress' : ticket.status === 'resolved' ? 'open' : 'resolved'; const actionLabel = ticket.status === 'open' ? t('startHandling') : ticket.status === 'resolved' ? t('reopenTicket') : t('resolveTicket'); const action = !customerOnly && canWrite() ? `` : badge(ticket.status); return `${esc(ticket.id)}${esc(fmtDate(ticket.createdAt, true))}${customer ? formatClient(customer) : esc(ticket.customerId)}${esc(ticket.subject)}${esc(ticket.category)} · ${esc(ticket.message)}${esc(ticket.assignedTo || t('unassigned'))}${customerOnly ? '—' : ``}${badge(ticket.status)}${action}`; }); const headingActions = customerOnly ? button('portal-support', t('newSupportTicket'), 'primary', 'plus') : button('new-support-ticket', t('newSupportTicket'), 'primary', 'plus'); return `${pageHeading('callCentre', customerOnly ? 'supportCustomerSub' : 'supportStaffSub', headingActions)}
!${esc(t('supportDemoDisclosure'))}
${esc(t('openTickets'))}${tickets.filter(ticket => ticket.status === 'open').length}${esc(t('localQueue'))}
${esc(t('inProgress'))}${tickets.filter(ticket => ticket.status === 'in_progress').length}${esc(t('callCentreRehearsal'))}
${esc(t('resolvedTickets'))}${tickets.filter(ticket => ticket.status === 'resolved').length}${esc(t('localOnly'))}
${esc(t('callCentre'))}${esc(t('notConnected'))}${esc(t('controlsNotWired'))}

${esc(t('callCentreQueue'))}

${esc(customerOnly ? t('supportHistory') : t('callCentreRehearsal'))}

${esc(t('demoOnly'))}
${tableShell([t('reference'), t('client'), t('subject'), t('assigned'), t('staffNote'), t('status'), t('actions')], rows)}
${panel(t('callHandlingGuide'), `
1 · ${esc(t('greetAndScope'))}

${esc(t('callStepOne'))}

2 · ${esc(t('verifyDemoOnly'))}

${esc(t('callStepTwo'))}

3 · ${esc(t('routeAndRecord'))}

${esc(t('callStepThree'))}

4 · ${esc(t('closeAndExplain'))}

${esc(t('callStepFour'))}

`, { description: t('supportDemoDisclosure') })}
`; } function renderComplaints() { const customerOnly = document.body.dataset.surface === 'client'; const scoped = (state.complaints || []).filter(complaint => !customerOnly || complaint.customerId === portalCustomerId); const f = filterState.complaints; const query = f.q.toLocaleLowerCase(locale()); const filtered = scoped.filter(complaint => { const customer = customerById(complaint.customerId); const text = `${complaint.id} ${complaint.subject} ${complaint.category} ${customer?.name || ''}`.toLocaleLowerCase(locale()); return (!query || text.includes(query)) && (f.status === 'all' || complaint.status === f.status); }); const page = paged(filtered, 'complaints'); const rows = page.items.map(complaint => { const customer = customerById(complaint.customerId); const response = complaint.customerVisibleResponse ? `${esc(t('complaintResponse'))}: ${esc(complaint.customerVisibleResponse)}` : ''; return `${esc(complaint.id)}${esc(fmtDate(complaint.createdAt, true))}${customerOnly ? '' : `${formatClient(customer)}`}${esc(complaint.subject)}${esc(complaint.category)} · ${esc(complaint.priority === 'high_demo' ? t('high') : t('standardDemo'))}${response}${badge(complaint.status)}${esc(complaint.assignedTo || t('unassigned'))}${customerOnly ? '—' : esc(fmtDate(complaint.practiceTargetAt))}`; }); const openCount = scoped.filter(complaint => !['resolved'].includes(complaint.status)).length; const responseCount = scoped.filter(complaint => complaint.status === 'response_ready').length; const resolvedCount = scoped.filter(complaint => complaint.status === 'resolved').length; const createAction = customerOnly ? 'portal-complaint' : 'new-complaint'; const titleSub = customerOnly ? 'complaintDemoDisclosure' : 'complaintWorkflow'; const statusValues = ['received', 'investigating', 'response_ready', 'resolved', 'escalated', 'reopened']; return `${pageHeading('complaints', titleSub, button(createAction, t('newComplaint'), 'primary', 'plus'))}
!${esc(t('complaintDemoDisclosure'))}
${esc(t('complaintQueue'))}${scoped.length}${esc(t('allSynthetic'))}
${esc(t('openCases'))}${openCount}${esc(t('inReview'))}
${esc(t('responseReady'))}${responseCount}${esc(t('localOnly'))}
${esc(t('resolved'))}${resolvedCount}${esc(t('localSimulation'))}
${esc(t('localOnly'))}
${tableShell([t('reference'), ...(customerOnly ? [] : [t('client')]), t('subject'), t('status'), t('assigned'), t('complaintTarget'), t('actions')], rows, page.footer)}
i${esc(t('complaintWorkflow'))}
`; } function renderInteroperability() { const lab = ensureInteropLabState(state); const system = activeInteropSystem(state); const language = state.settings.language || 'hr'; const copy = (item, hrKey, enKey) => interopLocalizedText(item, language, item.idea ? 'feature' : 'idea', hrKey === 'detailHr' || hrKey === 'descriptionHr' ? 'description' : 'title'); const systemBanks = lab.bankConnectors.filter(item => item.systemId === system.id); const systemVenues = lab.venues.filter(item => item.systemId === system.id); const enabledBanks = systemBanks.filter(item => item.status === 'connected_demo').length; const enabledVenues = systemVenues.filter(item => item.status === 'connected_demo').length; const readScopes = systemBanks.filter(item => item.status === 'connected_demo').reduce((sum, item) => sum + (item.permissions || []).length, 0); const latest = (lab.lastResult?.systemId === system.id ? lab.lastResult : null) || lab.runs.find(run => run.systemId === system.id) || null; const latestFeature = latest ? INTEROP_FEATURES.find(item => item.id === latest.featureId) : null; const latestSummary = latest ? (language === 'hr' ? latest.summaryHr : latest.summaryEn) : ''; const ideasById = new Map(INTEROP_IDEAS.map(idea => [idea.id, idea])); const categoryKeys = ['banking', 'payments', 'controls', 'crypto', 'operations']; const categoryLabel = category => t(({ banking: 'interopCategoryBanking', payments: 'interopCategoryPayments', controls: 'interopCategoryControls', crypto: 'interopCategoryCrypto', operations: 'interopCategoryOperations' })[category] || 'interopCategoryBanking'); const statusBadge = connector => `${esc(t(connector.status === 'connected_demo' ? 'interopActive' : 'interopPaused'))}`; const scopeLabel = scope => ({ balances: t('balance'), transactions: t('transactionHistory') })[scope] || scope; const connectorCard = (connector, bank = false) => { const scopes = bank ? (connector.permissions || []).map(scopeLabel).join(' · ') || '—' : Object.keys(connector.markets || {}).join(' · '); const detail = bank ? `${connector.country} · ${connector.protocol}` : `${connector.region} · ${connector.feeBps} bp sample fee`; const toggleLabel = connector.status === 'connected_demo' ? t('interopPause') : t('interopEnable'); const toggleIcon = connector.status === 'connected_demo' ? 'close' : 'plus'; const toggle = button('interop-connector-toggle', toggleLabel, 'small', toggleIcon, `data-id="${esc(connector.id)}" ${lab.killSwitch ? 'disabled' : ''}`); const refresh = bank && connector.status === 'connected_demo' ? button('interop-connector-refresh', t('interopRefresh'), 'small', 'clock', `data-id="${esc(connector.id)}" ${lab.killSwitch ? 'disabled' : ''}`) : ''; const lastSync = bank && connector.lastSyncAt ? `${esc(t('interopSnapshot'))}: ${esc(fmtDate(connector.lastSyncAt, true))}` : ''; const latency = Number.isFinite(connector.latencyMs) ? `${esc(t('interopLatency'))}: ${connector.latencyMs} ms` : ''; return `
${icon(bank ? 'bank' : 'exchange')}
${esc(connector.name)}${esc(detail)}
${statusBadge(connector)}
${esc(t(bank ? 'interopPermissions' : 'interopProtocol'))}${esc(scopes)}${latency}${lastSync}
${esc(bank ? t('interopReadOnlyValue') : t('interopNoOrders'))}
${toggle}${refresh}
`; }; const tabs = `
`; const headerActions = `${button('interop-export', t('exportJson'), '', 'download')}${button('interop-reset', t('interopReset'), '', 'close')}`; const heading = pageHeading('interoperability', 'interopSub', `${headerActions}${esc(t('localOnly'))}`); const boundary = `
!${esc(t('interopBoundary'))}
`; const systemSwitcher = renderInteropSystemSwitcher(); if (interopTab === 'ideas') { const ideaCards = INTEROP_IDEAS.map((idea, index) => `
${String(index + 1).padStart(2, '0')}
${esc(categoryLabel(idea.category || 'banking'))}

${esc(copy(idea, 'hr', 'en'))}

${esc(copy(idea, 'detailHr', 'detailEn'))}

${esc(t('demoOnly'))}
`).join(''); const ideaView = panel(t('interopIdeas'), `
${INTEROP_IDEAS.length}
${esc(t('interopAdditionalIdeas'))}

${esc(t('interopIdeasIntro'))}

${ideaCards}
`, { description: `${INTEROP_IDEAS.length} · ${t('localOnly')}` }); return `${heading}${tabs}${boundary}${systemSwitcher}${ideaView}`; } const bankCards = systemBanks.map(connector => connectorCard(connector, true)).join(''); const venueCards = systemVenues.map(connector => connectorCard(connector)).join(''); const categoryCounts = Object.fromEntries(categoryKeys.map(category => [category, INTEROP_FEATURES.filter(feature => feature.category === category).length])); const categoryTabs = `
${categoryKeys.map(category => ``).join('')}
`; const visibleFeatures = INTEROP_FEATURES.filter(feature => interopCategory === 'all' || feature.category === interopCategory); const features = visibleFeatures.map(feature => { const idea = ideasById.get(feature.idea); const index = INTEROP_FEATURES.indexOf(feature); const isKillSwitch = feature.id === 'kill-switch'; const actionName = isKillSwitch && lab.killSwitch ? t('interopRestore') : t('interopRun'); const actionIcon = isKillSwitch && lab.killSwitch ? 'arrow' : 'spark'; return `
${String(index + 1).padStart(2, '0')}${esc(categoryLabel(feature.category))}

${esc(copy(feature, 'hr', 'en'))}

${esc(copy(feature, 'descriptionHr', 'descriptionEn'))}

${button('interop-feature-run', actionName, 'small primary', actionIcon, `data-id="${esc(feature.id)}" ${isKillSwitch && lab.killSwitch ? 'aria-pressed="true"' : ''}`)}
`; }).join(''); const latestResult = latest ? `
${esc(t('interopResult'))}

${esc(latestFeature ? copy(latestFeature, 'hr', 'en') : latest.featureName || latest.featureId || t('interopWorkbench'))}

${esc(latestSummary || '')}

${esc(t('interopJsonEvidence'))}
${esc(JSON.stringify(latest.evidence || {}, null, 2))}
` : `
${icon('spark')}

${esc(t('interopNoActivity'))}

`; const scopedRuns = lab.runs.filter(run => run.systemId === system.id); const activityRows = scopedRuns.slice(0, 8).map(run => { const feature = INTEROP_FEATURES.find(item => item.id === run.featureId); const label = feature ? copy(feature, 'hr', 'en') : run.featureName || run.featureId || (language === 'hr' ? 'Lokalna aktivnost konektora' : 'Local connector activity'); return `
${icon(run.featureId === 'kill-switch' ? 'shield' : 'clock')}
${esc(label)}${esc(language === 'hr' ? run.summaryHr || run.summaryEn || '' : run.summaryEn || run.summaryHr || '')}
`; }).join(''); const activityPanel = panel(t('interopActivity'), activityRows ? `
${activityRows}
` : `

${esc(t('interopNoActivity'))}

`, { description: t('interopExportHint') }); const killSwitchState = lab.killSwitch ? `
!${esc(t('interopKillSwitch'))}: ${esc(t('interopPaused'))}. ${enabledBanks + enabledVenues} ${esc(t('interopEnabledCount'))}.
` : ''; return `${heading}${tabs}${boundary}${systemSwitcher}${killSwitchState}
${esc(t('interopConnectorCount'))}${systemBanks.length + systemVenues.length}${esc(system.name)}
${esc(t('interopBanks'))}${enabledBanks} / ${systemBanks.length}${esc(t('interopReadOnlyValue'))}
${esc(t('interopVenues'))}${enabledVenues} / ${systemVenues.length}${esc(t('interopNoOrders'))}
${esc(t('interopReadScopes'))}${readScopes}${esc(t('interopMostRecent'))}: ${latest ? esc(fmtDate(latest.at, true)) : '—'}
${panel(t('interopBanks'), `
${bankCards || `
${esc(t('interopNoActivity'))}
`}
`, { description: t('interopBankPanelSub') })}
${panel(t('interopVenues'), `
${venueCards || `
${esc(t('interopNoActivity'))}
`}
`, { description: t('interopVenuePanelSub') })}
${panel(t('interopResult'), latestResult, { description: t('interopRunSuccess') })}
${activityPanel}
${panel(t('interopFunctions'), `${categoryTabs}
${features}
`, { description: `${visibleFeatures.length} / ${INTEROP_FEATURES.length} · ${t('interopWorkbench')}` })}`; } function renderSettings() { const langOptions = supportedLanguages.map(language => ``).join(''); const activeRole = currentStaffRole(); const roleRows = STAFF_LEVELS.map(role => `L${role.level}${esc(t(role.label))}${role.id === activeRole.id ? esc(t('active')) : '—'}${esc(t(role.scope))}`); const accessPanel = panel(t('roleCapabilities'), `
i${esc(t('roleAccessHelp'))}
${tableShell([t('roleLevel'), t('operator'), t('roleCapabilities')], roleRows)}`, { description: '7 simulated banking role levels' }); const auditRows = state.auditEvents.slice(0, 7).map(event => `
${icon(event.severity === 'warning' ? 'shield' : 'clock')}
${esc(event.action)}${esc(event.actor)} · ${esc(event.detail)}
`).join(''); const settingsPanel = panel(t('settings'), `
${esc(t('languageSelect') || 'Interface language')}HR · EN · DE · FR · ES · AR · SW · JA · ZH
${esc(t('operator'))}${esc(state.settings.operator)} · L${activeRole.level} · ${esc(t(activeRole.label))}
${esc(t('localOnlyTitle'))}${esc(t('localOnlyBody'))}
LOCAL
${esc(t('syntheticData'))}${state.customers.length} customers · ${state.accounts.length} accounts · ${state.transactions.length} movements
DEMO
${esc(t('isBalanced') || 'Double-entry checks')}${esc(t('ledgerSub'))}
${isBalanced(state) ? 'PASS' : 'CHECK'}
`, { description: t('settingsSub') }); const desktopControls = Boolean(window.adriaDemo?.setBackgroundPreferences); const intervalOptions = [1, 5, 15, 60].map(minutes => ``).join(''); const backgroundActive = state.settings.backgroundSimulationEnabled !== false; const lastBackground = state.settings.backgroundLastTickAt ? fmtDate(state.settings.backgroundLastTickAt, true) : t('backgroundNotRunYet'); const backgroundPanel = panel(t('backgroundSimulationTitle'), `
i${esc(t('backgroundSimulationDescription'))}
${esc(t('status'))}${esc(t(backgroundActive ? 'backgroundActive' : 'backgroundPaused'))}
${esc(t('backgroundRunCount'))}${state.meta.backgroundSequence || 0}
${esc(t('backgroundLastTick'))}${esc(lastBackground)}
${esc(t('backgroundInterval'))}${esc(t('backgroundSafety'))}
${button('toggle-background-simulation', t(backgroundActive ? 'pauseBackground' : 'startBackground'), backgroundActive ? '' : 'primary')}
${desktopControls ? '' : `
${esc(t('desktopBackgroundOnly'))}
`}${state.settings.backgroundLastError ? `
!${esc(state.settings.backgroundLastError)}
` : ''}`, { description: t('backgroundSimulationTitle') }); const sources = `
${regulatorySources.map(source => `
${esc(source.title)}${esc(source.tag)}Open ↗
`).join('')}
`; return `${pageHeading('settings', 'settingsSub', `${button('download-csv', t('downloadCsv'), '', 'download')}${button('export-json', t('exportJson'), '', 'download')}${button('reset-demo', t('resetDemo'), 'danger', 'close')}`)}
${settingsPanel}${backgroundPanel}${accessPanel}${panel(t('compliance'), `
i${esc(t('complianceBody'))}
${sources}
Snapshot dateReviewed 10 Oct 2026 · not legal advice
`, { description: t('regulatorySources') || 'Selected primary sources' })}
${panel(t('history'), `
${auditRows}
`, { description: `${state.auditEvents.length} local events` })}

${esc(t('resetDemo'))}

Reset restores the original fictional scenario and clears local browser changes.

!The application does not implement production authentication, encryption/key management, immutable audit, bank settlement, access governance, DR, regulatory reporting or privacy controls.
`; } function renderOnboarding() { const applications = state.onboardingApplications || []; const filter = filterState.onboarding; const query = filter.q.toLocaleLowerCase(locale()); const filtered = applications.filter(application => { const customer = customerById(application.customerId); return (filter.status === 'all' || application.status === filter.status) && (!query || `${application.id} ${customer?.id || application.customerId} ${application.countryCode} ${application.customerKind}`.toLocaleLowerCase(locale()).includes(query)); }).sort((a, b) => new Date(b.submittedAt) - new Date(a.submittedAt)); const page = paged(filtered, 'onboarding'); const rows = page.items.map(application => { const customer = customerById(application.customerId); const productLabels = application.products.map(product => t(({ personal_account: 'onboardingProductPersonal', multi_currency: 'onboardingProductMultiCurrency', virtual_debit: 'onboardingProductVirtualDebit', prepaid_virtual: 'onboardingProductPrepaid', business_account: 'onboardingProductBusiness' })[product] || product)).join(' · '); const reviewAction = ``; let statusActions = `${esc(t('noAction'))}`; if (canReviewCompliance() && application.status === 'submitted_demo') { statusActions = `
`; } else if (canReviewCompliance() && application.status === 'needs_info_demo') { statusActions = `
`; } return `${esc(application.id)}${esc(fmtDate(application.submittedAt, true))}${customer ? formatClient(customer) : esc(application.customerId)}${esc(application.customerKind === 'business' ? t('business') : t('personal'))} · ${esc(application.countryCode)}${esc(productLabels)}${esc(t(application.reviewPath === 'enhanced_review_demo' ? 'onboardingEnhancedPath' : 'onboardingStandardPath'))}${esc(t(application.identityScenario))} · ${esc(t(application.screeningScenario))}${badge(application.status)}${reviewAction} ${statusActions}`; }); const pending = applications.filter(application => ['submitted_demo', 'needs_info_demo'].includes(application.status)).length; const reviewed = applications.filter(application => application.status === 'reviewed_demo').length; const enhanced = applications.filter(application => application.reviewPath === 'enhanced_review_demo').length; const statusOptions = ['submitted_demo', 'needs_info_demo', 'reviewed_demo', 'closed_demo']; return `${pageHeading('onlineOnboarding', 'onlineOnboardingSub', `${esc(t('demoOnly'))}`)}
${esc(t('demoApplications'))}${applications.length}${esc(t('allSynthetic'))}
${esc(t('pending'))}${pending}${esc(t('localQueue'))}
${esc(t('completedReviews'))}${reviewed}${esc(t('trainingOnly'))}
${esc(t('enhancedReviewDemo'))}${enhanced}${esc(t('noExternalScreening'))}
!${esc(t('onboardingBoundary'))}
${esc(t('localQueue'))}
${tableShell([t('applicationCode'), t('client'), t('requestedProducts'), t('reviewPath'), t('status'), t('actions')], rows, page.footer, t('noOnboardingApplications'))}
${panel(t('onboardingReviewGuide'), `
1 · ${esc(t('onboardingGuideRead'))}

${esc(t('onboardingGuideOne'))}

2 · ${esc(t('onboardingGuideReview'))}

${esc(t('onboardingGuideTwo'))}

3 · ${esc(t('onboardingGuideRecord'))}

${esc(t('onboardingGuideThree'))}

`, { description: t('trainingOnly') })}`; } function renderView() { updateStaticText(); renderNavigation(); const renderers = { overview: renderOverview, portal: renderPortal, onboarding: renderOnboarding, customers: renderCustomers, accounts: renderAccounts, payments: renderPayments, cards: renderCards, savings: renderSavings, loans: renderLoans, branches: renderBranches, atms: renderAtms, aml: renderAml, complaints: renderComplaints, swift: renderSwift, exchange: renderExchange, interop: renderInteroperability, institutions: renderInstitutions, checker: renderChecker, support: renderSupport, ledger: renderLedger, reports: renderReports, settings: renderSettings }; main.innerHTML = `${renderWorkspaceContextBar()}${(renderers[currentView] || renderOverview)()}`; const titleKey = ({ branches: 'branchesCash', atms: 'atmNetwork', aml: 'amlKyc', complaints: 'complaints', swift: 'swiftLab', portal: 'customerPortal', onboarding: 'onlineOnboarding', institutions: 'institutions', interop: 'interoperability', checker: 'localChecks', support: 'callCentre' })[currentView] || currentView; document.title = `${t(titleKey)} · Adria Bank Demo`; updateBackgroundRuntimeView(); document.body.classList.toggle('exchange-page', currentView === 'exchange'); document.body.classList.toggle('interop-page', currentView === 'interop'); document.body.classList.toggle('client-surface', document.body.dataset.surface === 'client'); $('#appShell').classList.remove('sidebar-open'); } function navigate(view, pushHash = true) { const allowedViews = visibleNavGroups().flatMap(group => group.items.map(([key]) => key)); if (!allowedViews.includes(view)) view = document.body.dataset.surface === 'client' ? 'portal' : 'overview'; if (view !== 'exchange') exchangeCustomerId = null; currentView = view; if (pushHash && location.hash !== `#${view}`) history.replaceState(null, '', `#${view}`); renderView(); main.focus({ preventScroll: true }); } function persist(render = true) { try { saveDemoState(state); } catch (error) { toast(`Local storage could not be updated: ${error.message}`, 'error'); } if (render) renderView(); } function updateBackgroundIndicator() { const indicator = $('#backgroundLoopIndicator'); if (!indicator) return; const active = state.settings.backgroundSimulationEnabled !== false; const interval = state.settings.backgroundIntervalMinutes || 5; indicator.textContent = `${t(active ? 'backgroundActive' : 'backgroundPaused')} · ${interval} min`; indicator.classList.toggle('paused', !active); indicator.title = `${t('backgroundSimulationDescription')} ${state.settings.backgroundLastTickAt ? `${t('backgroundLastTick')}: ${fmtDate(state.settings.backgroundLastTickAt, true)}` : t('backgroundNotRunYet')}`; } function updateBackgroundRuntimeView() { updateBackgroundIndicator(); const count = $('#backgroundRunCountValue'); if (count) count.textContent = String(state.meta.backgroundSequence || 0); const last = $('#backgroundLastTickValue'); if (last) last.textContent = state.settings.backgroundLastTickAt ? fmtDate(state.settings.backgroundLastTickAt, true) : t('backgroundNotRunYet'); } function executeBackgroundTick(payload = {}) { if (state.settings.backgroundSimulationEnabled === false) return; try { const result = runBackgroundSimulationTick(state, { now: payload.scheduledAt ? new Date(payload.scheduledAt) : new Date() }); if (!result.updated) return; delete state.settings.backgroundLastError; persist(false); updateBackgroundRuntimeView(); if (!modalRoot.childElementCount) renderView(); } catch (error) { state.settings.backgroundLastError = `${new Date().toISOString()} · ${String(error?.message || error).slice(0, 240)}`; persist(false); updateBackgroundRuntimeView(); console.error('Local synthetic background cycle failed:', error); } } async function syncBackgroundRuntime() { const preferences = { enabled: state.settings.backgroundSimulationEnabled !== false, intervalMinutes: state.settings.backgroundIntervalMinutes || 5, keepRunningInTray: Boolean(state.settings.keepRunningInTray), startWithWindows: Boolean(state.settings.startWithWindows) }; if (window.adriaDemo?.setBackgroundPreferences) { clearInterval(browserSimulationTimer); browserSimulationTimer = null; if (!backgroundTickUnsubscribe && window.adriaDemo.onBackgroundTick) { backgroundTickUnsubscribe = window.adriaDemo.onBackgroundTick(executeBackgroundTick); } try { const result = await window.adriaDemo.setBackgroundPreferences(preferences); backgroundDesktopSupported = Boolean(result?.supported); if (!backgroundDesktopSupported && state.settings.startWithWindows) { state.settings.startWithWindows = false; persist(false); } } catch (error) { backgroundDesktopSupported = false; console.warn('Desktop background preferences could not be applied:', error); } } else { backgroundDesktopSupported = false; if (browserSimulationTimer) clearInterval(browserSimulationTimer); browserSimulationTimer = preferences.enabled ? setInterval(() => executeBackgroundTick({ source: 'browser-background-loop' }), preferences.intervalMinutes * 60 * 1000) : null; } updateBackgroundRuntimeView(); } function toast(message, type = 'success') { const node = document.createElement('div'); node.className = `toast ${type === 'error' ? 'error' : type === 'warning' ? 'warning' : ''}`; node.innerHTML = `${type === 'error' ? '!' : type === 'warning' ? '⚠' : '✓'}${esc(message)}`; toastRegion.append(node); setTimeout(() => node.remove(), 4200); } function closeModal() { modalRoot.innerHTML = ''; pendingConfirmation = null; } function formField(labelKey, control, help = '', full = false) { return `
${control}${help ? `${esc(help)}` : ''}
`; } function makeSelect(name, options, placeholder, value = '') { return ``; } const DEMO_COUNTRIES = [ ['HR', 'Croatia'], ['SI', 'Slovenia'], ['AT', 'Austria'], ['DE', 'Germany'], ['FR', 'France'], ['ES', 'Spain'], ['IT', 'Italy'], ['GB', 'United Kingdom'], ['CH', 'Switzerland'], ['US', 'United States'], ['KE', 'Kenya'], ['AE', 'United Arab Emirates'], ['JP', 'Japan'], ['SG', 'Singapore'] ]; function demoCountrySelect(name = 'country', selected = 'HR') { return ``; } function modalShell(title, subtitle, formType, body, wide = false, submitLabel = 'create') { const actions = formType === 'onboarding' ? `` : ``; return ``; } function openModal(type, data = {}) { const accounts = activeEurAccounts().filter(account => !data.customerId || account.customerId === data.customerId); let title = '', subtitle = '', body = '', wide = false; switch (type) { case 'customer': { title = t('newCustomer'); subtitle = t('clientOnboardingNotice'); body = `
${formField('customerName', '', t('customerKindHelp'))}${formField('customerType', ``)}${formField('city', '')}${formField('country', '')}${formField('branchSelect', makeSelect('branchId', state.branches.map(branch => ({ value: branch.id, label: branch.name })), t('branchSelect')), '', true)}${formField('email', '', 'Use an .invalid address only.', true)}
${esc(t('clientOnboardingNotice'))}
`; break; } case 'account': { title = t('newAccount'); subtitle = t('accountHelp'); const customers = state.customers.slice().sort((a,b) => a.name.localeCompare(b.name, locale())).map(customer => ({ value: customer.id, label: `${customer.name} · ${customer.id} · ${labelStatus(customer.kyc)}` })); const accountCurrencyOptions = ['EUR', ...accountCurrencies()].map(code => ``).join(''); body = `
${formField('chooseCustomer', makeSelect('customerId', customers, t('chooseCustomer'), data.customerId), t('customerOnboardingNotice') || t('clientOnboardingNotice'), true)}${formField('accountCurrency', ``)}${formField('accountDetails', '')}
${esc(t('accountCurrencyNotice'))}
`; break; } case 'transfer': { title = t('internalTransfer'); subtitle = t('approvalNotice'); wide = true; const transferAccounts = state.accounts.filter(account => account.status === 'active' && (!data.customerId || account.customerId === data.customerId)); const options = transferAccounts.map(account => ({ value: account.id, label: accountLabel(account) })); body = `
${formField('sourceAccount', makeSelect('fromAccountId', options, t('chooseAccount'), options[0]?.value))}${formField('destinationAccount', makeSelect('toAccountId', options, t('chooseAccount'), options[1]?.value))}${formField('amountEur', '')}${formField('paymentReference', '')}
Same-currency only. Select accounts with the same currency. Demo ledger posts immediately to both fictional customer histories; no external network is involved.
`; break; } case 'account-conversion': { title = t('convertAccounts'); subtitle = t('indicativeRate'); wide = true; const eligibleAccounts = state.accounts.filter(account => account.status === 'active' && customerById(account.customerId)?.kyc === 'verified' && (!data.customerId || account.customerId === data.customerId) && !CURRENCIES[account.currency]?.crypto); const options = eligibleAccounts.map(account => ({ value: account.id, label: accountLabel(account) })); const context = data.customerId ? customerById(data.customerId)?.name || data.customerId : t('all'); const sourceLabel = `${fxReferenceData.ecb.source} · ${fxReferenceData.ecb.rateDate}; ${fxReferenceData.cbk.source} · ${fxReferenceData.cbk.rateDate}`; body = `
${formField('sourceAccount', makeSelect('fromAccountId', options, t('chooseAccount'), options[0]?.value))}${formField('destinationAccount', makeSelect('toAccountId', options, t('chooseAccount'), options[1]?.value))}${formField('amountEur', '', t('amountInputSell'))}
${esc(t('conversionRate'))}—
${esc(t('indicativeSpread'))}0.35%
${esc(t('indicativeOutput'))}—
${esc(t('fxConversionDisclosure'))}
${esc(sourceLabel)}
${esc(t('notLivePrice'))}
`; break; } case 'interbank-transfer': { title = t('interbankTransfer'); subtitle = t('interbankDemoDisclosure'); wide = true; const transferAccounts = state.accounts.filter(account => account.status === 'active' && customerById(account.customerId)?.kyc === 'verified' && (!data.customerId || account.customerId === data.customerId)); const accountOptions = transferAccounts.map(account => ({ value: account.id, label: accountLabel(account) })); const destinations = (state.interbankAccounts || []).filter(account => account.status === 'active').map(account => { const bank = state.demoBanks?.find(item => item.id === account.bankId); return { value: account.id, label: `${bank?.name || account.bankId} · ${account.accountNumber} · ${account.currency}` }; }); body = `
${formField('sourceAccount', makeSelect('fromAccountId', accountOptions, t('chooseAccount'), accountOptions[0]?.value), '', true)}${formField('destinationAccount', makeSelect('toAccountId', destinations, t('chooseDemoBank')), '', true)}${formField('amountEur', '')}${formField('paymentReference', '')}${formField('purpose', '', '', true)}
${esc(t('interbankDemoDisclosure'))} Beneficiary bank/account must use the same currency as the source. Requires synthetic verified KYC and local checker approval.
`; break; } case 'support-ticket': { title = t('newSupportTicket'); subtitle = t('supportDemoDisclosure'); wide = true; const customers = state.customers.slice().sort((a, b) => a.name.localeCompare(b.name, locale())).map(customer => ({ value: customer.id, label: `${customer.name} · ${customer.id}` })); body = `
${formField('chooseCustomer', makeSelect('customerId', customers, t('chooseCustomer'), data.customerId || portalCustomerId), '', true)}${formField('supportCategory', '')}${formField('subject', '', '', true)}${formField('message', '', t('noSensitiveData'), true)}
${esc(t('supportDemoDisclosure'))}
`; break; } case 'kyc-request': { const customer = customerById(data.customerId || portalCustomerId); if (!customer) return; title = t('startDemoKyc'); subtitle = t('kycFlowDisclosure'); wide = true; const sourceOptions = [['salary', 'kycSourceSalary'], ['business_revenue', 'kycSourceBusiness'], ['savings', 'kycSourceSavings'], ['pension', 'kycSourcePension'], ['investment', 'kycSourceInvestment'], ['other_synthetic', 'kycSourceOther']].map(([value, label]) => ``).join(''); const purposeOptions = [['daily_personal', 'kycPurposeDaily'], ['salary_receipts', 'kycPurposeSalary'], ['business_operations', 'kycPurposeBusiness'], ['savings', 'kycPurposeSavings'], ['cross_border_practice', 'kycPurposeCrossBorder'], ['other_synthetic', 'kycPurposeOther']].map(([value, label]) => ``).join(''); const docItems = [['synthetic_identity_sample', 'syntheticIdentitySample'], ['synthetic_address_sample', 'syntheticAddressSample'], ...(customer.kind === 'business' ? [['synthetic_register_sample', 'syntheticRegisterSample'], ['synthetic_ownership_chart', 'syntheticOwnershipChart']] : [])].map(([value, label]) => ``).join(''); body = `
i${esc(t('kycFlowDisclosure'))} · ${esc(customer.name)} · ${esc(customer.kind === 'business' ? t('business') : t('personal'))}
${formField('sourceFunds', ``)}${formField('kycPurpose', ``)}${formField('expectedMonthlyBand', '')}${formField('riskScenario', ``)}
${docItems}
${customer.kind === 'business' ? `
` : ''}
${esc(t('clientOnboardingNotice'))} ${esc(t('noSensitiveData'))}
`; break; } case 'payment': { title = t('newPayment'); subtitle = t('approvalNotice'); wide = true; const options = accounts.map(account => ({ value: account.id, label: accountLabel(account) })); body = `
${formField('sourceAccount', makeSelect('accountId', options, t('chooseAccount'), options[0]?.value), '', true)}${formField('beneficiaryName', '')}${formField('country', demoCountrySelect('country', 'HR'))}${formField('demoIbanOrDomesticId', '', 'Fictional country-aware IBAN-shaped or domestic identifier; never valid for routing.')}${formField('bic', '', 'Fictional DEMO BIC only.')}${formField('amountEur', '')}${formField('paymentRail', '')}${formField('paymentReference', '')}${formField('purpose', '', '', true)}
${esc(t('transactionThresholdNotice'))} Local checker queue only: this never sends a payment or validates a beneficiary.
`; break; } case 'incoming-payment': { title = t('receivePayment'); subtitle = t('incomingPaymentDisclosure'); wide = true; const options = accounts.filter(account => customerById(account.customerId)?.kyc === 'verified').map(account => ({ value: account.id, label: accountLabel(account) })); body = `
${formField('destinationAccount', makeSelect('accountId', options, t('chooseAccount'), options[0]?.value), '', true)}${formField('sender', '')}${formField('country', demoCountrySelect('country', 'HR'))}${formField('demoIbanOrDomesticId', '', 'Fictional country-aware IBAN-shaped or domestic identifier; never valid for routing.')}${formField('senderBic', '', 'Fictional DEMO BIC only.')}${formField('amountEur', '')}${formField('paymentReference', '')}${formField('paymentRail', '')}${formField('purpose', '', '', true)}
A local incoming-credit rehearsal is queued for checker review. It is not money received over SEPA or another network.
`; break; } case 'cash': { title = t('cashOperation'); subtitle = t('branchCashNote'); const accountOptions = accounts.map(account => ({ value: account.id, label: accountLabel(account) })); const branches = state.branches.map(branch => ({ value: branch.id, label: branch.name })); body = `
${formField('operation', '')}${formField('branchSelect', makeSelect('branchId', branches, t('branchSelect')))}${formField('chooseAccount', makeSelect('accountId', accountOptions, t('chooseAccount')), '', true)}${formField('amountEur', '')}
Teller, dual-cash count, customer ID checks, counterfeit detection and physical cash are not present in this simulation.
`; break; } case 'card': { title = t('newCard'); subtitle = t('clientOnboardingNotice'); const choices = accounts.filter(account => customerById(account.customerId)?.kyc === 'verified').map(account => ({ value: account.id, label: accountLabel(account) })); body = `
${formField('chooseAccount', makeSelect('accountId', choices, t('chooseAccount')), '', true)}${formField('productCard', ``, '', true)}
${esc(t('cardWorkflowBoundary'))}
`; break; } case 'card-controls': { const card = state.cards.find(item => item.id === data.cardId); if (!card || !['active', 'locked'].includes(card.status) || (data.customerId && card.customerId !== data.customerId)) return; title = t('cardControls'); subtitle = `${card.product} · ${t('cardTokenSuffix')} ${card.last4} · ${t('localSimulation')}`; const controls = card.controls || {}; body = `
${formField('cardDailyLimit', ``)}${formField('cardMonthlyLimit', ``)}
${esc(t('cardWorkflowBoundary'))}
`; wide = true; break; } case 'card-load': { const card = state.cards.find(item => item.id === data.cardId); if (!card || card.fundingType !== 'prepaid' || card.status !== 'active' || (data.customerId && card.customerId !== data.customerId)) return; title = t('cardLoad'); subtitle = `${card.product} · ${t('prepaidBalance')}: ${currency(card.prepaidBalanceCents || 0)} · ${t('localSimulation')}`; body = `
${formField('amountEur', '', t('prepaidLoadHelp'), true)}
${esc(t('cardWorkflowBoundary'))}
`; wide = true; break; } case 'card-purchase': { const scopedCustomerId = data.customerId || (document.body.dataset.surface === 'client' || currentView === 'portal' ? portalCustomerId : ''); const eligibleCards = state.cards.filter(card => card.status === 'active' && (!scopedCustomerId || card.customerId === scopedCustomerId)); if (data.cardId && !eligibleCards.some(card => card.id === data.cardId)) return; title = t('simulateCardPurchase'); subtitle = t('localSimulation'); wide = true; const cardOptions = eligibleCards.map(card => ({ value: card.id, label: `${card.product} · ${card.id} · DEMO ${card.last4}` })); const selectedCardId = data.cardId || cardOptions[0]?.value || ''; body = `
${formField('chooseCard', makeSelect('cardId', cardOptions, t('chooseCard'), selectedCardId), '', true)}${formField('amountEur', '')}${formField('cardMerchant', ``)}${formField('cardChannel', ``)}
${esc(t('cardWorkflowBoundary'))}
`; break; } case 'deposit': { title = t('newDeposit'); subtitle = t('savingsSub'); const choices = accounts.filter(account => customerById(account.customerId)?.kyc === 'verified').map(account => ({ value: account.id, label: accountLabel(account) })); body = `
${formField('chooseAccount', makeSelect('accountId', choices, t('chooseAccount')), '', true)}${formField('amountEur', '')}${formField('termMonths', '')}${formField('rate', '')}
Rates are fictional and not an offer. Deposit-guarantee membership or protection is not represented.
`; break; } case 'loan': { title = t('newLoan'); subtitle = t('loansSub'); const choices = accounts.filter(account => account.currency === 'EUR' && account.status === 'active' && customerById(account.customerId)?.kyc === 'verified' && customerById(account.customerId)?.risk !== 'high').map(account => ({ value: account.id, label: accountLabel(account) })); body = `
${formField('chooseAccount', makeSelect('accountId', choices, t('chooseAccount')), '', true)}${formField('loanProduct', ``)}${formField('amountEur', '', t('loanAmountHelp'))}${formField('termMonths', '')}${formField('loanPurpose', '', '', true)}
${esc(t('loanDisclosure'))}
`; break; } case 'loan-repayment': { const loan = state.loans.find(item => item.id === data.loanId); if (!loan || !['performing', 'arrears'].includes(loan.status)) return; title = t('repayLoan'); subtitle = `${loan.id} · ${currency(loan.outstandingCents)} ${t('outstanding').toLocaleLowerCase(locale())}`; const account = accountById(loan.accountId); const interest = Math.round(loan.outstandingCents * loan.annualRateBps / 10000 / 12); const principalPart = Math.min(loan.outstandingCents, Math.max(100, Math.round(loan.outstandingCents / loan.termMonths))); const suggested = ((principalPart + interest) / 100).toFixed(2); body = `
${formField('chooseAccount', ``, '', true)}
${esc(t('repaymentHelp'))}
${esc(t('loanDisclosure'))}
`; break; } case 'atm': { title = t('simulateWithdrawal'); subtitle = t('atmSub'); const online = state.atms.filter(atm => atm.status === 'online').map(atm => ({ value: atm.id, label: `${atm.id} · ${atm.name} · ${currency(atm.cashCents)}` })); const choices = accounts.map(account => ({ value: account.id, label: accountLabel(account) })); body = `
${formField('machine', makeSelect('atmId', online, t('machine'), data.atmId || ''))}${formField('chooseAccount', makeSelect('accountId', choices, t('chooseAccount')))}${formField('amountEur', '', 'Simulated demo limit applies.', true)}
No physical cash is dispensed. Do not connect this mock-up to a real ATM.
`; break; } case 'swift': { title = t('newSwift'); subtitle = t('swiftDisclaimer'); wide = true; const choices = accounts.map(account => ({ value: account.id, label: accountLabel(account) })); body = `
${formField('sourceAccount', makeSelect('accountId', choices, t('chooseAccount')), '', true)}${formField('beneficiaryName', '')}${formField('country', demoCountrySelect('country', 'GB'))}${formField('bank', '')}${formField('bic', '', 'Generated DEMO BIC must match the selected beneficiary country.')}${formField('demoIbanOrDomesticId', '', 'Fictional country-aware IBAN-shaped or domestic identifier; never valid for routing.')}${formField('amountEur', '')}${formField('purpose', '', '', true)}
MT103-format draft only. Screening, correspondent validation, sanctions, nostro reconciliation, SWIFT connectivity and transmission are not available.
`; break; } case 'crypto-transfer': { title = t('cryptoSend'); subtitle = t('digitalAssetNotice'); wide = true; const cryptoOwnerScope = data.customerId || (document.body.dataset.surface === 'client' || currentView === 'portal' ? portalCustomerId : ''); const owners = state.customers.filter(customer => customer.kyc === 'verified' && (!cryptoOwnerScope || customer.id === cryptoOwnerScope) && state.wallets.some(wallet => wallet.customerId === customer.id && wallet.units > 0)); const customerChoices = owners.map(customer => ({ value: customer.id, label: `${customer.name} · ${customer.id}` })); const walletOwnerId = data.customerId || owners[0]?.id; const assetChoices = digitalAssets().map(code => ({ value: code, label: `${code} · ${CURRENCIES[code].name}` })); body = `
${formField('chooseCustomer', makeSelect('customerId', customerChoices, t('chooseCustomer'), walletOwnerId), '', true)}${formField('asset', makeSelect('asset', assetChoices, t('chooseAsset')), '')}${formField('amount', '')}${formField('walletAddress', '', 'DEMO-WALLET address only. Real chain addresses are rejected.', true)}
Held for a simulated checker approval. No wallet provider, key, custody service or blockchain is connected.
`; break; } case 'crypto-receipt': { title = t('cryptoReceive'); subtitle = t('digitalAssetNotice'); wide = true; const cryptoOwnerScope = data.customerId || (document.body.dataset.surface === 'client' || currentView === 'portal' ? portalCustomerId : ''); const owners = state.customers.filter(customer => customer.kyc === 'verified' && (!cryptoOwnerScope || customer.id === cryptoOwnerScope)); const customerChoices = owners.map(customer => ({ value: customer.id, label: `${customer.name} · ${customer.id}` })); const assetChoices = digitalAssets().map(code => ({ value: code, label: `${code} · ${CURRENCIES[code].name}` })); body = `
${formField('chooseCustomer', makeSelect('customerId', customerChoices, t('chooseCustomer'), data.customerId || owners[0]?.id), '', true)}${formField('asset', makeSelect('asset', assetChoices, t('chooseAsset')), '')}${formField('amount', '')}${formField('walletAddress', '', 'DEMO-WALLET source only. No real address is accepted.', true)}
Fictional incoming-wallet rehearsal; checker review required. No token is received from a chain.
`; break; } case 'complaint': { title = t('newComplaint'); subtitle = t('complaintDemoDisclosure'); wide = true; const fixedCustomerId = data.customerId || (document.body.dataset.surface === 'client' || currentView === 'portal' ? portalCustomerId : ''); const customers = state.customers.filter(customer => !fixedCustomerId || customer.id === fixedCustomerId).map(customer => ({ value: customer.id, label: `${customer.name} · ${customer.id} · ${labelStatus(customer.kyc)}` })); body = `
${formField('chooseCustomer', makeSelect('customerId', customers, t('chooseCustomer'), fixedCustomerId), '', true)}${formField('complaintCategory', '')}${formField('subject', '', '', true)}${formField('priority', '')}${formField('message', '', t('noSensitiveData'), true)}
${esc(t('complaintDemoDisclosure'))}
`; break; } case 'onboarding': { const customer = customerById(data.customerId || portalCustomerId); if (!customer) return; title = t('startDemoOnboarding'); subtitle = t('onboardingFlowIntro'); wide = true; const marketOptions = (state.plannedMarkets || []).map(market => ({ value: market.countryCode, label: `${market.countryCode} · ${market.country}` })); const defaultMarket = marketOptions.some(option => option.value === customer.country) ? customer.country : 'HR'; const typeOptions = [{ value: customer.kind, label: t(customer.kind) }]; const productOptions = [ ['personal_account', 'onboardingProductPersonal'], ['multi_currency', 'onboardingProductMultiCurrency'], ['virtual_debit', 'onboardingProductVirtualDebit'], ['prepaid_virtual', 'onboardingProductPrepaid'], ['business_account', 'onboardingProductBusiness'] ].filter(([value]) => customer.kind === 'business' || value !== 'business_account'); const requiredProduct = customer.kind === 'business' ? 'business_account' : 'personal_account'; const productFields = productOptions.map(([value, key]) => ``).join(''); body = `
01${esc(t('onboardingStageProfile'))}02${esc(t('onboardingStageChecks'))}03${esc(t('onboardingStageReview'))}
${formField('customerType', makeSelect('customerKind', typeOptions, '', customer.kind))}${formField('country', makeSelect('countryCode', marketOptions, '', defaultMarket), t('onboardingMarketNote'))}
${productFields}
`; break; } case 'onboarding-review': { const application = (state.onboardingApplications || []).find(item => item.id === data.applicationId); if (!application) return; const customer = customerById(application.customerId); const products = application.products.map(product => t(({ personal_account: 'onboardingProductPersonal', multi_currency: 'onboardingProductMultiCurrency', virtual_debit: 'onboardingProductVirtualDebit', prepaid_virtual: 'onboardingProductPrepaid', business_account: 'onboardingProductBusiness' })[product] || product)).join(' · '); const history = (application.history || []).map(item => `
${esc(labelStatus(item.status))} · ${esc(item.actor)}${esc(item.note)}
${esc(fmtDate(item.at, true))}
`).join(''); title = `${t('applicationCode')} · ${application.id}`; subtitle = t('onboardingBoundary'); body = `
${esc(t('customer'))}${esc(customer?.id || application.customerId)} · ${esc(t(application.customerKind))}
${esc(t('country'))}${esc(application.countryCode)} · ${esc(t('status'))}: ${esc(labelStatus(application.status))}
${esc(t('requestedProducts'))}${esc(products)}
${esc(t('reviewPath'))}${esc(t(application.reviewPath === 'enhanced_review_demo' ? 'onboardingEnhancedPath' : 'onboardingStandardPath'))}
${esc(t('identityScenario'))}${esc(t(application.identityScenario))}
${esc(t('screeningScenario'))}${esc(t(application.screeningScenario))}
!${esc(t('onboardingBoundary'))}
${history || emptyState(t('noRecords'))}
`; modalRoot.innerHTML = ``; return; } case 'confirm': { title = data.title || t('operationTitle'); subtitle = data.subtitle || t('localSimulation'); body = `
${data.tone === 'danger' ? '!' : 'i'}${esc(data.message || '')}
`; modalRoot.innerHTML = ``; pendingConfirmation = data.onConfirm; return; } default: return; } const submitLabel = type === 'card-controls' ? 'save' : type === 'card-load' ? 'cardLoad' : type === 'card-purchase' ? 'simulateCardPurchase' : type === 'onboarding' ? 'submitDemoApplication' : 'create'; modalRoot.innerHTML = modalShell(title, subtitle, type, body, wide, submitLabel); if (type === 'account-conversion') updateAccountConversionQuote(); if (['payment', 'incoming-payment', 'swift'].includes(type)) updateDemoRoutingDisplay($('form', modalRoot)); const first = $('input:not([type="hidden"]):not([type="checkbox"]),select', modalRoot); if (first && type !== 'confirm') setTimeout(() => first.focus(), 30); } function openDrawer(title, subtitle, body) { modalRoot.innerHTML = `
`; } function openCustomer(customerId) { const customer = customerById(customerId); if (!customer) return; const accounts = customerAccounts(customerId); const recent = state.transactions.filter(tx => tx.customerId === customerId).slice(0, 8); const customerLoans = (state.loans || []).filter(loan => loan.customerId === customerId); const amlCases = state.cases.filter(item => item.customerId === customerId && item.status !== 'closed'); const kycAction = customer.kyc === 'verified' || !canReviewCompliance() ? '' : ``; const body = `
${esc(initials(customer.name))}
${esc(customer.name)}${esc(customer.id)} · ${esc(customer.kind === 'business' ? t('business') : t('personal'))} · ${esc(customer.city)}, ${esc(customer.country)}
${kycAction}
${esc(t('kyc'))}${badge(customer.kyc)}
${esc(t('riskProfile'))}${badge(customer.risk)}
${esc(t('segment'))}${esc(customer.segment)}
${esc(t('sourceFunds'))}${esc(customer.sourceOfFunds)}
${esc(t('expectedActivity'))}${esc(currency(customer.expectedMonthlyCents))}
${esc(t('branch'))}${esc(state.branches.find(item => item.id === customer.branchId)?.name || '—')}
${esc(t('owner'))}${esc(customer.kind === 'business' ? customer.beneficialOwner || t('noData') : t('noData'))}
${esc(t('date'))}${esc(fmtDate(customer.joinedAt))}
${esc(t('openCases'))}${amlCases.length}
${accounts.length ? accounts.map(account => ``).join('') : emptyState(t('noAccounts'))}
${customerLoans.length ? customerLoans.map(loan => ``).join('') : emptyState(t('noRecords'))}
${recent.length ? `
${recent.map(tx => `
${icon(tx.direction === 'in' ? 'arrow' : 'payments')}
${esc(txLabel(tx))}${esc(tx.counterparty)} · ${esc(fmtDate(tx.date, true))}
${esc(amountWithDirection(tx))}${esc(labelStatus(tx.status))}
`).join('')}
` : emptyState(t('noTransactions'))}
i${esc(t('clientOnboardingNotice'))}
`; openDrawer(t('customerRecord'), `${customer.id} · ${customer.email}`, body); } function openKycDetails(customerId) { const customer = customerById(customerId); if (!customer) return; const request = customer.kycRequest; const selectedKeys = { synthetic_identity_sample: 'syntheticIdentitySample', synthetic_address_sample: 'syntheticAddressSample', synthetic_register_sample: 'syntheticRegisterSample', synthetic_ownership_chart: 'syntheticOwnershipChart' }; const documents = request?.mockDocuments?.length ? request.mockDocuments.map(key => `
${esc(t(selectedKeys[key] || 'mockDocuments'))}${esc(t('syntheticData'))} · no uploaded file${badge('simulated')}
`).join('') : `
i${esc(t('noKycRequest'))}
`; const actions = canReviewCompliance() ? `` : `
i${esc(t('roleReadOnly'))}
`; const body = `
!${esc(t('kycFlowDisclosure'))}
${esc(t('client'))}${esc(customer.name)} · ${esc(customer.id)}
${esc(t('kyc'))}${badge(customer.kyc)}
${esc(t('riskProfile'))}${badge(customer.risk)}
${esc(t('sourceFunds'))}${esc(request?.sourceOfFunds || customer.sourceOfFunds || '—')}
${esc(t('kycPurpose'))}${esc(request?.purpose || '—')}
${esc(t('expectedMonthlyBand'))}${esc(request?.expectedMonthlyBand || '—')}
${esc(t('riskScenario'))}${esc(t(request?.syntheticRiskScenario === 'enhanced_review_demo' ? 'enhancedReviewDemo' : 'standardDemo'))}
${esc(t('date'))}${esc(request?.submittedAt ? fmtDate(request.submittedAt, true) : '—')}
${esc(t('ownershipReview'))}${request?.ownershipSectionReviewed ? esc(t('simulated')) : '—'}
${documents}${actions}
!Changing a demo KYC label does not identify a real person, validate an owner or screen a sanctions/PEP list.
`; openDrawer(t('kycReview'), `${customer.id} · ${customer.kind === 'business' ? t('business') : t('personal')} · ${t('localSimulation')}`, body); } function openComplaint(complaintId) { const complaint = (state.complaints || []).find(item => item.id === complaintId); if (!complaint) return; const customer = customerById(complaint.customerId); const customerOnly = document.body.dataset.surface === 'client'; if (customerOnly && complaint.customerId !== portalCustomerId) { toast(t('roleReadOnly'), 'warning'); return; } const history = (complaint.history || []).map(entry => `
${esc(t(entry.status) || labelStatus(entry.status))}${esc(fmtDate(entry.at, true))} · ${esc(entry.note || '')}
`).join(''); const reviewer = !customerOnly && currentView === 'complaints' && canReviewCompliance(); const response = complaint.customerVisibleResponse ? `
i${esc(t('complaintResponse'))}: ${esc(complaint.customerVisibleResponse)}
` : ''; const controls = reviewer ? `
${[['investigating','investigating'],['response_ready','responseReady'],['resolved','resolved'],['escalated','escalated'],['reopened','reopened']].map(([status,key]) => ``).join('')}
` : ''; const body = `
!${esc(t('complaintDemoDisclosure'))}
${esc(t('reference'))}${esc(complaint.id)}
${esc(t('client'))}${esc(customer?.name || complaint.customerId)}
${esc(t('complaintCategory'))}${esc(complaint.category)}
${esc(t('status'))}${badge(complaint.status)}
${esc(t('priority'))}${esc(t(complaint.priority === 'high_demo' ? 'high' : 'standardDemo'))}
${esc(t('complaintTarget'))}${esc(fmtDate(complaint.practiceTargetAt))} · illustrative

${esc(complaint.message)}

i${esc(complaint.acknowledgement || t('complaintLocalOnly'))}
${response}
${history}
${controls}`; openDrawer(`${complaint.id} · ${t('complaints')}`, `${customer?.name || complaint.customerId} · ${t('localSimulation')}`, body); } function openCase(caseId) { const item = state.cases.find(entry => entry.id === caseId); if (!item) return; const customer = customerById(item.customerId); const tx = state.transactions.find(entry => entry.id === item.transactionId); const caseActions = canReviewCompliance() ? `` : `
i${esc(t('roleReadOnly'))}
`; const body = `
!${esc(item.note)}
${esc(t('priority'))}${badge(item.severity)}
${esc(t('score'))}${item.score} / 100
${esc(t('status'))}${badge(item.status)}
${esc(t('assigned'))}${esc(item.assignedTo || '—')}
${esc(t('client'))}${esc(customer?.name || item.customerId)}
${esc(t('transaction'))}${esc(tx ? `${tx.id} · ${currency(tx.amountCents)}` : '—')}

Rule: ${esc(item.ruleCode)} · opened ${esc(fmtDate(item.createdAt, true))}

${caseActions}
!Demo alert only. No real transaction is blocked, reported to the Croatian FIU, or screened against a live list.
`; openDrawer(item.id, `${item.title} · ${customer?.name || item.customerId}`, body); } function openSwiftDetails(id) { const item = state.swiftMessages.find(message => message.id === id); if (!item) return; const body = `
!${esc(t('swiftDisclaimer'))}
${esc(t('type'))}${esc(item.type)}
${esc(t('status'))}${badge(item.status)}
${esc(t('beneficiary'))}${esc(item.beneficiary)}
${esc(t('bic'))}${esc(item.bic || '—')}
${esc(t('amount'))}${esc(currency(item.amountCents, item.currency))}
${esc(t('reference'))}${esc(item.reference)}
${esc(item.mt103 || `:20:${item.id}\n:23B:CRED\n:32A:DEMO\n:59:${item.beneficiary}\n:57A:${item.bic || 'TESTBICXXX'}`)}
`; openDrawer(item.id, t('swiftDisclaimer'), body); } function openJournalDetails(id) { const journal = state.ledgerJournals.find(item => item.id === id); if (!journal) return; const lines = journal.lines.map(line => `${esc(line.accountCode)}${esc(state.glAccounts.find(account => account.code === line.accountCode)?.name || '')}${esc(line.debitCents ? currency(line.debitCents) : '—')}${esc(line.creditCents ? currency(line.creditCents) : '—')}`); openDrawer(journal.id, journal.description, `
${esc(t('date'))}${esc(fmtDate(journal.at, true))}
${esc(t('reference'))}${esc(journal.ref || '—')}
${esc(t('debit'))} / ${esc(t('credit'))}${esc(currency(journal.debitCents))} = ${esc(currency(journal.creditCents))}
${tableShell([t('code'), t('account'), t('debit'), t('credit')], lines)}
${esc(t('cleared'))}Double-entry check: ${journal.debitCents === journal.creditCents ? 'PASS' : 'FAIL'}
`); } function openNotifications() { const openCases = state.cases.filter(item => item.status !== 'closed').length; const approvals = state.payments.filter(item => item.status === 'awaiting_approval').length + state.swiftMessages.filter(item => item.status === 'awaiting_approval').length + (state.walletTransfers || []).filter(item => item.status === 'awaiting_approval').length; const body = `
${openCases} synthetic AML cases need reviewIllustrative scenario alerts · no real screening
${approvals} instructions in checker queuePayment / SWIFT drafts · offline only
Local demo environment runningAll data stored in this browser profile
OK
`; openDrawer(t('notifications') === 'notifications' ? 'Notifications' : t('notifications'), t('localSimulation'), body); } function toggleRole() { const index = STAFF_LEVELS.findIndex(role => role.id === state.settings.role); const role = STAFF_LEVELS[(index + 1 + STAFF_LEVELS.length) % STAFF_LEVELS.length]; state.settings.role = role.id; state.settings.operator = role.operator; addAudit(state, 'Demo staff level switched', `Level ${role.level} · ${t(role.label)}; local-only simulation`); persist(); toast(`${t('roleLevel')} ${role.level}: ${t(role.label)}`, 'success'); } function downloadBlob(filename, content, type) { const blob = new Blob([content], { type }); const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = filename; document.body.append(anchor); anchor.click(); anchor.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000); } function exportJson() { const snapshot = { ...state, exportedAt: new Date().toISOString(), demoOnly: true, documentBrand: DEMO_BRAND, disclaimer: 'Fictional local training data. Not a live bank, payment, identity, SWIFT, card, crypto or compliance system.' }; downloadBlob('adria-bank-demo-scenario.json', JSON.stringify(snapshot, null, 2), 'application/json'); toast(t('exportReady')); } function exportInteropLabJson() { const lab = ensureInteropLabState(state); const snapshot = { documentType: 'synthetic_interoperability_lab_export', documentBrand: DEMO_BRAND, generatedAt: new Date().toISOString(), demoOnly: true, disclaimer: t('interopBoundary'), systems: INTEROP_SYSTEMS, activeSystemId: lab.activeSystemId, bankConnectors: lab.bankConnectors, venues: lab.venues, runs: lab.runs, alerts: lab.alerts, disputes: lab.disputes, demoSessions: lab.demoSessions, killSwitch: lab.killSwitch, containsCustomerData: false, containsExternalMarketData: false, containsCredentials: false, containsRealIdentifiers: false }; downloadBlob('adria-bank-demo-interoperability-lab.json', JSON.stringify(snapshot, null, 2), 'application/json'); toast(t('exportReady')); } function exportCsv() { downloadBlob('adria-bank-demo-transactions.csv', '\ufeff' + exportAsCsv(state), 'text/csv;charset=utf-8'); toast(t('exportReady')); } function csvCell(value) { let text = String(value ?? ''); if (/^[=+@\-\t\r]/.test(text)) text = `'${text}`; return `\"${text.replaceAll('\"', '\"\"')}\"`; } function exportStatementCsv(customerId = portalCustomerId) { const customer = customerById(customerId); if (!customer) return; const accounts = customerAccounts(customerId); const transactions = state.transactions.filter(tx => tx.customerId === customerId).sort((a, b) => new Date(b.date) - new Date(a.date)); const rows = [ ['document_type', 'fictional_demo_account_statement'], ['document_brand', DEMO_BRAND], ['demo_only', 'true'], ['customer_id', customer.id], ['customer_name', customer.name], ['customer_kind', customer.kind], ['created_at', new Date().toISOString()], [], ['account_id', 'account_label', 'country_code', 'demo_iban_or_domestic_id', 'demo_bic', 'currency', 'native_balance', 'routing_ready'], ...accounts.map(account => [account.id, account.label, account.countryCode || customer.country, account.iban || account.accountNumber, account.bic || createDemoBic(account.countryCode || customer.country), account.currency, (account.balanceCents / 100).toFixed(2), 'false']), [], ['transaction_id', 'date', 'account_id', 'type', 'direction', 'amount', 'currency', 'counterparty', 'reference', 'status', 'demo_only', 'document_brand'], ...transactions.map(tx => [tx.id, tx.date, tx.accountId, tx.type, tx.direction, (tx.amountCents / 100).toFixed(2), tx.currency, tx.counterparty, tx.reference, tx.status, 'true', DEMO_BRAND]) ]; downloadBlob(`adria-bank-demo-statement-${customer.id}.csv`, '\ufeff' + rows.map(row => row.map(csvCell).join(',')).join('\r\n'), 'text/csv;charset=utf-8'); toast(t('exportReady')); } function printCurrentPage() { const titleKey = ({ branches: 'branchesCash', atms: 'atmNetwork', aml: 'amlKyc', complaints: 'complaints', swift: 'swiftLab', portal: 'customerPortal', onboarding: 'onlineOnboarding', institutions: 'institutions', interop: 'interoperability', checker: 'localChecks', support: 'callCentre' })[currentView] || currentView; const clone = main.cloneNode(true); clone.querySelectorAll('.heading-actions, .panel-actions, .toolbar, .table-pager, button, input, select, textarea, form, .assistant-form, .toast').forEach(node => node.remove()); openPrintDocument(t(titleKey), clone.innerHTML); } function handleSearchPopover(query) { if (!currentSearchPopover) { currentSearchPopover = document.createElement('div'); currentSearchPopover.className = 'command-popover'; currentSearchPopover.id = 'commandPopover'; document.body.append(currentSearchPopover); } const value = query.trim().toLocaleLowerCase(locale()); if (!value) { currentSearchPopover.innerHTML = `
${esc(t('search'))} · ${esc(t('customers'))}, ${esc(t('accounts'))}, ${esc(t('transactionHistory'))}
${icon('spark')}${esc(t('safeLearning'))}${esc(t('syntheticData'))}
`; return; } const results = []; state.customers.filter(customer => `${customer.id} ${customer.name} ${customer.city}`.toLocaleLowerCase(locale()).includes(value)).slice(0, 5).forEach(customer => results.push({ kind: 'customer', icon: 'users', title: customer.name, sub: `${customer.id} · ${customer.city}`, id: customer.id })); state.accounts.filter(account => `${account.id} ${account.iban}`.toLocaleLowerCase(locale()).includes(value)).slice(0, 4).forEach(account => results.push({ kind: 'account', icon: 'account', title: account.id, sub: `${customerById(account.customerId)?.name || ''} · ${currency(account.balanceCents)}`, id: account.customerId })); state.transactions.filter(tx => `${tx.id} ${tx.reference} ${tx.counterparty} ${txLabel(tx)}`.toLocaleLowerCase(locale()).includes(value)).slice(0, 4).forEach(tx => results.push({ kind: 'transaction', icon: 'payments', title: txLabel(tx), sub: `${tx.id} · ${currency(tx.amountCents)} · ${fmtDate(tx.date)}`, id: tx.customerId })); currentSearchPopover.innerHTML = `
${results.length} ${esc(t('results'))} · ${esc(t('demoOnly'))}
${results.length ? results.map(result => ``).join('') : `
${esc(t('noRecords'))}
`}`; } function closeSearchPopover() { currentSearchPopover?.remove(); currentSearchPopover = null; } function changeOnboardingStep(offset) { const form = modalRoot.querySelector('form[data-form="onboarding"]'); if (!form) return false; const pages = [...form.querySelectorAll('[data-onboarding-step]')]; const currentStep = Number(form.dataset.activeStep) || 1; const nextStep = Math.max(1, Math.min(pages.length, currentStep + offset)); if (nextStep > currentStep) { const currentPage = pages.find(page => Number(page.dataset.onboardingStep) === currentStep); const invalid = [...(currentPage?.querySelectorAll('input, select, textarea') || [])].find(field => field.required && !field.checkValidity()); if (invalid) { invalid.reportValidity?.(); invalid.focus(); return false; } } form.dataset.activeStep = String(nextStep); pages.forEach(page => { const active = Number(page.dataset.onboardingStep) === nextStep; page.hidden = !active; page.setAttribute('aria-hidden', String(!active)); }); form.querySelectorAll('[data-onboarding-progress]').forEach(step => { const number = Number(step.dataset.onboardingProgress); step.classList.toggle('active', number === nextStep); step.classList.toggle('complete', number < nextStep); if (number === nextStep) step.setAttribute('aria-current', 'step'); else step.removeAttribute('aria-current'); }); const back = form.querySelector('[data-action="onboarding-back"]'); const next = form.querySelector('[data-action="onboarding-next"]'); const submit = form.querySelector('[data-action="onboarding-submit"]'); if (back) back.hidden = nextStep === 1; if (next) next.hidden = nextStep === pages.length; if (submit) submit.hidden = nextStep !== pages.length; form.querySelector(`[data-onboarding-step="${nextStep}"] input:not([type="hidden"]):not([type="checkbox"]), [data-onboarding-step="${nextStep}"] select`)?.focus(); return true; } function submitForm(form, event) { event.preventDefault(); if (form.dataset.form === 'onboarding' && (Number(form.dataset.activeStep) || 1) < 3) { changeOnboardingStep(1); return; } const formType = form.dataset.form; if (formType === 'onboarding' && !form.checkValidity()) { const invalid = [...form.querySelectorAll('input, select, textarea')].find(field => field.required && !field.checkValidity()); invalid?.reportValidity?.(); invalid?.focus(); return; } const readOnlySafeForms = ['assistant-staff', 'assistant-customer', 'reference-check']; if (document.body.dataset.surface !== 'client' && currentView !== 'portal' && !canWrite() && !readOnlySafeForms.includes(formType)) { toast(t('roleReadOnly'), 'warning'); return; } const formData = new FormData(form); const get = key => formData.get(key); try { switch (form.dataset.form) { case 'onboarding': { const application = submitDemoOnboardingApplication(state, { customerId: get('customerId'), customerKind: get('customerKind'), countryCode: get('countryCode'), products: formData.getAll('products'), identityScenario: get('identityScenario'), screeningScenario: get('screeningScenario'), syntheticConfirmed: formData.get('syntheticConfirmed') === 'on', noRealDataConfirmed: formData.get('noRealDataConfirmed') === 'on' }); closeModal(); persist(); toast(`${t('onboardingSubmitted')} · ${application.id}`); break; } case 'customer': createCustomer(state, { name: get('name'), kind: get('kind'), city: get('city'), country: get('country'), branchId: get('branchId'), email: get('email'), syntheticConfirmed: formData.get('syntheticConfirmed') === 'on' }); closeModal(); persist(); toast(t('customerCreated')); break; case 'account': openAccount(state, get('customerId'), get('currency')); closeModal(); persist(); toast(t('accountOpened')); break; case 'transfer': createInternalTransfer(state, { fromAccountId: get('fromAccountId'), toAccountId: get('toAccountId'), amountCents: moneyToCents(get('amount')), reference: get('reference') }); closeModal(); persist(); toast(t('transferSaved')); break; case 'account-conversion': { const details = conversionReferenceDetails(getAccount(state, get('fromAccountId'))?.currency, getAccount(state, get('toAccountId'))?.currency); convertBetweenAccounts(state, { fromAccountId: get('fromAccountId'), toAccountId: get('toAccountId'), amountCents: moneyToCents(get('amount')), referenceDate: details.referenceDate, source: details.source }); closeModal(); persist(); toast(t('fxConversionComplete')); break; } case 'interbank-transfer': { const transfer = createInterbankTransfer(state, { fromAccountId: get('fromAccountId'), toAccountId: get('toAccountId'), amountCents: moneyToCents(get('amount')), reference: get('reference'), purpose: get('purpose') }); closeModal(); persist(); toast(`${t('interbankQueued')} · ${transfer.id}`, 'warning'); break; } case 'support-ticket': { const ticket = createSupportTicket(state, { customerId: get('customerId'), category: get('category'), subject: get('subject'), message: get('message'), syntheticConfirmed: formData.get('syntheticConfirmed') === 'on' }); closeModal(); persist(); toast(`${t('ticketCreated')} · ${ticket.id}`); break; } case 'assistant-staff': case 'assistant-customer': { const audience = form.dataset.form.endsWith('staff') ? 'staff' : 'customer'; const customer = audience === 'customer' ? customerById(portalCustomerId) : null; const question = String(get('question') || '').trim(); if (!question) break; const messages = assistantChat(audience, customer?.id || ''); messages.push({ role: 'user', text: question }); const context = { rateDate: fxReferenceData.ecb.rateDate, accountSummary: customer ? customerAccounts(customer.id).filter(account => account.status === 'active').map(account => `${account.currency} ${currency(account.balanceCents, account.currency)}`).join(' · ') : '' }; messages.push({ role: 'assistant', text: localAssistantReply({ question, audience, context, language: state.settings.language }) }); if (messages.length > 12) messages.splice(0, messages.length - 12); renderView(); break; } case 'reference-check': { referenceCheckResult = verifyDemoReference(state, { type: get('type'), value: get('value') }); renderView(); break; } case 'payment': createExternalPayment(state, { accountId: get('accountId'), beneficiary: get('beneficiary'), country: get('country'), bic: get('bic'), iban: get('iban'), amountCents: moneyToCents(get('amount')), rail: get('rail'), reference: get('reference'), notes: get('notes') }); closeModal(); persist(); toast(t('paymentQueued'), 'warning'); break; case 'incoming-payment': createIncomingPayment(state, { accountId: get('accountId'), sender: get('sender'), country: get('country'), amountCents: moneyToCents(get('amount')), rail: get('rail'), reference: get('reference'), notes: get('notes') }); closeModal(); persist(); toast(t('paymentQueued'), 'warning'); break; case 'crypto-transfer': { const transfer = createCryptoTransfer(state, { customerId: get('customerId'), asset: get('asset'), units: get('units'), destination: get('destination') }); closeModal(); persist(); toast(`${t('cryptoQueued')} · ${transfer.id}`, 'warning'); break; } case 'crypto-receipt': { const transfer = createCryptoReceipt(state, { customerId: get('customerId'), asset: get('asset'), units: get('units'), sender: get('sender') }); closeModal(); persist(); toast(`${t('cryptoQueued')} · ${transfer.id}`, 'warning'); break; } case 'complaint': { const complaint = createComplaint(state, { customerId: get('customerId'), category: get('category'), subject: get('subject'), message: get('message'), priority: get('priority'), syntheticConfirmed: formData.get('syntheticConfirmed') === 'on' }); closeModal(); persist(); toast(`${t('complaintCreated')} · ${complaint.id}`); break; } case 'kyc-request': { submitDemoKycRequest(state, get('customerId'), formData.get('syntheticConfirmed') === 'on', { sourceOfFunds: get('sourceOfFunds'), purpose: get('purpose'), expectedMonthlyBand: get('expectedMonthlyBand'), mockDocuments: formData.getAll('mockDocuments'), ownershipSectionReviewed: formData.get('ownershipSectionReviewed') === 'on', syntheticRiskScenario: get('riskScenario') }); closeModal(); persist(); toast(t('kycRequestSaved'), 'warning'); break; } case 'cash': recordCashOperation(state, { operation: get('operation'), branchId: get('branchId'), accountId: get('accountId'), amountCents: moneyToCents(get('amount')) }); closeModal(); persist(); toast(t('cashComplete')); break; case 'card': issueCard(state, { accountId: get('accountId'), product: get('product') }); closeModal(); persist(); toast(t('cardCreated')); break; case 'card-controls': setCardControls(state, get('cardId'), { dailyLimitCents: moneyToCents(get('dailyLimit')), monthlyLimitCents: moneyToCents(get('monthlyLimit')), onlineEnabled: formData.get('onlineEnabled') === 'on', internationalEnabled: formData.get('internationalEnabled') === 'on', recurringEnabled: formData.get('recurringEnabled') === 'on' }); closeModal(); persist(); toast(t('cardControlsSaved')); break; case 'card-load': loadPrepaidCard(state, get('cardId'), moneyToCents(get('amount'))); closeModal(); persist(); toast(t('cardLoaded')); break; case 'card-purchase': simulateCardPurchase(state, { cardId: get('cardId'), amountCents: moneyToCents(get('amount')), merchant: get('merchant'), channel: get('channel'), international: formData.get('international') === 'on' }); closeModal(); persist(); toast(t('cardPurchaseComplete')); break; case 'deposit': openSavingsDeposit(state, { accountId: get('accountId'), amountCents: moneyToCents(get('amount')), termMonths: Number(get('termMonths')) }); closeModal(); persist(); toast(t('depositOpened')); break; case 'loan': createLoanApplication(state, { accountId: get('accountId'), amountCents: moneyToCents(get('amount')), termMonths: Number(get('termMonths')), product: get('product'), purpose: get('purpose') }); closeModal(); persist(); toast(t('loanApplied'), 'warning'); break; case 'loan-repayment': makeLoanRepayment(state, { loanId: get('loanId'), accountId: get('accountId'), amountCents: moneyToCents(get('amount')) }); closeModal(); persist(); toast(t('loanRepaid')); break; case 'atm': simulateAtmWithdrawal(state, { atmId: get('atmId'), accountId: get('accountId'), amountCents: moneyToCents(get('amount')) }); closeModal(); persist(); toast(t('atmComplete')); break; case 'swift': createSwiftMessage(state, { accountId: get('accountId'), beneficiary: get('beneficiary'), country: get('country'), bank: get('bank'), bic: get('bic'), iban: get('iban'), amountCents: moneyToCents(get('amount')), purpose: get('purpose') }); closeModal(); persist(); toast(t('swiftPrepared'), 'warning'); break; case 'exchange': { if (get('acknowledge') !== 'on') throw new Error(t('quoteDisclosure')); const trade = executeExchangeTrade(state, { accountId: get('accountId'), side: exchangeSide, asset: get('asset'), amount: Number(get('amount')) }); persist(); toast(`${t('tradeComplete')} · ${trade.id}`); break; } } } catch (error) { toast(error.message || t('invalid'), 'error'); } } function handleAction(actionElement, sourceEvent) { const action = actionElement.dataset.action; const id = actionElement.dataset.id; const displayedProfileId = document.getElementById('portalCustomerSelect')?.value; if (displayedProfileId && state.customers.some(customer => customer.id === displayedProfileId)) portalCustomerId = displayedProfileId; const writeActions = ['new-customer', 'new-account', 'new-account-for', 'transfer', 'new-payment', 'incoming-payment', 'cancel-payment', 'crypto-send', 'crypto-receive', 'cancel-crypto', 'new-complaint', 'cash-operation', 'issue-card', 'new-deposit', 'new-loan', 'repay-loan', 'atm-withdrawal', 'new-swift', 'mature-savings', 'card-status', 'card-controls', 'card-load', 'card-purchase', 'card-cancel', 'case-status', 'kyc-verify', 'kyc-edd', 'reconcile-atms', 'new-interbank', 'new-support-ticket', 'support-status', 'account-conversion', 'toggle-background-simulation', 'onboarding-status', 'interop-connector-toggle', 'interop-connector-refresh', 'interop-feature-run', 'interop-system-select', 'interop-reset']; const approvalActions = ['approve-loan', 'disburse-loan', 'approve-payment', 'approve-swift', 'approve-interbank', 'approve-crypto']; const complianceActions = ['case-status', 'kyc-verify', 'kyc-edd', 'complaint-status', 'onboarding-status']; if (document.body.dataset.surface !== 'client' && currentView !== 'portal' && writeActions.includes(action) && !canWrite()) { toast(t('roleReadOnly'), 'warning'); return; } if (approvalActions.includes(action) && !canApprove()) { toast(t('roleCheckerOnly'), 'warning'); return; } if (complianceActions.includes(action) && !canReviewCompliance()) { toast(t('roleComplianceOnly'), 'warning'); return; } if (action === 'reset-demo' && !canAdminister()) { toast(t('roleAdminOnly'), 'warning'); return; } try { switch (action) { case 'toggle-sidebar': $('#appShell').classList.toggle('sidebar-open'); break; case 'modal-close': closeModal(); break; case 'modal-scrim': if (sourceEvent.target === actionElement) closeModal(); break; case 'drawer-scrim': if (sourceEvent.target === actionElement) closeModal(); break; case 'portal-open-account': openModal('account', { customerId: portalCustomerId }); break; case 'portal-onboarding': openModal('onboarding', { customerId: portalCustomerId }); break; case 'onboarding-detail': openModal('onboarding-review', { applicationId: id }); break; case 'onboarding-next': changeOnboardingStep(1); break; case 'onboarding-back': changeOnboardingStep(-1); break; case 'onboarding-status': updateDemoOnboardingApplication(state, id, actionElement.dataset.status); persist(); toast(t('statusChanged')); break; case 'portal-kyc': openModal('kyc-request', { customerId: portalCustomerId }); break; case 'portal-transfer': openModal('transfer', { customerId: portalCustomerId }); break; case 'portal-convert': openModal('account-conversion', { customerId: portalCustomerId }); break; case 'portal-interbank': openModal('interbank-transfer', { customerId: portalCustomerId }); break; case 'portal-support': openModal('support-ticket', { customerId: portalCustomerId }); break; case 'portal-payment': openModal('payment', { customerId: portalCustomerId }); break; case 'portal-incoming': openModal('incoming-payment', { customerId: portalCustomerId }); break; case 'portal-crypto-send': openModal('crypto-transfer', { customerId: portalCustomerId }); break; case 'portal-crypto-receive': openModal('crypto-receipt', { customerId: portalCustomerId }); break; case 'portal-complaint': openModal('complaint', { customerId: portalCustomerId }); break; case 'new-complaint': openModal('complaint'); break; case 'new-payment': openModal('payment'); break; case 'incoming-payment': openModal('incoming-payment'); break; case 'portal-loan': openModal('loan', { customerId: portalCustomerId }); break; case 'portal-deposit': openModal('deposit', { customerId: portalCustomerId }); break; case 'portal-card': openModal('card', { customerId: portalCustomerId }); break; case 'card-controls': openModal('card-controls', { cardId: id, ...(document.body.dataset.surface === 'client' || currentView === 'portal' ? { customerId: portalCustomerId } : {}) }); break; case 'card-load': openModal('card-load', { cardId: id, ...(document.body.dataset.surface === 'client' || currentView === 'portal' ? { customerId: portalCustomerId } : {}) }); break; case 'card-purchase': openModal('card-purchase', { cardId: id || '', ...(document.body.dataset.surface === 'client' || currentView === 'portal' ? { customerId: portalCustomerId } : {}) }); break; case 'card-cancel': { const card = state.cards.find(item => item.id === id); if (!card || ((document.body.dataset.surface === 'client' || currentView === 'portal') && card.customerId !== portalCustomerId)) throw new Error(t('roleReadOnly')); openModal('confirm', { title: t('closeCard'), subtitle: t('localSimulation'), message: `${t('cardCancellationNote')} ${card.fundingType === 'prepaid' ? t('prepaidCancellationRefund') : ''}`, tone: 'danger', confirmLabel: t('closeCard'), onConfirm: () => { try { setCardStatus(state, id, 'cancelled'); closeModal(); persist(); toast(t('cardCancelled')); } catch (error) { closeModal(); toast(error.message, 'error'); } } }); break; } case 'portal-exchange': exchangeCustomerId = portalCustomerId; exchangeTab = 'fx'; navigate('exchange'); break; case 'new-customer': openModal('customer'); break; case 'new-account': openModal('account'); break; case 'new-account-for': closeModal(); openModal('account', { customerId: id }); break; case 'account-conversion': openModal('account-conversion', (document.body.dataset.surface === 'client' || currentView === 'portal') ? { customerId: portalCustomerId } : exchangeCustomerId ? { customerId: exchangeCustomerId } : {}); break; case 'new-interbank': openModal('interbank-transfer'); break; case 'new-support-ticket': openModal('support-ticket'); break; case 'crypto-send': openModal('crypto-transfer'); break; case 'crypto-receive': openModal('crypto-receipt'); break; case 'local-checker': navigate('checker'); break; case 'refresh-fx': void refreshFxReferences(true); break; case 'fetch-market-reference': void refreshOptionalMarketFeed(); break; case 'clear-check-result': referenceCheckResult = null; renderView(); break; case 'assistant-prompt': { const audience = actionElement.dataset.audience === 'staff' ? 'staff' : 'customer'; const customer = audience === 'customer' ? customerById(portalCustomerId) : null; const question = actionElement.dataset.question || ''; const messages = assistantChat(audience, customer?.id || ''); messages.push({ role: 'user', text: question }); const context = { rateDate: fxReferenceData.ecb.rateDate, accountSummary: customer ? customerAccounts(customer.id).filter(account => account.status === 'active').map(account => `${account.currency} ${currency(account.balanceCents, account.currency)}`).join(' · ') : '' }; messages.push({ role: 'assistant', text: localAssistantReply({ question, audience, context, language: state.settings.language }) }); if (messages.length > 12) messages.splice(0, messages.length - 12); renderView(); break; } case 'support-status': { const note = document.getElementById(`support-note-${id}`)?.value || ''; updateSupportTicket(state, id, actionElement.dataset.status, note); persist(); toast(t('ticketUpdated')); break; } case 'transfer': openModal('transfer'); break; case 'cash-operation': openModal('cash'); break; case 'issue-card': openModal('card'); break; case 'new-deposit': openModal('deposit'); break; case 'new-loan': openModal('loan'); break; case 'repay-loan': openModal('loan-repayment', { loanId: id }); break; case 'approve-loan': openModal('confirm', { title: t('approveLoan'), subtitle: t('checkerRole'), message: t('approveLoanConfirm'), confirmLabel: t('approveLoan'), onConfirm: () => { try { approveLoanApplication(state, id); closeModal(); persist(); toast(t('loanApproved')); } catch (error) { closeModal(); toast(error.message, 'error'); } } }); break; case 'disburse-loan': openModal('confirm', { title: t('disburseLoan'), subtitle: t('localSimulation'), message: t('disburseLoanConfirm'), confirmLabel: t('disburseLoan'), onConfirm: () => { try { disburseLoan(state, id); closeModal(); persist(); toast(t('loanDisbursed')); } catch (error) { closeModal(); toast(error.message, 'error'); } } }); break; case 'atm-withdrawal': openModal('atm', { atmId: id }); break; case 'new-swift': openModal('swift'); break; case 'interop-tab': interopTab = actionElement.dataset.tab === 'ideas' ? 'ideas' : 'workbench'; renderView(); break; case 'interop-category': interopCategory = ['all', 'banking', 'payments', 'controls', 'crypto', 'operations'].includes(actionElement.dataset.category) ? actionElement.dataset.category : 'all'; renderView(); break; case 'interop-context-open': interopTab = 'workbench'; navigate('interop'); break; case 'interop-system-select': { const system = selectInteropSystem(state, id); interopCategory = 'all'; persist(); toast(`${t('interopSystemChanged')}: ${system.name}`, 'success'); break; } case 'interop-connector-toggle': { const connector = toggleInteropConnector(state, id); persist(); toast(`${connector.name} · ${t(connector.status === 'connected_demo' ? 'interopActive' : 'interopPaused')}`, 'success'); break; } case 'interop-connector-refresh': { const connector = refreshInteropConnector(state, id); persist(); toast(`${connector.name} · ${t('interopSnapshot')}`, 'success'); break; } case 'interop-feature-run': { const run = runInteropFeature(state, id); persist(); if (['audit-export', 'audit-evidence-pack', 'portability-kit'].includes(id)) exportInteropLabJson(); toast(`${t('interopRunSuccess')}: ${state.settings.language === 'hr' ? run.summaryHr : run.summaryEn}`, 'success'); break; } case 'interop-export': exportInteropLabJson(); break; case 'interop-reset': openModal('confirm', { title: t('interopReset'), subtitle: t('localOnly'), message: t('interopResetConfirm'), tone: 'warning', confirmLabel: t('interopReset'), onConfirm: () => { resetInteropLab(state); interopTab = 'workbench'; closeModal(); persist(); toast(t('interopReset'), 'success'); } }); break; case 'reset-demo': openModal('confirm', { title: t('resetDemo'), message: t('resetConfirm'), tone: 'danger', confirmLabel: t('resetDemo'), onConfirm: () => { localStorage.removeItem(STORE_KEY); state = createDemoState(); ensureInteropLabState(state); portalCustomerId = state.accounts.find(account => account.status === 'active' && state.customers.find(customer => customer.id === account.customerId)?.kyc === 'verified')?.customerId || state.customers[0]?.id; exchangeCustomerId = null; currentView = 'overview'; closeModal(); persist(); toast(t('statusChanged')); } }); break; case 'confirm-pending': if (pendingConfirmation) pendingConfirmation(); break; case 'download-csv': exportCsv(); break; case 'export-json': exportJson(); break; case 'operator-menu': toggleRole(); break; case 'toggle-background-simulation': { state.settings.backgroundSimulationEnabled = state.settings.backgroundSimulationEnabled === false; persist(); void syncBackgroundRuntime(); toast(t(state.settings.backgroundSimulationEnabled ? 'backgroundStarted' : 'backgroundPausedToast'), state.settings.backgroundSimulationEnabled ? 'success' : 'warning'); break; } case 'notifications': openNotifications(); break; case 'customer-open': openCustomer(id); break; case 'print-current': printCurrentPage(); break; case 'print-statement': printStatement(id || portalCustomerId, actionElement.dataset.accountId || ''); break; case 'export-statement': exportStatementCsv(id || portalCustomerId); break; case 'print-transaction': printTransaction(id); break; case 'print-payment': printPayment(id); break; case 'print-swift': printSwift(id); break; case 'case-open': openCase(id); break; case 'kyc-review': openKycDetails(id); break; case 'complaint-open': openComplaint(id); break; case 'swift-open': openSwiftDetails(id); break; case 'journal-open': openJournalDetails(id); break; case 'payment-tab': paymentTab = actionElement.dataset.tab; renderView(); break; case 'aml-tab': amlTab = actionElement.dataset.tab; renderView(); break; case 'exchange-tab': exchangeTab = actionElement.dataset.tab; renderView(); break; case 'exchange-side': exchangeSide = actionElement.dataset.side; renderView(); break; case 'approve-interbank': openModal('confirm', { title: t('simulateSettlement'), subtitle: t('checkerRole'), message: t('interbankSettlementConfirm'), confirmLabel: t('simulateSettlement'), onConfirm: () => { try { approveInterbankTransfer(state, id); closeModal(); persist(); toast(t('interbankSettled')); } catch (error) { closeModal(); toast(error.message, 'error'); } } }); break; case 'approve-payment': openModal('confirm', { title: t('approve'), subtitle: t('checkerRole'), message: `${t('approvalNotice')} Payment ${id} will only change the fictional local ledger. No money is transmitted or received.`, confirmLabel: t('approve'), onConfirm: () => { try { approvePayment(state, id); closeModal(); persist(); toast(t('approvedNotice')); } catch (error) { closeModal(); toast(error.message, 'error'); } } }); break; case 'cancel-payment': { const payment = state.payments.find(item => item.id === id); if (!payment || payment.direction !== 'out' || payment.status !== 'awaiting_approval') throw new Error(t('stopBeforeApproval')); if ((document.body.dataset.surface === 'client' || currentView === 'portal') && payment.customerId !== portalCustomerId) throw new Error(t('roleReadOnly')); openModal('confirm', { title: t('stopPayment'), subtitle: t('localSimulation'), message: `${t('stopBeforeApproval')} ${payment.id} is still in the local maker/checker queue.`, tone: 'danger', confirmLabel: t('stopPayment'), onConfirm: () => { try { cancelPayment(state, id); closeModal(); persist(); toast(t('statusChanged'), 'warning'); } catch (error) { closeModal(); toast(error.message, 'error'); } } }); break; } case 'approve-crypto': { openModal('confirm', { title: t('approveCrypto'), subtitle: t('checkerRole'), message: `${t('digitalAssetNotice')} No wallet provider or blockchain is contacted.`, confirmLabel: t('approveCrypto'), onConfirm: () => { try { approveCryptoTransfer(state, id); closeModal(); persist(); toast(t('cryptoApproved')); } catch (error) { closeModal(); toast(error.message, 'error'); } } }); break; } case 'cancel-crypto': { const transfer = (state.walletTransfers || []).find(item => item.id === id); if (!transfer || transfer.status !== 'awaiting_approval') throw new Error(t('cancelCrypto')); if ((document.body.dataset.surface === 'client' || currentView === 'portal') && transfer.customerId !== portalCustomerId) throw new Error(t('roleReadOnly')); openModal('confirm', { title: t('cancelCrypto'), subtitle: t('localSimulation'), message: `${t('digitalAssetNotice')} This only releases a local reservation or removes a pending receipt.`, tone: 'danger', confirmLabel: t('cancelCrypto'), onConfirm: () => { try { cancelCryptoTransfer(state, id); closeModal(); persist(); toast(t('cryptoCancelled'), 'warning'); } catch (error) { closeModal(); toast(error.message, 'error'); } } }); break; } case 'approve-swift': openModal('confirm', { title: t('approve'), subtitle: t('checkerRole'), message: `${t('swiftDisclaimer')}. Approve the local rehearsal only?`, confirmLabel: t('approve'), onConfirm: () => { try { approveSwiftMessage(state, id); closeModal(); persist(); toast(t('approvedNotice')); } catch (error) { closeModal(); toast(error.message, 'error'); } } }); break; case 'mature-savings': openModal('confirm', { title: t('mature'), message: 'This will move the principal and indicative interest to the linked synthetic current account. No real interest is earned.', confirmLabel: t('mature'), onConfirm: () => { try { matureSavingsDeposit(state, id); closeModal(); persist(); toast(t('statusChanged')); } catch (error) { closeModal(); toast(error.message, 'error'); } } }); break; case 'card-status': { const card = state.cards.find(item => item.id === id); if (!card || ((document.body.dataset.surface === 'client' || currentView === 'portal') && card.customerId !== portalCustomerId)) throw new Error(t('roleReadOnly')); setCardStatus(state, id, actionElement.dataset.status); persist(); toast(t('statusChanged')); break; } case 'kyc-edd': openModal('confirm', { title: t('enhancedReviewDemo'), subtitle: t('localSimulation'), message: t('kycFlowDisclosure'), confirmLabel: t('enhancedReviewDemo'), onConfirm: () => { try { updateCustomerKyc(state, id, 'edd'); closeModal(); persist(); toast(t('statusChanged')); } catch (error) { closeModal(); toast(error.message, 'error'); } } }); break; case 'kyc-verify': openModal('confirm', { title: t('markVerified'), message: 'This only changes a fictional status field. It does not verify identity, beneficial ownership, PEP or sanctions.', confirmLabel: t('markVerified'), onConfirm: () => { updateCustomerKyc(state, id, 'verified'); closeModal(); persist(); toast(t('statusChanged')); } }); break; case 'case-status': { const note = $('#caseReviewNote')?.value || ''; updateCaseStatus(state, id, actionElement.dataset.status, note); closeModal(); persist(); toast(t('caseUpdated')); break; } case 'complaint-status': { const note = $('#complaintReviewNote')?.value || ''; updateComplaint(state, id, actionElement.dataset.status, note); closeModal(); persist(); toast(t('complaintUpdated')); break; } case 'reconcile-atms': addAudit(state, 'ATM balances reconciled (demo)', 'Synthetic ATM totals checked against simulated general ledger.'); persist(); toast(t('statusChanged')); break; case 'page': { const name = actionElement.dataset.list; const target = filterState[name]; if (!target) break; target.page = Math.max(1, target.page + (actionElement.dataset.dir === 'next' ? 1 : -1)); renderView(); break; } case 'search-result': closeSearchPopover(); $('#globalSearch').value = ''; if (actionElement.dataset.kind === 'customer' || actionElement.dataset.kind === 'transaction' || actionElement.dataset.kind === 'account') openCustomer(actionElement.dataset.id); break; case 'view-compliance': navigate('settings'); break; case 'modal-close': closeModal(); break; } } catch (error) { toast(error.message || t('invalid'), 'error'); } } // Main event delegation keeps the bundled UI offline and avoids remote scripts. document.addEventListener('click', event => { const viewButton = event.target.closest('[data-view]'); if (viewButton) { event.preventDefault(); navigate(viewButton.dataset.view); return; } const actionElement = event.target.closest('[data-action]'); if (actionElement) { handleAction(actionElement, event); return; } if (currentSearchPopover && !event.target.closest('#commandPopover') && !event.target.closest('#globalSearch')) closeSearchPopover(); }); document.addEventListener('submit', event => { const form = event.target.closest('form[data-form]'); if (form) submitForm(form, event); }); document.addEventListener('input', event => { const input = event.target; if (input.id === 'globalSearch') { handleSearchPopover(input.value); return; } if (input.closest('#modalRoot form[data-form="account-conversion"]')) { updateAccountConversionQuote(); return; } if (input.id === 'exchangeAmount' || input.id === 'exchangeAsset') { updateExchangeQuote(); return; } if (input.matches('[data-filter-list][data-filter-field]')) { const listName = input.dataset.filterList; const field = input.dataset.filterField; if (filterState[listName]) { filterState[listName][field] = input.value; filterState[listName].page = 1; const position = input.selectionStart; const selector = `[data-filter-list="${listName}"][data-filter-field="${field}"]`; renderView(); const next = $(selector); if (next) { next.focus(); try { next.setSelectionRange(position, position); } catch { /* select elements do not support selection */ } } } } }); document.addEventListener('change', event => { const input = event.target; if (input.name === 'type' && input.closest('form[data-form="reference-check"]')) { const valueField = input.form?.elements?.value; if (valueField) valueField.inputMode = input.value === 'pan' ? 'numeric' : 'text'; return; } if (input.matches('[data-demo-country]')) { updateDemoRoutingDisplay(input.closest('form')); return; } if (input.closest('#modalRoot form[data-form="account-conversion"]')) { updateAccountConversionQuote(); return; } if (input.id === 'backgroundInterval') { const interval = Number(input.value); if ([1, 5, 15, 60].includes(interval)) { state.settings.backgroundIntervalMinutes = interval; persist(false); void syncBackgroundRuntime(); renderView(); } return; } if (input.id === 'backgroundKeepInTray') { state.settings.keepRunningInTray = input.checked; if (!input.checked) state.settings.startWithWindows = false; persist(false); void syncBackgroundRuntime(); renderView(); return; } if (input.id === 'backgroundStartWithWindows') { state.settings.startWithWindows = input.checked && state.settings.keepRunningInTray && backgroundDesktopSupported; persist(false); void syncBackgroundRuntime(); renderView(); return; } if (input.id === 'portalCustomerSelect') { portalCustomerId = input.value; renderView(); return; } if (input.id === 'languageSelect' || input.id === 'settingsLanguage') { state.settings.language = input.value; persist(); return; } if (input.matches('[data-filter-list][data-filter-field]')) { const listName = input.dataset.filterList; const field = input.dataset.filterField; if (filterState[listName]) { filterState[listName][field] = input.value; filterState[listName].page = 1; renderView(); } } }); document.addEventListener('keydown', event => { if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k' && $('#globalSearch')) { event.preventDefault(); $('#globalSearch').focus(); $('#globalSearch').select(); } if (event.key === 'Escape') { closeSearchPopover(); if (modalRoot.childElementCount) closeModal(); } }); window.addEventListener('hashchange', () => { const view = location.hash.slice(1); if (view && view !== currentView) navigate(view, false); }); function updateAccountConversionQuote() { const box = $('#accountConversionQuote'); if (!box) return; const form = box.closest('form[data-form="account-conversion"]'); const from = state.accounts.find(account => account.id === form?.elements.fromAccountId?.value); const to = state.accounts.find(account => account.id === form?.elements.toAccountId?.value); const amount = Number(form?.elements.amount?.value || 0); if (!from || !to || !Number.isFinite(amount) || amount <= 0) { box.innerHTML = `
${esc(t('conversionRate'))}—
${esc(t('indicativeSpread'))}0.35%
${esc(t('indicativeOutput'))}—
`; return; } if (from.currency === to.currency) { box.innerHTML = `
${esc(t('conversionRate'))}${esc(t('conversionNeedDistinct'))}
${esc(t('indicativeSpread'))}0.35%
${esc(t('indicativeOutput'))}—
`; return; } const sourceMeta = CURRENCIES[from.currency]; const targetMeta = CURRENCIES[to.currency]; if (!sourceMeta || !targetMeta || sourceMeta.crypto || targetMeta.crypto || !Number.isFinite(sourceMeta.eurRate) || sourceMeta.eurRate <= 0 || !Number.isFinite(targetMeta.eurRate) || targetMeta.eurRate <= 0) { box.innerHTML = `
${esc(t('noReferenceRate'))}
`; return; } try { const amountCents = moneyToCents(amount); const sourceEurCents = Math.round(amountCents * sourceMeta.eurRate); const quantumCents = 10 ** (2 - targetMeta.decimals); const targetCents = Math.floor((sourceEurCents * (1 - FX_SPREAD) / targetMeta.eurRate) / quantumCents) * quantumCents; const rate = sourceMeta.eurRate / targetMeta.eurRate; const rateText = `1 ${from.currency} ≈ ${new Intl.NumberFormat(locale(), { maximumFractionDigits: targetMeta.decimals ? 4 : 2 }).format(rate)} ${to.currency}`; box.innerHTML = `
${esc(t('conversionRate'))}${esc(rateText)}
${esc(t('indicativeSpread'))}${(FX_SPREAD * 100).toFixed(2)}%
${esc(t('indicativeOutput'))}${esc(currency(targetCents, to.currency))}
`; } catch { /* incomplete form input is normal */ } } function updateExchangeQuote() { const quoteBox = $('#exchangeQuote'); if (!quoteBox) return; const asset = $('#exchangeAsset')?.value || (exchangeTab === 'fx' ? fiatAssets()[0] : digitalAssets()[0]); const raw = Number($('#exchangeAmount')?.value || 0); if (!raw || raw <= 0) { quoteBox.innerHTML = `
${esc(t('indicativeRate'))}${esc(t('notLivePrice'))}
${esc(t('fee'))}—
${esc(t('expectedOutput'))}—
`; return; } try { const quote = quoteExchange(asset, exchangeSide, raw); const assetRate = CURRENCIES[asset].eurRate; const quoteText = CURRENCIES[asset].crypto ? `1 ${asset} = ${currencyMajor(assetRate)} ` : `1 EUR = ${new Intl.NumberFormat(locale(), { maximumFractionDigits: asset === 'JPY' ? 2 : 4 }).format(1 / assetRate)} ${asset}`; const out = exchangeSide === 'buy' ? `${new Intl.NumberFormat(locale(), { maximumFractionDigits: CURRENCIES[asset].decimals }).format(quote.units)} ${asset}` : currencyMajor(quote.payoutEur); quoteBox.innerHTML = `
${esc(t('indicativeRate'))}${esc(quoteText)}
${esc(t('fee'))} · ${(quote.spread * 100).toFixed(2)}%${esc(currencyMajor(quote.grossEur * quote.spread))}
${esc(t('expectedOutput'))}${esc(out)}
`; } catch { /* incomplete input is expected while typing */ } } function initialize() { // If this is served as the separate exchange desk, begin directly on that module. const requested = new URLSearchParams(location.search).get('module') || location.hash.slice(1); if (requested && visibleNavGroups().some(group => group.items.some(([view]) => view === requested))) currentView = requested; updateStaticText(); renderView(); if (!localStorage.getItem(STORE_KEY) || languageFromEntry) { try { saveDemoState(state); } catch { /* storage can be disabled; session remains usable */ } } void refreshFxReferences(false); void syncBackgroundRuntime(); setInterval(() => { void refreshFxReferences(false); }, 6 * 60 * 60 * 1000); } initialize();