From 280e0677ccd3a1b24149148c8cda61ecbcc6cdc6 Mon Sep 17 00:00:00 2001 From: Charlie Date: Thu, 11 Jun 2026 08:49:46 +0200 Subject: [PATCH] feat: add Invoice document-style card layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bespoke renderTypeSpecific for Invoice type — styled like a real invoice document with INVOICE in spaced caps, from/to two-column block, huge amount due, due date, stamped-look status pill (rotated), and notes section. Class .card.invoice-doc. Gate passes (324 apps). --- app.css | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ app.js | 101 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 243 insertions(+) diff --git a/app.css b/app.css index 71a817c..62ab1e0 100644 --- a/app.css +++ b/app.css @@ -4057,3 +4057,145 @@ footer a:hover { color: var(--accent-2); } .order-conf-label { font-size: 1.15rem; } .order-summary-box { padding: .8rem; } } + +/* ── INVOICE (real invoice document) ── */ +.card.invoice-doc { + background: var(--card); + border-radius: var(--radius); + font-family: var(--font); +} +.card.invoice-doc .hd.hidden { display: none; } + +.inv-header { + padding: 1.8rem 1.5rem 1rem; +} +.inv-label { + font-size: 1.6rem; + font-weight: 800; + letter-spacing: .18em; + color: var(--ink); + text-transform: uppercase; +} +.inv-number { + font-family: var(--mono); + font-size: .85rem; + color: var(--muted); + margin-top: .3rem; + letter-spacing: .02em; +} + +.inv-parties { + display: flex; + gap: 1rem; + padding: 0 1.5rem; + margin-bottom: 1.2rem; +} +.inv-party { + flex: 1; + background: var(--surface); + border: 1px solid var(--line); + border-radius: var(--radius-sm); + padding: .8rem 1rem; +} +.inv-party-label { + font-size: .7rem; + text-transform: uppercase; + letter-spacing: .08em; + color: var(--muted-2); + font-weight: 600; + margin-bottom: .25rem; +} +.inv-party-name { + font-size: .92rem; + font-weight: 600; + color: var(--ink); +} + +.inv-amount-block { + text-align: center; + padding: 1.4rem 1.5rem; + margin: 0 1.5rem; + background: var(--surface); + border: 1px solid var(--line); + border-radius: var(--radius-sm); +} +.inv-amount-label { + font-size: .72rem; + text-transform: uppercase; + letter-spacing: .08em; + color: var(--muted-2); + font-weight: 600; + margin-bottom: .3rem; +} +.inv-amount-val { + font-size: 2.4rem; + font-weight: 800; + color: var(--ink); + letter-spacing: -.02em; + line-height: 1.1; +} +.inv-due-date { + font-size: .82rem; + color: var(--muted); + margin-top: .35rem; +} + +/* Stamped-look status pill */ +.inv-status-stamp { + display: inline-block; + margin: 1.2rem auto 0; + padding: .4rem 1.3rem; + font-size: .78rem; + font-weight: 800; + letter-spacing: .1em; + text-transform: uppercase; + border: 2.5px solid; + border-radius: 6px; + transform: rotate(-3deg); + opacity: .85; + text-align: center; + width: 100%; + box-sizing: border-box; +} +.inv-status-paid { + color: #166534; + border-color: #166534; + background: rgba(22,101,52,.06); +} +.inv-status-due { + color: #92400e; + border-color: #92400e; + background: rgba(146,64,14,.06); +} +.inv-status-overdue { + color: #991b1b; + border-color: #991b1b; + background: rgba(153,27,27,.06); +} + +.inv-notes { + padding: 1rem 1.5rem; + margin-top: .8rem; +} +.inv-notes-label { + font-size: .7rem; + text-transform: uppercase; + letter-spacing: .08em; + color: var(--muted-2); + font-weight: 600; + margin-bottom: .3rem; +} +.inv-notes-body { + font-size: .84rem; + color: var(--muted); + line-height: 1.55; +} + +@media (max-width: 480px) { + .inv-header { padding: 1.2rem 1rem .6rem; } + .inv-label { font-size: 1.3rem; } + .inv-parties { flex-direction: column; padding: 0 1rem; } + .inv-amount-block { margin: 0 1rem; } + .inv-amount-val { font-size: 1.9rem; } + .inv-notes { padding: 1rem; } +} diff --git a/app.js b/app.js index fd25a38..fd9d4f5 100644 --- a/app.js +++ b/app.js @@ -2426,6 +2426,107 @@ handled.add("expectedArrivalUntil"); handled.add("itemShipped"); handled.add("deliveryAddress"); } + // ── INVOICE (real invoice document) ── + if (cfg.type === "Invoice") { + const card = document.querySelector(".card"); + if (card) card.classList.add("invoice-doc"); + + // Hide normal header + const hd = document.querySelector(".hd"); + if (hd) hd.classList.add("hidden"); + + // Remove generic hero if present + const heroMount = document.getElementById("hero-mount"); + if (heroMount) heroMount.querySelector('.hero')?.remove(); + + // ── INVOICE header: big spaced caps + invoice number ── + const invHeader = document.createElement("div"); invHeader.className = "inv-header"; + const invLabel = document.createElement("div"); invLabel.className = "inv-label"; + invLabel.textContent = "INVOICE"; + invHeader.appendChild(invLabel); + + const invNum = data["name"]; + if (invNum) { + const numEl = document.createElement("div"); numEl.className = "inv-number"; + numEl.textContent = invNum; + invHeader.appendChild(numEl); + } + view.appendChild(invHeader); + + // ── From / To two-column block ── + const partiesRow = document.createElement("div"); partiesRow.className = "inv-parties"; + + const provider = data["provider"]; + if (provider) { + const from = document.createElement("div"); from.className = "inv-party"; + from.innerHTML = "
From
" + provider + "
"; + partiesRow.appendChild(from); + } + + const customer = data["customer"]; + if (customer) { + const to = document.createElement("div"); to.className = "inv-party"; + to.innerHTML = "
To
" + customer + "
"; + partiesRow.appendChild(to); + } + + if (partiesRow.children.length) view.appendChild(partiesRow); + + // ── Amount due — HUGE ── + const totalDue = data["totalPaymentDue"]; + if (totalDue) { + const amountBlock = document.createElement("div"); amountBlock.className = "inv-amount-block"; + const amountLabel = document.createElement("div"); amountLabel.className = "inv-amount-label"; + amountLabel.textContent = "Amount Due"; + const amountVal = document.createElement("div"); amountVal.className = "inv-amount-val"; + amountVal.textContent = totalDue; + amountBlock.appendChild(amountLabel); + amountBlock.appendChild(amountVal); + + // Due date underneath + const dueDate = data["paymentDueDate"]; + if (dueDate) { + const d = formatDate(dueDate); + const dueEl = document.createElement("div"); dueEl.className = "inv-due-date"; + dueEl.textContent = "Due " + (d || dueDate); + amountBlock.appendChild(dueEl); + } + + view.appendChild(amountBlock); + } + + // ── Payment status — stamped pill ── + const status = data["paymentStatus"]; + if (status) { + const pill = document.createElement("div"); pill.className = "inv-status-stamp"; + const shortStatus = status.split("/").pop().replace("Payment", ""); + pill.textContent = shortStatus.toUpperCase(); + if (status.includes("Paid") || status.includes("Complete")) pill.classList.add("inv-status-paid"); + else if (status.includes("Due") || status.includes("Pending")) pill.classList.add("inv-status-due"); + else if (status.includes("Overdue") || status.includes("Cancelled")) pill.classList.add("inv-status-overdue"); + else pill.classList.add("inv-status-due"); + view.appendChild(pill); + } + + // ── Description as notes ── + const desc = data["description"]; + if (desc) { + const notes = document.createElement("div"); notes.className = "inv-notes"; + const notesLabel = document.createElement("div"); notesLabel.className = "inv-notes-label"; + notesLabel.textContent = "Notes"; + const notesBody = document.createElement("div"); notesBody.className = "inv-notes-body"; + notesBody.textContent = desc; + notes.appendChild(notesLabel); + notes.appendChild(notesBody); + view.appendChild(notes); + } + + handled.add("name"); handled.add("image"); handled.add("description"); + handled.add("totalPaymentDue"); handled.add("paymentDueDate"); + handled.add("paymentStatus"); + handled.add("provider"); handled.add("customer"); + } + // ── ORDER (confirmation email card) ── if (cfg.type === "Order") { const card = document.querySelector(".card");