From 501404ed7f8c868edb49fec8dc7476892855c886 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 1 Jun 2026 17:56:18 +0000 Subject: [PATCH] Add bidirectional FX rate entry Replace the fixed-convention exchange rate label with an inline expression: [amount] per {other}, where the currency dropdown lets users pick which side of the rate to enter. Covers both "35 THB per USD" and "0.028 USD per THB" without requiring mental inversion. Formula: price_local = per * rate when local currency is in numerator, price_local = per / rate when foreign currency is in numerator. rcur is persisted to localStorage and included in the PDF note. https://claude.ai/code/session_0151QtsUhzXmgzEhSvXG2SDt --- CLAUDE.md | 2 +- app/index.html | 50 ++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8410bf2..e3dab88 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,7 +60,7 @@ Do not break these behaviours: - **`addLine()` must `return i`** — `loadLines()` depends on the returned ID. - **`saveStorage()`** is triggered by `change` events on all `[data-ls]` elements; do not remove that listener. - **Invoice number bump:** occurs on the next page load after Generate via the `inv_generated_v1` flag; do not bump on Generate itself. -- **FX rate convention:** 1 foreign unit = X local units (market-standard quote). `price_local = price_foreign * exchange_rate`. +- **FX rate convention:** User picks which currency goes in the numerator via `rcur` dropdown. If `rcur === lcy`: `price_local = per * rate`. If `rcur === fcy`: `price_local = per / rate`. ## Config structure (`config.yml`) diff --git a/app/index.html b/app/index.html index 5b4bd44..0c38434 100644 --- a/app/index.html +++ b/app/index.html @@ -636,7 +636,7 @@ function buildForm() { document.querySelectorAll("[data-ls]").forEach(el => el.addEventListener("change", saveStorage)); document.getElementById("icur").addEventListener("change", () => { Object.keys(lines).forEach(k => { - if (document.getElementById(`frate-lbl-${k}`)) updateFxLabels(+k); + if (document.getElementById(`rcur-${k}`)) updateFxLabels(+k); }); }); calcPayBy(); // compute initial pay-by from default 7-day term @@ -881,8 +881,16 @@ function toggleFx(i) {
-
Exchange rate (1 ${h(defaultFcy)} = X ${h(lcy)})
- +
Exchange rate
+
+ + + per + ${h(defaultFcy)} +
Price per item in ${h(defaultFcy)}
@@ -899,8 +907,12 @@ function toggleFx(i) { function calcFxFromPer(i) { const per = pn(document.getElementById(`fper-${i}`)?.value); const rate = pn(document.getElementById(`frate-${i}`)?.value) || 1; + const rcur = document.getElementById(`rcur-${i}`)?.value; + const lcy = document.getElementById("icur")?.value || ""; const prEl = document.getElementById(`price-${i}`); - if (prEl) prEl.value = (per * rate).toFixed(6); + if (prEl) prEl.value = rcur === lcy + ? (per * rate).toFixed(6) + : (per / rate).toFixed(6); calcLine(i); saveLines(); } @@ -1145,10 +1157,11 @@ function gatherData(renderLang) { let fxNote = null; if (isFx) { const cur = document.getElementById(`fcur-${i}`)?.value || ""; + const rcur = document.getElementById(`rcur-${i}`)?.value || ""; const rate = pn(document.getElementById(`frate-${i}`)?.value) || 1; const per = pn(document.getElementById(`fper-${i}`)?.value); const foreignTot = per * qty; // line total in foreign currency - fxNote = { cur, rate, per, foreignTot, td }; + fxNote = { cur, rcur, rate, per, foreignTot, td }; } rows.push({ qty, uomLbl, desc, price, tot, fxNote }); }); @@ -1326,8 +1339,9 @@ function buildPDF() { const descH = Math.max(0, (dLines.length - 1) * 3.8); let fxH = 0; if (row.fxNote) { + const fxOther = row.fxNote.rcur === iCur ? row.fxNote.cur : iCur; const fxStr = `${td("per-item")}: ${row.fxNote.cur} ${fmt(row.fxNote.per)} ` - + `(1 ${row.fxNote.cur} = ${(+row.fxNote.rate).toFixed(5)} ${iCur})`; + + `(${(+row.fxNote.rate).toFixed(5)} ${row.fxNote.rcur} per ${fxOther})`; const fxLines = sp(fxStr, CD + CP - 4); fxH = fxLines.length * 3.5 + 1; } @@ -1359,8 +1373,9 @@ function buildPDF() { fb(8.5); tc(17,24,39); tR(fmt(row.tot), XR-2, yt); if (row.fxNote) { + const fxOther = row.fxNote.rcur === iCur ? row.fxNote.cur : iCur; const fxStr = `${td("per-item")}: ${row.fxNote.cur} ${fmt(row.fxNote.per)} ` - + `(1 ${row.fxNote.cur} = ${(+row.fxNote.rate).toFixed(5)} ${iCur})`; + + `(${(+row.fxNote.rate).toFixed(5)} ${row.fxNote.rcur} per ${fxOther})`; fn(7); tc(107,114,128); const fxLines = sp(fxStr, CD + CP - 4); fxLines.forEach((fl, fi) => tL(fl, xD+2, y + ROW_H + descH + 3.2 + fi * 3.5)); @@ -1406,11 +1421,17 @@ function buildPDF() { // ── Update FX labels when currency or invoice currency changes ──────────────── function updateFxLabels(i) { - const fcy = document.getElementById(`fcur-${i}`)?.value || "FCY"; - const lcy = document.getElementById("icur")?.value || ""; - const rl = document.getElementById(`frate-lbl-${i}`); - const pl = document.getElementById(`fper-lbl-${i}`); - if (rl) rl.textContent = `Exchange rate (1 ${fcy} = X ${lcy})`; + const fcy = document.getElementById(`fcur-${i}`)?.value || "FCY"; + const lcy = document.getElementById("icur")?.value || ""; + const rcurEl = document.getElementById(`rcur-${i}`); + const rotherEl = document.getElementById(`rother-${i}`); + const pl = document.getElementById(`fper-lbl-${i}`); + if (rcurEl) { + const prev = rcurEl.value; + rcurEl.innerHTML = ``; + rcurEl.value = (prev === lcy || prev === fcy) ? prev : lcy; + } + if (rotherEl) rotherEl.textContent = rcurEl?.value === lcy ? fcy : lcy; if (pl) pl.textContent = `Price per item in ${fcy}`; } @@ -1430,6 +1451,7 @@ function saveLines() { price: document.getElementById(`price-${i}`)?.value || "", fx: document.getElementById(`fx-${i}`)?.value || "no", fcur: document.getElementById(`fcur-${i}`)?.value || "", + rcur: document.getElementById(`rcur-${i}`)?.value || "", frate: document.getElementById(`frate-${i}`)?.value || "", fper: document.getElementById(`fper-${i}`)?.value || "", }); @@ -1483,6 +1505,10 @@ function loadLines() { const el = document.getElementById(`fcur-${i}`); if (el) { el.value = ld.fcur; updateFxLabels(i); } } + if (ld.rcur) { + const el = document.getElementById(`rcur-${i}`); + if (el) { el.value = ld.rcur; updateFxLabels(i); } + } if (ld.frate) { const el = document.getElementById(`frate-${i}`); if (el) el.value = ld.frate; } if (ld.fper) { const el = document.getElementById(`fper-${i}`); if (el) el.value = ld.fper; } calcFxFromPer(i);