invoices
This commit is contained in:
@@ -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 === "") {
|
currentEmail || "—";
|
||||||
document.getElementById("user-panel-email-val").textContent =
|
const invInput = document.getElementById("invoice-email-input");
|
||||||
currentEmail || "—";
|
if (invInput) invInput.value = localStorage.getItem("al_invoice_email") || "";
|
||||||
panel.style.display = "block";
|
backdrop.classList.add("open");
|
||||||
} else {
|
loadInvoices();
|
||||||
closeUserPanel();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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">€ ${escHtml(String(inv.price ?? "—"))}</span>
|
||||||
|
</div>`,
|
||||||
|
)
|
||||||
|
.join("")}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
/* ═══════════════════════════════════════════════════════════════
|
/* ═══════════════════════════════════════════════════════════════
|
||||||
RESTART
|
RESTART
|
||||||
═══════════════════════════════════════════════════════════════ */
|
═══════════════════════════════════════════════════════════════ */
|
||||||
|
|||||||
+127
-78
@@ -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()"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</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
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user