/* global React, Icon, chf, SpecPills */ // ACN-Cars · Page: Vehicle Detail // v2.2.13 — Deal-Badge auf Detail-Seite hinzugefügt, gleiche Max-Rabatt- // Berechnung wie VehicleCard, damit der Badge zwischen Grid und // Detail-Seite identisch ist. // v2.2.16 — Galerie korrekt aus v.gallery (statt 4× Hero-Bild) + // Aktion-Kontext-Banner: wenn man aus einer Aktion kommt, zeigt die // Detail-Seite die Aktion (Titel, Countdown, Aktions-Preis, // „Mit Aktion buchen"-CTA) statt nur den generischen Default. // v2.2.17 — Tier-Picker (3 Std / 6 Std / 1 Tag / 1 Woche) wie auf der // VehicleCard: 4 Pills direkt unter dem Hauptpreis, Klick auf eine // Pille → Hauptpreis + alter Preis + „Jetzt buchen" updaten live, // Buchen-Flow erhält die gewählte Mietdauer (`activeTier`). Bei // Aktion-Kontext wird der Picker ausgeblendet (Aktion bestimmt den // Preis). Vorteils-Bullets unter den CTAs sind jetzt vom Backend // steuerbar (3 fixe Felder mit Toggle in „Erscheinungsbild"). function DetailPage({ vehicle, deal, contact, onBook, onDealBook, onBack, onCompare, comparing, fav, onFav }) { // Index für Galerie — Klick auf Thumbnail wechselt das Hauptbild. const [activeImg, setActiveImg] = React.useState(0); if (!vehicle) { return (
Kein Fahrzeug ausgewählt.
); } const v = vehicle; // v2.2.16 — Galerie-Bilder de-duplizieren. Wenn nur 1 Bild da ist, // Thumbnail-Strip ausblenden. const galleryImages = (() => { const arr = [v.image, ...((v.gallery && Array.isArray(v.gallery)) ? v.gallery : [])] .filter(Boolean); return Array.from(new Set(arr)); })(); const heroImage = galleryImages[Math.min(activeImg, galleryImages.length - 1)] || v.image; // v2.2.13 / v2.2.17 — Tier-Picker + identische Max-Rabatt-Berechnung wie in // VehicleCard (so dass Card-Badge und Detail-Badge synchron bleiben). const tierOldPrices = v.tierOldPrices || {}; const tp = v.tierPrices || {}; // Volle Tier-Defs inkl. Label/Per für die Pill-UI (s. VehicleCard). const tierDefs = [ { hours: 3, label: "3 Std", short: "3h", per: "/3 Std", price: tp["3"] }, { hours: 6, label: "6 Std", short: "6h", per: "/6 Std", price: tp["6"] }, { hours: 24, label: "1 Tag", short: "1d", per: "/Tag", price: tp["24"] || v.pricePerDay }, { hours: 48, label: "2 Tage", short: "2d", per: "/2 Tage", price: tp["48"] }, // v2.2.37 { hours: 168, label: "1 Woche", short: "1w", per: "/Woche", price: tp["168"] }, ].filter(t => t.price && t.price > 0); // Default-Auswahl: günstigster absoluter Preis (entspricht der Card-Logik // ab v2.2.11), so dass Kund:innen den niedrigsten Einstiegspreis sehen. const defaultIdx = tierDefs.length ? tierDefs.reduce((minI, t, i, arr) => (t.price < arr[minI].price ? i : minI), 0) : 0; const [tierIdx, setTierIdx] = React.useState(defaultIdx); const activeTierDef = tierDefs[tierIdx] || null; const activeTier = activeTierDef ? activeTierDef.hours : null; const showTierPicker = tierDefs.length > 1; const displayPrice = activeTierDef ? activeTierDef.price : v.pricePerDay; const displayPer = activeTierDef ? activeTierDef.per : "/Tag"; // Tier-aware alter Preis (wie in VehicleCard). const activeOldPrice = activeTier ? (tierOldPrices[String(activeTier)] && tierOldPrices[String(activeTier)] > 0 ? Number(tierOldPrices[String(activeTier)]) : (activeTier === 24 ? (v.oldPrice || 0) : 0)) : (v.oldPrice || 0); const tierHasDeal = activeOldPrice > 0 && activeOldPrice > displayPrice; const allDealPcts = []; if (v.oldPrice && v.oldPrice > v.pricePerDay && v.pricePerDay > 0) { allDealPcts.push(Math.round((1 - v.pricePerDay / v.oldPrice) * 100)); } if (typeof v.dealPct === "number" && v.dealPct > 0) { allDealPcts.push(v.dealPct); } tierDefs.forEach(td => { const op = tierOldPrices[String(td.hours)]; if (op && Number(op) > 0 && Number(op) > td.price && td.price > 0) { allDealPcts.push(Math.round((1 - td.price / Number(op)) * 100)); } }); const dealPct = allDealPcts.length ? Math.max.apply(null, allDealPcts) : 0; const hasDeal = dealPct > 0; // v2.2.16 — Aktion-Kontext: wenn aus einer Aktion gekommen, Aktions- // spezifische Daten berechnen. // v2.2.44 — Hint-Modus: wenn der User aus der unteren Flotte (NICHT aus einer // Aktion-Karte) auf das Auto kommt, übergibt HomePage `{...deal, __hint:true}`. // Dann zeigen wir die Aktion nur als Hinweis-Streifen, NICHT als Default — // der User kann frei zwischen normalem Tier-Buchungs-Flow und Aktion wählen. const isHintMode = !!(deal && typeof deal === "object" && deal.__hint); const ctx = (deal && typeof deal === "object" && !deal.__hint) ? deal : null; const hint = isHintMode ? deal : null; let ctxPrice = null; // Aktions-Preis (Tagespreis oder Wochenend-Pauschale) let ctxOldPrice = null; // Original-Preis let ctxPriceLabel = "/Tag"; let ctxBadge = null; // z. B. „−20%" oder „Weekend-Pauschale" if (ctx) { if (ctx.isWeekend && ctx.weekend) { const vp = ctx.weekend.vehiclePrices && typeof ctx.weekend.vehiclePrices === "object" ? ctx.weekend.vehiclePrices : {}; const vehiclePrice = vp[v.id] && Number(vp[v.id]) > 0 ? Number(vp[v.id]) : (ctx.weekend.price > 0 ? Number(ctx.weekend.price) : 0); if (vehiclePrice > 0) { ctxPrice = vehiclePrice; ctxPriceLabel = "Pauschale"; ctxBadge = "Weekend-Pauschale"; } } else if (ctx.discount && Number(ctx.discount) > 0) { const pct = Number(ctx.discount); ctxOldPrice = v.pricePerDay; ctxPrice = Math.round(v.pricePerDay * (1 - pct / 100)); ctxBadge = `−${pct}%`; } } const handleBook = () => { // v2.2.23 — Bugfix: Wenn der User auf der Detail-Seite einen Kurz-Tier // (3h / 6h / 24h) gewählt hat, NICHT in den Weekend-Deal-Flow umleiten. // Weekend-Modus nur via expliziten Weekend-Button oder Weekend-Datum. // Bei prozentualen %-Aktionen bleibt der bisherige Aktion-Flow erhalten, // weil dort kein Datum gelockt wird — nur ein Rabatt vorbelegt. const isWeekendCtx = !!(ctx && ctx.isWeekend); const pickedShortTier = activeTier && activeTier > 0 && activeTier < 168; if (ctx && typeof onDealBook === "function" && !(isWeekendCtx && pickedShortTier)) { onDealBook(ctx); return; } // v2.2.17 — Buchen-Flow erhält die ausgewählte Mietdauer (Stunden), so // dass das Buchungsformular Standardwerte richtig vorbelegt. onBook(v, activeTier); }; // v2.2.17 — Vorteils-Bullets aus Backend (Erscheinungsbild → Detail-Seite // Bullets). Fallback auf alte Hardcoded-Texte, falls boot-Daten fehlen. const bootDetailBullets = (typeof window !== "undefined" && window.ACN_DATA && window.ACN_DATA.boot && Array.isArray(window.ACN_DATA.boot.detailBullets)) ? window.ACN_DATA.boot.detailBullets : null; const detailBullets = bootDetailBullets && bootDetailBullets.length ? bootDetailBullets.filter(b => b && b.text && (b.enabled === undefined || b.enabled)) : [ { text: "Vollkasko ohne Selbstbehalt verfügbar", enabled: 1 }, { text: "Lieferung nach Zürich Flughafen kostenlos", enabled: 1 }, { text: "Kostenlose Stornierung bis 24h vorher", enabled: 1 }, ]; return (
{ctxBadge ? (
{ctxBadge}
) : hasDeal && (
−{dealPct}%
)}
{galleryImages.length > 1 && (
{galleryImages.slice(0, 6).map((src, i) => (
setActiveImg(i)} style={{ aspectRatio: "16/10", backgroundImage: `url(${src})`, backgroundSize: "cover", backgroundPosition: "center", borderRadius: "var(--r-md)", border: i === activeImg ? "2px solid var(--ink)" : "1px solid var(--line)", opacity: i === activeImg ? 1 : 0.7, cursor: "pointer", transition: "opacity .15s ease, border-color .15s ease", }} /> ))}
)}
{/* v2.2.16 — Aktion-Banner (nur wenn aus einer Aktion gekommen) */} {ctx && (
Aktion {ctx.endsAt && }
{ctx.title}
{ctx.subtitle &&
{ctx.subtitle}
}
)} {/* v2.2.44 — Hint-Stripe: Aktion verfügbar, aber nicht erzwungen */} {hint && (() => { let hintPrice = 0; let hintLabel = ""; if (hint.isWeekend && hint.weekend) { const vp = hint.weekend.vehiclePrices && typeof hint.weekend.vehiclePrices === "object" ? hint.weekend.vehiclePrices : {}; hintPrice = vp[v.id] && Number(vp[v.id]) > 0 ? Number(vp[v.id]) : (hint.weekend.price > 0 ? Number(hint.weekend.price) : 0); hintLabel = "Pauschale fürs Wochenende"; } else if (hint.discount && Number(hint.discount) > 0) { const pct = Number(hint.discount); hintPrice = Math.round((v.pricePerDay || 0) * (1 - pct / 100)); hintLabel = `−${pct}% pro Tag`; } return (
Aktion verfügbar: {hint.title}
{hintPrice > 0 ? <>Mit dieser Aktion ab CHF {chf(hintPrice)} {hintLabel}. : <>Sonderkonditionen für dieses Fahrzeug.} {" "}Sonst regulär nach Mietdauer buchen.
); })()}
{v.brand} · {v.year}

{v.model}

{v.tagline}

{ctxPrice ? ( <>
CHF {chf(ctxPrice)} {ctxPriceLabel === "Pauschale" ? "Pauschale" : "/Tag"}
{ctxOldPrice && ( CHF {chf(ctxOldPrice)} )} {ctxPriceLabel === "Pauschale" && ( für das ganze Wochenende )} ) : ( <>
CHF {chf(displayPrice)} {displayPer}
{tierHasDeal && ( CHF {chf(activeOldPrice)}{displayPer} )} )}
{/* v2.2.17 — Tier-Picker (nur wenn nicht aus einer Aktion gekommen, weil die Aktion den Preis bestimmt) */} {!ctx && showTierPicker && (
Mietdauer wählen
{tierDefs.map((t, i) => ( ))}
)}
{(() => { // v2.2.29 — Backend-konfigurierbare WhatsApp-Vorlage const waTpl = (contact && contact.whatsappTemplateShort) ? contact.whatsappTemplateShort : `Hallo ACN-Cars, ich möchte den {brand} {model} (CHF {pricePerDay}/Tag) buchen. Bitte um Verfügbarkeitsprüfung.`; 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"); const waBase = (contact && contact.whatsappLink) ? contact.whatsappLink.split("?")[0] : "https://wa.me/41789480304"; return ( Per WhatsApp buchen ); })()} {detailBullets.length > 0 && (
{detailBullets.map((b, i) => ( {b.text} ))}
)}
Technische Daten
); } function Bullet({ children }) { return (
{children}
); } function Spec({ n, l }) { return (
{n}
{l}
); } // v2.2.16 — Kompakter Inline-Countdown für die Aktion-Banner-Zeile // (bewusst kleiner als der grosse Countdown auf Aktionen-Seite, damit er // neben der „Aktion"-Pille im Banner-Kopf Platz findet). function DealCountdownInline({ to }) { const [, setTick] = React.useState(0); React.useEffect(() => { const id = setInterval(() => setTick(t => t + 1), 1000); return () => clearInterval(id); }, []); const ms = Math.max(0, Number(to) - Date.now()); const s = Math.floor(ms / 1000); const d = Math.floor(s / 86400); const h = Math.floor((s % 86400) / 3600); const m = Math.floor((s % 3600) / 60); const sec = s % 60; const pad = (n) => String(n).padStart(2, "0"); const txt = d > 0 ? `${d}T ${pad(h)}:${pad(m)}:${pad(sec)}` : `${pad(h)}:${pad(m)}:${pad(sec)}`; return {txt}; } window.DetailPage = DetailPage;