diff --git a/app/index.html b/app/index.html index 0d42c68..12baf70 100644 --- a/app/index.html +++ b/app/index.html @@ -80,7 +80,7 @@ textarea { resize: vertical; min-height: 48px; width: 100%; } /* Tooltip */ .tip { position: relative; cursor: help; } -.tip::after { content: attr(data-tip); position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); background: #333; color: #fff; padding: 5px 9px; border-radius: 4px; font-size: 11px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity .15s; } +.tip::after { content: attr(data-tip); position: absolute; top: calc(100% + 6px); left: 0; background: #333; color: #fff; padding: 6px 10px; border-radius: 4px; font-size: 11px; line-height: 1.45; white-space: normal; max-width: 280px; z-index: 100; opacity: 0; pointer-events: none; transition: opacity .15s; } .tip:hover::after { opacity: 1; } /* Validation summary */ @@ -173,7 +173,7 @@ function newLine() { return { id: uid(), date: state.periodFrom || '', description: '', currency: state.baseCurrency, fxRate: '1.00000', vendor: '', hasReceipt: true, receipts: [], - noReceiptExplanation: '', amount: '', account: '', + noReceiptExplanation: '', amount: '', account: '', customCurrency: false, programs: [{ program: '', percent: '', programOther: '' }] }; } @@ -198,6 +198,17 @@ function recalc() { if (ge) ge.textContent = `${state.baseCurrency} ${fmtAmt(grand)}`; } +// ========== CURRENCY HELPERS ========== +function getCurrencyName(code) { + const c = (CFG.currencies || []).find(c => c.code === code); + return c ? c.name : code; +} +function buildFxTip(code, base) { + if (!code || code === base || code.length < 3) return ''; + const fn = getCurrencyName(code), bn = getCurrencyName(base); + return `Enter the exchange rate expressed as the amount of ${fn} you pay for 1 ${bn}. E.g., XX.XX ${fn} per 1 ${bn} if your expense was in ${fn} and you submit your reimbursement form in ${bn}.`; +} + // ========== CURRENCY DROPDOWN ========== function makeCDD(currencies, value, onChange) { const wrap = el('div', {className:'cdd'}); @@ -384,9 +395,8 @@ function renderLine(ln, item) { const vendIn = el('input', {type:'text', value: ln.vendor, placeholder:'Vendor name'}); vendIn.addEventListener('input', () => { ln.vendor = vendIn.value; }); - const curDD = makeCDD(currencies, ln.currency, code => { - ln.currency = code; - fxIn.dataset.tip = `Units of ${code} per 1 ${baseCur}`; + function applyFxCurrency(code) { + fxIn.dataset.tip = buildFxTip(code, baseCur); if (code === baseCur) { ln.fxRate = '1.00000'; fxIn.value = '1.00000'; fxIn.readOnly = true; } else { @@ -397,12 +407,64 @@ function renderLine(ln, item) { } recalc(); if (progAreaEl) progAreaEl._refresh(); + } + + const currenciesWithOther = [...currencies, {code: '__OTHER__', name: 'Other (enter ISO code)'}]; + const curDD = makeCDD(currenciesWithOther, ln.customCurrency ? '__OTHER__' : ln.currency, code => { + if (code === '__OTHER__') { + ln.customCurrency = true; + ln.currency = ''; + curDD.style.display = 'none'; + otherCurWrap.style.display = 'flex'; + ln.fxRate = ''; fxIn.value = ''; fxIn.readOnly = false; fxIn.dataset.tip = ''; + recalc(); if (progAreaEl) progAreaEl._refresh(); + otherCurIn.focus(); + } else { + ln.customCurrency = false; + ln.currency = code; + applyFxCurrency(code); + } + }); + if (ln.customCurrency) curDD.style.display = 'none'; + + const otherCurIn = el('input', {type:'text', maxlength:'3', placeholder:'e.g. THB', + style:{width:'70px', textTransform:'uppercase', letterSpacing:'1px', fontFamily:'inherit'}}); + otherCurIn.className = 'tip'; + otherCurIn.dataset.tip = 'You have selected other currency. Please insert the three-letter ISO code (THB for Thai baht, USD for US dollars).'; + if (ln.customCurrency && ln.currency) otherCurIn.value = ln.currency; + otherCurIn.addEventListener('input', () => { + const val = otherCurIn.value.toUpperCase().replace(/[^A-Z]/g, '').slice(0, 3); + otherCurIn.value = val; + ln.currency = val; + if (val.length === 3) { + applyFxCurrency(val); + } else { + ln.fxRate = ''; fxIn.value = ''; fxIn.readOnly = false; fxIn.dataset.tip = ''; + recalc(); if (progAreaEl) progAreaEl._refresh(); + } }); - const fxIn = el('input', {type:'text', value: ln.fxRate, style:{width:'120px', textAlign:'right'}, placeholder:'0.00000'}); - fxIn.readOnly = ln.currency === baseCur; + const cancelOtherBtn = el('button', {type:'button', className:'btn btn-rm', style:{padding:'2px 6px', fontSize:'12px'}}, '×'); + cancelOtherBtn.addEventListener('click', () => { + ln.customCurrency = false; + ln.currency = baseCur; + otherCurWrap.style.display = 'none'; + curDD.style.display = ''; + curDD._setValue(baseCur); + applyFxCurrency(baseCur); + }); + + const otherCurWrap = el('div', {style:{gap:'4px', alignItems:'center', display: ln.customCurrency ? 'flex' : 'none'}}); + otherCurWrap.append(otherCurIn, cancelOtherBtn); + + const curWrap = el('div'); + curWrap.append(curDD, otherCurWrap); + + const fxIn = el('input', {type:'text', style:{width:'120px', textAlign:'right'}, placeholder:'0.00000'}); + fxIn.value = ln.fxRate; + fxIn.readOnly = !ln.customCurrency && ln.currency === baseCur; fxIn.className = 'tip'; - fxIn.dataset.tip = `Units of ${ln.currency || '?'} per 1 ${baseCur}`; + fxIn.dataset.tip = buildFxTip(ln.currency, baseCur); fxIn.addEventListener('input', () => { ln.fxRate = fxIn.value; const rate = parseFloat(fxIn.value); @@ -414,7 +476,7 @@ function renderLine(ln, item) { blk.appendChild(el('div', {className:'frow'}, [ el('div', {className:'fgrp'}, [el('label', null, 'Date'), dateIn]), el('div', {className:'fgrp grow'}, [el('label', null, 'Vendor'), vendIn]), - el('div', {className:'fgrp'}, [el('label', null, 'Currency'), curDD]), + el('div', {className:'fgrp'}, [el('label', null, 'Currency'), curWrap]), el('div', {className:'fgrp'}, [el('label', null, 'FX rate'), fxIn]), ]));