548 lines
22 KiB
JavaScript
548 lines
22 KiB
JavaScript
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4";
|
||
import { translations, REVIEWS, getLang, setLang, t, applyI18n } from "./i18n.js";
|
||
|
||
const SUPA_URL = window.MCCARS_CONFIG?.SUPABASE_URL ?? "";
|
||
const SUPA_KEY = window.MCCARS_CONFIG?.SUPABASE_ANON_KEY || "";
|
||
|
||
export const supabase = createClient(SUPA_URL, SUPA_KEY, {
|
||
auth: { persistSession: false, storageKey: "mccars.public" },
|
||
});
|
||
|
||
// ---------------- State ----------------
|
||
const state = {
|
||
vehicles: [],
|
||
filtered: [],
|
||
brand: "all",
|
||
sort: "sort_order",
|
||
maxPrice: null,
|
||
reviewIdx: 0,
|
||
};
|
||
|
||
// ---------------- Elements ----------------
|
||
const grid = document.querySelector("#vehicleGrid");
|
||
const emptyState = document.querySelector("#emptyState");
|
||
const brandFilter = document.querySelector("#brandFilter");
|
||
const sortFilter = document.querySelector("#sortFilter");
|
||
const priceFilter = document.querySelector("#priceFilter");
|
||
const langToggle = document.querySelector(".lang-toggle");
|
||
const menuToggle = document.querySelector(".menu-toggle");
|
||
const mainNav = document.querySelector(".main-nav");
|
||
const dialog = document.querySelector("#carDialog");
|
||
const dialogTitle = document.querySelector("#dialogTitle");
|
||
const dialogBody = document.querySelector("#dialogBody");
|
||
const dialogClose = document.querySelector("#dialogClose");
|
||
const reviewStrip = document.querySelector("#reviewStrip");
|
||
const reviewDots = document.querySelector("#reviewDots");
|
||
const statCarsCount = document.querySelector("#statCarsCount");
|
||
const bookingFeedback = document.querySelector("#bookingFeedback");
|
||
|
||
// BPF elements
|
||
const bpfCar = document.querySelector("#bpfCar");
|
||
const bpfFrom = document.querySelector("#bpfFrom");
|
||
const bpfTo = document.querySelector("#bpfTo");
|
||
const bpfDayDate = document.querySelector("#bpfDayDate");
|
||
const bpfWeekendDate = document.querySelector("#bpfWeekendDate");
|
||
const bpfName = document.querySelector("#bpfName");
|
||
const bpfEmail = document.querySelector("#bpfEmail");
|
||
const bpfPhone = document.querySelector("#bpfPhone");
|
||
const bpfMessage = document.querySelector("#bpfMessage");
|
||
const bpfFileId = document.querySelector("#bpfFileId");
|
||
const bpfFileIncome = document.querySelector("#bpfFileIncome");
|
||
const bpfSubmitBtn = document.querySelector("#bpfSubmit");
|
||
const bpfSidebar = document.querySelector("#bpfSidebar");
|
||
const bpfSidebarContent = document.querySelector("#bpfSidebarContent");
|
||
const bpfSidebarPlaceholder = document.querySelector(".bpf-sidebar-placeholder");
|
||
let bpfDurationMode = ""; // "day" | "weekend" | "custom" | ""
|
||
let bpfSubmitting = false;
|
||
|
||
function formatYmdLocal(d) {
|
||
const y = d.getFullYear();
|
||
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||
const day = String(d.getDate()).padStart(2, "0");
|
||
return `${y}-${m}-${day}`;
|
||
}
|
||
|
||
function parseYmdLocal(ymd) {
|
||
const [y, m, d] = (ymd || "").split("-").map(Number);
|
||
if (!y || !m || !d) return null;
|
||
return new Date(y, m - 1, d);
|
||
}
|
||
|
||
function addDaysYmd(ymd, days) {
|
||
const d = parseYmdLocal(ymd);
|
||
if (!d) return null;
|
||
d.setDate(d.getDate() + days);
|
||
return formatYmdLocal(d);
|
||
}
|
||
|
||
function isSaturdayYmd(ymd) {
|
||
const d = parseYmdLocal(ymd);
|
||
return !!d && d.getDay() === 6;
|
||
}
|
||
|
||
function nextSaturdayYmd(ymd) {
|
||
const d = parseYmdLocal(ymd);
|
||
if (!d) return null;
|
||
const delta = (6 - d.getDay() + 7) % 7;
|
||
d.setDate(d.getDate() + delta);
|
||
return formatYmdLocal(d);
|
||
}
|
||
|
||
// Set min dates
|
||
const today = formatYmdLocal(new Date());
|
||
[bpfFrom, bpfTo, bpfDayDate, bpfWeekendDate].forEach(el => {
|
||
if (el) el.min = today;
|
||
});
|
||
|
||
document.querySelector("#year").textContent = new Date().getFullYear();
|
||
|
||
// ----------------Toast Notification ----------------
|
||
function showToast(message, duration = 3000) {
|
||
const toast = document.querySelector("#toast");
|
||
if (!toast) return;
|
||
toast.textContent = message;
|
||
toast.classList.add("show");
|
||
setTimeout(() => {
|
||
toast.classList.remove("show");
|
||
}, duration);
|
||
}
|
||
|
||
// ---------------- Vehicles ----------------
|
||
async function loadVehicles() {
|
||
const { data, error } = await supabase
|
||
.from("vehicles")
|
||
.select("*")
|
||
.eq("is_active", true)
|
||
.order("sort_order", { ascending: true });
|
||
|
||
if (error) {
|
||
console.error("Failed to load vehicles", error);
|
||
grid.innerHTML = `<p style="color:var(--danger);">Unable to load vehicles: ${error.message}</p>`;
|
||
return;
|
||
}
|
||
|
||
state.vehicles = data || [];
|
||
statCarsCount.textContent = state.vehicles.length;
|
||
|
||
const brands = [...new Set(state.vehicles.map(v => v.brand))].sort();
|
||
brandFilter.innerHTML = `<option value="all">${t("all")}</option>` +
|
||
brands.map(b => `<option value="${b}">${b}</option>`).join("");
|
||
|
||
// Populate BPF car selector
|
||
bpfCar.innerHTML = `<option value="">${t("bpfSelectVehicle")}</option>` +
|
||
state.vehicles.map(v => `<option value="${v.id}">${v.brand} ${v.model}</option>`).join("");
|
||
|
||
applyFilters();
|
||
}
|
||
|
||
function applyFilters() {
|
||
let rows = [...state.vehicles];
|
||
if (state.brand !== "all") rows = rows.filter(v => v.brand === state.brand);
|
||
if (state.maxPrice) rows = rows.filter(v => v.daily_price_eur <= state.maxPrice);
|
||
|
||
switch (state.sort) {
|
||
case "priceAsc": rows.sort((a, b) => a.daily_price_eur - b.daily_price_eur); break;
|
||
case "priceDesc": rows.sort((a, b) => b.daily_price_eur - a.daily_price_eur); break;
|
||
case "powerDesc": rows.sort((a, b) => b.power_hp - a.power_hp); break;
|
||
default: rows.sort((a, b) => a.sort_order - b.sort_order);
|
||
}
|
||
|
||
state.filtered = rows;
|
||
renderGrid();
|
||
}
|
||
|
||
function renderGrid() {
|
||
grid.innerHTML = "";
|
||
emptyState.style.display = state.filtered.length ? "none" : "block";
|
||
|
||
for (const v of state.filtered) {
|
||
const card = document.createElement("article");
|
||
card.className = "vehicle-card";
|
||
card.innerHTML = `
|
||
<div class="vehicle-photo" role="img" aria-label="${escapeAttr(v.brand)} ${escapeAttr(v.model)}" style="background-image:url('${escapeAttr(v.photo_url)}');">
|
||
<span class="badge" aria-hidden="true">${escapeHtml(v.brand)}</span>
|
||
</div>
|
||
<div class="vehicle-body">
|
||
<p class="model-brand" aria-hidden="true">${escapeHtml(v.brand)}</p>
|
||
<h3>${escapeHtml(v.model)}</h3>
|
||
<div class="spec-row">
|
||
<div><strong>${v.power_hp}</strong><span>${t("hp")}</span></div>
|
||
<div><strong>${v.top_speed_kmh}</strong><span>${t("kmh")}</span></div>
|
||
<div><strong>${escapeHtml(v.acceleration)}</strong><span>${t("accel")}</span></div>
|
||
</div>
|
||
<div class="vehicle-footer">
|
||
<div class="vehicle-price">€ ${v.daily_price_eur}<span> / ${t("perDay")}</span></div>
|
||
<div style="display:flex;gap:0.4rem;">
|
||
<button class="btn ghost small" data-details="${v.id}" aria-label="${t("details")} ${escapeAttr(v.brand)} ${escapeAttr(v.model)}">${t("details")}</button>
|
||
<button class="btn small" data-book="${v.id}" aria-label="${t("book")} ${escapeAttr(v.brand)} ${escapeAttr(v.model)}">${t("book")}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
grid.appendChild(card);
|
||
}
|
||
|
||
grid.querySelectorAll("[data-details]").forEach(b => {
|
||
b.addEventListener("click", () => openDetails(b.dataset.details));
|
||
});
|
||
grid.querySelectorAll("[data-book]").forEach(b => {
|
||
b.addEventListener("click", () => {
|
||
bpfCar.value = b.dataset.book;
|
||
bpfCar.dispatchEvent(new Event("change"));
|
||
document.querySelector("#buchen").scrollIntoView({ behavior: "smooth" });
|
||
});
|
||
});
|
||
}
|
||
|
||
function openDetails(id) {
|
||
const v = state.vehicles.find(x => x.id === id);
|
||
if (!v) return;
|
||
const lang = getLang();
|
||
const desc = lang === "en" ? v.description_en : v.description_de;
|
||
|
||
dialogTitle.textContent = `${v.brand} ${v.model}`;
|
||
dialogBody.innerHTML = `
|
||
<img src="${escapeAttr(v.photo_url)}" alt="${escapeAttr(v.brand + ' ' + v.model)}" />
|
||
<p>${escapeHtml(desc || "")}</p>
|
||
<div class="spec-row" style="margin:1rem 0;">
|
||
<div><strong>${v.power_hp}</strong><span>${t("hp")}</span></div>
|
||
<div><strong>${v.top_speed_kmh}</strong><span>${t("kmh")}</span></div>
|
||
<div><strong>${escapeHtml(v.acceleration)}</strong><span>${t("accel")}</span></div>
|
||
</div>
|
||
<div class="spec-row" style="margin:1rem 0;">
|
||
<div><strong>${v.seats}</strong><span>${t("seats")}</span></div>
|
||
<div><strong>€ ${v.weekend_price_eur || v.daily_price_eur}</strong><span>${t("bpfWeekendRate")}</span></div>
|
||
<div><strong>${v.max_daily_km || 150}</strong><span>${t("bpfMaxKm")}</span></div>
|
||
</div>
|
||
<div class="spec-row" style="margin:1rem 0;grid-template-columns:1fr;">
|
||
<div><strong>€ ${(v.kaution_eur || 5000).toLocaleString("de-DE")}</strong><span>${t("bpfDeposit")}</span></div>
|
||
</div>
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:1rem;">
|
||
<div class="vehicle-price">€ ${v.daily_price_eur}<span> / ${t("perDay")}</span></div>
|
||
<button class="btn" id="dialogBook">${t("bookNow")}</button>
|
||
</div>
|
||
`;
|
||
dialog.showModal();
|
||
document.querySelector("#dialogBook").addEventListener("click", () => {
|
||
dialog.close();
|
||
bpfCar.value = v.id;
|
||
bpfCar.dispatchEvent(new Event("change"));
|
||
document.querySelector("#buchen").scrollIntoView({ behavior: "smooth" });
|
||
});
|
||
}
|
||
|
||
// ---------------- Reviews ----------------
|
||
function renderReviews() {
|
||
state.reviewIdx = state.reviewIdx % REVIEWS.length;
|
||
const r = REVIEWS[state.reviewIdx];
|
||
reviewStrip.innerHTML = `
|
||
<p class="review-quote">"${escapeHtml(r.quote)}"</p>
|
||
<p class="review-author">${escapeHtml(r.author)}</p>
|
||
`;
|
||
reviewDots.innerHTML = REVIEWS.map((_, i) =>
|
||
`<button class="${i === state.reviewIdx ? 'active' : ''}" data-rev="${i}" aria-label="${t("review")} ${i + 1}"></button>`
|
||
).join("");
|
||
reviewDots.querySelectorAll("button").forEach(b => {
|
||
b.addEventListener("click", () => { state.reviewIdx = +b.dataset.rev; renderReviews(); });
|
||
});
|
||
}
|
||
setInterval(() => { state.reviewIdx++; renderReviews(); }, 5000);
|
||
|
||
// ---------------- BPF WIZARD ----------------
|
||
const bpfStepPanels = [
|
||
document.querySelector("#bpfStep1"),
|
||
document.querySelector("#bpfStep2"),
|
||
document.querySelector("#bpfStep3"),
|
||
];
|
||
const bpfStepButtons = document.querySelectorAll(".bpf-step");
|
||
let bpfCurrentStep = 1;
|
||
|
||
function showBpfStep(n) {
|
||
bpfCurrentStep = n;
|
||
bpfStepPanels.forEach((p, i) => p.style.display = (i === n - 1) ? "block" : "none");
|
||
bpfStepButtons.forEach((b, i) => {
|
||
b.classList.toggle("active", i === n - 1);
|
||
b.classList.toggle("done", i < n - 1);
|
||
});
|
||
}
|
||
|
||
document.querySelector("#bpfNext1").addEventListener("click", () => {
|
||
if (!bpfCar.value) { bpfCar.focus(); return; }
|
||
// Resolve effective from/to based on duration mode
|
||
const { from, to } = getBpfDates();
|
||
if (!from || !to || new Date(to) <= new Date(from)) {
|
||
bookingFeedback.textContent = bpfDurationMode === "weekend" ? t("weekendSaturdayOnly") : t("invalidDates");
|
||
bookingFeedback.className = "form-feedback error";
|
||
return;
|
||
}
|
||
// Sync hidden from/to for downstream use
|
||
bpfFrom.value = from;
|
||
bpfTo.value = to;
|
||
bookingFeedback.textContent = "";
|
||
showBpfStep(2);
|
||
});
|
||
document.querySelector("#bpfBack2").addEventListener("click", () => showBpfStep(1));
|
||
document.querySelector("#bpfNext2").addEventListener("click", () => {
|
||
if (!bpfName.value || !bpfEmail.value) { bpfEmail.focus(); return; }
|
||
showBpfStep(3);
|
||
});
|
||
document.querySelector("#bpfBack3").addEventListener("click", () => showBpfStep(2));
|
||
|
||
// File upload display
|
||
bpfFileId.addEventListener("change", () => {
|
||
document.querySelector("#bpfFileIdName").textContent = bpfFileId.files[0]?.name || "";
|
||
});
|
||
bpfFileIncome.addEventListener("change", () => {
|
||
document.querySelector("#bpfFileIncomeName").textContent = bpfFileIncome.files[0]?.name || "";
|
||
});
|
||
|
||
// ---------------- Duration Presets ----------------
|
||
function setDurationMode(mode) {
|
||
bpfDurationMode = mode || "";
|
||
document.querySelectorAll(".bpf-preset").forEach(b => b.classList.toggle("active", b.dataset.preset === mode));
|
||
document.querySelector("#bpfDateDay").style.display = mode === "day" ? "block" : "none";
|
||
document.querySelector("#bpfDateWeekend").style.display = mode === "weekend" ? "block" : "none";
|
||
document.querySelector("#bpfDateCustom").style.display = mode === "custom" ? "block" : "none";
|
||
updateSidebar();
|
||
}
|
||
|
||
// Fresh page load: no duration selected, so no date inputs are visible.
|
||
setDurationMode("");
|
||
|
||
document.querySelectorAll(".bpf-preset").forEach(btn => {
|
||
btn.addEventListener("click", () => setDurationMode(btn.dataset.preset));
|
||
});
|
||
|
||
// Restrict weekend picker to Saturdays only
|
||
function enforceWeekendSaturday() {
|
||
if (!bpfWeekendDate.value) return;
|
||
if (!isSaturdayYmd(bpfWeekendDate.value)) {
|
||
bpfWeekendDate.value = "";
|
||
bookingFeedback.className = "form-feedback error";
|
||
bookingFeedback.textContent = t("weekendSaturdayOnly");
|
||
bpfWeekendDate.focus();
|
||
updateSidebar();
|
||
return;
|
||
}
|
||
bookingFeedback.textContent = "";
|
||
updateSidebar();
|
||
}
|
||
bpfWeekendDate.addEventListener("input", enforceWeekendSaturday);
|
||
bpfWeekendDate.addEventListener("change", enforceWeekendSaturday);
|
||
bpfDayDate.addEventListener("change", () => {
|
||
updateSidebar();
|
||
});
|
||
|
||
function getBpfDates() {
|
||
if (bpfDurationMode === "day") {
|
||
const d = bpfDayDate.value;
|
||
if (!d) return { from: null, to: null };
|
||
return { from: d, to: addDaysYmd(d, 1) };
|
||
}
|
||
if (bpfDurationMode === "weekend") {
|
||
const sat = bpfWeekendDate.value;
|
||
if (!sat) return { from: null, to: null };
|
||
if (!isSaturdayYmd(sat)) return { from: null, to: null };
|
||
return { from: sat, to: addDaysYmd(sat, 2) };
|
||
}
|
||
// custom
|
||
return { from: bpfFrom.value, to: bpfTo.value };
|
||
}
|
||
|
||
// Pricing calculation
|
||
function calcWeekendDays(from, to) {
|
||
// Count Saturday and Sunday days in a date interval
|
||
let count = 0;
|
||
const start = parseYmdLocal(from);
|
||
const end = parseYmdLocal(to);
|
||
if (!start || !end) return 0;
|
||
const cur = new Date(start);
|
||
while (cur < end) {
|
||
const day = cur.getDay(); // 0=Sun, 6=Sat
|
||
if (day === 6 || day === 0) count++;
|
||
cur.setDate(cur.getDate() + 1);
|
||
}
|
||
return count;
|
||
}
|
||
|
||
async function updateSidebar() {
|
||
const v = state.vehicles.find(x => x.id === bpfCar.value);
|
||
const { from, to } = getBpfDates();
|
||
if (!v || !from || !to) {
|
||
bpfSidebarContent.style.display = "none";
|
||
bpfSidebarPlaceholder.style.display = "block";
|
||
return;
|
||
}
|
||
const fromD = parseYmdLocal(from);
|
||
const toD = parseYmdLocal(to);
|
||
if (!fromD || !toD) return;
|
||
if (toD <= fromD) return;
|
||
|
||
// Fetch price from backend RPC
|
||
const { data: price, error } = await supabase.rpc("calculate_price", {
|
||
p_vehicle_id: v.id,
|
||
p_date_from: from,
|
||
p_date_to: to,
|
||
});
|
||
if (error || !price) { console.error("calculate_price error:", error, "data:", price); return; }
|
||
|
||
const totalDays = price.total_days;
|
||
const weekdays = price.weekday_count;
|
||
const weekendDays = price.weekend_day_count;
|
||
const weekdayCost = price.daily_subtotal;
|
||
const weekendCost = price.weekend_subtotal;
|
||
const subtotal = price.subtotal_eur;
|
||
const vat = price.vat_eur;
|
||
const total = price.total_eur;
|
||
const deposit = price.deposit_eur;
|
||
const kmPerWeekday = price.max_daily_km;
|
||
const kmPerWeekendDay = price.max_km_weekend;
|
||
const includedKm = (weekdays * kmPerWeekday) + (weekendDays * kmPerWeekendDay);
|
||
|
||
bpfSidebarPlaceholder.style.display = "none";
|
||
bpfSidebarContent.style.display = "block";
|
||
bpfSidebarContent.innerHTML = `
|
||
<h4>${t("bpfPriceOverview")}</h4>
|
||
<div class="bpf-price-row"><span>${v.brand} ${v.model} · ${totalDays} ${t("bpfDays")}</span></div>
|
||
${weekdays > 0 ? `<div class="bpf-price-row"><span>${t("bpfWeekdays")} (${weekdays} × € ${price.daily_price_eur})</span><span>€ ${weekdayCost.toLocaleString("de-DE")}</span></div>` : ""}
|
||
${weekendDays > 0 ? `<div class="bpf-price-row"><span>${t("bpfWeekendDays")} (${weekendDays} × € ${price.weekend_price_eur})</span><span>€ ${weekendCost.toLocaleString("de-DE")}</span></div>` : ""}
|
||
<div class="bpf-price-row"><span>${t("bpfSubtotal")}</span><span>€ ${subtotal.toLocaleString("de-DE")}</span></div>
|
||
<div class="bpf-price-row muted"><span>${t("bpfVat")}</span><span>€ ${vat.toLocaleString("de-DE")}</span></div>
|
||
<div class="bpf-price-row total"><span>${t("bpfTotal")}</span><span>€ ${total.toLocaleString("de-DE")}</span></div>
|
||
<div class="bpf-price-row muted" style="margin-top:0.8rem;"><span>${t("bpfDeposit")}</span><span>€ ${deposit.toLocaleString("de-DE")}</span></div>
|
||
<div class="bpf-price-row muted"><span>${t("bpfIncludedKm")}</span><span>${includedKm} km</span></div>
|
||
<div class="bpf-price-row muted"><span>${t("bpfExtraKm")}</span><span>€ 1,50${t("bpfPerKm")}</span></div>
|
||
<div class="bpf-car-preview" style="background-image:url('${escapeAttr(v.photo_url)}');"></div>
|
||
<p class="bpf-car-name">${escapeHtml(v.brand)} ${escapeHtml(v.model)}</p>
|
||
<p class="bpf-car-specs">${v.power_hp} ${t("hp")} • ${v.top_speed_kmh} ${t("kmh")} • ${escapeHtml(v.acceleration)}</p>
|
||
`;
|
||
}
|
||
|
||
bpfCar.addEventListener("change", updateSidebar);
|
||
bpfFrom.addEventListener("change", updateSidebar);
|
||
bpfTo.addEventListener("change", updateSidebar);
|
||
|
||
// Submit BPF
|
||
document.querySelector("#bpfSubmit").addEventListener("click", async () => {
|
||
if (bpfSubmitting) return;
|
||
bpfSubmitting = true;
|
||
if (bpfSubmitBtn) bpfSubmitBtn.disabled = true;
|
||
bookingFeedback.className = "form-feedback";
|
||
bookingFeedback.textContent = "...";
|
||
|
||
const vehicle = state.vehicles.find(v => v.id === bpfCar.value);
|
||
const { from, to } = getBpfDates();
|
||
const payload = {
|
||
p_name: bpfName.value,
|
||
p_email: bpfEmail.value,
|
||
p_phone: bpfPhone.value || "",
|
||
p_vehicle_id: bpfCar.value || null,
|
||
p_vehicle_label: vehicle ? `${vehicle.brand} ${vehicle.model}` : "",
|
||
p_date_from: from || null,
|
||
p_date_to: to || null,
|
||
p_message: bpfMessage.value || "",
|
||
p_source: "website",
|
||
};
|
||
|
||
// Create lead via RPC (returns inserted id without anon SELECT privileges)
|
||
const { data: leadId, error } = await supabase.rpc("create_lead", payload);
|
||
if (error) {
|
||
console.error(error);
|
||
bookingFeedback.className = "form-feedback error";
|
||
bookingFeedback.textContent = t("bookingFailed");
|
||
bpfSubmitting = false;
|
||
if (bpfSubmitBtn) bpfSubmitBtn.disabled = false;
|
||
return;
|
||
}
|
||
|
||
// Upload files
|
||
const uploads = [];
|
||
if (bpfFileId.files[0]) {
|
||
uploads.push(uploadDoc(leadId, bpfFileId.files[0], "id_document"));
|
||
}
|
||
if (bpfFileIncome.files[0]) {
|
||
uploads.push(uploadDoc(leadId, bpfFileIncome.files[0], "income_proof"));
|
||
}
|
||
await Promise.all(uploads);
|
||
|
||
bookingFeedback.className = "form-feedback";
|
||
bookingFeedback.textContent = "";
|
||
showToast(t("bookingSuccess"), 4000);
|
||
showBpfStep(1);
|
||
bpfCar.value = "";
|
||
bpfFrom.value = "";
|
||
bpfTo.value = "";
|
||
bpfDayDate.value = "";
|
||
bpfWeekendDate.value = "";
|
||
bpfName.value = "";
|
||
bpfEmail.value = "";
|
||
bpfPhone.value = "";
|
||
bpfMessage.value = "";
|
||
document.querySelector("#bpfFileIdName").textContent = "";
|
||
document.querySelector("#bpfFileIncomeName").textContent = "";
|
||
setDurationMode("");
|
||
updateSidebar();
|
||
bpfSubmitting = false;
|
||
if (bpfSubmitBtn) bpfSubmitBtn.disabled = false;
|
||
});
|
||
|
||
async function uploadDoc(leadId, file, kind) {
|
||
try {
|
||
const ext = (file.name.split(".").pop() || "bin").toLowerCase();
|
||
const path = `${leadId}/${kind}.${ext}`;
|
||
const { error: upErr } = await supabase.storage
|
||
.from("customer-documents")
|
||
.upload(path, file, { contentType: file.type });
|
||
if (upErr) { console.error("Upload failed:", upErr); return; }
|
||
await supabase.from("lead_attachments").insert({
|
||
lead_id: leadId,
|
||
bucket: "customer-documents",
|
||
file_path: path,
|
||
file_name: file.name,
|
||
mime_type: file.type,
|
||
kind: kind,
|
||
});
|
||
} catch (e) { console.error("Doc upload error:", e); }
|
||
}
|
||
|
||
// ---------------- Events ----------------
|
||
brandFilter.addEventListener("change", e => { state.brand = e.target.value; applyFilters(); });
|
||
sortFilter.addEventListener("change", e => { state.sort = e.target.value; applyFilters(); });
|
||
priceFilter.addEventListener("input", e => { state.maxPrice = e.target.value ? +e.target.value : null; applyFilters(); });
|
||
|
||
dialogClose.addEventListener("click", () => dialog.close());
|
||
|
||
menuToggle.addEventListener("click", () => mainNav.classList.toggle("open"));
|
||
mainNav.addEventListener("click", e => { if (e.target.tagName === "A") mainNav.classList.remove("open"); });
|
||
|
||
langToggle.addEventListener("click", () => {
|
||
const next = getLang() === "de" ? "en" : "de";
|
||
setLang(next);
|
||
langToggle.textContent = next === "de" ? "EN" : "DE";
|
||
applyI18n();
|
||
renderReviews();
|
||
applyFilters();
|
||
});
|
||
|
||
// ---------------- Helpers ----------------
|
||
function escapeHtml(s) {
|
||
return String(s ?? "").replace(/[&<>"']/g, c => ({
|
||
"&": "&", "<": "<", ">": ">", '"': """, "'": "'"
|
||
})[c]);
|
||
}
|
||
function escapeAttr(s) { return escapeHtml(s); }
|
||
|
||
// ---------------- Boot ----------------
|
||
langToggle.textContent = getLang() === "de" ? "EN" : "DE";
|
||
applyI18n();
|
||
renderReviews();
|
||
loadVehicles();
|
||
|
||
// Load hero image from site_settings
|
||
(async () => {
|
||
const { data } = await supabase.from("site_settings").select("value").eq("key", "hero_image_url").single();
|
||
if (data && data.value) {
|
||
document.querySelector(".hero").style.setProperty("--hero-bg", `url('${data.value}')`);
|
||
}
|
||
})();
|