import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4";
import { getLang, setLang, t, applyI18n } from "./i18n.js";
const SUPA_URL = window.MCCARS_CONFIG?.SUPABASE_URL ?? "";
const SUPA_KEY = window.MCCARS_CONFIG?.SUPABASE_ANON_KEY || "";
// Only the public anon key lives here. Write access is gated by RLS policies
// that require an `authenticated` JWT obtained via signInWithPassword.
const supabase = createClient(SUPA_URL, SUPA_KEY, {
auth: { persistSession: true, storageKey: "mccars.auth" },
});
// ----- DOM -----
const loginView = document.querySelector("#loginView");
const adminView = document.querySelector("#adminView");
const langToggle = document.querySelector(".lang-toggle");
const rotateView = document.querySelector("#rotateView");
const loginForm = document.querySelector("#loginForm");
const loginError = document.querySelector("#loginError");
const rotateForm = document.querySelector("#rotateForm");
const rotateError = document.querySelector("#rotateError");
const logoutBtn = document.querySelector("#logoutBtn");
const changePwBtn = document.querySelector("#changePwBtn");
const adminWho = document.querySelector("#adminWho");
const leadsTableBody = document.querySelector("#leadsTable tbody");
const leadsEmpty = document.querySelector("#leadsEmpty");
const leadsBadge = document.querySelector("#leadsBadge");
const customersTableBody = document.querySelector("#customersTable tbody");
const customersEmpty = document.querySelector("#customersEmpty");
const customersBadge = document.querySelector("#customersBadge");
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");
const formTitle = document.querySelector("#formTitle");
const saveBtn = document.querySelector("#saveBtn");
const resetBtn = document.querySelector("#resetBtn");
const photoInput = document.querySelector("#photoInput");
const photoPreview = document.querySelector("#photoPreview");
const tableBody = document.querySelector("#adminTable tbody");
// ----- State -----
const state = {
user: null,
loginPassword: null, // captured at signInWithPassword to block same-value rotation
leadView: "active", // "active" | "inactive"
leads: [],
customers: [],
vehicles: [],
vehicleMap: new Map(),
currentPhotoPath: null,
realtimeChannel: null,
forcedRotation: false,
};
// =========================================================================
// AUTH FLOW
// =========================================================================
async function bootstrap() {
applyI18n();
const { data: { session } } = await supabase.auth.getSession();
if (session) {
// Always fetch fresh user from server so metadata (must_change_password) is current.
const { data: { user }, error } = await supabase.auth.getUser();
if (error || !user) { await supabase.auth.signOut(); show("login"); return; }
await onAuthenticated(user);
} else {
show("login");
}
}
loginForm.addEventListener("submit", async (e) => {
e.preventDefault();
loginError.textContent = "";
const fd = new FormData(loginForm);
const pw = fd.get("password");
const { data, error } = await supabase.auth.signInWithPassword({
email: fd.get("email"),
password: pw,
});
if (error) { loginError.textContent = error.message; return; }
state.loginPassword = pw;
await onAuthenticated(data.user);
});
logoutBtn.addEventListener("click", async () => {
if (state.realtimeChannel) await supabase.removeChannel(state.realtimeChannel);
await supabase.auth.signOut();
location.reload();
});
changePwBtn.addEventListener("click", () => {
state.forcedRotation = false;
show("rotate");
});
rotateForm.addEventListener("submit", async (e) => {
e.preventDefault();
rotateError.textContent = "";
const fd = new FormData(rotateForm);
const pw1 = fd.get("pw1");
const pw2 = fd.get("pw2");
if (pw1 !== pw2) { rotateError.textContent = "Passwörter stimmen nicht überein."; return; }
if (pw1.length < 10) { rotateError.textContent = "Mindestens 10 Zeichen."; return; }
if (state.loginPassword && pw1 === state.loginPassword) {
rotateError.textContent = "Neues Passwort muss sich vom alten unterscheiden.";
return;
}
const { error } = await supabase.auth.updateUser({
password: pw1,
data: { must_change_password: false },
});
if (error) { rotateError.textContent = error.message; return; }
state.loginPassword = pw1;
rotateForm.reset();
if (state.forcedRotation) {
state.forcedRotation = false;
await enterAdmin();
} else {
show("admin");
}
});
async function onAuthenticated(user) {
state.user = user;
adminWho.textContent = user.email;
// Force rotation path (first-login bootstrap)
const meta = user.user_metadata || {};
if (meta.must_change_password) {
state.forcedRotation = true;
show("rotate");
return;
}
await enterAdmin();
}
async function enterAdmin() {
show("admin");
await Promise.all([loadVehicles(), loadLeads(), loadCustomers()]);
renderActiveTab();
attachRealtime();
}
function show(which) {
loginView.style.display = which === "login" ? "block" : "none";
rotateView.style.display = which === "rotate" ? "block" : "none";
adminView.style.display = which === "admin" ? "block" : "none";
}
// =========================================================================
// TABS
// =========================================================================
const tabButtons = document.querySelectorAll(".admin-tabs .tab");
const tabPanels = {
leads: document.querySelector("#tab-leads"),
customers: document.querySelector("#tab-customers"),
vehicles: document.querySelector("#tab-vehicles"),
};
let activeTab = "leads";
tabButtons.forEach(btn => btn.addEventListener("click", () => {
activeTab = btn.dataset.tab;
tabButtons.forEach(b => b.classList.toggle("active", b === btn));
for (const [k, el] of Object.entries(tabPanels)) el.style.display = (k === activeTab) ? "block" : "none";
renderActiveTab();
}));
function renderActiveTab() {
if (activeTab === "leads") renderLeads();
if (activeTab === "customers") renderCustomers();
if (activeTab === "vehicles") renderVehicles();
}
// Sub-tabs (active/inactive leads)
document.querySelectorAll(".sub-tab").forEach(btn => btn.addEventListener("click", () => {
state.leadView = btn.dataset.lview;
document.querySelectorAll(".sub-tab").forEach(b => b.classList.toggle("active", b === btn));
renderLeads();
}));
// =========================================================================
// VEHICLES (existing CRUD preserved)
// =========================================================================
async function loadVehicles() {
const { data, error } = await supabase
.from("vehicles")
.select("*")
.order("sort_order", { ascending: true });
if (error) { console.error(error); return; }
state.vehicles = data || [];
state.vehicleMap = new Map(state.vehicles.map(v => [v.id, v]));
}
function renderVehicles() {
tableBody.innerHTML = "";
for (const v of state.vehicles) {
const tr = document.createElement("tr");
tr.innerHTML = `
|
${esc(v.brand)} ${esc(v.model)} |
€ ${v.daily_price_eur} |
${v.is_active ? "✅" : "—"} |
| `;
tableBody.appendChild(tr);
}
tableBody.querySelectorAll("[data-edit]").forEach(b => b.addEventListener("click", () => loadForEdit(b.dataset.edit)));
tableBody.querySelectorAll("[data-del]").forEach(b => b.addEventListener("click", () => deleteVehicle(b.dataset.del)));
}
function loadForEdit(id) {
const v = state.vehicleMap.get(id);
if (!v) return;
formTitle.textContent = `Fahrzeug bearbeiten · ${v.brand} ${v.model}`;
vehicleForm.vid.value = v.id;
vehicleForm.brand.value = v.brand;
vehicleForm.model.value = v.model;
vehicleForm.power_hp.value = v.power_hp;
vehicleForm.top_speed_kmh.value = v.top_speed_kmh;
vehicleForm.acceleration.value = v.acceleration;
vehicleForm.seats.value = v.seats;
vehicleForm.daily_price_eur.value = v.daily_price_eur;
vehicleForm.weekend_price_eur.value = v.weekend_price_eur || 0;
vehicleForm.max_daily_km.value = v.max_daily_km || 150;
vehicleForm.kaution_eur.value = v.kaution_eur || 5000;
vehicleForm.max_km_weekend.value = v.max_km_weekend || '';
vehicleForm.sort_order.value = v.sort_order;
vehicleForm.location.value = v.location;
vehicleForm.description_de.value = v.description_de;
vehicleForm.description_en.value = v.description_en;
vehicleForm.photo_url.value = v.photo_url;
vehicleForm.is_active.checked = v.is_active;
state.currentPhotoPath = v.photo_path || null;
updatePreview(v.photo_url);
window.scrollTo({ top: 0, behavior: "smooth" });
}
resetBtn.addEventListener("click", () => {
vehicleForm.reset();
vehicleForm.vid.value = "";
vehicleForm.is_active.checked = true;
vehicleForm.sort_order.value = 100;
vehicleForm.location.value = "Steiermark (TBD)";
vehicleForm.seats.value = 2;
vehicleForm.max_daily_km.value = 150;
vehicleForm.weekend_price_eur.value = 0;
vehicleForm.kaution_eur.value = 5000;
vehicleForm.max_km_weekend.value = '';
state.currentPhotoPath = null;
updatePreview("");
formTitle.textContent = "Neues Fahrzeug";
formFeedback.textContent = "";
});
vehicleForm.addEventListener("submit", async (e) => {
e.preventDefault();
saveBtn.disabled = true;
formFeedback.className = "form-feedback";
formFeedback.textContent = "Saving...";
try {
const fd = new FormData(vehicleForm);
const payload = {
brand: fd.get("brand"),
model: fd.get("model"),
power_hp: +fd.get("power_hp") || 0,
top_speed_kmh: +fd.get("top_speed_kmh") || 0,
acceleration: fd.get("acceleration") || "",
seats: +fd.get("seats") || 2,
daily_price_eur: +fd.get("daily_price_eur") || 0,
weekend_price_eur: +fd.get("weekend_price_eur") || 0,
max_daily_km: +fd.get("max_daily_km") || 150,
kaution_eur: +fd.get("kaution_eur") || 5000,
max_km_weekend: fd.get("max_km_weekend") ? +fd.get("max_km_weekend") : null,
sort_order: +fd.get("sort_order") || 100,
location: fd.get("location") || "Steiermark (TBD)",
description_de: fd.get("description_de") || "",
description_en: fd.get("description_en") || "",
photo_url: fd.get("photo_url") || "",
photo_path: state.currentPhotoPath,
is_active: !!fd.get("is_active"),
};
const vid = fd.get("vid");
const { error } = vid
? await supabase.from("vehicles").update(payload).eq("id", vid)
: await supabase.from("vehicles").insert(payload);
if (error) throw error;
formFeedback.textContent = "Gespeichert.";
await loadVehicles();
renderVehicles();
if (!vid) resetBtn.click();
} catch (err) {
formFeedback.className = "form-feedback error";
formFeedback.textContent = err.message || String(err);
} finally {
saveBtn.disabled = false;
}
});
async function deleteVehicle(id) {
const v = state.vehicleMap.get(id);
if (!v) return;
if (!confirm(`Delete ${v.brand} ${v.model}?`)) return;
if (v.photo_path) await supabase.storage.from("vehicle-photos").remove([v.photo_path]);
const { error } = await supabase.from("vehicles").delete().eq("id", id);
if (error) { alert(error.message); return; }
await loadVehicles();
renderVehicles();
}
// Photo upload
photoInput.addEventListener("change", async () => {
const file = photoInput.files?.[0];
if (!file) return;
formFeedback.className = "form-feedback";
formFeedback.textContent = "Uploading photo...";
try {
// Delete old photo if exists
if (state.currentPhotoPath) {
await supabase.storage.from("vehicle-photos").remove([state.currentPhotoPath]);
}
const ext = (file.name.split(".").pop() || "jpg").toLowerCase();
const path = `${crypto.randomUUID()}.${ext}`;
const { error: upErr } = await supabase.storage
.from("vehicle-photos")
.upload(path, file, { contentType: file.type, upsert: true });
if (upErr) throw upErr;
const { data: pub } = supabase.storage.from("vehicle-photos").getPublicUrl(path);
state.currentPhotoPath = path;
vehicleForm.photo_url.value = pub.publicUrl;
updatePreview(pub.publicUrl);
formFeedback.textContent = "Upload ok.";
} catch (err) {
formFeedback.className = "form-feedback error";
formFeedback.textContent = err.message || String(err);
}
});
function updatePreview(url) { photoPreview.style.backgroundImage = url ? `url('${url}')` : ""; }
// =========================================================================
// LEADS
// =========================================================================
async function loadLeads() {
const { data, error } = await supabase
.from("leads")
.select("*")
.order("created_at", { ascending: false });
if (error) { console.error(error); return; }
state.leads = data || [];
updateBadges();
}
function renderLeads() {
const wantActive = state.leadView === "active";
const rows = state.leads.filter(l => l.is_active === wantActive);
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 = `
${fmtDate(l.created_at)} |
${esc(l.name)} ${esc(l.email)}${l.phone ? " · " + esc(l.phone) : ""} |
${esc(l.vehicle_label || "—")} |
${esc(l.date_from || "—")} → ${esc(l.date_to || "—")} |
${totalStr} |
${esc(l.status)} |
${wantActive ? `
` : `
`}
| `;
leadsTableBody.appendChild(tr);
}
leadsTableBody.querySelectorAll("[data-open]").forEach(b => b.addEventListener("click", () => openLead(b.dataset.open)));
leadsTableBody.querySelectorAll("[data-qual]").forEach(b => b.addEventListener("click", () => qualifyLead(b.dataset.qual)));
leadsTableBody.querySelectorAll("[data-disq]").forEach(b => b.addEventListener("click", () => disqualifyLead(b.dataset.disq)));
leadsTableBody.querySelectorAll("[data-reopen]").forEach(b => b.addEventListener("click", () => reopenLead(b.dataset.reopen)));
}
// ----- 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 `${lang === "de" ? t("adminNoDocuments") : t("adminNoDocumentsEn")}
`;
}
let html = "";
for (const d of docs) {
html += `
${docKindIcon(d.kind)}
${esc(d.file_name)}
${docKindLabel(d.kind)} · ${fmtDate(d.created_at)}
⬇
`;
}
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}`;
// Build tabs
leadDialogTabs.innerHTML = leadTabOrder.map((tab, i) =>
``
).join("");
// Render first tab
await renderLeadTab("general", l);
leadDialog.showModal();
// 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 = `
- ${t("adminReceived")}
- ${fmtDate(l.created_at)}
- E-Mail
- ${esc(l.email)}
- ${t("adminPhone")}
- ${esc(l.phone || "—")}
- Fahrzeug
- ${esc(l.vehicle_label || "—")}
- ${t("adminPeriod")}
- ${esc(l.date_from || "—")} → ${esc(l.date_to || "—")}${l.total_days ? " (" + l.total_days + " " + (getLang() === "de" ? t("bpfDays") : t("bpfDaysEn")) + ")" : ""}
- Quelle
- ${esc(l.source || "website")}
- ${t("adminStatus")}
- ${esc(l.status)}
- ${t("adminNote")}
`;
// 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 = `
${lang === "de" ? t("adminWeekdays") : t("adminWeekdaysEn")} (${l.weekday_count || 0} × € ${l.daily_subtotal && l.weekday_count ? Math.round(daily / l.weekday_count) : "—"})€ ${daily.toLocaleString("de-DE")}
${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) : "—"})€ ${weekend.toLocaleString("de-DE")}
${lang === "de" ? t("adminSubtotalLabel") : t("adminSubtotalLabelEn")}€ ${sub.toLocaleString("de-DE")}
${lang === "de" ? t("adminVatLabel") : t("adminVatLabelEn")}€ ${vat.toLocaleString("de-DE")}
${lang === "de" ? t("adminTotalLabel") : t("adminTotalLabelEn")}€ ${total.toLocaleString("de-DE")}
${lang === "de" ? t("adminDepositLabel") : t("adminDepositLabelEn")}€ ${deposit.toLocaleString("de-DE")}
${lang === "de" ? t("adminIncludedKmLabel") : t("adminIncludedKmLabelEn")}${((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
${lang === "de" ? t("adminTotalDaysLabel") : t("adminTotalDaysLabelEn")}${l.total_days || 0}
`;
} 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 = `
`;
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 = `
`;
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 = `
`;
document.querySelector("#dlgReopen")?.addEventListener("click", () => reopenLead(l.id));
}
}
async function qualifyLead(id, notes = "") {
const { error } = await supabase.rpc("qualify_lead", { p_lead_id: id, p_notes: notes });
if (error) { alert(error.message); return; }
leadDialog.open && leadDialog.close();
// Realtime will refresh; still trigger a quick reload for responsiveness.
await Promise.all([loadLeads(), loadCustomers()]);
renderActiveTab();
}
async function disqualifyLead(id, notes = "") {
const { error } = await supabase.rpc("disqualify_lead", { p_lead_id: id, p_notes: notes });
if (error) { alert(error.message); return; }
leadDialog.open && leadDialog.close();
await loadLeads();
renderLeads();
updateBadges();
}
async function reopenLead(id) {
if (!confirm("Lead wieder in 'Aktive' verschieben und Kunde ggf. entfernen?")) return;
const { error } = await supabase.rpc("reopen_lead", { p_lead_id: id });
if (error) { alert(error.message); return; }
leadDialog.open && leadDialog.close();
await Promise.all([loadLeads(), loadCustomers()]);
renderActiveTab();
}
// =========================================================================
// 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) =>
``
).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 = `
- ${lang === "de" ? "Name" : "Name"}
- ${esc(c.name)}
- E-Mail
- ${esc(c.email)}
- ${lang === "de" ? t("adminPhone") : "Phone"}
- ${esc(c.phone || "—")}
- ${lang === "de" ? t("adminFirstContacted") : t("adminFirstContactedEn")}
- ${fmtDate(c.first_contacted_at)}
- ${lang === "de" ? "Status" : "Status"}
- ${esc(c.status)}
- ${lang === "de" ? t("adminNote") : t("adminNoteEn")}
`;
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 += `${c.lead_id ? "─ Von Lead übernommen" : ""}
`;
html += renderDocList(leadDocs);
}
if (custDocs.length) {
html += `─ Direkt hochgeladen
`;
html += renderDocList(custDocs);
}
if (!leadDocs.length && !custDocs.length) {
html = `${getLang() === "de" ? t("adminNoDocuments") : t("adminNoDocumentsEn")}
`;
}
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 += `
| ${lang === "de" ? "Eingang" : "Received"} |
Fahrzeug |
${lang === "de" ? "Zeitraum" : "Period"} |
${lang === "de" ? t("adminTotalPrice") : t("adminTotalPrice")} |
${lang === "de" ? "Status" : "Status"} |
`;
for (const o of orders) {
const total = o.total_eur || 0;
html += `
| ${fmtDate(o.created_at)} |
${esc(o.vehicle_label || "—")} |
${esc(o.date_from || "—")} → ${esc(o.date_to || "—")} |
${total > 0 ? "€ " + total.toLocaleString("de-DE") : "—"} |
${esc(o.status)} |
`;
}
html += `
`;
const lifetime = calcCustomerLifetimeValue(c);
html += `
${lang === "de" ? t("adminLifetimeValue") : t("adminLifetimeValueEn")}€ ${lifetime.toLocaleString("de-DE")}
`;
} else {
html = `Keine Buchungen gefunden.
`;
}
customerDialogBody.innerHTML = html;
}
// Footer
customerDialogFooter.innerHTML = `
`;
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")
.select("*")
.order("created_at", { ascending: false });
if (error) { console.error(error); return; }
state.customers = data || [];
updateBadges();
}
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 = `
${fmtDate(c.first_contacted_at)} |
${esc(c.name)} ${esc(c.email)} |
${esc(c.phone || "—")} |
${esc(c.lead_id?.slice(0, 8) || "—")} |
${lifetimeStr} |
${esc(c.status)} |
| `;
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";
const { error } = await supabase.from("customers").update({ status: next }).eq("id", id);
if (error) { alert(error.message); return; }
await loadCustomers();
renderCustomers();
}));
}
function updateBadges() {
const active = state.leads.filter(l => l.is_active).length;
leadsBadge.textContent = String(active);
customersBadge.textContent = String(state.customers.length);
}
// =========================================================================
// REALTIME
// =========================================================================
function attachRealtime() {
if (state.realtimeChannel) return;
state.realtimeChannel = supabase
.channel("mccars-admin")
.on("postgres_changes", { event: "*", schema: "public", table: "leads" }, async () => { await loadLeads(); if (activeTab === "leads") renderLeads(); })
.on("postgres_changes", { event: "*", schema: "public", table: "customers" }, async () => { await loadCustomers(); if (activeTab === "customers") renderCustomers(); })
.on("postgres_changes", { event: "*", schema: "public", table: "vehicles" }, async () => { await loadVehicles(); if (activeTab === "vehicles") renderVehicles(); })
.subscribe();
}
// =========================================================================
// HELPERS
// =========================================================================
function esc(s) { return String(s ?? "").replace(/[&<>"']/g, c => ({ "&":"&","<":"<",">":">",'"':""","'":"'" })[c]); }
function attr(s) { return esc(s); }
function fmtDate(iso) {
if (!iso) return "—";
const d = new Date(iso);
return d.toLocaleString("de-AT", { dateStyle: "short", timeStyle: "short" });
}
if (langToggle) {
langToggle.addEventListener("click", () => {
const current = getLang();
setLang(current === "de" ? "en" : "de");
langToggle.textContent = getLang() === "de" ? "EN" : "DE";
applyI18n();
// Re-render JS injected text correctly
if (state.vehicles) renderVehicles();
if (state.leads) renderLeads();
if (state.customers) renderCustomers();
});
langToggle.textContent = getLang() === "de" ? "EN" : "DE";
}
bootstrap();