mirror of
https://github.com/kbenestad/invoice.git
synced 2026-06-18 08:04:32 +00:00
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:
parent
ad2aa079a9
commit
474d2a7a71
2 changed files with 96 additions and 14 deletions
|
|
@ -457,11 +457,46 @@ translations:
|
|||
de: Zu zahlen
|
||||
fr: À payer
|
||||
"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:
|
||||
en: Generate Invoice
|
||||
de: Rechnung erstellen
|
||||
fr: Générer la facture
|
||||
"no": Generer faktura
|
||||
en: Download Invoice
|
||||
de: Rechnung herunterladen
|
||||
fr: Télécharger la facture
|
||||
"no": Last ned faktura
|
||||
other:
|
||||
en: Other
|
||||
de: Andere
|
||||
|
|
|
|||
|
|
@ -934,14 +934,18 @@ function buildForm() {
|
|||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Generate -->
|
||||
<button type="submit" id="btn-generate" class="kb-btn kb-btn--primary kb-btn--lg kb-btn--block">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round" width="16" height="16">
|
||||
<path d="M8 2v9m0 0 4-4M8 11 4 7"/><line x1="2" y1="14" x2="14" y2="14"/>
|
||||
</svg>
|
||||
${t("generate-invoice")}
|
||||
</button>
|
||||
<!-- Action row -->
|
||||
<div style="display:flex;gap:10px;margin-top:8px">
|
||||
<button type="button" id="btn-save" class="kb-btn kb-btn--ghost kb-btn--lg" style="flex:2" onclick="saveInvoice()">${t("save")}</button>
|
||||
<button type="button" id="btn-validate" class="kb-btn kb-btn--ghost kb-btn--lg" style="flex:2" onclick="validateInvoice()">${t("validate")}</button>
|
||||
<button type="submit" id="btn-generate" class="kb-btn kb-btn--primary kb-btn--lg" style="flex:6">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round" width="16" height="16">
|
||||
<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>`;
|
||||
|
||||
document.getElementById("pcode").addEventListener("change", function () {
|
||||
|
|
@ -1393,8 +1397,12 @@ function relabel() {
|
|||
|
||||
const alBtn = document.getElementById("btn-al");
|
||||
if (alBtn) alBtn.textContent = t("add-line");
|
||||
const genBtn = document.getElementById("btn-generate");
|
||||
if (genBtn) genBtn.textContent = t("generate-invoice");
|
||||
const saveBtn = document.getElementById("btn-save");
|
||||
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 => {
|
||||
const ttEl = document.getElementById(`tt-${i}`);
|
||||
|
|
@ -1423,6 +1431,45 @@ function relabel() {
|
|||
}
|
||||
|
||||
// ── 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() {
|
||||
saveStorage();
|
||||
localStorage.setItem(LS_GEN, "true");
|
||||
|
|
|
|||
Loading…
Reference in a new issue