feat: rework admin portal with pricing breakdown and document management

- Added pricing snapshot columns to the leads table: daily_subtotal, weekend_subtotal, subtotal_eur, vat_eur, total_eur, deposit_eur, total_days, weekday_count, weekend_day_count.
- Updated create_lead RPC to accept and store new pricing parameters.
- Enhanced frontend app.js to compute and send pricing details during lead creation.
- Introduced new UI elements in admin.html for displaying pricing and documents in a tabbed dialog.
- Updated i18n.js with new translation keys for pricing and document management.
- Improved styles in styles.css for new dialog components and pricing cards.
- Added migration script 06-admin-pricing-documents.sql for database schema changes.
This commit is contained in:
LagoESP
2026-04-29 17:27:37 +02:00
parent bc61ffa206
commit 30e296f61b
9 changed files with 1187 additions and 40 deletions
+395 -23
View File
@@ -33,6 +33,15 @@ const leadDialog = document.querySelector("#leadDialog");
const leadDialogTitle = document.querySelector("#leadDialogTitle");
const leadDialogBody = document.querySelector("#leadDialogBody");
const leadDialogClose = document.querySelector("#leadDialogClose");
const leadDialogTabs = document.querySelector("#leadDialogTabs");
const leadDialogFooter = document.querySelector("#leadDialogFooter");
const customerDialog = document.querySelector("#customerDialog");
const customerDialogTitle = document.querySelector("#customerDialogTitle");
const customerDialogBody = document.querySelector("#customerDialogBody");
const customerDialogClose = document.querySelector("#customerDialogClose");
const customerDialogTabs = document.querySelector("#customerDialogTabs");
const customerDialogFooter = document.querySelector("#customerDialogFooter");
const vehicleForm = document.querySelector("#vehicleForm");
const formFeedback = document.querySelector("#formFeedback");
@@ -363,12 +372,15 @@ function renderLeads() {
leadsEmpty.style.display = rows.length ? "none" : "block";
leadsTableBody.innerHTML = "";
for (const l of rows) {
const total = l.total_eur || 0;
const totalStr = total > 0 ? "€ " + total.toLocaleString("de-DE") : "—";
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${fmtDate(l.created_at)}</td>
<td><strong>${esc(l.name)}</strong><br /><span class="muted">${esc(l.email)}${l.phone ? " · " + esc(l.phone) : ""}</span></td>
<td>${esc(l.vehicle_label || "—")}</td>
<td>${esc(l.date_from || "—")}${esc(l.date_to || "—")}</td>
<td style="font-weight:600;color:var(--accent-strong);">${totalStr}</td>
<td><span class="pill pill-${esc(l.status)}">${esc(l.status)}</span></td>
<td style="white-space:nowrap;">
<button class="btn small ghost" data-open="${l.id}">${t("adminDetails")}</button>
@@ -387,34 +399,203 @@ function renderLeads() {
leadsTableBody.querySelectorAll("[data-reopen]").forEach(b => b.addEventListener("click", () => reopenLead(b.dataset.reopen)));
}
function openLead(id) {
// ----- LEAD DOCUMENTS -----
async function loadLeadAttachments(leadId) {
const { data, error } = await supabase
.from("lead_attachments")
.select("*")
.eq("lead_id", leadId)
.order("created_at", { ascending: false });
if (error) { console.error(error); return []; }
return data || [];
}
async function getAttachmentUrl(attachment) {
try {
const { data: pub } = supabase.storage
.from(attachment.bucket)
.getPublicUrl(attachment.file_path);
return pub?.publicUrl || null;
} catch { return null; }
}
function docKindIcon(kind) {
switch (kind) {
case "id_document": return "🪪";
case "income_proof": return "💰";
default: return "📄";
}
}
function docKindLabel(kind) {
const lang = getLang();
switch (kind) {
case "id_document": return lang === "de" ? t("adminIdDoc") : t("adminIdDocEn");
case "income_proof": return lang === "de" ? t("adminIncomeDoc") : t("adminIncomeDocEn");
default: return lang === "de" ? t("adminOtherDoc") : t("adminOtherDocEn");
}
}
function renderDocList(docs) {
if (!docs.length) {
const lang = getLang();
return `<p class="muted" style="text-align:center;padding:1.5rem 0;">${lang === "de" ? t("adminNoDocuments") : t("adminNoDocumentsEn")}</p>`;
}
let html = "";
for (const d of docs) {
html += `
<div class="doc-item">
<div class="doc-info">
<span class="doc-icon">${docKindIcon(d.kind)}</span>
<div>
<strong>${esc(d.file_name)}</strong>
<div class="muted">${docKindLabel(d.kind)} · ${fmtDate(d.created_at)}</div>
</div>
</div>
<a class="btn small ghost" href="#" data-download="${d.file_path}" title="${t("adminDownload")}">⬇</a>
</div>`;
}
return html;
}
// ----- LEAD DIALOG (tabbed) -----
const leadTabOrder = ["general", "pricing", "documents", "notes"];
const leadTabLabels = {
general: () => getLang() === "de" ? t("adminTabGeneral") : t("adminTabGeneralEn"),
pricing: () => getLang() === "de" ? t("adminTabPricing") : t("adminTabPricingEn"),
documents: () => getLang() === "de" ? t("adminTabDocuments") : t("adminTabDocumentsEn"),
notes: () => getLang() === "de" ? t("adminTabNotes") : t("adminTabNotesEn"),
};
async function openLead(id) {
const l = state.leads.find(x => x.id === id);
if (!l) return;
leadDialogTitle.textContent = `${l.name} · ${l.status}`;
leadDialogBody.innerHTML = `
<dl class="kv">
<dt>Eingang</dt><dd>${fmtDate(l.created_at)}</dd>
<dt>E-Mail</dt><dd><a href="mailto:${attr(l.email)}">${esc(l.email)}</a></dd>
<dt>Telefon</dt><dd>${esc(l.phone || "")}</dd>
<dt>Fahrzeug</dt><dd>${esc(l.vehicle_label || "—")}</dd>
<dt>Zeitraum</dt><dd>${esc(l.date_from || "—")}${esc(l.date_to || "—")}</dd>
<dt>Nachricht</dt><dd style="white-space:pre-wrap;">${esc(l.message || "—")}</dd>
<dt>Status</dt><dd><span class="pill pill-${esc(l.status)}">${esc(l.status)}</span></dd>
<dt>Notiz</dt><dd><textarea id="leadNote" rows="3" style="width:100%;">${esc(l.admin_notes || "")}</textarea></dd>
</dl>
<div style="display:flex;gap:0.5rem;justify-content:flex-end;margin-top:0.8rem;">
${l.is_active ? `
<button class="btn danger" id="dlgDisq">${t("adminReject")}</button>
<button class="btn" id="dlgQual">${t("adminQualify")}</button>
` : `<button class="btn ghost" id="dlgReopen">${t("adminReopen")}</button>`}
</div>`;
// Build tabs
leadDialogTabs.innerHTML = leadTabOrder.map((tab, i) =>
`<button class="lead-tab${i === 0 ? " active" : ""}" data-lead-tab="${tab}">${leadTabLabels[tab]()}</button>`
).join("");
// Render first tab
await renderLeadTab("general", l);
leadDialog.showModal();
const note = () => document.querySelector("#leadNote").value;
document.querySelector("#dlgQual")?.addEventListener("click", () => qualifyLead(l.id, note()));
document.querySelector("#dlgDisq")?.addEventListener("click", () => disqualifyLead(l.id, note()));
document.querySelector("#dlgReopen")?.addEventListener("click", () => reopenLead(l.id));
// Tab switching
leadDialogTabs.querySelectorAll(".lead-tab").forEach(btn => {
btn.addEventListener("click", () => {
leadDialogTabs.querySelectorAll(".lead-tab").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
renderLeadTab(btn.dataset.leadTab, l);
});
});
// Download handlers
leadDialogBody.querySelectorAll("[data-download]").forEach(btn => {
btn.addEventListener("click", async (e) => {
e.preventDefault();
const path = btn.dataset.download;
const { data: pub } = supabase.storage.from("customer-documents").getPublicUrl(path);
if (pub?.publicUrl) window.open(pub.publicUrl, "_blank");
});
});
leadDialogClose.addEventListener("click", () => leadDialog.close(), { once: true });
}
async function renderLeadTab(tab, l) {
if (tab === "general") {
leadDialogBody.innerHTML = `
<dl class="kv">
<dt>${t("adminReceived")}</dt><dd>${fmtDate(l.created_at)}</dd>
<dt>E-Mail</dt><dd><a href="mailto:${attr(l.email)}">${esc(l.email)}</a></dd>
<dt>${t("adminPhone")}</dt><dd>${esc(l.phone || "—")}</dd>
<dt>Fahrzeug</dt><dd>${esc(l.vehicle_label || "—")}</dd>
<dt>${t("adminPeriod")}</dt><dd>${esc(l.date_from || "—")}${esc(l.date_to || "—")}${l.total_days ? " (" + l.total_days + " " + (getLang() === "de" ? t("bpfDays") : t("bpfDaysEn")) + ")" : ""}</dd>
<dt>Quelle</dt><dd>${esc(l.source || "website")}</dd>
<dt>${t("adminStatus")}</dt><dd><span class="pill pill-${esc(l.status)}">${esc(l.status)}</span></dd>
<dt>${t("adminNote")}</dt><dd><textarea id="leadNote" rows="3" style="width:100%;resize:vertical;">${esc(l.admin_notes || "")}</textarea></dd>
</dl>`;
// Re-bind note save
const noteArea = document.querySelector("#leadNote");
const saveNoteBtn = document.createElement("button");
saveNoteBtn.className = "btn small";
saveNoteBtn.textContent = t("adminSave");
saveNoteBtn.addEventListener("click", async () => {
const { error } = await supabase.from("leads").update({ admin_notes: noteArea.value }).eq("id", l.id);
if (error) { alert(error.message); }
else { saveNoteBtn.textContent = "✓"; setTimeout(() => { saveNoteBtn.textContent = t("adminSave"); }, 1500); }
});
leadDialogBody.appendChild(saveNoteBtn);
} else if (tab === "pricing") {
const daily = l.daily_subtotal || 0;
const weekend = l.weekend_subtotal || 0;
const sub = l.subtotal_eur || 0;
const vat = l.vat_eur || 0;
const total = l.total_eur || 0;
const deposit = l.deposit_eur || 0;
const lang = getLang();
leadDialogBody.innerHTML = `
<div class="pricing-card">
<div class="price-row"><span>${lang === "de" ? t("adminWeekdays") : t("adminWeekdaysEn")} (${l.weekday_count || 0} ×${l.daily_subtotal && l.weekday_count ? Math.round(daily / l.weekday_count) : "—"})</span><span>€ ${daily.toLocaleString("de-DE")}</span></div>
<div class="price-row"><span>${lang === "de" ? t("adminWeekendRateLabel") : t("adminWeekendRateLabelEn")} (${l.weekend_day_count || 0} ×${l.weekend_subtotal && l.weekend_day_count ? Math.round(weekend / l.weekend_day_count) : "—"})</span><span>€ ${weekend.toLocaleString("de-DE")}</span></div>
<div class="price-row divider"><span>${lang === "de" ? t("adminSubtotalLabel") : t("adminSubtotalLabelEn")}</span><span>€ ${sub.toLocaleString("de-DE")}</span></div>
<div class="price-row muted"><span>${lang === "de" ? t("adminVatLabel") : t("adminVatLabelEn")}</span><span>€ ${vat.toLocaleString("de-DE")}</span></div>
<div class="price-row total"><span>${lang === "de" ? t("adminTotalLabel") : t("adminTotalLabelEn")}</span><span>€ ${total.toLocaleString("de-DE")}</span></div>
<div class="price-row muted" style="margin-top:0.8rem;"><span>${lang === "de" ? t("adminDepositLabel") : t("adminDepositLabelEn")}</span><span>€ ${deposit.toLocaleString("de-DE")}</span></div>
<div class="price-row muted"><span>${lang === "de" ? t("adminIncludedKmLabel") : t("adminIncludedKmLabelEn")}</span><span>${((l.weekday_count || 0) * (state.vehicleMap.get(l.vehicle_id)?.max_daily_km || 150) + (l.weekend_day_count || 0) * (state.vehicleMap.get(l.vehicle_id)?.max_km_weekend || state.vehicleMap.get(l.vehicle_id)?.max_daily_km || 150))} km</span></div>
<div class="price-row muted"><span>${lang === "de" ? t("adminTotalDaysLabel") : t("adminTotalDaysLabelEn")}</span><span>${l.total_days || 0}</span></div>
</div>`;
} else if (tab === "documents") {
const docs = await loadLeadAttachments(l.id);
leadDialogBody.innerHTML = renderDocList(docs);
// Re-bind downloads
leadDialogBody.querySelectorAll("[data-download]").forEach(btn => {
btn.addEventListener("click", async (e) => {
e.preventDefault();
const path = btn.dataset.download;
const { data: pub } = supabase.storage.from("customer-documents").getPublicUrl(path);
if (pub?.publicUrl) window.open(pub.publicUrl, "_blank");
});
});
} else if (tab === "notes") {
const lang = getLang();
leadDialogBody.innerHTML = `
<textarea id="leadNoteFull" rows="8" style="width:100%;resize:vertical;font-family:inherit;">${esc(l.admin_notes || "")}</textarea>
<div style="display:flex;justify-content:flex-end;margin-top:0.8rem;">
<button class="btn small" id="saveNoteFull">${lang === "de" ? t("adminSave") : t("adminSaveEn")}</button>
</div>`;
document.querySelector("#saveNoteFull").addEventListener("click", async () => {
const { error } = await supabase.from("leads").update({ admin_notes: document.querySelector("#leadNoteFull").value }).eq("id", l.id);
if (error) { alert(error.message); }
else { document.querySelector("#saveNoteFull").textContent = "✓"; setTimeout(() => { document.querySelector("#saveNoteFull").textContent = t("adminSave"); }, 1500); }
});
}
// Footer buttons
if (l.is_active) {
leadDialogFooter.innerHTML = `
<div style="display:flex;gap:0.5rem;justify-content:flex-end;">
<button class="btn danger" id="dlgDisq">${t("adminReject")}</button>
<button class="btn" id="dlgQual">${t("adminQualify")}</button>
</div>`;
document.querySelector("#dlgQual")?.addEventListener("click", () => {
const note = document.querySelector("#leadNote")?.value || document.querySelector("#leadNoteFull")?.value || "";
qualifyLead(l.id, note);
});
document.querySelector("#dlgDisq")?.addEventListener("click", () => {
const note = document.querySelector("#leadNote")?.value || document.querySelector("#leadNoteFull")?.value || "";
disqualifyLead(l.id, note);
});
} else {
leadDialogFooter.innerHTML = `
<div style="display:flex;justify-content:flex-end;">
<button class="btn ghost" id="dlgReopen">${t("adminReopen")}</button>
</div>`;
document.querySelector("#dlgReopen")?.addEventListener("click", () => reopenLead(l.id));
}
}
leadDialogClose.addEventListener("click", () => leadDialog.close());
async function qualifyLead(id, notes = "") {
const { error } = await supabase.rpc("qualify_lead", { p_lead_id: id, p_notes: notes });
@@ -444,6 +625,192 @@ async function reopenLead(id) {
// =========================================================================
// CUSTOMERS
// =========================================================================
// ----- CUSTOMER LIFETIME VALUE -----
function calcCustomerLifetimeValue(customer) {
let total = 0;
for (const l of state.leads) {
if (l.email.toLowerCase() === customer.email.toLowerCase()) {
total += l.total_eur || 0;
}
}
return total;
}
// ----- CUSTOMER ATTACHMENTS -----
async function loadCustomerAttachments(customerId) {
const { data, error } = await supabase
.from("customer_attachments")
.select("*")
.eq("customer_id", customerId)
.order("created_at", { ascending: false });
if (error) { console.error(error); return []; }
return data || [];
}
// ----- ORDER HISTORY -----
async function loadOrderHistory(email) {
const { data, error } = await supabase
.from("leads")
.select("*")
.eq("email", email)
.order("created_at", { ascending: false });
if (error) { console.error(error); return []; }
return data || [];
}
// ----- CUSTOMER DIALOG (tabbed) -----
const customerTabOrder = ["info", "documents", "orderHistory"];
const customerTabLabels = {
info: () => getLang() === "de" ? t("adminTabGeneral") : t("adminTabGeneralEn"),
documents: () => getLang() === "de" ? t("adminTabDocuments") : t("adminTabDocumentsEn"),
orderHistory: () => getLang() === "de" ? t("adminTabOrderHistory") : t("adminTabOrderHistory"),
};
async function openCustomer(id) {
const c = state.customers.find(x => x.id === id);
if (!c) return;
customerDialogTitle.textContent = `${c.name} · ${c.status}`;
// Build tabs
customerDialogTabs.innerHTML = customerTabOrder.map((tab, i) =>
`<button class="customer-tab${i === 0 ? " active" : ""}" data-customer-tab="${tab}">${customerTabLabels[tab]()}</button>`
).join("");
// Render first tab
await renderCustomerTab("info", c);
customerDialog.showModal();
// Tab switching
customerDialogTabs.querySelectorAll(".customer-tab").forEach(btn => {
btn.addEventListener("click", () => {
customerDialogTabs.querySelectorAll(".customer-tab").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
renderCustomerTab(btn.dataset.customerTab, c);
});
});
// Download handlers
customerDialogBody.querySelectorAll("[data-download]").forEach(btn => {
btn.addEventListener("click", async (e) => {
e.preventDefault();
const path = btn.dataset.download;
const { data: pub } = supabase.storage.from("customer-documents").getPublicUrl(path);
if (pub?.publicUrl) window.open(pub.publicUrl, "_blank");
});
});
customerDialogClose.addEventListener("click", () => customerDialog.close(), { once: true });
}
async function renderCustomerTab(tab, c) {
if (tab === "info") {
const lang = getLang();
customerDialogBody.innerHTML = `
<dl class="kv">
<dt>${lang === "de" ? "Name" : "Name"}</dt><dd><strong>${esc(c.name)}</strong></dd>
<dt>E-Mail</dt><dd><a href="mailto:${attr(c.email)}">${esc(c.email)}</a></dd>
<dt>${lang === "de" ? t("adminPhone") : "Phone"}</dt><dd>${esc(c.phone || "—")}</dd>
<dt>${lang === "de" ? t("adminFirstContacted") : t("adminFirstContactedEn")}</dt><dd>${fmtDate(c.first_contacted_at)}</dd>
<dt>${lang === "de" ? "Status" : "Status"}</dt><dd><span class="pill pill-${esc(c.status)}">${esc(c.status)}</span></dd>
<dt>${lang === "de" ? t("adminNote") : t("adminNoteEn")}</dt><dd><textarea id="custNote" rows="4" style="width:100%;resize:vertical;">${esc(c.notes || "")}</textarea></dd>
</dl>`;
const noteArea = document.querySelector("#custNote");
const saveBtn = document.createElement("button");
saveBtn.className = "btn small";
saveBtn.textContent = t("adminSave");
saveBtn.addEventListener("click", async () => {
const { error } = await supabase.from("customers").update({ notes: noteArea.value }).eq("id", c.id);
if (error) { alert(error.message); }
else { saveBtn.textContent = "✓"; setTimeout(() => { saveBtn.textContent = t("adminSave"); }, 1500); }
});
customerDialogBody.appendChild(saveBtn);
} else if (tab === "documents") {
// Show customer_attachments + inherited lead_attachments
const custDocs = await loadCustomerAttachments(c.id);
let leadDocs = [];
if (c.lead_id) {
const leadDocsRaw = await loadLeadAttachments(c.lead_id);
// Filter out docs already in custDocs (by file_path)
const custPaths = new Set(custDocs.map(d => d.file_path));
leadDocs = leadDocsRaw.filter(d => !custPaths.has(d.file_path));
}
let html = "";
if (leadDocs.length) {
html += `<h4 style="margin:0 0 0.8rem;font-size:0.9rem;color:var(--muted);">${c.lead_id ? "─ Von Lead übernommen" : ""}</h4>`;
html += renderDocList(leadDocs);
}
if (custDocs.length) {
html += `<h4 style="margin:1.2rem 0 0.8rem;font-size:0.9rem;color:var(--muted);">─ Direkt hochgeladen</h4>`;
html += renderDocList(custDocs);
}
if (!leadDocs.length && !custDocs.length) {
html = `<p class="muted" style="text-align:center;padding:1.5rem 0;">${getLang() === "de" ? t("adminNoDocuments") : t("adminNoDocumentsEn")}</p>`;
}
customerDialogBody.innerHTML = html;
// Re-bind downloads
customerDialogBody.querySelectorAll("[data-download]").forEach(btn => {
btn.addEventListener("click", async (e) => {
e.preventDefault();
const path = btn.dataset.download;
const { data: pub } = supabase.storage.from("customer-documents").getPublicUrl(path);
if (pub?.publicUrl) window.open(pub.publicUrl, "_blank");
});
});
} else if (tab === "orderHistory") {
const orders = await loadOrderHistory(c.email);
const lang = getLang();
let html = "";
if (orders.length) {
html += `<table class="admin-table" style="margin-bottom:1rem;">
<thead><tr>
<th>${lang === "de" ? "Eingang" : "Received"}</th>
<th>Fahrzeug</th>
<th>${lang === "de" ? "Zeitraum" : "Period"}</th>
<th style="text-align:right;">${lang === "de" ? t("adminTotalPrice") : t("adminTotalPrice")}</th>
<th>${lang === "de" ? "Status" : "Status"}</th>
</tr></thead>
<tbody>`;
for (const o of orders) {
const total = o.total_eur || 0;
html += `<tr>
<td>${fmtDate(o.created_at)}</td>
<td>${esc(o.vehicle_label || "—")}</td>
<td>${esc(o.date_from || "—")}${esc(o.date_to || "—")}</td>
<td style="text-align:right;font-weight:600;color:var(--accent-strong);">${total > 0 ? "€ " + total.toLocaleString("de-DE") : "—"}</td>
<td><span class="pill pill-${esc(o.status)}">${esc(o.status)}</span></td>
</tr>`;
}
html += `</tbody></table>`;
const lifetime = calcCustomerLifetimeValue(c);
html += `<div class="pricing-card" style="margin-top:0.5rem;">
<div class="price-row total"><span>${lang === "de" ? t("adminLifetimeValue") : t("adminLifetimeValueEn")}</span><span>€ ${lifetime.toLocaleString("de-DE")}</span></div>
</div>`;
} else {
html = `<p class="muted" style="text-align:center;padding:2rem 0;">Keine Buchungen gefunden.</p>`;
}
customerDialogBody.innerHTML = html;
}
// Footer
customerDialogFooter.innerHTML = `
<div style="display:flex;justify-content:flex-end;">
<button class="btn ghost" id="dlgCustToggle" data-status="${c.status}">
${c.status === "active" ? t("adminSetInactive") : t("adminSetActive")}
</button>
</div>`;
document.querySelector("#dlgCustToggle")?.addEventListener("click", async () => {
const next = c.status === "active" ? "inactive" : "active";
const { error } = await supabase.from("customers").update({ status: next }).eq("id", c.id);
if (error) { alert(error.message); }
else {
customerDialog.close();
await loadCustomers();
renderCustomers();
}
});
}
async function loadCustomers() {
const { data, error } = await supabase
.from("customers")
@@ -458,20 +825,25 @@ function renderCustomers() {
customersEmpty.style.display = state.customers.length ? "none" : "block";
customersTableBody.innerHTML = "";
for (const c of state.customers) {
const lifetime = calcCustomerLifetimeValue(c);
const lifetimeStr = lifetime > 0 ? "€ " + lifetime.toLocaleString("de-DE") : "—";
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${fmtDate(c.first_contacted_at)}</td>
<td><strong>${esc(c.name)}</strong><br /><span class="muted">${esc(c.email)}</span></td>
<td>${esc(c.phone || "—")}</td>
<td><code class="muted">${esc(c.lead_id?.slice(0, 8) || "—")}</code></td>
<td style="font-weight:600;color:var(--accent-strong);">${lifetimeStr}</td>
<td><span class="pill pill-${esc(c.status)}">${esc(c.status)}</span></td>
<td style="white-space:nowrap;">
<button class="btn small ghost" data-open-cust="${c.id}">${t("adminDetails")}</button>
<button class="btn small ghost" data-toggle="${c.id}" data-status="${c.status}">
${c.status === "active" ? t("adminSetInactive") : t("adminSetActive")}
</button>
</td>`;
customersTableBody.appendChild(tr);
}
customersTableBody.querySelectorAll("[data-open-cust]").forEach(b => b.addEventListener("click", () => openCustomer(b.dataset.openCust)));
customersTableBody.querySelectorAll("[data-toggle]").forEach(b => b.addEventListener("click", async () => {
const id = b.dataset.toggle;
const next = b.dataset.status === "active" ? "inactive" : "active";