Add Save / Validate / Download invoice action row at bottom of form

Replaces single generate button with a 20/20/60 flex row:
- Save (20%): calls saveStorage(), shows brief ✓ confirmation
- Validate (20%): checks invoice no, sender name, charge-to, line items; shows inline message
- Download Invoice (60%): existing PDF generation

https://claude.ai/code/session_01MkM7p8Us3L8YAfLKGA13NS
This commit is contained in:
Claude 2026-06-08 16:41:27 +00:00
parent ad2aa079a9
commit 474d2a7a71
No known key found for this signature in database
2 changed files with 96 additions and 14 deletions

View file

@ -457,11 +457,46 @@ translations:
de: Zu zahlen de: Zu zahlen
fr: À payer fr: À payer
"no": Å betale "no": Å betale
save:
en: Save
de: Speichern
fr: Enregistrer
"no": Lagre
validate:
en: Validate
de: Prüfen
fr: Valider
"no": Valider
val-ok:
en: All required fields are filled.
de: Alle Pflichtfelder sind ausgefüllt.
fr: Tous les champs obligatoires sont remplis.
"no": Alle obligatoriske felt er fylt ut.
val-invoice-no:
en: Invoice number is required
de: Rechnungsnummer ist erforderlich
fr: Le numéro de facture est requis
"no": Fakturanummer er påkrevd
val-from-name:
en: Sender name is required
de: Absendername ist erforderlich
fr: Le nom de l'expéditeur est requis
"no": Avsendernavn er påkrevd
val-charge-to:
en: Charge-to is required
de: Empfänger ist erforderlich
fr: Le destinataire est requis
"no": Mottaker er påkrevd
val-lines:
en: At least one line item is required
de: Mindestens eine Zeile ist erforderlich
fr: Au moins une ligne est requise
"no": Minst én linje er påkrevd
generate-invoice: generate-invoice:
en: Generate Invoice en: Download Invoice
de: Rechnung erstellen de: Rechnung herunterladen
fr: Générer la facture fr: Télécharger la facture
"no": Generer faktura "no": Last ned faktura
other: other:
en: Other en: Other
de: Andere de: Andere

View file

@ -934,14 +934,18 @@ function buildForm() {
</section> </section>
</div> </div>
<!-- Generate --> <!-- Action row -->
<button type="submit" id="btn-generate" class="kb-btn kb-btn--primary kb-btn--lg kb-btn--block"> <div style="display:flex;gap:10px;margin-top:8px">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" <button type="button" id="btn-save" class="kb-btn kb-btn--ghost kb-btn--lg" style="flex:2" onclick="saveInvoice()">${t("save")}</button>
stroke-linecap="round" stroke-linejoin="round" width="16" height="16"> <button type="button" id="btn-validate" class="kb-btn kb-btn--ghost kb-btn--lg" style="flex:2" onclick="validateInvoice()">${t("validate")}</button>
<path d="M8 2v9m0 0 4-4M8 11 4 7"/><line x1="2" y1="14" x2="14" y2="14"/> <button type="submit" id="btn-generate" class="kb-btn kb-btn--primary kb-btn--lg" style="flex:6">
</svg> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"
${t("generate-invoice")} stroke-linecap="round" stroke-linejoin="round" width="16" height="16">
</button> <path d="M8 2v9m0 0 4-4M8 11 4 7"/><line x1="2" y1="14" x2="14" y2="14"/>
</svg>
<span id="btn-generate-lbl">${t("generate-invoice")}</span>
</button>
</div>
</form>`; </form>`;
document.getElementById("pcode").addEventListener("change", function () { document.getElementById("pcode").addEventListener("change", function () {
@ -1393,8 +1397,12 @@ function relabel() {
const alBtn = document.getElementById("btn-al"); const alBtn = document.getElementById("btn-al");
if (alBtn) alBtn.textContent = t("add-line"); if (alBtn) alBtn.textContent = t("add-line");
const genBtn = document.getElementById("btn-generate"); const saveBtn = document.getElementById("btn-save");
if (genBtn) genBtn.textContent = t("generate-invoice"); if (saveBtn) saveBtn.textContent = t("save");
const valBtn = document.getElementById("btn-validate");
if (valBtn) valBtn.textContent = t("validate");
const genLbl = document.getElementById("btn-generate-lbl");
if (genLbl) genLbl.textContent = t("generate-invoice");
Object.keys(tLines).forEach(i => { Object.keys(tLines).forEach(i => {
const ttEl = document.getElementById(`tt-${i}`); const ttEl = document.getElementById(`tt-${i}`);
@ -1423,6 +1431,45 @@ function relabel() {
} }
// ── Generate invoice ────────────────────────────────────────────────────────── // ── Generate invoice ──────────────────────────────────────────────────────────
function saveInvoice() {
saveStorage();
const btn = document.getElementById("btn-save");
if (!btn) return;
const orig = btn.textContent;
btn.textContent = "✓ " + orig;
btn.disabled = true;
setTimeout(() => { btn.textContent = orig; btn.disabled = false; }, 1500);
}
function validateInvoice() {
const errors = [];
const ino = (document.getElementById("ino")?.value || "").trim();
if (!ino) errors.push(t("val-invoice-no") || "Invoice number is required");
const fromName = (document.getElementById("from_name")?.value || "").trim();
if (!fromName) errors.push(t("val-from-name") || "Sender name is required");
const ctPick = document.getElementById("ct-pick");
if (!ctPick || !ctPick.value) errors.push(t("val-charge-to") || "Charge-to is required");
if (Object.keys(lines).length === 0) errors.push(t("val-lines") || "At least one line item is required");
const btn = document.getElementById("btn-validate");
const existing = document.getElementById("validate-msg");
if (existing) existing.remove();
const msg = document.createElement("div");
msg.id = "validate-msg";
msg.style.cssText = "margin-top:8px;padding:10px 14px;border-radius:8px;font-size:var(--fs-small)";
if (errors.length === 0) {
msg.style.background = "color-mix(in srgb, var(--accent) 12%, transparent)";
msg.style.color = "var(--accent)";
msg.textContent = t("val-ok") || "All required fields are filled.";
} else {
msg.style.background = "color-mix(in srgb, #e74c3c 12%, transparent)";
msg.style.color = "#e74c3c";
msg.innerHTML = errors.map(e => `• ${e}`).join("<br>");
}
btn.closest("div").after(msg);
setTimeout(() => msg.remove(), 5000);
}
function generateInvoice() { function generateInvoice() {
saveStorage(); saveStorage();
localStorage.setItem(LS_GEN, "true"); localStorage.setItem(LS_GEN, "true");