This commit is contained in:
oliver
2026-06-09 13:51:41 -03:00
parent d0c5c1285b
commit 59871432b9
3 changed files with 376 additions and 91 deletions
+92 -12
View File
@@ -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} 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 }) // 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 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_EXTEND_URL = "https://derez.ai"; // ← replace with Stripe payment link
const CONTRACT_CANCEL_URL = "https://derez.ai"; // ← replace with Stripe cancellation 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); 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) => { document.addEventListener("click", (e) => {
const wrap = document.getElementById("user-panel-wrap"); 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(); if (wrap && !wrap.contains(e.target)) closeUserPanel();
}); });
}); });
@@ -1008,25 +1012,23 @@ async function savePassword() {
ACCOUNT / USER PANEL ACCOUNT / USER PANEL
═══════════════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════════════ */
function toggleUserPanel() { function toggleUserPanel() {
const panel = document.getElementById("user-panel"); const backdrop = document.getElementById("account-backdrop");
if (!panel) return; document.getElementById("acct-email-display").textContent =
if (panel.style.display === "none" || panel.style.display === "") {
document.getElementById("user-panel-email-val").textContent =
currentEmail || "—"; currentEmail || "—";
panel.style.display = "block"; const invInput = document.getElementById("invoice-email-input");
} else { if (invInput) invInput.value = localStorage.getItem("al_invoice_email") || "";
closeUserPanel(); backdrop.classList.add("open");
} loadInvoices();
} }
function closeUserPanel() { function closeUserPanel() {
const panel = document.getElementById("user-panel"); document.getElementById("account-backdrop").classList.remove("open");
if (!panel) return;
panel.style.display = "none";
const inp = document.getElementById("acct-pw-input"); const inp = document.getElementById("acct-pw-input");
if (inp) inp.value = ""; if (inp) inp.value = "";
const btn = document.getElementById("acct-pw-save-btn"); const btn = document.getElementById("acct-pw-save-btn");
if (btn) btn.style.display = "none"; if (btn) btn.style.display = "none";
const invBtn = document.getElementById("invoice-email-save-btn");
if (invBtn) invBtn.style.display = "none";
} }
function toggleAccountPwSave(value) { 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 = '<div class="loading-row"><div class="spinner"></div></div>';
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 =
'<p class="text-muted" style="font-size:13px;padding:4px 0;">Could not load invoices.</p>';
}
}
function renderInvoices(data) {
const body = document.getElementById("invoices-body");
if (!data.length) {
body.innerHTML =
'<p class="text-muted" style="font-size:13px;padding:4px 0;">No invoices yet.</p>';
return;
}
body.innerHTML = `
<div class="invoice-list">
<div class="invoice-row invoice-row--head">
<span>Date</span>
<span>Agent</span>
<span>Product</span>
<span>Price</span>
</div>
${data
.map(
(inv) => `
<div class="invoice-row">
<span class="invoice-date">${escHtml(inv.date || "—")}</span>
<span class="invoice-uuid">${escHtml(inv.uuid || "—")}</span>
<span class="invoice-product">${escHtml(inv.product || "—")}</span>
<span class="invoice-price">&euro;&thinsp;${escHtml(String(inv.price ?? "—"))}</span>
</div>`,
)
.join("")}
</div>`;
}
/* ═══════════════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════════════
RESTART RESTART
═══════════════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════════════ */
+127 -78
View File
@@ -329,84 +329,6 @@
<span id="user-email-label"></span> <span id="user-email-label"></span>
</span> </span>
<!-- ── User account panel ────────────────────── -->
<div
id="user-panel"
class="user-panel"
style="display: none"
onclick="event.stopPropagation()"
>
<div class="user-panel-header">
<svg
width="13"
height="13"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="8" r="4" />
<path d="M4 20c0-4 3.6-7 8-7s8 3 8 7" />
</svg>
Account
</div>
<div class="user-panel-row">
<span class="user-panel-label">Email</span>
<span
class="user-panel-val"
id="user-panel-email-val"
></span
>
</div>
<div class="user-panel-divider"></div>
<div class="user-panel-pw">
<div class="form-group">
<label>Change Password</label>
<div class="pw-input-wrap">
<input
type="password"
id="acct-pw-input"
placeholder="New password"
autocomplete="new-password"
oninput="
toggleAccountPwSave(this.value)
"
onkeydown="
if (event.key === 'Enter')
saveAccountPassword();
"
/>
<button
type="button"
id="acct-pw-save-btn"
class="pw-save-btn"
onclick="saveAccountPassword()"
title="Save password"
style="display: none"
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
</button>
</div>
</div>
<p class="ssh-hint" style="margin-bottom: 0">
Min. 8 characters.
</p>
</div>
</div>
<button class="btn btn-ghost btn-sm" onclick="doSignOut()"> <button class="btn btn-ghost btn-sm" onclick="doSignOut()">
<svg <svg
width="13" width="13"
@@ -875,6 +797,133 @@
</div> </div>
<!-- /app-shell --> <!-- /app-shell -->
<!-- ═══════════════════════════════════════════════════════════
ACCOUNT SETTINGS MODAL
════════════════════════════════════════════════════════════════ -->
<div
id="account-backdrop"
class="modal-backdrop"
onclick="if (event.target === this) closeUserPanel();"
>
<div class="modal account-modal">
<div class="modal-header">
<span class="modal-title">Account Settings</span>
<button
type="button"
class="modal-close-btn"
onclick="closeUserPanel()"
>
&#x2715;
</button>
</div>
<div class="account-modal-body">
<!-- ── Left: settings ───────────────────────── -->
<div class="account-col account-col--settings">
<div class="acct-section">
<div class="acct-field-label">Email</div>
<div class="acct-field-val" id="acct-email-display">
</div>
</div>
<div class="acct-divider"></div>
<div class="acct-section">
<div class="form-group">
<label>Change Password</label>
<div class="pw-input-wrap">
<input
type="password"
id="acct-pw-input"
placeholder="New password"
autocomplete="new-password"
oninput="
toggleAccountPwSave(this.value)
"
onkeydown="
if (event.key === 'Enter')
saveAccountPassword();
"
/>
<button
type="button"
id="acct-pw-save-btn"
class="pw-save-btn"
onclick="saveAccountPassword()"
title="Save password"
style="display: none"
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
</button>
</div>
<p class="ssh-hint" style="margin-bottom: 0">
Min. 8 characters.
</p>
</div>
</div>
<div class="acct-divider"></div>
<div class="acct-section">
<div class="form-group">
<label>Invoice Email</label>
<div class="pw-input-wrap">
<input
type="email"
id="invoice-email-input"
placeholder="invoices@example.com"
autocomplete="email"
oninput="
toggleInvoiceEmailSave(this.value)
"
onkeydown="
if (event.key === 'Enter')
saveInvoiceEmail();
"
/>
<button
type="button"
id="invoice-email-save-btn"
class="pw-save-btn"
onclick="saveInvoiceEmail()"
title="Save invoice email"
style="display: none"
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
</button>
</div>
</div>
</div>
</div>
<!-- ── Vertical divider ──────────────────────── -->
<div class="account-col-divider"></div>
<!-- ── Right: invoices ──────────────────────── -->
<div class="account-col account-col--invoices">
<div class="acct-invoices-title">Invoices</div>
<div id="invoices-body"></div>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════ <!-- ═══════════════════════════════════════════════════════════
BUY CREDITS MODAL BUY CREDITS MODAL
════════════════════════════════════════════════════════════════ --> ════════════════════════════════════════════════════════════════ -->
+156
View File
@@ -1438,6 +1438,162 @@ body::after {
gap: 10px; 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 Modal ────────────────────────────────────────── */
.credits-key-label { .credits-key-label {
font-size: 12px; font-size: 12px;