diff --git a/app/index.html b/app/index.html index cadca36..7b651c3 100644 --- a/app/index.html +++ b/app/index.html @@ -339,6 +339,7 @@ function renderItem(item) { function renderLine(ln, item) { const blk = el('div', {className:'line-blk', id:`line-${ln.id}`}); + let progAreaEl; const currencies = CFG.currencies || []; const baseCur = state.baseCurrency; @@ -362,6 +363,7 @@ function renderLine(ln, item) { else if (ln.fxRate === '1.00000') { ln.fxRate = ''; fxIn.value = ''; } } recalc(); + if (progAreaEl) progAreaEl._refresh(); }); const fxIn = el('input', {type:'text', value: ln.fxRate, style:{width:'120px'}, placeholder:'0.00000'}); @@ -373,6 +375,7 @@ function renderLine(ln, item) { const rate = parseFloat(fxIn.value); if (ln.currency && ln.currency !== baseCur && rate > 0) state.fxRateMemory[ln.currency] = fxIn.value; recalc(); + if (progAreaEl) progAreaEl._refresh(); }); blk.appendChild(el('div', {className:'frow'}, [ @@ -393,7 +396,7 @@ function renderLine(ln, item) { }); const amtIn = el('input', {type:'text', value: ln.amount, style:{width:'120px', textAlign:'right'}, placeholder:'0.00'}); - amtIn.addEventListener('input', () => { ln.amount = amtIn.value; recalc(); }); + amtIn.addEventListener('input', () => { ln.amount = amtIn.value; recalc(); if (progAreaEl) progAreaEl._refresh(); }); blk.appendChild(el('div', {className:'frow'}, [ el('div', {className:'fgrp grow'}, [el('label', null, 'Description'), descIn]), @@ -403,7 +406,8 @@ function renderLine(ln, item) { // Row 3: Account, Program const acctSel = makeSelect(CFG.accounts || [], ln.account, v => { ln.account = v; }, 'Select account'); - const progGrp = el('div', {className:'fgrp grow'}, [el('label', null, 'Program'), buildProgramArea(ln)]); + progAreaEl = buildProgramArea(ln); + const progGrp = el('div', {className:'fgrp grow'}, [el('label', null, 'Program'), progAreaEl]); blk.appendChild(el('div', {className:'frow'}, [ el('div', {className:'fgrp grow'}, [el('label', null, 'Account'), acctSel]), @@ -460,76 +464,101 @@ function buildReceiptArea(ln) { } function buildProgramArea(ln) { - const container = el('div'); + const wrapper = el('div'); const progOptions = CFG.programs || []; - const isMulti = ln.programs.length > 1; - let totalSpan = null; - function updateTotal() { - if (!totalSpan) return; - const sum = ln.programs.reduce((s, pe) => s + (parseFloat(pe.percent) || 0), 0); - totalSpan.textContent = sum.toFixed(2) + '%'; - totalSpan.style.color = sum < 99.995 ? '#c6a800' : sum <= 100.005 ? '#2e7d32' : '#c62828'; - } + function rebuild() { + while (wrapper.firstChild) wrapper.removeChild(wrapper.firstChild); + const isMulti = ln.programs.length > 1; + const amtSpans = []; + let totalSpan = null; - ln.programs.forEach((pe, pi) => { - const progSel = makeSelect(progOptions, pe.program, v => { - pe.program = v; - otherIn.style.display = v === 'Other' ? '' : 'none'; - }, 'Select program'); - progSel.style.flex = '1'; + function getBaseAmt() { + const amt = parseFloat(ln.amount) || 0; + const rate = parseFloat(ln.fxRate) || 1; + return rate > 0 ? amt / rate : 0; + } - const otherIn = el('input', {type:'text', value: pe.programOther, placeholder:'Specify program', - style:{display: pe.program === 'Other' ? '' : 'none', marginTop:'4px', width:'100%'}}); - otherIn.addEventListener('input', () => { pe.programOther = otherIn.value; }); + function updateDerived() { + const base = getBaseAmt(); + amtSpans.forEach((span, i) => { + const pct = parseFloat((ln.programs[i] || {}).percent) || 0; + span.textContent = `${state.baseCurrency} ${fmtAmt(base * pct / 100)}`; + }); + if (totalSpan) { + const sum = ln.programs.reduce((s, pe) => s + (parseFloat(pe.percent) || 0), 0); + totalSpan.textContent = sum.toFixed(2) + '%'; + totalSpan.style.color = sum < 99.995 ? '#c6a800' : sum <= 100.005 ? '#2e7d32' : '#c62828'; + } + } - if (!isMulti) { - const addBtn = el('button', {type:'button', className:'btn btn-add'}, '+ Add program'); + ln.programs.forEach((pe, pi) => { + const progSel = makeSelect(progOptions, pe.program, v => { + pe.program = v; + otherIn.style.display = v === 'Other' ? '' : 'none'; + }, 'Select program'); + progSel.style.flex = '1'; + + const otherIn = el('input', {type:'text', value: pe.programOther, placeholder:'Specify program', + style:{display: pe.program === 'Other' ? '' : 'none', marginTop:'4px', width:'100%'}}); + otherIn.addEventListener('input', () => { pe.programOther = otherIn.value; }); + + if (!isMulti) { + const addBtn = el('button', {type:'button', className:'btn btn-add'}, '+ Add program'); + addBtn.addEventListener('click', () => { + ln.programs.push({program:'', percent:'', programOther:''}); + rebuild(); + }); + const row = el('div', {style:{display:'flex', gap:'8px', alignItems:'center'}}); + row.append(progSel, addBtn); + wrapper.append(row, otherIn); + } else { + const pctIn = el('input', {type:'number', min:'0', max:'100', step:'0.01', + value: pe.percent, style:{width:'70px', textAlign:'right'}}); + pctIn.addEventListener('input', () => { pe.percent = pctIn.value; updateDerived(); }); + + const amtSpan = el('span', {style:{fontSize:'13px', color:'var(--muted)', whiteSpace:'nowrap'}}); + amtSpans.push(amtSpan); + + const rmBtn = el('button', {type:'button', className:'btn btn-rm', style:{padding:'3px 7px'}}, '×'); + rmBtn.addEventListener('click', () => { + ln.programs.splice(pi, 1); + rebuild(); + }); + + const row = el('div', {style:{display:'flex', gap:'8px', alignItems:'center', marginBottom:'4px'}}); + row.append(progSel, + el('span', {style:{fontSize:'13px', color:'var(--muted)', whiteSpace:'nowrap'}}, 'Percent:'), + pctIn, + el('span', {style:{fontSize:'13px'}}, '%'), + el('span', {style:{fontSize:'13px', color:'var(--muted)'}}, '–'), + amtSpan, + rmBtn); + wrapper.append(row, otherIn); + } + }); + + if (isMulti) { + totalSpan = el('span', {style:{fontWeight:'600', fontSize:'13px'}}); + const totalRow = el('div', + {style:{display:'flex', justifyContent:'flex-end', alignItems:'center', gap:'6px', marginTop:'4px', marginBottom:'4px'}}, + [el('span', {style:{fontSize:'13px', color:'var(--muted)'}}, 'Total percent:'), totalSpan]); + wrapper.appendChild(totalRow); + + const addBtn = el('button', {type:'button', className:'btn btn-add', style:{marginTop:'2px'}}, '+ Add program'); addBtn.addEventListener('click', () => { ln.programs.push({program:'', percent:'', programOther:''}); - container.replaceWith(buildProgramArea(ln)); + rebuild(); }); - const row = el('div', {style:{display:'flex', gap:'8px', alignItems:'center'}}); - row.append(progSel, addBtn); - container.append(row, otherIn); - } else { - const pctIn = el('input', {type:'number', min:'0', max:'100', step:'0.01', - value: pe.percent, style:{width:'70px', textAlign:'right'}}); - pctIn.addEventListener('input', () => { pe.percent = pctIn.value; updateTotal(); }); - - const rmBtn = el('button', {type:'button', className:'btn btn-rm', style:{padding:'3px 7px'}}, '×'); - rmBtn.addEventListener('click', () => { - ln.programs.splice(pi, 1); - container.replaceWith(buildProgramArea(ln)); - }); - - const row = el('div', {style:{display:'flex', gap:'8px', alignItems:'center', marginBottom:'4px'}}); - row.append(progSel, - el('span', {style:{fontSize:'13px', color:'var(--muted)', whiteSpace:'nowrap'}}, 'Percent:'), - pctIn, - el('span', {style:{fontSize:'13px'}}, '%'), - rmBtn); - container.append(row, otherIn); + wrapper.appendChild(addBtn); } - }); - if (isMulti) { - totalSpan = el('span', {style:{fontWeight:'600', fontSize:'13px'}}); - const totalRow = el('div', - {style:{display:'flex', justifyContent:'flex-end', alignItems:'center', gap:'6px', marginTop:'4px', marginBottom:'4px'}}, - [el('span', {style:{fontSize:'13px', color:'var(--muted)'}}, 'Total percent:'), totalSpan]); - container.appendChild(totalRow); - updateTotal(); - - const addBtn = el('button', {type:'button', className:'btn btn-add', style:{marginTop:'2px'}}, '+ Add program'); - addBtn.addEventListener('click', () => { - ln.programs.push({program:'', percent:'', programOther:''}); - container.replaceWith(buildProgramArea(ln)); - }); - container.appendChild(addBtn); + updateDerived(); } - return container; + wrapper._refresh = rebuild; + rebuild(); + return wrapper; } // ========== VALIDATION ========== @@ -748,13 +777,16 @@ async function generatePDF() { pg.drawText(truncate(progStr, fontBody, sz, W * 0.5 - 8), {x:M.left+W*0.5, y, size:sz, font:fontBody, color:black}); y -= lh; } else { + const lineBaseAmt = (() => { const a = parseFloat(ln.amount)||0, r = parseFloat(ln.fxRate)||1; return r>0?a/r:0; })(); progs.forEach((pe, pi) => { if (pi > 0) { needSpace(lh); } const progStr = pe.program === 'Other' ? `Other: ${pe.programOther}` : (pe.program || '–'); - const pctStr = ` (${parseFloat(pe.percent).toFixed(2)}%)`; + const pct = parseFloat(pe.percent) || 0; + const progAmt = lineBaseAmt * pct / 100; + const suffix = ` ${pct.toFixed(2)}% – ${baseCur} ${fmtAmt(progAmt)}`; pg.drawText(truncate(progStr, fontBody, sz, W * 0.45 - 8), {x:M.left+W*0.5, y, size:sz, font:fontBody, color:black}); - const pctW = fontBody.widthOfTextAtSize(pctStr, szSm); - pg.drawText(pctStr, {x:M.left+W-pctW, y, size:szSm, font:fontBody, color:gray}); + const sfxW = fontBody.widthOfTextAtSize(suffix, szSm); + pg.drawText(suffix, {x:M.left+W-sfxW, y, size:szSm, font:fontBody, color:gray}); y -= lh; }); }