/* global React, Icon, chf */ // ACN-Cars · Page: Booking (multi-step) + live calculator + smart hints const DOW_LABELS_DE = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"]; // Findet das nächste Datum mit Wochentag = targetDow (0..6) ab Startdatum (inkl. heute) function nextDateForDow(start, targetDow, hour) { const d = new Date(start.getTime()); d.setHours(hour || 9, 0, 0, 0); // Wenn Startdatum bereits dieser Wochentag ist UND Zeit >= jetzt: nimm es. Sonst suche das nächste Vorkommen. let diff = (targetDow - d.getDay() + 7) % 7; if (diff === 0 && d.getTime() < start.getTime()) diff = 7; d.setDate(d.getDate() + diff); return d; } function toLocalIso(d) { const off = d.getTimezoneOffset(); const local = new Date(d.getTime() - off * 60 * 1000); return local.toISOString().slice(0, 16); } function BookingPage({ vehicles, addons, locations, initialVehicleId, dealSeed, tierHours, contact, onComplete, onHome, onFleet }) { // v2.2.29 — Fallback wenn contact-prop fehlt (alte Aufrufe). contact = contact || (window.ACN_DATA && window.ACN_DATA.contact) || {}; const boot = (window.ACN_DATA && window.ACN_DATA.boot) || {}; const vatRate = (typeof boot.vatRate === "number" ? boot.vatRate : 8.1) / 100; const showLocations = !!boot.showLocations; const smartHints = boot.smartHints || {}; // v2.2.4 — Aktive Wochenend-Aktion (über Aktions-CTA reingekommen) const weekendDeal = (dealSeed && dealSeed.isWeekend && dealSeed.weekend) ? dealSeed : null; const w = weekendDeal ? weekendDeal.weekend : null; const bonusEnabled = !!(w && w.bonusEnabled && w.bonusDay !== w.returnDay); // Default-Termine: morgen + 24h. Bei Aktion: nächster passender Wochentag/Stunde. // v2.2.6 — Tier-Hint aus VehicleCard: tierHours bestimmt initialReturn = pickup + tierHours const tomorrow = new Date(Date.now() + 24 * 3600 * 1000); const initialDurationH = (tierHours && tierHours > 0) ? tierHours : 24; const tPlusInitial = new Date(tomorrow.getTime() + initialDurationH * 3600 * 1000); let initialPickup = tomorrow; let initialReturn = tPlusInitial; if (weekendDeal && w) { const pStart = new Date(Date.now() + 24 * 3600 * 1000); // ab morgen const pickup = nextDateForDow(pStart, w.pickupDay, w.pickupHour || 9); const ret = nextDateForDow(pickup, w.returnDay, w.returnHour || 18); if (ret.getTime() <= pickup.getTime()) ret.setDate(ret.getDate() + 7); initialPickup = pickup; initialReturn = ret; } // Wenn ein Fahrzeug bereits vorausgewählt wurde (z.B. aus Fahrzeug-Karte heraus), Step 2 (Fahrzeug-Wahl) überspringen // → Nutzer startet trotzdem bei Step 1 (Daten), Stepper zeigt nur 3 Schritte (Daten → Extras → Bestätigung) const hasPreselected = !!initialVehicleId && vehicles.some(v => v.id === initialVehicleId); const [step, setStep] = React.useState(1); // v2.2.4 — Bonus-Tag-Toggle (verlängert die Aktion um den optionalen Bonus-Tag, z. B. Mo) const [useBonusDay, setUseBonusDay] = React.useState(false); const [data, setData] = React.useState({ pickupLoc: locations[0], returnLoc: locations[0], pickupAt: toLocalIso(initialPickup), returnAt: toLocalIso(initialReturn), vehicleId: initialVehicleId || vehicles[0].id, addons: addons.filter(a => a.default || a.locked).map(a => a.id), extraKm: 0, name: "", email: "", phone: "", license: "", message: "", consent: false, website: "", // Honeypot }); // v2.2.4 — Bonus-Tag An/Aus → returnAt entsprechend setzen React.useEffect(() => { if (!weekendDeal || !w || !bonusEnabled) return; const pickupDate = new Date(data.pickupAt); if (isNaN(pickupDate.getTime())) return; const targetDow = useBonusDay ? w.bonusDay : w.returnDay; const targetHour = useBonusDay ? (w.returnHour || 18) : (w.returnHour || 18); const newReturn = nextDateForDow(pickupDate, targetDow, targetHour); if (newReturn.getTime() <= pickupDate.getTime()) { newReturn.setDate(newReturn.getDate() + 7); } const newReturnIso = toLocalIso(newReturn); if (newReturnIso !== data.returnAt) { setData(d => ({ ...d, returnAt: newReturnIso })); } // eslint-disable-next-line }, [useBonusDay]); // Server-Quote (mit Smart-Rabatten) — wird bei jedem Datums/Fahrzeug/Addon-Wechsel neu geholt const [serverQuote, setServerQuote] = React.useState(null); const [quoting, setQuoting] = React.useState(false); React.useEffect(() => { if (!boot.apiBase) return; let cancelled = false; setQuoting(true); const controller = new AbortController(); const t = setTimeout(() => { fetch(boot.apiBase + "/quote", { method: "POST", headers: { "Content-Type": "application/json", "X-WP-Nonce": boot.nonce }, body: JSON.stringify({ vehicle_id: data.vehicleId, pickup_at: data.pickupAt, return_at: data.returnAt, addons: data.addons, extra_km: parseInt(data.extraKm || 0, 10), // v2.2.37 — Aktion-Kontext mitsenden, damit Weekend-Pauschale auch // bei abweichenden Wochentagen greift (z. B. Fr→So obwohl Aktion // Fr→Mo konfiguriert ist). deal_id: weekendDeal ? weekendDeal.id : "", }), signal: controller.signal, }) .then(r => r.json()) .then(j => { if (!cancelled) setServerQuote(j); }) .catch(() => { /* silent */ }) .finally(() => { if (!cancelled) setQuoting(false); }); }, 250); // Debounce return () => { cancelled = true; clearTimeout(t); controller.abort(); }; }, [data.vehicleId, data.pickupAt, data.returnAt, data.addons.join(","), data.extraKm, weekendDeal && weekendDeal.id, boot.apiBase]); const update = (k, v) => setData(d => ({ ...d, [k]: v })); const toggleAddon = (id) => { setData(d => { const a = addons.find(x => x.id === id); if (a && a.locked) return d; // locked Addons können nicht abgewählt werden const has = d.addons.includes(id); return { ...d, addons: has ? d.addons.filter(x => x !== id) : [...d.addons, id] }; }); }; const vehicle = vehicles.find(v => v.id === data.vehicleId) || vehicles[0]; const minHours = Math.max(1, parseInt(vehicle.minHours || (vehicle.minDays ? vehicle.minDays * 24 : 24), 10)); const minDays = Math.max(1, Math.ceil(minHours / 24)); const rawHours = Math.round((new Date(data.returnAt) - new Date(data.pickupAt)) / (1000 * 60 * 60)); const hours = Math.max(1, rawHours); const days = Math.max(1, Math.ceil(hours / 24)); const tooShort = hours < minHours; // Lokale Berechnung als Fallback / Sofortige Anzeige // Auto-pick: günstigeres Modell aus (Stunden × Stundenpreis) vs (Tage × Tagespreis) const pricePerHour = vehicle.pricePerHour || 0; const baseDayTotal = vehicle.pricePerDay * days; const baseHourTotal = pricePerHour > 0 ? pricePerHour * hours : 0; let priceMode = "day"; let baseTotal = baseDayTotal; if (pricePerHour > 0 && baseHourTotal > 0 && baseHourTotal < baseDayTotal) { priceMode = "hour"; baseTotal = baseHourTotal; } const addonTotal = data.addons.reduce((sum, id) => { const a = addons.find(x => x.id === id); if (!a) return sum; return sum + (a.per === "Tag" ? a.price * days : a.price); }, 0); // Server-Werte bevorzugen, wenn vorhanden const sq = serverQuote && !serverQuote.code ? serverQuote : null; const extraKmCost = sq ? (sq.extraKmCost || 0) : 0; const kmIncluded = sq ? (sq.kmIncluded || 0) : 0; const extraKmPrice= sq ? (sq.extraKmPrice || 1.5) : (vehicle.kmExtraPrice || 1.5); const subtotal_pre = sq ? (sq.baseTotal + sq.addonTotal + extraKmCost) : (baseTotal + addonTotal); const discountAmount = sq ? (sq.discountAmount || 0) : 0; const discountPct = sq ? (sq.discountPct || 0) : 0; const appliedDisc = sq ? (sq.appliedDiscounts || []) : []; const hourTip = sq ? (sq.hourTip || null) : null; const weekendApplied = sq ? !!sq.weekendApplied : false; const weekendLabel = sq ? (sq.weekendLabel || "") : ""; const weekendBonusUsed = sq ? !!sq.weekendBonusUsed : false; const weekendBonusSurcharge = sq ? (sq.weekendBonusSurcharge || 0) : 0; const afterHoursCost = sq ? (sq.afterHoursCost || 0) : 0; const afterHoursReasons = sq ? (sq.afterHoursReasons || []) : []; // Preise sind INKL. MwSt — Total = Brutto, MwSt wird daraus extrahiert. const total = sq ? sq.total : (baseTotal + addonTotal); const subtotal = sq ? sq.subtotal : Math.round(total / (1 + vatRate)); const vat = sq ? sq.vat : (total - subtotal); const sqHours = sq && typeof sq.hours === "number" ? sq.hours : hours; const sqMode = sq && sq.priceMode ? sq.priceMode : priceMode; // v2.2.6 — Tier / Inquiry-Mode aus Server-Quote const isInquiry = sq ? !!sq.isInquiry : false; const inquiryThresholdH= sq ? (sq.inquiryThresholdH || 168) : (vehicle.inquiryThresholdH || 168); const tierMatched = sq ? !!sq.tierMatched : false; // Achtung: NICHT tierHours nennen — Kollision mit dem Prop weiter oben würde // einen SyntaxError („Identifier already declared") werfen und das ganze Bundle killen. const matchedTierHours = sq ? (sq.tierHours || null) : null; const summary = { vehicle, days, hours: sqHours, priceMode: sqMode, pricePerHour, baseTotal: sq ? sq.baseTotal : baseTotal, addonTotal: sq ? sq.addonTotal : addonTotal, extraKm: parseInt(data.extraKm || 0, 10), extraKmCost, kmIncluded, extraKmPrice, subtotal_pre, discountAmount, discountPct, appliedDisc, hourTip, weekendApplied, weekendLabel, weekendBonusUsed, weekendBonusSurcharge, afterHoursCost, afterHoursReasons, subtotal, vat, total, vatRate, minDays, minHours, tooShort, quoting, // v2.2.6 isInquiry, inquiryThresholdH, tierMatched, tierHours: matchedTierHours, }; // Stepper — Fahrzeug nur anzeigen wenn nicht vorausgewählt (oder wenn Nutzer dort schon war) const stepperItems = hasPreselected ? [ { n: 1, l: "Daten" }, { n: 3, l: "Extras" }, { n: 4, l: "Bestätigung" }, ] : [ { n: 1, l: "Daten" }, { n: 2, l: "Fahrzeug" }, { n: 3, l: "Extras" }, { n: 4, l: "Bestätigung" }, ]; return (
{/* Back-Navigation oben — schnell zurück zur Startseite oder Flotte */}
{onFleet && ( )}
{/* v2.2.3 — Buchungs-Hero: Auto-Bild + Marke/Modell + Specs + Trust-Badges */} {hasPreselected ? (
Reservierung {vehicle.dealPct > 0 && −{vehicle.dealPct}% Deal}
{vehicle.brand}

{vehicle.model}

{vehicle.tagline &&

{vehicle.tagline}

}
{vehicle.power} PS {vehicle.transmission} {vehicle.fuel} {vehicle.seats} Plätze ab CHF {chf(vehicle.pricePerDay)}/Tag
Sofort-Bestätigung innerhalb von 2 Std
Vollkasko inkl. Versicherung enthalten
Persönliche Übergabe am Standort Neuhausen
) : ( <>
Buchung

Reservierung in 4 Schritten.

)} {/* v2.2.4 — Aktions-Banner (Weekend-Pauschale aus Aktions-CTA) */} {weekendDeal && w && (
Aktion aktiv: {weekendDeal.title}
Diese Pauschale gilt für Abholung am {DOW_LABELS_DE[w.pickupDay] || "—"} {" "}({String(w.pickupHour || 9).padStart(2, "0")}:00 Uhr) und Rückgabe am {" "}{DOW_LABELS_DE[w.returnDay] || "—"} {" "}({String(w.returnHour || 18).padStart(2, "0")}:00 Uhr). {" "}Wir haben die Daten passend vorausgefüllt – Sie können sie aber jederzeit anpassen.
{bonusEnabled && (() => { // v2.2.5 — Saldo: After-Hours-Fee fällt durch späteren Werktag-Rückgabe weg // Saldo (negativ = Kunde spart). Wir zeigen ihn nur, wenn After-Hours aktiv ist // UND der Bonus-Tag wirklich innerhalb der Öffnungszeiten liegt. const ahEnabled = !!(boot.afterHours && boot.afterHours.enabled); const ahFee = ahEnabled ? (boot.afterHours.fee || 0) : 0; const surcharge = parseInt(w.bonusSurcharge || 0, 10); const showSaldo = ahFee > 0; const saved = ahFee; const net = saved - surcharge; return ( <> {showSaldo && (
{net > 0 ? ( <> Effektiv CHF {chf(net)} günstiger: {" "}Sonntag-Rückgabe wäre After-Hours (+CHF {chf(saved)}) {surcharge > 0 ? <> – Bonus-Aufpreis CHF {chf(surcharge)} : null} {" "}= Sie sparen CHF {chf(net)}. ) : net === 0 ? ( <> Bonus-Tag ist preisneutral: After-Hours fällt weg {" "}(−CHF {chf(saved)}), Aufpreis CHF {chf(surcharge)} gleicht das aus. ) : ( <> Bonus-Tag kostet effektiv CHF {chf(-net)} mehr {" "}(After-Hours wäre −CHF {chf(saved)}, Aufpreis +CHF {chf(surcharge)}). )}
)} ); })()}
)} {/* Stepper */}
{stepperItems.map((s, i, arr) => ( {i < arr.length - 1 &&
} ))}
{step === 1 && ( setStep(hasPreselected ? 3 : 2)} weekendActive={!!weekendDeal} /> )} {step === 2 && !hasPreselected && ( setStep(3)} onBack={() => setStep(1)} /> )} {step === 3 && ( setStep(4)} onBack={() => setStep(hasPreselected ? 1 : 2)} /> )} {step === 4 && ( setStep(3)} /> )}
{/* Summary-Sidebar wird unten gerendert (wegen Reihenfolge im DOM/CSS) */}
); } // v2.2.5 — Doppel-Slider für Mietdauer (Tage 0-30 + Stunden 0-23) // onChangeHours(totalHours) — totalHours = days*24 + hours, mindestens 1 function DurationSlider({ hours, onChangeHours, maxDays }) { const safeMax = Math.max(1, parseInt(maxDays || 30, 10)); const total = Math.max(1, parseInt(hours || 1, 10)); const days = Math.floor(total / 24); const extraH = total - days * 24; const setDays = (d) => { const next = Math.max(0, Math.min(safeMax, parseInt(d, 10))); onChangeHours && onChangeHours(Math.max(1, next * 24 + extraH)); }; const setHours = (h) => { const next = Math.max(0, Math.min(23, parseInt(h, 10))); onChangeHours && onChangeHours(Math.max(1, days * 24 + next)); }; // Schöne Label-Anzeige const label = days > 0 ? (extraH > 0 ? `${days} Tag${days === 1 ? "" : "e"} ${extraH} Std` : `${days} Tag${days === 1 ? "" : "e"}`) : `${extraH || 1} Std`; return (
Mietdauer
{label}
{days * 24 + extraH} Std
Tage {days} / {safeMax}
setDays(e.target.value)} aria-label="Tage" />
Stunden {extraH} / 23
setHours(e.target.value)} aria-label="Zusätzliche Stunden" />
); } window.DurationSlider = DurationSlider; function Step1({ data, update, locations, showLocations, summary, onNext, weekendActive }) { // Mietdauer-Buckets für Schnellauswahl-Pills (kürzere Auswahl als Dropdown — die typischen Fälle) const buckets = [ { hours: 3, label: "3 Std" }, { hours: 6, label: "6 Std" }, { hours: 24, label: "1 Tag" }, { hours: 48, label: "2 Tage" }, { hours: 168, label: "1 Woche" }, ]; // v2.2.20 — Mietdauer-Dropdown (statt Doppel-Slider). Optionen wie SC Sportcars: // 3H / 6H / 12H / 24H (1 Tag) / 48H (2 Tage) / 72H (3 Tage) / 96H (4 Tage) / // 120H (5 Tage) / Länger (Anfrage) const durationOptions = [ { value: "3", hours: 3, label: "3 Stunden" }, { value: "6", hours: 6, label: "6 Stunden" }, { value: "12", hours: 12, label: "12 Stunden" }, { value: "24", hours: 24, label: "24 Stunden (1 Tag)" }, { value: "48", hours: 48, label: "48 Stunden (2 Tage)" }, { value: "72", hours: 72, label: "72 Stunden (3 Tage)" }, { value: "96", hours: 96, label: "96 Stunden (4 Tage)" }, { value: "120", hours: 120, label: "120 Stunden (5 Tage)" }, { value: "longer", hours: 192, label: "Länger – auf Anfrage" }, ]; const currentHours = Math.max(1, Math.round((new Date(data.returnAt) - new Date(data.pickupAt)) / 3600000)); const setHours = (h) => { const start = new Date(data.pickupAt); if (isNaN(start.getTime())) return; const end = new Date(start.getTime() + h * 3600 * 1000); const off = end.getTimezoneOffset(); const local = new Date(end.getTime() - off * 60 * 1000); update("returnAt", local.toISOString().slice(0, 16)); }; const setBucket = (h) => setHours(h); // Aktuell gewählten Wert auf eine Dropdown-Option matchen, sonst "custom" (zeigt aktuellen Wert an) const matchedDuration = durationOptions.find(o => o.hours === currentHours); const durationSelectValue = matchedDuration ? matchedDuration.value : (currentHours > 120 ? "longer" : "custom"); const handleDurationChange = (val) => { if (val === "custom") return; const opt = durationOptions.find(o => o.value === val); if (opt) setHours(opt.hours); }; // v2.2.6 — Tier-Preise des aktuell gewählten Fahrzeugs const v = summary && summary.vehicle ? summary.vehicle : null; const tp = (v && v.tierPrices) ? v.tierPrices : {}; const tierDefs = [ { hours: 3, label: "3 Std", price: tp["3"] }, { hours: 6, label: "6 Std", price: tp["6"] }, { hours: 24, label: "1 Tag", price: tp["24"] }, { hours: 168, label: "1 Woche", price: tp["168"] }, ].filter(t => t.price && t.price > 0); const hasTiers = tierDefs.length > 0; const inquiryThresholdH = (v && v.inquiryThresholdH) ? v.inquiryThresholdH : 168; const isInquiry = inquiryThresholdH > 0 && currentHours > inquiryThresholdH; return (
Wann{showLocations ? " und wo" : ""}?
{showLocations && ( <>
)} {!showLocations && (
{locations[0]} · Abholung & Rückgabe am gleichen Standort
)} {/* v2.2.21 — Datum + Zeit prominenter (eyebrow-Label + grössere Box, konsistent mit dem Mietdauer-Feld darunter) v2.2.41 — Im Aktion-Slot-Modus (weekendActive) sind die Felder readOnly und visuell als „fixiert" markiert. Der Slot wurde von der Aktion-Karte vorgegeben und darf nicht editiert werden. */}
update("pickupAt", e.target.value)} />
update("returnAt", e.target.value)} />
{weekendActive && (
Aktion-Slot fixiert. Diese Daten kommen aus der Aktion und sind nicht änderbar. Brauchst du einen anderen Zeitraum? Buche das Fahrzeug regulär ohne Aktion.
)}
{/* v2.2.5 — Im Weekend-Modus: keine Mietdauer-Auswahl (Pauschalpreis ist fix) */} {!weekendActive && (
{/* v2.2.7 — Tier-Pauschalen-Pills entfernt: nur noch Slider + Schnellauswahl, um die UI klarer zu halten. Der Slider rastet auf den Tier-Stunden ein (siehe DurationSlider) und der Server-Quote zeigt den passenden Tier-Preis an. */} {/* v2.2.6 — Inquiry-Mode: über Schwelle = Anfrage statt Live-Preis */} {isInquiry ? (
Längere Mieten — auf Anfrage
Über {inquiryThresholdH} Std. ({Math.round(inquiryThresholdH/24)} Tage) erstellen wir gerne ein individuelles Angebot. Senden Sie uns Ihre Anfrage über das Formular — wir melden uns innerhalb von 2 Stunden mit einer persönlichen Offerte.
) : null} {/* v2.2.20 — Mietdauer-Dropdown (ersetzt den Doppel-Slider) v2.2.21 — Box-Wrapper mit prominentem Eyebrow-Header für bessere Lesbarkeit und Konsistenz mit den Datum-Feldern. */}
Mietdauer
Wie lange möchtest du fahren?
Brauchst du etwas dazwischen? Wähle „Länger – auf Anfrage" und wir erstellen dir ein individuelles Angebot.
Schnellauswahl: {buckets.map(b => { const active = currentHours === b.hours; return ( ); })}
)} {/* Mindestmietdauer-Warnung */} {summary && summary.tooShort && (
Für dieses Fahrzeug beträgt die Mindestmietdauer {summary.minHours} Std. {" "}Aktuell sind nur {summary.hours} Std. ausgewählt.
)} {/* Inkl. km + Extra-km kaufen */} update("extraKm", Math.max(0, parseInt(n || 0, 10)))} /> {/* Stundenmodus-Hint: günstigerer Stundentarif aktiv */} {summary && summary.priceMode === "hour" && summary.pricePerHour > 0 && (
Stundentarif aktiv: {summary.hours} Std. × CHF {chf(summary.pricePerHour)} {" "}— günstiger als der Tagestarif für diese Mietdauer.
)} {/* Smart-Hint: Stunden-Tipp (weniger als 24h gewählt → Tag günstiger) */} {summary && summary.hourTip && (
{summary.hourTip.message}
)} {/* Smart-Hint: Aktive Rabatte */} {summary && summary.appliedDisc && summary.appliedDisc.length > 0 && !summary.weekendApplied && summary.discountAmount > 0 && (
Smart-Rabatt aktiv:{" "} {summary.appliedDisc.filter(d => d.kind !== 'weekend_deal').map((d, i) => ( {i > 0 && ", "} {d.label} (−{d.pct}%) ))} {" — Sie sparen CHF "}{chf(summary.discountAmount)}.
)} {/* Weekend-Pauschale-Hint */} {summary && summary.weekendApplied && (
Weekend-Pauschale aktiv {summary.weekendLabel ? : {summary.weekendLabel} : null} {" — alles inklusive für Ihr Wochenende. Pauschalpreis: CHF "}{chf(summary.baseTotal)}.
)} {/* After-Hours-Warnung */} {summary && summary.afterHoursReasons && summary.afterHoursReasons.length > 0 && (
Hinweis:{" "} {summary.afterHoursReasons.map((r, i) => ( {i > 0 ? ", " : ""}{r.label} ))} {" — Aufschlag CHF "}{chf(summary.afterHoursCost)}{" wird der Buchung automatisch hinzugefügt."}
)}
); } function Step2({ data, update, vehicles, onNext, onBack }) { return (
Welches Fahrzeug?
{vehicles.map(v => ( ))}
); } function Step3({ data, addons, toggleAddon, onNext, onBack }) { return (
Extras & Versicherung
{addons.map(a => { const active = data.addons.includes(a.id); return (
!a.locked && toggleAddon(a.id)} className="card addon-row" style={{ padding: "16px 20px", display: "flex", alignItems: "center", gap: 16, cursor: a.locked ? "default" : "pointer", borderColor: active ? "var(--accent)" : "var(--line)", }} >
{active && }
{a.label}
{a.note &&
{a.note}
}
{a.price === 0 ? "Inklusive" : `CHF ${chf(a.price)} / ${a.per}`}
); })}
); } function Step4({ data, update, summary, locations, contact, onConfirm, onBack }) { // v2.2.29 — WhatsApp-Vorlage mit Buchungs-Daten füllen const fmtDt = (iso) => { if (!iso) return "—"; try { const d = new Date(iso); const pad = n => String(n).padStart(2, "0"); return `${pad(d.getDate())}.${pad(d.getMonth() + 1)}.${d.getFullYear()}`; } catch { return iso; } }; const fmtTm = (iso) => { if (!iso) return "—"; try { const d = new Date(iso); const pad = n => String(n).padStart(2, "0"); return `${pad(d.getHours())}:${pad(d.getMinutes())}`; } catch { return ""; } }; const totalH = Math.max(1, Math.round((new Date(data.returnAt) - new Date(data.pickupAt)) / 3600000)); const days = Math.floor(totalH / 24); const extraH = totalH - days * 24; const durationStr = days > 0 ? (extraH > 0 ? `${days} Tag${days === 1 ? "" : "e"} ${extraH} Std` : `${days} Tag${days === 1 ? "" : "e"}`) : `${totalH} Std`; const locName = (() => { if (!data.pickupLoc) return "Nach Absprache"; const lst = Array.isArray(locations) ? locations : []; const m = lst.find(l => String(l.id) === String(data.pickupLoc)); return m ? (m.name || m.label || m.title || "Nach Absprache") : "Nach Absprache"; })(); const v = summary.vehicle || {}; const waTpl = (contact && contact.whatsappTemplateBooking) ? contact.whatsappTemplateBooking : "Hallo ACN-Cars, ich möchte den {brand} {model} buchen. Abholung {pickup_date} bis {return_date}. Total CHF {total_price}. Name: {customer_name}"; const waText = waTpl .replace(/\{brand\}/g, v.brand || "") .replace(/\{model\}/g, v.model || "") .replace(/\{pricePerDay\}/g, chf(v.pricePerDay)) .replace(/\{price\}/g, "CHF " + chf(v.pricePerDay) + "/Tag") .replace(/\{pickup_date\}/g, fmtDt(data.pickupAt)) .replace(/\{pickup_time\}/g, fmtTm(data.pickupAt)) .replace(/\{return_date\}/g, fmtDt(data.returnAt)) .replace(/\{return_time\}/g, fmtTm(data.returnAt)) .replace(/\{duration\}/g, durationStr) .replace(/\{pickup_location\}/g, locName) .replace(/\{total_price\}/g, chf(summary.total)) .replace(/\{customer_name\}/g, data.name || "—"); const waBase = (contact && contact.whatsappLink) ? contact.whatsappLink.split("?")[0] : "https://wa.me/41789480304"; const waUrl = `${waBase}?text=${encodeURIComponent(waText)}`; // v2.2.6 — Im Inquiry-Mode ist tooShort egal (kein Live-Preis nötig) const can = data.name && data.email && data.phone && data.consent && (summary.isInquiry || !summary.tooShort); return (
Letzter Schritt

Ihre Kontaktdaten

Wir bestätigen Ihre Anfrage innerhalb von 2 Stunden per E-Mail oder WhatsApp.

update("name", e.target.value)} placeholder=" " id="acn-name" />
update("email", e.target.value)} placeholder=" " id="acn-email" />
update("phone", e.target.value)} placeholder=" " id="acn-phone" />
update("license", e.target.value)} placeholder=" " id="acn-license" />