From 59871432b9341103c50332222ec2c69971075786 Mon Sep 17 00:00:00 2001 From: oliver Date: Tue, 9 Jun 2026 13:51:41 -0300 Subject: [PATCH] invoices --- app.js | 106 +++++++++++++++++++++++---- index.html | 205 +++++++++++++++++++++++++++++++++-------------------- styles.css | 156 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 376 insertions(+), 91 deletions(-) diff --git a/app.js b/app.js index 16ab83e..854bd01 100644 --- a/app.js +++ b/app.js @@ -21,6 +21,8 @@ const CHAT_URL = `${API_BASE}/etc/chat`; // POST {message,sessionId,email} const REFERRAL_URL = `${API_BASE}/etc/referral`; // POST {referrer,friend_name,friend_email,uuid} // WIZZARD=false is written via AGENT_INFO_URL (POST { uuid, key, value }) const CREDITS_URL = `${API_BASE}/payment/credits`; // hidden form POST → 302 Stripe redirect +const INVOICES_URL = `${API_BASE}/invoices`; // GET ?email= +const INVOICE_EMAIL_URL = `${API_BASE}/account/invoice-email`; // POST {email, invoice_email} const CONTRACT_EXTEND_URL = "https://derez.ai"; // ← replace with Stripe payment link const CONTRACT_CANCEL_URL = "https://derez.ai"; // ← replace with Stripe cancellation link @@ -95,9 +97,11 @@ document.addEventListener("DOMContentLoaded", () => { if (e.target === e.currentTarget) confirmClose(false); }); - // Close user panel when clicking outside of it + // Close user panel when clicking outside of it (skip while modal is open) document.addEventListener("click", (e) => { const wrap = document.getElementById("user-panel-wrap"); + const modal = document.getElementById("account-backdrop"); + if (modal && modal.classList.contains("open")) return; if (wrap && !wrap.contains(e.target)) closeUserPanel(); }); }); @@ -1008,25 +1012,23 @@ async function savePassword() { ACCOUNT / USER PANEL ═══════════════════════════════════════════════════════════════ */ function toggleUserPanel() { - const panel = document.getElementById("user-panel"); - if (!panel) return; - if (panel.style.display === "none" || panel.style.display === "") { - document.getElementById("user-panel-email-val").textContent = - currentEmail || "—"; - panel.style.display = "block"; - } else { - closeUserPanel(); - } + const backdrop = document.getElementById("account-backdrop"); + document.getElementById("acct-email-display").textContent = + currentEmail || "—"; + const invInput = document.getElementById("invoice-email-input"); + if (invInput) invInput.value = localStorage.getItem("al_invoice_email") || ""; + backdrop.classList.add("open"); + loadInvoices(); } function closeUserPanel() { - const panel = document.getElementById("user-panel"); - if (!panel) return; - panel.style.display = "none"; + document.getElementById("account-backdrop").classList.remove("open"); const inp = document.getElementById("acct-pw-input"); if (inp) inp.value = ""; const btn = document.getElementById("acct-pw-save-btn"); if (btn) btn.style.display = "none"; + const invBtn = document.getElementById("invoice-email-save-btn"); + if (invBtn) invBtn.style.display = "none"; } function toggleAccountPwSave(value) { @@ -1079,6 +1081,84 @@ async function saveAccountPassword() { } } +function toggleInvoiceEmailSave(value) { + const btn = document.getElementById("invoice-email-save-btn"); + if (btn) btn.style.display = value ? "flex" : "none"; +} + +async function saveInvoiceEmail() { + const input = document.getElementById("invoice-email-input"); + const btn = document.getElementById("invoice-email-save-btn"); + const email = input.value.trim(); + + if (!email || !isValidEmail(email)) { + toast("Please enter a valid email address.", "warning"); + return; + } + + btnLoad(btn, ""); + try { + const res = await apiFetch(INVOICE_EMAIL_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email: currentEmail, invoice_email: email }), + }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + localStorage.setItem("al_invoice_email", email); + toast("Invoice email saved.", "success"); + btn.style.display = "none"; + } catch (err) { + toast(err.message, "error"); + } finally { + btnReset(btn); + } +} + +async function loadInvoices() { + const body = document.getElementById("invoices-body"); + body.innerHTML = '
'; + try { + const res = await apiFetch( + `${INVOICES_URL}?email=${encodeURIComponent(currentEmail || "")}`, + ); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = await res.json(); + renderInvoices(Array.isArray(data) ? data : []); + } catch (err) { + body.innerHTML = + '

Could not load invoices.

'; + } +} + +function renderInvoices(data) { + const body = document.getElementById("invoices-body"); + if (!data.length) { + body.innerHTML = + '

No invoices yet.

'; + return; + } + body.innerHTML = ` +
+
+ Date + Agent + Product + Price +
+ ${data + .map( + (inv) => ` +
+ ${escHtml(inv.date || "—")} + ${escHtml(inv.uuid || "—")} + ${escHtml(inv.product || "—")} + € ${escHtml(String(inv.price ?? "—"))} +
`, + ) + .join("")} +
`; +} + /* ═══════════════════════════════════════════════════════════════ RESTART ═══════════════════════════════════════════════════════════════ */ diff --git a/index.html b/index.html index ed0b2a9..a49e198 100644 --- a/index.html +++ b/index.html @@ -329,84 +329,6 @@ - - - + +
+ + + + + + +
+ + + diff --git a/styles.css b/styles.css index 1401b72..45a115f 100644 --- a/styles.css +++ b/styles.css @@ -1438,6 +1438,162 @@ body::after { gap: 10px; } +/* ─── Account Modal ────────────────────────────────────────────────── */ +.account-modal { + max-width: 760px; + width: 100%; +} + +.account-modal .modal-header { + display: flex; + align-items: center; + justify-content: space-between; +} + +.modal-close-btn { + background: none; + border: none; + color: var(--text-muted); + font-size: 16px; + cursor: pointer; + padding: 0 2px; + line-height: 1; + transition: color 0.15s; +} + +.modal-close-btn:hover { + color: var(--text); +} + +.account-modal-body { + display: flex; + min-height: 320px; +} + +.account-col { + display: flex; + flex-direction: column; +} + +.account-col--settings { + width: 260px; + flex-shrink: 0; + padding: 18px 20px; +} + +.account-col-divider { + width: 1px; + background: var(--border); + flex-shrink: 0; +} + +.account-col--invoices { + flex: 1; + padding: 18px 20px; + min-width: 0; + overflow: hidden; +} + +.acct-section { + padding: 4px 0 10px; +} + +.acct-divider { + height: 1px; + background: var(--border); + margin: 6px 0 14px; +} + +.acct-field-label { + font-size: 11px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.06em; + margin-bottom: 4px; +} + +.acct-field-val { + font-size: 13px; + color: var(--text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.acct-invoices-title { + font-size: 11px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.06em; + margin-bottom: 14px; +} + +.invoice-list { + display: flex; + flex-direction: column; + gap: 2px; + max-height: 360px; + overflow-y: auto; +} + +.invoice-row { + display: grid; + grid-template-columns: 100px 1fr 1fr 80px; + gap: 10px; + align-items: center; + padding: 7px 10px; + border-radius: var(--radius); + font-size: 12px; +} + +.invoice-row--head { + color: var(--text-muted); + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + background: none; + padding-bottom: 4px; + margin-bottom: 2px; + border-bottom: 1px solid var(--border); +} + +.invoice-row:not(.invoice-row--head) { + background: var(--bg-inner); + border: 1px solid var(--border); + color: var(--text); +} + +.invoice-row:not(.invoice-row--head):hover { + border-color: var(--border-hi); +} + +.invoice-date { + color: var(--text-muted); + white-space: nowrap; +} + +.invoice-uuid { + color: var(--accent); + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.invoice-product { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.invoice-price { + text-align: right; + font-weight: 600; + color: var(--success); + white-space: nowrap; +} + /* ─── Credits Modal ────────────────────────────────────────── */ .credits-key-label { font-size: 12px;