mirror of
https://github.com/kbenestad/invoice.git
synced 2026-06-18 08:04:32 +00:00
Apply UX feedback: field locking, dynamic FX labels, line persistence
Charge to: - All fields locked when "Select"; fill+lock on predefined entity; unlock all fields only when "Other" is chosen Invoice lines: - QTY, UOM, Price start disabled; Description dropdown is always active - Predefined item: fills UOM (locked) and rate (editable) from config - "Other": unlocks all three fields Foreign currency: - Unit Price locked and calculated-only when FX is enabled - Re-enables Unit Price when FX is toggled back to No - Exchange rate label dynamically shows "Exchange rate (X USD = 1 EUR)" using the selected foreign currency and invoice currency - Per-item label shows "Price per item in USD" (selected FCY) - Label updates when either currency changes Invoice output: - FX note shows "Price per item (foreign currency): USD …" — the "Converted from …" prefix is removed from preview and PDF Invoice lines persistence: - Lines saved to localStorage on every change (desc, qty, UOM, price, FX toggle, FCY, rate, per-item) - Lines fully restored on page reload including FX state - "Reset invoice lines" button added to the INVOICE LINES card header
This commit is contained in:
parent
89a310a7e5
commit
ff5f4bdb19
2 changed files with 207 additions and 36 deletions
|
|
@ -510,3 +510,8 @@ translations:
|
|||
de: Bitte Referenz angeben
|
||||
fr: "Merci d'indiquer la référence"
|
||||
"no": Vennligst oppgi referanse
|
||||
reset-lines:
|
||||
en: Reset invoice lines
|
||||
de: Positionen zurücksetzen
|
||||
fr: Réinitialiser les lignes
|
||||
"no": Tilbakestill linjer
|
||||
|
|
|
|||
238
app/index.html
238
app/index.html
|
|
@ -49,6 +49,10 @@
|
|||
}
|
||||
input:focus, select:focus { border-color: var(--accent); box-shadow: 0 0 0 2px #dbeafe; }
|
||||
input[type="number"] { text-align: right; }
|
||||
input:disabled, select:disabled {
|
||||
background: #f8f9fa; color: var(--text-muted);
|
||||
border-color: var(--border-light); cursor: not-allowed;
|
||||
}
|
||||
|
||||
button { cursor: pointer; border: none; border-radius: var(--radius); font-size: 13px; }
|
||||
|
||||
|
|
@ -158,6 +162,8 @@
|
|||
.btn-remove:hover { background: #fef2f2; }
|
||||
.btn-add-line { background: none; color: var(--accent); font-size: 13px; font-weight: 500; padding: 5px 10px; border: 1px dashed var(--accent); border-radius: var(--radius); }
|
||||
.btn-add-line:hover { background: #eff6ff; }
|
||||
#btn-reset-lines { background: none; border: 1px solid var(--border); color: var(--text-muted); font-size: 11px; padding: 3px 10px; border-radius: 3px; }
|
||||
#btn-reset-lines:hover { background: #fee2e2; border-color: var(--danger); color: var(--danger); }
|
||||
|
||||
/* ── Totals ─────────────────────────────────────────────────────────────── */
|
||||
#totals-card { background: var(--white); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 14px; }
|
||||
|
|
@ -368,6 +374,7 @@ let lid = 0;
|
|||
const lines = {};
|
||||
let tlid = 0;
|
||||
const tLines = {};
|
||||
let _loading = false;
|
||||
|
||||
// ── i18n ──────────────────────────────────────────────────────────────────────
|
||||
function t(key) {
|
||||
|
|
@ -461,7 +468,7 @@ function boot() {
|
|||
buildLangBar();
|
||||
buildForm();
|
||||
restoreStorage();
|
||||
addLine();
|
||||
if (!loadLines()) addLine();
|
||||
document.getElementById("loading").style.display = "none";
|
||||
}
|
||||
|
||||
|
|
@ -570,7 +577,7 @@ function buildForm() {
|
|||
<option value="__other__">${t("other")}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="ct-fields">
|
||||
<div id="ct-fields" class="locked">
|
||||
<div class="fg"><label id="lbl-ctn" for="ctn">${t("charge-to-name")}</label>
|
||||
<input id="ctn" type="text"></div>
|
||||
<div class="fg"><label id="lbl-ca1" for="ca1">${t("charge-to-address1")}</label>
|
||||
|
|
@ -621,7 +628,10 @@ function buildForm() {
|
|||
</div>
|
||||
|
||||
<div id="lines-card">
|
||||
<div class="card-title" id="sec-lines">${t("invoice-lines")}</div>
|
||||
<div class="card-title" style="display:flex;align-items:center;justify-content:space-between">
|
||||
<span id="sec-lines">${t("invoice-lines")}</span>
|
||||
<button type="button" id="btn-reset-lines" onclick="resetLines()">${t("reset-lines")}</button>
|
||||
</div>
|
||||
<table class="line-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -675,6 +685,11 @@ function buildForm() {
|
|||
document.getElementById("paid-inp").addEventListener("input", calcTotals);
|
||||
document.getElementById("the-form").addEventListener("submit", e => { e.preventDefault(); generateInvoice(); });
|
||||
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);
|
||||
});
|
||||
});
|
||||
calcPayBy(); // compute initial pay-by from default 7-day term
|
||||
}
|
||||
|
||||
|
|
@ -686,8 +701,12 @@ function fillChargeTo(v) {
|
|||
const bsec = document.getElementById("bank-section");
|
||||
if (bsec) bsec.style.display = v === "__other__" ? "" : "none";
|
||||
|
||||
if (v === "" || v === "__other__") {
|
||||
if (v === "") ["ctn","ca1","ca2","ca3","ca4","cc","cph","cem","cvat","creg"].forEach(id => f(id, ""));
|
||||
if (v === "") {
|
||||
["ctn","ca1","ca2","ca3","ca4","cc","cph","cem","cvat","creg"].forEach(id => f(id, ""));
|
||||
fields?.classList.add("locked");
|
||||
return;
|
||||
}
|
||||
if (v === "__other__") {
|
||||
fields?.classList.remove("locked");
|
||||
return;
|
||||
}
|
||||
|
|
@ -785,13 +804,13 @@ function addLine() {
|
|||
const tr = document.createElement("tr");
|
||||
tr.className = "lr"; tr.id = `lr-${i}`;
|
||||
tr.innerHTML = `
|
||||
<td class="col-qty"><input type="number" id="qty-${i}" value="1" min="0" step="any"
|
||||
oninput="calcLine(${i})"></td>
|
||||
<td class="col-uom"><select id="uom-${i}" onchange="calcLine(${i})">${uomOpts("")}</select></td>
|
||||
<td class="col-qty"><input type="number" id="qty-${i}" value="" min="0" step="any"
|
||||
oninput="calcLine(${i});saveLines()" disabled></td>
|
||||
<td class="col-uom"><select id="uom-${i}" onchange="calcLine(${i});saveLines()" disabled>${uomOpts("")}</select></td>
|
||||
<td class="col-desc">
|
||||
<select id="dsel-${i}" onchange="pickProduct(${i})">${prodOpts("")}</select>
|
||||
<input type="text" id="dtxt-${i}" placeholder="${h(t("description"))}"
|
||||
style="margin-top:4px;display:none" oninput="calcLine(${i})">
|
||||
style="margin-top:4px;display:none" oninput="calcLine(${i});saveLines()">
|
||||
<div style="display:flex;align-items:center;gap:6px;margin-top:5px">
|
||||
<label style="font-size:11px;color:var(--text-muted);white-space:nowrap"
|
||||
id="lbl-fx-${i}">${t("foreign-currency")}:</label>
|
||||
|
|
@ -801,7 +820,7 @@ function addLine() {
|
|||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td class="col-price"><input type="number" id="price-${i}" value="0" min="0" step="any" oninput="calcLine(${i})"></td>
|
||||
<td class="col-price"><input type="number" id="price-${i}" value="" min="0" step="any" oninput="calcLine(${i});saveLines()" disabled></td>
|
||||
<td class="col-tot"><div class="line-total-val" id="ltv-${i}">0.00</div></td>
|
||||
<td class="col-act"><button type="button" class="btn-remove" onclick="removeLine(${i})">×</button></td>`;
|
||||
tbody.appendChild(tr);
|
||||
|
|
@ -813,6 +832,7 @@ function addLine() {
|
|||
</td>`;
|
||||
tbody.appendChild(alNew);
|
||||
calcTotals();
|
||||
return i;
|
||||
}
|
||||
|
||||
function removeLine(i) {
|
||||
|
|
@ -820,32 +840,68 @@ function removeLine(i) {
|
|||
document.getElementById(`fx-row-${i}`)?.remove();
|
||||
delete lines[i];
|
||||
calcTotals();
|
||||
saveLines();
|
||||
}
|
||||
|
||||
function pickProduct(i) {
|
||||
const v = document.getElementById(`dsel-${i}`)?.value;
|
||||
const txt = document.getElementById(`dtxt-${i}`);
|
||||
const v = document.getElementById(`dsel-${i}`)?.value;
|
||||
const txt = document.getElementById(`dtxt-${i}`);
|
||||
const qtyEl = document.getElementById(`qty-${i}`);
|
||||
const uomEl = document.getElementById(`uom-${i}`);
|
||||
const priceEl= document.getElementById(`price-${i}`);
|
||||
const fxOn = document.getElementById(`fx-${i}`)?.value === "yes";
|
||||
|
||||
txt.style.display = v === "__other__" ? "block" : "none";
|
||||
if (v !== "" && v !== "__other__") {
|
||||
|
||||
if (v === "") {
|
||||
// Nothing selected — lock all three
|
||||
if (qtyEl) { qtyEl.disabled = true; qtyEl.value = ""; }
|
||||
if (uomEl) { uomEl.disabled = true; uomEl.value = ""; }
|
||||
if (priceEl) { priceEl.disabled = true; priceEl.value = ""; }
|
||||
} else if (v === "__other__") {
|
||||
// Free text — unlock all three (price stays locked if FX is on)
|
||||
if (qtyEl) qtyEl.disabled = false;
|
||||
if (uomEl) uomEl.disabled = false;
|
||||
if (!fxOn && priceEl) priceEl.disabled = false;
|
||||
} else {
|
||||
// Predefined — fill UOM + price from config; lock UOM, unlock qty + price
|
||||
const p = (cfg.products || [])[+v];
|
||||
if (p) {
|
||||
const uomEl = document.getElementById(`uom-${i}`);
|
||||
const priceEl = document.getElementById(`price-${i}`);
|
||||
if (uomEl && p.uom) uomEl.value = p.uom;
|
||||
if (uomEl && p.uom) uomEl.value = p.uom;
|
||||
if (priceEl && p.price != null) priceEl.value = p.price;
|
||||
}
|
||||
if (qtyEl) qtyEl.disabled = false;
|
||||
if (uomEl) uomEl.disabled = true; // UOM locked for predefined items
|
||||
if (!fxOn && priceEl) priceEl.disabled = false;
|
||||
}
|
||||
calcLine(i);
|
||||
saveLines();
|
||||
}
|
||||
|
||||
// ── Foreign currency ──────────────────────────────────────────────────────────
|
||||
function toggleFx(i) {
|
||||
const on = document.getElementById(`fx-${i}`)?.value === "yes";
|
||||
const lr = document.getElementById(`lr-${i}`);
|
||||
const on = document.getElementById(`fx-${i}`)?.value === "yes";
|
||||
const lr = document.getElementById(`lr-${i}`);
|
||||
const priceEl = document.getElementById(`price-${i}`);
|
||||
document.getElementById(`fx-row-${i}`)?.remove();
|
||||
if (!on) { lr?.classList.remove("open"); calcTotals(); return; }
|
||||
|
||||
if (!on) {
|
||||
lr?.classList.remove("open");
|
||||
// Re-enable price if a description is selected
|
||||
const dv = document.getElementById(`dsel-${i}`)?.value;
|
||||
if (dv && dv !== "" && priceEl) priceEl.disabled = false;
|
||||
calcTotals();
|
||||
saveLines();
|
||||
return;
|
||||
}
|
||||
|
||||
// Lock unit price — it will only be set by calcFxFromPer
|
||||
if (priceEl) { priceEl.disabled = true; priceEl.value = ""; }
|
||||
lr?.classList.add("open");
|
||||
|
||||
const lcy = document.getElementById("icur")?.value || "";
|
||||
const defaultFcy = (cfg.currencies || ["USD"])[0] || "USD";
|
||||
|
||||
const fxTr = document.createElement("tr");
|
||||
fxTr.className = "fx"; fxTr.id = `fx-row-${i}`;
|
||||
fxTr.innerHTML = `
|
||||
|
|
@ -853,21 +909,22 @@ function toggleFx(i) {
|
|||
<div class="fx-grid">
|
||||
<div>
|
||||
<div class="fx-label">${t("currency-code")}</div>
|
||||
<select id="fcur-${i}" onchange="calcLine(${i})">${currOpts("USD")}</select>
|
||||
<select id="fcur-${i}" onchange="updateFxLabels(${i});calcLine(${i});saveLines()">${currOpts(defaultFcy)}</select>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fx-label">${t("exchange-rate")}</div>
|
||||
<input type="number" id="frate-${i}" value="1" min="0" step="any" oninput="calcLine(${i})">
|
||||
<div class="fx-label" id="frate-lbl-${i}">Exchange rate (X ${h(defaultFcy)} = 1 ${h(lcy)})</div>
|
||||
<input type="number" id="frate-${i}" value="" min="0" step="any" oninput="calcFxFromPer(${i})">
|
||||
</div>
|
||||
<div>
|
||||
<div class="fx-label">${t("per-item")}</div>
|
||||
<input type="number" id="fper-${i}" value="0" min="0" step="any" oninput="calcFxFromPer(${i})">
|
||||
<div class="fx-label" id="fper-lbl-${i}">Price per item in ${h(defaultFcy)}</div>
|
||||
<input type="number" id="fper-${i}" value="" min="0" step="any" oninput="calcFxFromPer(${i})">
|
||||
</div>
|
||||
</div>
|
||||
<div class="fx-note">${t("total-local")}: <strong id="fltot-${i}">0.00</strong></div>
|
||||
</td>`;
|
||||
lr?.insertAdjacentElement("afterend", fxTr);
|
||||
calcLine(i);
|
||||
saveLines();
|
||||
}
|
||||
|
||||
function calcFxFromPer(i) {
|
||||
|
|
@ -876,6 +933,7 @@ function calcFxFromPer(i) {
|
|||
const prEl = document.getElementById(`price-${i}`);
|
||||
if (prEl) prEl.value = (per / rate).toFixed(6);
|
||||
calcLine(i);
|
||||
saveLines();
|
||||
}
|
||||
|
||||
// ── Calculations ──────────────────────────────────────────────────────────────
|
||||
|
|
@ -1031,6 +1089,9 @@ function relabel() {
|
|||
const atBtn = document.getElementById("btn-add-tax");
|
||||
if (atBtn) atBtn.textContent = t("add-tax");
|
||||
|
||||
const rstBtn = document.getElementById("btn-reset-lines");
|
||||
if (rstBtn) rstBtn.textContent = t("reset-lines");
|
||||
|
||||
const ctPick = document.getElementById("ct-pick");
|
||||
if (ctPick) {
|
||||
const cur = ctPick.value;
|
||||
|
|
@ -1160,10 +1221,8 @@ function buildPreviewHTML() {
|
|||
|
||||
const linesHTML = rows.map(row => {
|
||||
const fxLine = row.fxNote
|
||||
? `<div class="d-fx-note">${h(row.fxNote.td("converted-from"))} ${h(row.fxNote.cur)}: `
|
||||
+ `${(+row.fxNote.rate).toFixed(5)} ${h(row.fxNote.cur)} = 1 ${h(iCur)}. `
|
||||
+ `${h(row.fxNote.td("per-item"))}: ${h(row.fxNote.cur)} ${fmt(row.fxNote.per)}, `
|
||||
+ `${h(row.fxNote.td("line-total")).toLowerCase()}: ${h(row.fxNote.cur)} ${fmt(row.fxNote.foreignTot)}</div>`
|
||||
? `<div class="d-fx-note">${h(row.fxNote.td("per-item"))}: ${h(row.fxNote.cur)} ${fmt(row.fxNote.per)} `
|
||||
+ `(${(+row.fxNote.rate).toFixed(5)} ${h(row.fxNote.cur)} = 1 ${h(iCur)})</div>`
|
||||
: "";
|
||||
const qStr = row.qty % 1 === 0 ? row.qty : fmt(row.qty);
|
||||
return `<tr>
|
||||
|
|
@ -1396,9 +1455,8 @@ function buildPDF() {
|
|||
const descH = Math.max(0, (dLines.length - 1) * 3.8);
|
||||
let fxH = 0;
|
||||
if (row.fxNote) {
|
||||
const fxStr = `${td("converted-from")} ${row.fxNote.cur}: ${(+row.fxNote.rate).toFixed(5)} ${row.fxNote.cur} = 1 ${iCur}. `
|
||||
+ `${td("per-item")}: ${row.fxNote.cur} ${fmt(row.fxNote.per)}, `
|
||||
+ `${td("line-total").toLowerCase()}: ${row.fxNote.cur} ${fmt(row.fxNote.foreignTot)}`;
|
||||
const fxStr = `${td("per-item")}: ${row.fxNote.cur} ${fmt(row.fxNote.per)} `
|
||||
+ `(${(+row.fxNote.rate).toFixed(5)} ${row.fxNote.cur} = 1 ${iCur})`;
|
||||
const fxLines = sp(fxStr, CD + CP - 4);
|
||||
fxH = fxLines.length * 3.5 + 1;
|
||||
}
|
||||
|
|
@ -1430,12 +1488,9 @@ function buildPDF() {
|
|||
fb(8.5); tc(17,24,39); tR(fmt(row.tot), XR-2, yt);
|
||||
|
||||
if (row.fxNote) {
|
||||
const fxStr = `${td("converted-from")} ${row.fxNote.cur}: `
|
||||
+ `${(+row.fxNote.rate).toFixed(5)} ${row.fxNote.cur} = 1 ${iCur}. `
|
||||
+ `${td("per-item")}: ${row.fxNote.cur} ${fmt(row.fxNote.per)}, `
|
||||
+ `${td("line-total").toLowerCase()}: ${row.fxNote.cur} ${fmt(row.fxNote.foreignTot)}`;
|
||||
const fxStr = `${td("per-item")}: ${row.fxNote.cur} ${fmt(row.fxNote.per)} `
|
||||
+ `(${(+row.fxNote.rate).toFixed(5)} ${row.fxNote.cur} = 1 ${iCur})`;
|
||||
fn(7); tc(107,114,128);
|
||||
// Split if too long for the description column
|
||||
const fxLines = sp(fxStr, CD + CP - 4);
|
||||
fxLines.forEach((fl, fi) => tL(fl, xD+2, y + ROW_H + descH + 3.2 + fi * 3.5));
|
||||
}
|
||||
|
|
@ -1478,6 +1533,117 @@ function buildPDF() {
|
|||
doc.save(iNo ? `invoice-${iNo}.pdf` : "invoice.pdf");
|
||||
}
|
||||
|
||||
// ── 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 (X ${fcy} = 1 ${lcy})`;
|
||||
if (pl) pl.textContent = `Price per item in ${fcy}`;
|
||||
}
|
||||
|
||||
// ── Save / load invoice lines ──────────────────────────────────────────────────
|
||||
const LS_LINES = "inv_lines_v1";
|
||||
|
||||
function saveLines() {
|
||||
if (_loading) return;
|
||||
const data = [];
|
||||
Object.keys(lines).sort((a, b) => +a - +b).forEach(k => {
|
||||
const i = +k;
|
||||
data.push({
|
||||
dsel: document.getElementById(`dsel-${i}`)?.value || "",
|
||||
dtxt: document.getElementById(`dtxt-${i}`)?.value || "",
|
||||
qty: document.getElementById(`qty-${i}`)?.value || "",
|
||||
uom: document.getElementById(`uom-${i}`)?.value || "",
|
||||
price: document.getElementById(`price-${i}`)?.value || "",
|
||||
fx: document.getElementById(`fx-${i}`)?.value || "no",
|
||||
fcur: document.getElementById(`fcur-${i}`)?.value || "",
|
||||
frate: document.getElementById(`frate-${i}`)?.value || "",
|
||||
fper: document.getElementById(`fper-${i}`)?.value || "",
|
||||
});
|
||||
});
|
||||
localStorage.setItem(LS_LINES, JSON.stringify(data));
|
||||
}
|
||||
|
||||
function loadLines() {
|
||||
try {
|
||||
const raw = localStorage.getItem(LS_LINES);
|
||||
if (!raw) return false;
|
||||
const data = JSON.parse(raw);
|
||||
if (!Array.isArray(data) || data.length === 0) return false;
|
||||
|
||||
// Remove the default first empty line added by boot()
|
||||
const firstKey = Object.keys(lines)[0];
|
||||
if (firstKey !== undefined) {
|
||||
document.getElementById(`lr-${firstKey}`)?.remove();
|
||||
document.getElementById(`fx-row-${firstKey}`)?.remove();
|
||||
delete lines[+firstKey];
|
||||
}
|
||||
|
||||
_loading = true;
|
||||
data.forEach(ld => {
|
||||
const i = addLine();
|
||||
|
||||
// Restore description selection
|
||||
if (ld.dsel) {
|
||||
const dsel = document.getElementById(`dsel-${i}`);
|
||||
if (dsel) {
|
||||
dsel.value = ld.dsel;
|
||||
pickProduct(i); // enables/disables fields, fills UOM+price from config
|
||||
if (ld.dsel === "__other__" && ld.dtxt) {
|
||||
const dtxt = document.getElementById(`dtxt-${i}`);
|
||||
if (dtxt) dtxt.value = ld.dtxt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore UOM (covers "Other" lines where user picked their own)
|
||||
if (ld.uom) { const el = document.getElementById(`uom-${i}`); if (el) el.value = ld.uom; }
|
||||
// Restore qty
|
||||
if (ld.qty) { const el = document.getElementById(`qty-${i}`); if (el) el.value = ld.qty; }
|
||||
|
||||
if (ld.fx === "yes") {
|
||||
const fxEl = document.getElementById(`fx-${i}`);
|
||||
if (fxEl) {
|
||||
fxEl.value = "yes";
|
||||
toggleFx(i); // creates FX row and locks price
|
||||
if (ld.fcur) {
|
||||
const el = document.getElementById(`fcur-${i}`);
|
||||
if (el) { el.value = ld.fcur; 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);
|
||||
}
|
||||
} else {
|
||||
if (ld.price) { const el = document.getElementById(`price-${i}`); if (el) el.value = ld.price; }
|
||||
calcLine(i);
|
||||
}
|
||||
});
|
||||
|
||||
_loading = false;
|
||||
saveLines();
|
||||
calcTotals();
|
||||
return true;
|
||||
} catch (e) {
|
||||
_loading = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function resetLines() {
|
||||
if (!confirm(t("reset-lines") + "?")) return;
|
||||
Object.keys(lines).forEach(k => {
|
||||
document.getElementById(`lr-${+k}`)?.remove();
|
||||
document.getElementById(`fx-row-${+k}`)?.remove();
|
||||
delete lines[+k];
|
||||
});
|
||||
localStorage.removeItem(LS_LINES);
|
||||
addLine();
|
||||
calcTotals();
|
||||
}
|
||||
|
||||
// ── Start ─────────────────────────────────────────────────────────────────────
|
||||
loadCfg();
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Reference in a new issue