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
|
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
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue