mirror of
https://github.com/kbenestad/invoice.git
synced 2026-06-18 08:04:32 +00:00
Replace PDF preview overlay with direct download on Generate
Clicking Generate now immediately triggers a PDF download instead of opening a full-screen overlay preview that was inaccessible on smaller screens. Removes the overlay HTML, its CSS, buildPreviewHTML(), and closeOverlay(). https://claude.ai/code/session_0127ywsQA7qWcX3tGX4itqN5
This commit is contained in:
parent
0da4956a5a
commit
30fe67fd6e
1 changed files with 2 additions and 214 deletions
216
app/index.html
216
app/index.html
|
|
@ -196,106 +196,6 @@
|
|||
}
|
||||
#btn-generate:hover { background: var(--accent-hover); }
|
||||
|
||||
/* ── Invoice overlay ────────────────────────────────────────────────────── */
|
||||
#overlay {
|
||||
display: none;
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(15,23,42,.65);
|
||||
z-index: 900;
|
||||
overflow-y: auto;
|
||||
padding: 32px 16px 48px;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
#overlay.on { display: flex; }
|
||||
|
||||
#overlay-actions {
|
||||
display: flex; gap: 10px;
|
||||
margin: 0 auto 16px;
|
||||
width: 794px; max-width: 100%;
|
||||
}
|
||||
.btn-dl {
|
||||
background: var(--success); color: var(--white);
|
||||
padding: 10px 20px; font-size: 14px; font-weight: 600;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.btn-dl:hover { background: #166534; }
|
||||
.btn-close {
|
||||
background: var(--white); color: var(--text);
|
||||
padding: 10px 20px; font-size: 14px;
|
||||
border: 1px solid var(--border); border-radius: var(--radius);
|
||||
}
|
||||
.btn-close:hover { background: var(--bg); }
|
||||
|
||||
/* ── Invoice preview (screen only) ──────────────────────────────────────── */
|
||||
#inv-doc {
|
||||
background: var(--white);
|
||||
width: 794px; max-width: 100%; min-height: 1100px;
|
||||
padding: 52px 60px 60px;
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,.25);
|
||||
margin: 0 auto;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", Arial, sans-serif;
|
||||
font-size: 11.5px; color: #111827;
|
||||
}
|
||||
|
||||
/* 2×2 invoice header grid */
|
||||
.d-4hdr {
|
||||
display: grid; grid-template-columns: 1fr 1fr;
|
||||
border-bottom: 3px solid var(--navy);
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
.d-4hdr > div { padding: 0; }
|
||||
.d-4hdr > .d-tl { padding: 0 20px 16px 0; border-bottom: 1px solid #e5e7eb; border-right: 1px solid #e5e7eb; }
|
||||
.d-4hdr > .d-tr { padding: 0 0 16px 20px; border-bottom: 1px solid #e5e7eb; }
|
||||
.d-4hdr > .d-bl { padding: 16px 20px 16px 0; border-right: 1px solid #e5e7eb; }
|
||||
.d-4hdr > .d-br { padding: 16px 0 16px 20px; }
|
||||
|
||||
.d-sender .name { font-size: 17px; font-weight: 700; color: var(--navy); margin-bottom: 6px; }
|
||||
.d-sender p { font-size: 10.5px; color: #4b5563; margin-bottom: 2px; }
|
||||
|
||||
.d-pay-hdr .ph-lbl { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.2px; color: #6b7280; margin-bottom: 6px; }
|
||||
.d-pay-hdr .ph-terms { font-size: 10.5px; color: #4b5563; margin-bottom: 6px; }
|
||||
.d-pay-hdr .ph-grid { display: grid; grid-template-columns: 130px 1fr; gap: 3px 8px; font-size: 10.5px; margin-top: 6px; }
|
||||
.d-pay-hdr .ph-grid .pl { color: #6b7280; }
|
||||
.d-pay-hdr .ph-grid .pv { font-weight: 500; word-break: break-all; }
|
||||
.d-pay-hdr .ph-ref { margin-top: 6px; font-size: 10.5px; color: #4b5563; }
|
||||
.d-pay-hdr .ph-ref strong { color: #111827; }
|
||||
|
||||
.d-bill .bt-lbl { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.2px; color: #6b7280; margin-bottom: 5px; }
|
||||
.d-bill .bt-name { font-size: 13px; font-weight: 700; color: var(--navy); margin-bottom: 3px; }
|
||||
.d-bill p { font-size: 10.5px; color: #4b5563; margin-bottom: 2px; }
|
||||
.bt-meta { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 6px; }
|
||||
.bt-meta span { font-size: 10px; color: #4b5563; }
|
||||
.bt-meta strong { color: #111827; }
|
||||
|
||||
.d-inv-meta { text-align: right; }
|
||||
.d-inv-meta h1 { font-size: 28px; font-weight: 800; letter-spacing: 5px; color: var(--navy); margin-bottom: 14px; }
|
||||
.d-meta { border-collapse: collapse; margin-left: auto; }
|
||||
.d-meta td { font-size: 10.5px; padding: 2px 0 2px 12px; }
|
||||
.d-meta .ml { color: #6b7280; text-align: right; }
|
||||
.d-meta .mv { font-weight: 600; text-align: right; }
|
||||
|
||||
.d-lines { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
|
||||
.d-lines thead tr { background: var(--navy); color: white; }
|
||||
.d-lines thead th { padding: 7px 10px; font-size: 9.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; text-align: left; }
|
||||
.d-lines thead th.r { text-align: right; }
|
||||
.d-lines tbody tr { border-bottom: 1px solid #e5e7eb; }
|
||||
.d-lines tbody tr:nth-child(even) { background: #f9fafb; }
|
||||
.d-lines tbody td { padding: 7px 10px; font-size: 10.5px; vertical-align: top; }
|
||||
.d-lines tbody td.r { text-align: right; }
|
||||
.d-lines tbody td.b { font-weight: 600; }
|
||||
.d-fx-note { font-size: 9.5px; color: #6b7280; margin-top: 3px; }
|
||||
|
||||
.d-tots { width: 100%; border-collapse: collapse; }
|
||||
.d-tots td { padding: 5px 10px; font-size: 11.5px; }
|
||||
.d-tots .sp { width: 55%; }
|
||||
.d-tots .tl { text-align: right; color: #6b7280; padding-right: 20px; }
|
||||
.d-tots .tv { text-align: right; width: 130px; font-weight: 600; }
|
||||
.d-tots .sub td { border-top: 1px solid #e5e7eb; }
|
||||
.d-tots .fin td { background: var(--navy); color: white; font-size: 14px; font-weight: 700; border-top: 2px solid var(--navy); }
|
||||
.d-tots .fin .tl { color: rgba(255,255,255,.75); }
|
||||
|
||||
/* ── Payment card form fields ────────────────────────────────────────────── */
|
||||
.pay-section-title { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; color: var(--navy); margin-bottom: 10px; }
|
||||
|
|
@ -348,14 +248,6 @@
|
|||
|
||||
</div><!-- /.wrap -->
|
||||
|
||||
<!-- ── Invoice preview overlay ───────────────────────────────────────────────── -->
|
||||
<div id="overlay">
|
||||
<div id="overlay-actions">
|
||||
<button class="btn-dl" id="btn-dl" onclick="buildPDF()">⬇ Download PDF</button>
|
||||
<button class="btn-close" id="btn-close" onclick="closeOverlay()">✕ Close</button>
|
||||
</div>
|
||||
<div id="inv-doc"></div>
|
||||
</div>
|
||||
|
||||
<!-- js-yaml -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"
|
||||
|
|
@ -1183,24 +1075,13 @@ function relabel() {
|
|||
ctPick.value = cur;
|
||||
}
|
||||
|
||||
document.getElementById("btn-dl").textContent = "⬇ " + t("download-pdf");
|
||||
document.getElementById("btn-close").textContent = "✕ " + t("close");
|
||||
}
|
||||
|
||||
// ── Generate preview ──────────────────────────────────────────────────────────
|
||||
// ── Generate invoice ──────────────────────────────────────────────────────────
|
||||
function generateInvoice() {
|
||||
saveStorage();
|
||||
localStorage.setItem(LS_GEN, "true");
|
||||
document.getElementById("inv-doc").innerHTML = buildPreviewHTML();
|
||||
const ov = document.getElementById("overlay");
|
||||
ov.classList.add("on");
|
||||
ov.scrollTop = 0;
|
||||
document.getElementById("btn-dl").textContent = "⬇ " + t("download-pdf");
|
||||
document.getElementById("btn-close").textContent = "✕ " + t("close");
|
||||
}
|
||||
|
||||
function closeOverlay() {
|
||||
document.getElementById("overlay").classList.remove("on");
|
||||
buildPDF();
|
||||
}
|
||||
|
||||
// ── Shared: gather invoice data ───────────────────────────────────────────────
|
||||
|
|
@ -1295,99 +1176,6 @@ function gatherData(renderLang) {
|
|||
}
|
||||
|
||||
// ── Build HTML preview ────────────────────────────────────────────────────────
|
||||
function buildPreviewHTML() {
|
||||
const { td, sName, sAddr, sCntry, sPh, sEm, sTax, iDate, pCode, iNo, iCur,
|
||||
ctName, ctAddr, ctCntry, ctPh, ctEm, ctVat, ctReg,
|
||||
pTerm, pPayBy, pAcct, pIban, pBic, pBadr1, pBadr2, pRef, hasBank, hidePaymentOut,
|
||||
rows, sub, taxes, paid, toPay } = gatherData();
|
||||
|
||||
const linesHTML = rows.map(row => {
|
||||
const fxLine = row.fxNote
|
||||
? `<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>
|
||||
<td>${qStr}</td><td>${h(row.uomLbl)}</td>
|
||||
<td>${h(row.desc)}${fxLine}</td>
|
||||
<td class="r">${fmt(row.price)}</td>
|
||||
<td class="r b">${fmt(row.tot)}</td>
|
||||
</tr>`;
|
||||
}).join("") || `<tr><td colspan="5" style="text-align:center;color:#9ca3af;padding:20px">—</td></tr>`;
|
||||
|
||||
const showBank = hasBank && !hidePaymentOut;
|
||||
return `
|
||||
<div class="d-4hdr">
|
||||
<div class="d-tl d-sender">
|
||||
${sName ? `<div class="name">${h(sName)}</div>` : ""}
|
||||
${sAddr.map(a=>`<p>${h(a)}</p>`).join("")}
|
||||
${sCntry ? `<p>${h(sCntry)}</p>` : ""}
|
||||
<div class="bt-meta">
|
||||
${sPh ? `<span><strong>${h(td("sender-phone"))}:</strong> ${h(sPh)}</span>` : ""}
|
||||
${sEm ? `<span><strong>${h(td("sender-email"))}:</strong> ${h(sEm)}</span>` : ""}
|
||||
${sTax ? `<span><strong>${h(td("vat-id"))}:</strong> ${h(sTax)}</span>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-tr d-inv-meta">
|
||||
<h1>${h(td("invoice"))}</h1>
|
||||
<table class="d-meta">
|
||||
${iNo ? `<tr><td class="ml">${h(td("invoice-no"))}</td><td class="mv">${h(iNo)}</td></tr>` : ""}
|
||||
${iDate ? `<tr><td class="ml">${h(td("invoice-date"))}</td><td class="mv">${h(fmtDate(iDate))}</td></tr>` : ""}
|
||||
${pCode ? `<tr><td class="ml">${h(td("project-code"))}</td><td class="mv">${h(pCode)}</td></tr>` : ""}
|
||||
${iCur ? `<tr><td class="ml">${h(td("invoice-currency"))}</td><td class="mv">${h(iCur)}</td></tr>` : ""}
|
||||
</table>
|
||||
</div>
|
||||
<div class="d-bl d-bill">
|
||||
<div class="bt-lbl">${h(td("charge-to"))}</div>
|
||||
${ctName ? `<div class="bt-name">${h(ctName)}</div>` : ""}
|
||||
${ctAddr.map(a=>`<p>${h(a)}</p>`).join("")}
|
||||
${ctCntry ? `<p>${h(ctCntry)}</p>` : ""}
|
||||
<div class="bt-meta">
|
||||
${ctPh ? `<span><strong>${h(td("charge-to-phone"))}:</strong> ${h(ctPh)}</span>` : ""}
|
||||
${ctEm ? `<span><strong>${h(td("charge-to-email"))}:</strong> ${h(ctEm)}</span>` : ""}
|
||||
${ctVat ? `<span><strong>${h(td("vat-id"))}:</strong> ${h(ctVat)}</span>` : ""}
|
||||
${ctReg ? `<span><strong>${h(td("registration-no"))}:</strong> ${h(ctReg)}</span>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-br d-pay-hdr">
|
||||
<div class="ph-lbl">${h(td("payment"))}</div>
|
||||
${pTerm > 0 ? `<div class="ph-terms">${h(td("payment-terms"))}: <strong>${pTerm}</strong> ${h(td("payment-days"))}${pPayBy ? ` — ${h(td("pay-by"))}: <strong>${h(pPayBy)}</strong>` : ""}</div>` : ""}
|
||||
${showBank ? `<div class="ph-grid">
|
||||
${pAcct ? `<span class="pl">${h(td("account-holder"))}</span><span class="pv">${h(pAcct)}</span>` : ""}
|
||||
${pIban ? `<span class="pl">${h(td("account-no"))}</span><span class="pv">${h(pIban)}</span>` : ""}
|
||||
${pBic ? `<span class="pl">${h(td("bank-bic"))}</span><span class="pv">${h(pBic)}</span>` : ""}
|
||||
${pBadr1 || pBadr2 ? `<span class="pl">${h(td("bank-address"))}</span><span class="pv">${[pBadr1,pBadr2].filter(Boolean).map(l=>h(l)).join("<br>")}</span>` : ""}
|
||||
</div>
|
||||
${pRef ? `<div class="ph-ref">${h(td("payment-ref"))}: <strong>${h(pRef)}</strong></div>` : ""}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<table class="d-lines">
|
||||
<thead><tr>
|
||||
<th style="width:50px">${h(td("qty"))}</th>
|
||||
<th style="width:70px">${h(td("uom"))}</th>
|
||||
<th>${h(td("description"))}</th>
|
||||
<th class="r" style="width:100px">${h(td("price"))}</th>
|
||||
<th class="r" style="width:115px">${h(td("line-total"))}</th>
|
||||
</tr></thead>
|
||||
<tbody>${linesHTML}</tbody>
|
||||
</table>
|
||||
<table class="d-tots">
|
||||
<tr class="sub">
|
||||
<td class="sp"></td>
|
||||
<td class="tl">${h(td("subtotal"))}</td>
|
||||
<td class="tv">${fmt(sub)}</td>
|
||||
</tr>
|
||||
${taxes.map(tx => `<tr><td class="sp"></td><td class="tl">${h(tx.lineLabel)}</td><td class="tv">${fmt(tx.amt)}</td></tr>`).join("")}
|
||||
${paid > 0 ? `<tr><td class="sp"></td><td class="tl">${h(td("paid"))}</td><td class="tv">−${fmt(paid)}</td></tr>` : ""}
|
||||
<tr class="fin">
|
||||
<td class="sp"></td>
|
||||
<td class="tl">${h(td("to-pay"))}</td>
|
||||
<td class="tv">${fmt(toPay)}</td>
|
||||
</tr>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
// ── Build PDF with jsPDF + Helvetica ─────────────────────────────────────────
|
||||
function buildPDF() {
|
||||
if (!window.jspdf) { alert("PDF library not loaded — check your internet connection."); return; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue