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 ordersTableBody = document.querySelector("#ordersTable tbody"); const ordersEmpty = document.querySelector("#ordersEmpty"); const ordersBadge = document.querySelector("#ordersBadge"); 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: [], salesOrders: [], vehicles: [], vehicleMap: new Map(), currentPhotoPath: null, realtimeChannel: null, forcedRotation: false, }; function notify(message, duration = 3000) { if (typeof window.showToast === "function") { window.showToast(message, duration); return; } showAdminPopup(message, duration); } function showAdminPopup(message, duration = 3000) { const host = document.querySelector("dialog[open]") || document.body; let popup = host.querySelector("[data-admin-notify-popup]"); if (!popup) { popup = document.createElement("div"); popup.setAttribute("data-admin-notify-popup", "1"); popup.setAttribute("role", "status"); popup.setAttribute("aria-live", "polite"); popup.style.position = host.tagName === "DIALOG" ? "absolute" : "fixed"; popup.style.left = "50%"; popup.style.top = "50%"; popup.style.transform = "translate(-50%, -50%) scale(0.96)"; popup.style.minWidth = "320px"; popup.style.maxWidth = "min(92vw, 560px)"; popup.style.padding = "1rem 1.2rem"; popup.style.borderRadius = "12px"; popup.style.border = "1px solid var(--line)"; popup.style.background = "var(--bg-card)"; popup.style.color = "var(--text)"; popup.style.boxShadow = "0 16px 40px rgba(0,0,0,0.35)"; popup.style.textAlign = "center"; popup.style.fontSize = "1rem"; popup.style.fontWeight = "600"; popup.style.opacity = "0"; popup.style.zIndex = "3000"; popup.style.pointerEvents = "none"; popup.style.transition = "opacity 0.18s ease, transform 0.18s ease"; host.appendChild(popup); } popup.textContent = message; popup.style.opacity = "1"; popup.style.transform = "translate(-50%, -50%) scale(1)"; clearTimeout(popup._hideTimer); popup._hideTimer = setTimeout(() => { popup.style.opacity = "0"; popup.style.transform = "translate(-50%, -50%) scale(0.96)"; }, duration); } // ========================================================================= // 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(), loadSalesOrders()]); setActiveTab(getTabFromHash()); 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"), orders: document.querySelector("#tab-orders"), vehicles: document.querySelector("#tab-vehicles"), settings: document.querySelector("#tab-settings"), }; let activeTab = "leads"; function setActiveTab(tab) { activeTab = tab; window.location.hash = tab; tabButtons.forEach(b => b.classList.toggle("active", b.dataset.tab === tab)); for (const [k, el] of Object.entries(tabPanels)) el.style.display = (k === activeTab) ? "block" : "none"; renderActiveTab(); } tabButtons.forEach(btn => btn.addEventListener("click", () => setActiveTab(btn.dataset.tab))); // Restore tab from URL hash on load function getTabFromHash() { const hash = window.location.hash.replace("#", ""); if (hash && tabPanels[hash]) return hash; return "leads"; } window.addEventListener("hashchange", () => { const tab = getTabFromHash(); if (tab !== activeTab) setActiveTab(tab); }); function renderActiveTab() { if (activeTab === "leads") renderLeads(); if (activeTab === "customers") renderCustomers(); if (activeTab === "orders") renderOrders(); if (activeTab === "vehicles") renderVehicles(); if (activeTab === "settings") renderSettings(); } // 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.included_km_per_day.value = v.included_km_per_day || 150; vehicleForm.kaution_eur.value = v.kaution_eur || 5000; vehicleForm.price_per_km_eur.value = v.price_per_km_eur || 1.50; 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.included_km_per_day.value = 150; vehicleForm.weekend_price_eur.value = 0; vehicleForm.kaution_eur.value = 5000; vehicleForm.price_per_km_eur.value = 1.50; 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, included_km_per_day: +fd.get("included_km_per_day") || 150, kaution_eur: +fd.get("kaution_eur") || 5000, price_per_km_eur: parseFloat(fd.get("price_per_km_eur")) || 1.50, 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 rental = rentalTypeMeta(l.rental_type); 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 || "—")} ${esc(rental.label)} ${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 openAttachmentInNewTab(filePath, bucket = "customer-documents") { const { data, error } = await supabase.storage.from(bucket).createSignedUrl(filePath, 60 * 5); if (error || !data?.signedUrl) { alert(error?.message || "Document URL could not be created."); return; } window.open(data.signedUrl, "_blank", "noopener,noreferrer"); } 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, orderLabelById = new Map()) { 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)}
${d.sales_order_id ? `
Reservation: ${esc(orderLabelById.get(d.sales_order_id) || d.sales_order_id.slice(0, 8))}
` : ""}
`; } 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); }); }); leadDialogBody.querySelectorAll("[data-open-file]").forEach(btn => { btn.addEventListener("click", async (e) => { e.preventDefault(); await openAttachmentInNewTab(btn.dataset.openFile, btn.dataset.openBucket || "customer-documents"); }); }); 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")) + ")" : ""}
Nachricht
${esc(l.message || "—")}
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 rental = rentalTypeMeta(l.rental_type); 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.total_days || 0) * (state.vehicleMap.get(l.vehicle_id)?.included_km_per_day || 150)} km
${lang === "de" ? t("adminTotalDaysLabel") : t("adminTotalDaysLabelEn")}${l.total_days || 0}
${lang === "de" ? t("adminRentalType") : t("Rental type")}${esc(rental.label)}
`; } else if (tab === "documents") { const docs = await loadLeadAttachments(l.id); leadDialogBody.innerHTML = renderDocList(docs); leadDialogBody.querySelectorAll("[data-open-file]").forEach(btn => { btn.addEventListener("click", async (e) => { e.preventDefault(); await openAttachmentInNewTab(btn.dataset.openFile, btn.dataset.openBucket || "customer-documents"); }); }); } 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(), loadSalesOrders()]); 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(), loadSalesOrders()]); renderActiveTab(); } // ========================================================================= // CUSTOMERS // ========================================================================= // ----- CUSTOMER LIFETIME VALUE ----- function calcCustomerLifetimeValue(customer) { let total = 0; for (const order of state.salesOrders) { if (order.customer_id === customer.id) { total += order.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 || []; } async function loadSalesOrders() { const { data, error } = await supabase .from("sales_orders") .select("*") .order("created_at", { ascending: false }); if (error) { console.error(error); return []; } state.salesOrders = data || []; return state.salesOrders; } function getOrdersForCustomer(customerId) { return state.salesOrders.filter((order) => order.customer_id === customerId); } function renderOrders() { ordersEmpty.style.display = state.salesOrders.length ? "none" : "block"; ordersTableBody.innerHTML = ""; for (const o of state.salesOrders) { const rental = rentalTypeMeta(o.rental_type); const total = o.total_eur || 0; const totalStr = total > 0 ? "€ " + total.toLocaleString("de-DE") : "—"; const cust = state.customers.find(c => c.id === o.customer_id); const tr = document.createElement("tr"); tr.innerHTML = ` ${esc(o.order_number || o.id.slice(0, 8))} ${cust ? `${esc(cust.name)}
${esc(cust.email)}` : `${esc(o.customer_id?.slice(0, 8) || "—")}`} ${esc(o.vehicle_label || "—")} ${esc(o.date_from || "—")} → ${esc(o.date_to || "—")} ${esc(rental.label)} ${totalStr} ${o.kaution_paid ? "✓" : "—"} ${o.rental_paid ? "✓" : "—"} ${o.rental_complete ? t("adminCompleteDone") : t("adminCompletePending")} ${o.email_sent === 0 ? '—' : o.email_sent === 1 ? '✓' : '✗'} `; ordersTableBody.appendChild(tr); } ordersTableBody.querySelectorAll("[data-open-order]").forEach(b => b.addEventListener("click", () => openOrder(b.dataset.openOrder))); if (ordersBadge) ordersBadge.textContent = String(state.salesOrders.length); } // ----- ORDER DETAIL DIALOG ----- const orderDialog = document.querySelector("#orderDialog"); const orderDialogTitle = document.querySelector("#orderDialogTitle"); const orderDialogTabs = document.querySelector("#orderDialogTabs"); const orderDialogBody = document.querySelector("#orderDialogBody"); const orderDialogFooter = document.querySelector("#orderDialogFooter"); const orderDialogClose = document.querySelector("#orderDialogClose"); async function sendOrderEmailDirect(orderId) { const sendBtn = orderDialogBody.querySelector("[data-manual-email-send]"); if (sendBtn) sendBtn.disabled = true; const n8nUrl = window.MCCARS_CONFIG?.N8N_WEBHOOK_URL || "http://localhost:55521/webhook/manual-email-send"; try { // Use urlencoded payload to avoid browser preflight/CORS issues with JSON headers. const payload = new URLSearchParams({ sales_order_id: orderId }); const res = await fetch(n8nUrl, { method: "POST", body: payload, }); if (!res.ok) { throw new Error(`HTTP ${res.status}`); } notify(t("emailSentToast"), 5000); } catch (err) { console.error("Webhook error:", err); alert(`Email senden fehlgeschlagen: ${err?.message || err}`); } } orderDialogBody.addEventListener("click", async (e) => { const sendBtn = e.target.closest("[data-manual-email-send]"); if (!sendBtn) return; e.preventDefault(); const orderId = sendBtn.dataset.orderId; if (!orderId) return; await sendOrderEmailDirect(orderId); await loadSalesOrders(); const fresh = state.salesOrders.find(x => x.id === orderId); if (fresh) await renderOrderTab("general", fresh, orderId); }); const orderTabOrder = ["general", "pricing"]; const orderTabLabels = { general: () => getLang() === "de" ? t("adminTabGeneral") : t("adminTabGeneralEn"), pricing: () => getLang() === "de" ? t("adminTabPricing") : t("adminTabPricingEn"), }; async function openOrder(id) { const o = state.salesOrders.find(x => x.id === id); if (!o) return; const cust = state.customers.find(c => c.id === o.customer_id); orderDialogTitle.textContent = `${o.order_number || o.id.slice(0, 8)} · ${cust?.name || "—"}`; // Build tabs orderDialogTabs.innerHTML = orderTabOrder.map((tab, i) => `` ).join(""); // Render first tab await renderOrderTab("general", o, id); orderDialog.showModal(); // Tab switching orderDialogTabs.querySelectorAll(".order-tab").forEach(btn => { btn.addEventListener("click", () => { orderDialogTabs.querySelectorAll(".order-tab").forEach(b => b.classList.remove("active")); btn.classList.add("active"); renderOrderTab(btn.dataset.orderTab, o, id); }); }); orderDialogClose.addEventListener("click", () => orderDialog.close(), { once: true }); } async function renderOrderTab(tab, o, id) { const rental = rentalTypeMeta(o.rental_type); const isIndividuell = rental.type === "individuell"; const lang = getLang(); const cust = state.customers.find(c => c.id === o.customer_id); const total = o.total_eur || 0; const deposit = o.deposit_eur || 0; const emailSent = o.email_sent || 0; const emailSentText = emailSent === 1 ? '✓' : emailSent === 2 ? '✗' : '—'; const emailSentPillClass = emailSent === 1 ? 'active' : emailSent === 2 ? 'disqualified' : 'new'; const isEmailLocked = emailSent === 1; if (tab === "general") { // Load attachments for this order const { data: attachments } = await supabase .from("sales_order_attachments") .select("*") .eq("sales_order_id", id) .order("created_at", { ascending: false }); const docs = attachments || []; orderDialogBody.innerHTML = `
${lang === "de" ? "Kunde" : "Customer"}
${cust ? `${esc(cust.name)} (${esc(cust.email)})` : esc(o.customer_id?.slice(0, 8) || "—")}
${lang === "de" ? "Fahrzeug" : "Vehicle"}
${esc(o.vehicle_label || "—")}
${lang === "de" ? "Zeitraum" : "Period"}
${esc(o.date_from || "—")} → ${esc(o.date_to || "—")}
${lang === "de" ? t("adminRentalType") : t("Rental type")}
${esc(rental.label)}
${t("adminEmailSent")}
${emailSentText}

${t("adminTabDocuments")}

${docs.length ? renderDocList(docs) : `

${t("adminNoDocuments")}

`}
${emailSent !== 1 ? `` : ''}
`; // Customer lookup link orderDialogBody.querySelectorAll("[data-goto-cust]").forEach(a => { a.addEventListener("click", (e) => { e.preventDefault(); orderDialog.close(); openCustomer(a.dataset.gotoCust); }); }); // Document open links orderDialogBody.querySelectorAll("[data-open-file]").forEach(btn => { btn.addEventListener("click", async (e) => { e.preventDefault(); await openAttachmentInNewTab(btn.dataset.openFile, btn.dataset.openBucket || "customer-documents"); }); }); // Toggle buttons orderDialogBody.querySelectorAll("[data-so-toggle]").forEach(btn => { btn.addEventListener("click", async () => { await toggleSalesOrderState(btn.dataset.soId, btn.dataset.soToggle); await loadSalesOrders(); const fresh = state.salesOrders.find(x => x.id === id); if (fresh) await renderOrderTab("general", fresh, id); }); }); // Dirty form tracking let noteIsDirty = false; const orderNoteEl = document.querySelector("#orderNote"); const originalNoteValue = o.private_notes || ""; const saveBtn = document.querySelector("#orderNoteSave"); if (orderNoteEl && saveBtn) { saveBtn.classList.add("ghost"); orderNoteEl.addEventListener("input", () => { noteIsDirty = orderNoteEl.value !== originalNoteValue; if (noteIsDirty) { saveBtn.classList.remove("ghost"); saveBtn.style.backgroundColor = "var(--accent-strong)"; saveBtn.style.color = "#fff"; saveBtn.textContent = t("adminSaveNotes") + " (unsaved)"; } else { saveBtn.classList.add("ghost"); saveBtn.style.backgroundColor = ""; saveBtn.style.color = ""; saveBtn.textContent = t("adminSaveNotes"); } }); } // Save notes document.querySelector("#orderNoteSave")?.addEventListener("click", async () => { const ok = await saveSalesOrderPrivateNotes(o.id, document.querySelector("#orderNote").value); if (ok) { noteIsDirty = false; document.querySelector("#orderNoteSave").textContent = "✓"; setTimeout(() => { document.querySelector("#orderNoteSave").textContent = t("adminSaveNotes"); }, 1500); } }); } else if (tab === "pricing") { let daily = o.daily_subtotal || 0; let weekend = o.weekend_subtotal || 0; let sub = o.subtotal_eur || 0; let vat = o.vat_eur || 0; const days = o.total_days || 0; const inclVatLabel = lang === "de" ? t("adminInclVat") : t("adminInclVatEn"); // For individuell: derive pricing from total (which includes VAT) if (isIndividuell && total > 0) { sub = Math.round(total / 1.2 * 100) / 100; vat = Math.round((total - sub) * 100) / 100; daily = sub; // all days counted as weekdays weekend = 0; } const weekdayCount = isIndividuell ? days : (o.weekday_count || 0); const weekendCount = isIndividuell ? 0 : (o.weekend_day_count || 0); const perDay = daily && weekdayCount ? Math.round(daily / weekdayCount) : 0; const perWeekend = weekend && weekendCount ? Math.round(weekend / weekendCount) : 0; orderDialogBody.innerHTML = `
${lang === "de" ? t("adminWeekdays") : t("adminWeekdaysEn")} (${weekdayCount} × € ${perDay || "—"})€ ${daily.toLocaleString("de-DE")}
${lang === "de" ? t("adminWeekendRateLabel") : t("adminWeekendRateLabelEn")} (${weekendCount} × € ${perWeekend || "—"})€ ${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")} ${isIndividuell && !isEmailLocked ? `` : '€ ' + total.toLocaleString("de-DE") }
${!isEmailLocked ? `
${inclVatLabel}
` : ''}
${lang === "de" ? t("adminDepositLabel") : t("adminDepositLabelEn")} ${isEmailLocked || !isIndividuell ? '€ ' + deposit.toLocaleString("de-DE") : `` }
${lang === "de" ? t("adminIncludedKmLabel") : t("adminIncludedKmLabelEn")}${days * (state.vehicleMap.get(o.vehicle_id)?.included_km_per_day || 150)} km
${lang === "de" ? t("adminTotalDaysLabel") : t("adminTotalDaysLabelEn")}${days}
${lang === "de" ? t("adminRentalType") : t("Rental type")}${esc(rental.label)}
${isIndividuell && !isEmailLocked ? `
` : ''}`; // Single save for both total + deposit document.querySelector("#orderPricingSave")?.addEventListener("click", async () => { const btn = document.querySelector("#orderPricingSave"); btn.disabled = true; const totalInput = document.querySelector("#orderTotalInput"); const depositInput = document.querySelector("#orderDepositInput"); const errors = []; if (totalInput) { const { error } = await supabase.rpc("sales_order_set_total", { p_so_id: o.id, p_total_eur: +totalInput.value }); if (error) errors.push(error.message); } if (depositInput) { const { error } = await supabase.rpc("sales_order_set_deposit", { p_so_id: o.id, p_deposit_eur: +depositInput.value }); if (error) errors.push(error.message); } if (errors.length) { alert(errors.join("\n")); btn.disabled = false; return; } await loadSalesOrders(); const fresh = state.salesOrders.find(x => x.id === id); if (fresh) await renderOrderTab("pricing", fresh, id); }); } orderDialogFooter.innerHTML = ""; } async function toggleSalesOrderState(orderId, action) { const rpcName = { kaution: "sales_order_toggle_kaution", rental: "sales_order_toggle_rental", complete: "sales_order_toggle_complete", }[action]; if (!rpcName) return; const { error } = await supabase.rpc(rpcName, { p_so_id: orderId }); if (error) { alert(error.message); return; } await loadSalesOrders(); } async function saveSalesOrderPrivateNotes(orderId, notes) { const { error } = await supabase.rpc("sales_order_update_private_notes", { p_so_id: orderId, p_notes: notes, }); if (error) { alert(error.message); return false; } await loadSalesOrders(); return true; } // ----- CUSTOMER DIALOG (tabbed) ----- const customerTabOrder = ["info", "documents", "salesOrders"]; const customerTabLabels = { info: () => getLang() === "de" ? t("adminTabGeneral") : t("adminTabGeneralEn"), documents: () => getLang() === "de" ? t("adminTabDocuments") : t("adminTabDocumentsEn"), salesOrders: () => getLang() === "de" ? t("adminTabOrderHistory") : t("adminTabOrderHistoryEn"), }; 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); }); }); customerDialogClose.addEventListener("click", () => customerDialog.close(), { once: true }); } async function renderCustomerTab(tab, c) { if (tab === "info") { const lang = getLang(); const sourceLead = state.leads.find((lead) => lead.id === c.lead_id); customerDialogBody.innerHTML = `
${lang === "de" ? "Name" : "Name"}
E-Mail
${lang === "de" ? t("adminPhone") : "Phone"}
${lang === "de" ? t("adminFirstContacted") : t("adminFirstContactedEn")}
${fmtDate(c.first_contacted_at)}
${lang === "de" ? "Status" : "Status"}
${esc(c.status)}
${lang === "de" ? "Website-Nachricht" : "Website message"}
${esc(sourceLead?.message || "—")}
${lang === "de" ? t("adminPrivateNotes") : t("adminPrivateNotesEn")}
`; const saveBtn = document.createElement("button"); saveBtn.className = "btn small"; saveBtn.textContent = t("adminSave"); saveBtn.addEventListener("click", async () => { const payload = { name: document.querySelector("#custName").value, email: document.querySelector("#custEmail").value, phone: document.querySelector("#custPhone").value, }; const { error } = await supabase.from("customers").update(payload).eq("id", c.id); if (error) { alert(error.message); return; } const { error: privateErr } = await supabase.rpc("customer_update_private_notes", { p_customer_id: c.id, p_notes: document.querySelector("#custPrivateNote").value, }); if (privateErr) { alert(privateErr.message); return; } saveBtn.textContent = "✓"; await Promise.all([loadCustomers(), loadSalesOrders()]); setTimeout(() => { saveBtn.textContent = t("adminSave"); }, 1500); }); customerDialogBody.appendChild(saveBtn); } else if (tab === "documents") { const orders = getOrdersForCustomer(c.id); const orderLabelById = new Map(orders.map((order) => [order.id, order.order_number || order.id.slice(0, 8)])); const custDocs = await loadCustomerAttachments(c.id); let leadDocs = []; if (c.lead_id) { const leadDocsRaw = await loadLeadAttachments(c.lead_id); 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, orderLabelById); } if (custDocs.length) { html += `

─ Direkt hochgeladen

`; html += renderDocList(custDocs, orderLabelById); } if (!leadDocs.length && !custDocs.length) { html = `

${getLang() === "de" ? t("adminNoDocuments") : t("adminNoDocumentsEn")}

`; } customerDialogBody.innerHTML = html; customerDialogBody.querySelectorAll("[data-open-file]").forEach(btn => { btn.addEventListener("click", async (e) => { e.preventDefault(); await openAttachmentInNewTab(btn.dataset.openFile, btn.dataset.openBucket || "customer-documents"); }); }); } else if (tab === "salesOrders") { const orders = getOrdersForCustomer(c.id); const lang = getLang(); let html = ""; if (orders.length) { for (const o of orders) { const rental = rentalTypeMeta(o.rental_type); const total = o.total_eur || 0; html += `
${esc(o.order_number || "SO")} ${total > 0 ? "€ " + total.toLocaleString("de-DE") : "—"}
${lang === "de" ? "Fahrzeug" : "Vehicle"}${esc(o.vehicle_label || "—")}
${lang === "de" ? "Zeitraum" : "Period"}${esc(o.date_from || "—")} → ${esc(o.date_to || "—")}
${lang === "de" ? "Miettyp" : "Rental type"}${esc(rental.label)}
`; } const lifetime = calcCustomerLifetimeValue(c); html += `
${lang === "de" ? t("adminLifetimeValue") : t("adminLifetimeValueEn")}€ ${lifetime.toLocaleString("de-DE")}
`; } else { html = `

${t("adminNoOrders")}

`; } customerDialogBody.innerHTML = html; customerDialogBody.querySelectorAll("[data-so-toggle]").forEach((btn) => { btn.addEventListener("click", async () => { await toggleSalesOrderState(btn.dataset.soId, btn.dataset.soToggle); await renderCustomerTab("salesOrders", c); }); }); customerDialogBody.querySelectorAll("[data-so-save-note]").forEach((btn) => { btn.addEventListener("click", async () => { const noteEl = customerDialogBody.querySelector(`[data-so-note="${btn.dataset.soSaveNote}"]`); const ok = await saveSalesOrderPrivateNotes(btn.dataset.soSaveNote, noteEl?.value || ""); if (ok) { btn.classList.remove("ghost"); btn.style.backgroundColor = ""; btn.style.color = ""; btn.textContent = "✓"; setTimeout(() => { btn.textContent = t("adminSaveNotes"); }, 1500); } }); }); customerDialogBody.querySelectorAll("[data-so-note]").forEach((noteEl) => { const btn = customerDialogBody.querySelector(`[data-so-save-note="${noteEl.dataset.soNote}"]`); const originalValue = noteEl.value; noteEl.addEventListener("input", () => { const isDirty = noteEl.value !== originalValue; if (isDirty) { btn.classList.remove("ghost"); btn.style.backgroundColor = "var(--accent-strong)"; btn.style.color = "#fff"; btn.textContent = "Speichern (unsaved)"; } else { btn.classList.add("ghost"); btn.style.backgroundColor = ""; btn.style.color = ""; btn.textContent = t("adminSaveNotes"); } }); }); } // 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 leadShort = c.lead_id?.slice(0, 8) || "—"; const tr = document.createElement("tr"); tr.innerHTML = ` ${fmtDate(c.first_contacted_at)} ${esc(c.name)}
${esc(c.email)} ${esc(c.phone || "—")} ${c.lead_id ? `${esc(leadShort)}` : ``} ${lifetimeStr} ${esc(c.status)} `; customersTableBody.appendChild(tr); } customersTableBody.querySelectorAll("[data-open-cust]").forEach(b => b.addEventListener("click", () => openCustomer(b.dataset.openCust))); customersTableBody.querySelectorAll("[data-goto-lead]").forEach(b => b.addEventListener("click", (e) => { e.preventDefault(); openLead(b.dataset.gotoLead); })); 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); if (ordersBadge) ordersBadge.textContent = String(state.salesOrders.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: "sales_orders" }, async () => { await loadSalesOrders(); if (activeTab === "customers") renderCustomers(); if (activeTab === "orders") renderOrders(); }) .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 normalizeRentalType(rawType) { const value = String(rawType ?? "").trim().toLowerCase(); if (!value) return "weekend"; if (value === "individual" || value === "custom") return "individuell"; if (value === "day" || value === "daily" || value === "1 tag" || value === "1_tag" || value === "single_day") return "single_day"; if (value === "weekend") return "weekend"; return value; } function rentalTypeMeta(rawType) { const type = normalizeRentalType(rawType); const lang = getLang(); if (type === "single_day") return { type, label: lang === "de" ? "1 Tag" : "1 Day" }; if (type === "individuell") return { type, label: lang === "de" ? "individuell" : "individual" }; if (type === "weekend") return { type, label: "weekend" }; return { type: "weekend", label: type }; } 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"; } // Refresh displayed data when any dialog closes function onDialogClose() { Promise.all([loadLeads(), loadCustomers(), loadSalesOrders()]).then(renderActiveTab); } leadDialog.addEventListener("close", onDialogClose); customerDialog.addEventListener("close", onDialogClose); orderDialog.addEventListener("close", onDialogClose); // ========================================================================= // SETTINGS // ========================================================================= const heroPreview = document.querySelector("#heroPreview"); const heroImageInput = document.querySelector("#heroImageInput"); const heroFeedback = document.querySelector("#heroFeedback"); const mietvertragStatus = document.querySelector("#mietvertragStatus"); const mietvertragInput = document.querySelector("#mietvertragInput"); const mietvertragFeedback = document.querySelector("#mietvertragFeedback"); async function renderSettings() { const { data } = await supabase.from("site_settings").select("value").eq("key", "hero_image_url").single(); const url = data?.value || "/images/ferrari-main-car-mobile.jpg"; heroPreview.style.backgroundImage = `url('${url}')`; // Mietvertrag template status const { data: tplData } = await supabase.from("site_settings").select("value").eq("key", "mietvertrag_template_path").single(); const tplPath = tplData?.value; if (tplPath) { mietvertragStatus.textContent = "✓ " + tplPath.split("/").pop(); mietvertragStatus.style.color = "var(--success, green)"; } else { mietvertragStatus.textContent = t("adminMietvertragEmpty") || "Keine Vorlage hochgeladen."; mietvertragStatus.style.color = "var(--muted)"; } } heroImageInput.addEventListener("change", async () => { const file = heroImageInput.files?.[0]; if (!file) return; heroFeedback.className = "form-feedback"; heroFeedback.textContent = "Uploading..."; try { const ext = (file.name.split(".").pop() || "jpg").toLowerCase(); const path = `site/hero.${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); const publicUrl = pub.publicUrl; // Save to site_settings const { error: dbErr } = await supabase .from("site_settings") .upsert({ key: "hero_image_url", value: publicUrl, updated_at: new Date().toISOString() }, { onConflict: "key" }); if (dbErr) throw dbErr; heroPreview.style.backgroundImage = `url('${publicUrl}')`; heroFeedback.className = "form-feedback"; heroFeedback.textContent = "Gespeichert."; } catch (err) { heroFeedback.className = "form-feedback error"; heroFeedback.textContent = err.message || String(err); } }); mietvertragInput.addEventListener("change", async () => { const file = mietvertragInput.files?.[0]; if (!file) return; mietvertragFeedback.className = "form-feedback"; mietvertragFeedback.textContent = "Uploading..."; try { const path = `mietvertrag/vorlage.docx`; const { error: upErr } = await supabase.storage .from("document-templates") .upload(path, file, { contentType: file.type, upsert: true }); if (upErr) throw upErr; // Save path to site_settings const { error: dbErr } = await supabase .from("site_settings") .upsert({ key: "mietvertrag_template_path", value: path, updated_at: new Date().toISOString() }, { onConflict: "key" }); if (dbErr) throw dbErr; mietvertragStatus.textContent = "✓ " + file.name; mietvertragStatus.style.color = "var(--success, green)"; mietvertragFeedback.className = "form-feedback"; mietvertragFeedback.textContent = "Gespeichert."; } catch (err) { mietvertragFeedback.className = "form-feedback error"; mietvertragFeedback.textContent = err.message || String(err); } }); bootstrap();