/* global React, Icon, chf */ // ACN-Cars · Page: Home (Hero + featured + deals + marquee) // v2.2.41 — Slot-Helper für Aktion-Karten. Berechnet das nächste Vorkommen // eines konkreten Wochentag+Stunden-Slots ab `now`. Wenn heute der Slot-Tag // ist UND die Slot-Uhrzeit bereits verstrichen ist, springt er auf nächste // Woche — exakt das Verhalten, das v2.2.41 für „Aktion-Slot-Strict" will. function acnNextDateForDow(now, targetDow, hour) { const d = new Date(now.getTime()); d.setHours(hour || 9, 0, 0, 0); let diff = (targetDow - d.getDay() + 7) % 7; if (diff === 0 && d.getTime() < now.getTime()) diff = 7; d.setDate(d.getDate() + diff); return d; } function acnFmtSlot(d) { if (!d || isNaN(d.getTime())) return ""; const dows = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"]; const months = ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"]; const hh = String(d.getHours()).padStart(2, "0"); const mm = String(d.getMinutes()).padStart(2, "0"); return `${dows[d.getDay()]} ${d.getDate()}. ${months[d.getMonth()]} · ${hh}:${mm}`; } function HomePage({ vehicles, deals, locations, favs, onFav, onCompare, comparing, onBook, onOpen, onDealBook, goPage, contact, boot }) { // v2.2.23 — Zwei separate Featured-Toggles statt einem: // featuredHero → steuert nur den Hero-Slider oben // featuredCards → steuert Aktion-Karten + Live-Kalkulator // Damit kann der User z.B. 3 Premium-Autos im Hero zeigen und 6 andere // Fahrzeuge in den Aktion-Karten / im Kalkulator. Bleibt eines der beiden // Felder komplett leer, fällt es auf die alte v2.2.22-Logik zurück (alle // Vehicles), damit nichts leer wirkt. Die Flotte-Seite zeigt unverändert // immer alle Fahrzeuge — unabhängig von beiden Toggles. const heroFeatured = vehicles.filter(v => v.featuredHero); const cardFeatured = vehicles.filter(v => v.featuredCards); const heroVehicles = heroFeatured.length > 0 ? heroFeatured : vehicles; const cardVehicles = cardFeatured.length > 0 ? cardFeatured : vehicles; // homeVehicles bleibt als Convenience-Alias für Stellen, die „auf Startseite // sichtbar" allgemein meinen (wir nehmen die Karten-Liste als kanonisch, // weil das die grössere Auswahl-Sektion ist). const homeVehicles = cardVehicles; // Hero-Slider: Top-Modelle, automatischer Wechsel alle 5 s const heroSlides = heroVehicles.slice(0, 5); const [heroIdx, setHeroIdx] = React.useState(0); React.useEffect(() => { if (heroSlides.length < 2) return; const t = setInterval(() => setHeroIdx(i => (i + 1) % heroSlides.length), 5000); return () => clearInterval(t); }, [heroSlides.length]); const heroVehicle = heroSlides[heroIdx] || heroVehicles[0] || vehicles[0]; // v2.2.14 — Bewertung aus Backend (boot.rating / boot.ratingCount), mit // sinnvollen Defaults, falls noch nicht gesetzt. const ratingValue = (boot && boot.rating != null && String(boot.rating).trim() !== "") ? String(boot.rating).trim() : "4.9"; const ratingCount = (boot && boot.ratingCount != null && String(boot.ratingCount).trim() !== "") ? String(boot.ratingCount).trim() : "327"; const showLocations = boot ? !!boot.showLocations : (locations && locations.length > 1); // v2.2.20 — „Warum ACN-Cars"-Karten aus dem Backend (Title + Text editierbar, // Icons bleiben hardcoded). Fallback auf Defaults, falls boot.whyCards fehlt. const whyCardsBoot = (boot && Array.isArray(boot.whyCards)) ? boot.whyCards : []; const whyCardFallbacks = [ { title: "Geprüfte Premium-Fahrzeuge", text: "Jedes Auto wird vor jeder Übergabe von unserem Team komplett aufbereitet — Innenraum, Lack, Technik. Sie steigen ein und fahren los.", }, { title: "24/7 Concierge", text: "Persönlicher Ansprechpartner per WhatsApp, SMS oder Anruf — rund um die Uhr.", }, { title: "Lieferung zum Wunschort", text: "Wir bringen Ihr Fahrzeug — bis vor die Haustür, zum Hotel oder direkt zum Flughafen. Schweizweit.", }, { title: "Transparente Preise", text: "Keine versteckten Gebühren, keine bösen Überraschungen. Inkl. MwSt., inkl. km, inkl. Versicherung — was Sie sehen, ist was Sie zahlen.", }, ]; const whyCard = (i) => { const fallback = whyCardFallbacks[i] || { title: "", text: "" }; const fromBoot = whyCardsBoot[i]; if (!fromBoot) return { ...fallback, enabled: true }; return { title: (fromBoot.title && String(fromBoot.title).trim() !== "") ? fromBoot.title : fallback.title, text: (fromBoot.text && String(fromBoot.text).trim() !== "") ? fromBoot.text : fallback.text, enabled: fromBoot.enabled !== false, }; }; // v2.2.13 — dealVehicles erkennt jetzt auch Tier-Aktionen (3h/6h/1w), // damit ein Fahrzeug mit z.B. nur einer 6h-Aktion korrekt in der // Aktionen-Sektion erscheint. const hasAnyTierDeal = (v) => { const tp = v.tierPrices || {}; const top = v.tierOldPrices || {}; return ["3", "6", "24", "168"].some(h => { const price = h === "24" ? (tp[h] || v.pricePerDay) : tp[h]; const oldP = top[h]; return price && oldP && Number(oldP) > 0 && Number(oldP) > Number(price); }); }; // v2.2.42 — Map vehicle.id → matchender Deal-CPT, plus dealType-Info. // Wird genutzt, um Duplikate zwischen Aktions-Sektion (oben) und // „Alle Fahrzeuge"-Grid (unten) zu vermeiden: // - Discount-Aktion → Auto NUR oben sichtbar (unten ausgeblendet) // - Package-Aktion → Auto OBEN als Aktion-Karte UND UNTEN mit Slot-Hinweis // (damit es Mo-Do trotzdem buchbar bleibt) // Bei Mehrfach-Treffern gewinnt 'package' über 'discount', weil package // mehr Info trägt (Slot + Pauschale). const vehicleDealMap = {}; (deals || []).forEach(deal => { if (!Array.isArray(deal.vehicles)) return; deal.vehicles.forEach(vid => { const existing = vehicleDealMap[vid]; if (!existing) { vehicleDealMap[vid] = deal; } else if (deal.dealType === 'package' && existing.dealType !== 'package') { vehicleDealMap[vid] = deal; } }); }); // v2.2.22 — Aktion-Karten zeigen nur Fahrzeuge, die auch auf der Startseite // sichtbar sind (homeVehicles). So bleibt die Steuerung über `featured` // konsistent — wer den Toggle abschaltet, taucht weder im Slider noch in // den Aktion-Karten oder im Live-Kalkulator auf. // v2.2.42 — Filter eng gezogen: Auto erscheint NUR in der Aktion-Sektion, // wenn es tatsächlich in einem aktiven Deal-CPT ist. Vorher landete z.B. // ein Crafter mit reduzierter Tier-Pauschale (ohne Deal-CPT) hier, // ohne Aktion-Badge — Naim's Wunsch: nur „echte" Aktionen anzeigen. // Tier-Rabatte allein sind keine Aktion mehr; sie bleiben unten im // „Alle Fahrzeuge"-Grid mit der ganz normalen VehicleCard sichtbar. const dealVehicles = homeVehicles.filter(v => !!vehicleDealMap[v.id]); const dealsCount = (deals ? deals.length : 0) + dealVehicles.length; // v2.2.13 — Höchster verfügbarer Rabatt für die Hero-Sektions-Headline, // damit der Wert der Master-Aussage entspricht ("bis zu X% reduziert"). // v2.2.22 — Konsistent mit homeVehicles-Filter: nur Fahrzeuge, die auf der // Startseite sichtbar sind, fließen in den Headline-Wert ein. const maxDealPct = homeVehicles.reduce((acc, v) => { const candidates = []; if (v.oldPrice && v.oldPrice > v.pricePerDay && v.pricePerDay > 0) { candidates.push(Math.round((1 - v.pricePerDay / v.oldPrice) * 100)); } if (typeof v.dealPct === "number" && v.dealPct > 0) candidates.push(v.dealPct); const tp = v.tierPrices || {}; const top = v.tierOldPrices || {}; ["3", "6", "24", "168"].forEach(h => { const price = h === "24" ? (tp[h] || v.pricePerDay) : tp[h]; const oldP = top[h]; if (price && oldP && Number(oldP) > 0 && Number(oldP) > Number(price)) { candidates.push(Math.round((1 - Number(price) / Number(oldP)) * 100)); } }); return candidates.length ? Math.max(acc, Math.max.apply(null, candidates)) : acc; }, 0); return (
{/* HERO mit Auto-Slider */}
{heroSlides.map((s, i) => (
))}
{/* v2.2.9 — Magazin-Masthead: dezente Topline mit Nummer + Brand + Standort */}
01 PREMIUM-AUTOVERMIETUNG NEUHAUSEN AM RHEINFALL · CH

{(boot.heroTitle || "Premium.\nAuf Abruf.").split("\n").map((line, i, arr) => ( {line} {i < arr.length - 1 &&
}
))}

{boot.heroLead || "Sportwagen und Luxuslimousinen aus der Schweiz. Heute reservieren — morgen am Steuer."}

{/* v2.2.14 — „Im Bild"-Card komplett überarbeitet: grösser, Auto-Mini-Thumbnail links, klare Typo, prominente CTAs, grössere Slider-Dots mit Progress. */} {heroSlides.length > 1 && (
Im Bild {heroVehicle.brand} {heroVehicle.model} ab CHF {chf(heroVehicle.pricePerDay)} /Tag
{heroSlides.map((_, i) => ( ))}
)} {/* v2.2.9 — CTAs + dezenter Rating-Chip nebeneinander */}
★★★★★ {ratingValue} aus {ratingCount} Bewertungen
{/* v2.2.42 — QuickBookingBar entfernt: die Datums-Eingaben wurden nie an die Buchungsseite weitergegeben (reine Deko), der Button dupliziert „Jetzt buchen" oben. Naim's Wunsch: so einfach wie möglich für den Kunden — eine Aktion pro Stelle. */} {/* v2.2.6 — Hero-Stats Redesign: Bento-Cards mit Icons + Hover-Pulse */}
{/* DEALS — v2.2.19: Cream+Gold-Hero komplett raus. Stattdessen pro Aktion eine grosse Magazin-Karte (Foto-2-Layout aus DealsPage): Bild + „Aktion 0X" Eyebrow + Titel + Subtitle + Countdown + „Verfügbare Fahrzeuge"-Liste + „Jetzt sichern"-CTA. Klick auf eine Auto-Reihe in der Karte → Buchungs-Flow mit Aktion + Auto vorbelegt. Vehicle-Karten-Grid darunter bleibt (Naim's Wunsch). */} {deals.length > 0 && (
{/* v2.2.19 — Magazin-Karten (eine pro Aktion) */}
{deals.map((deal, idx) => { // v2.2.22 — Magazin-Karte filtert auf homeVehicles, damit // ausgeblendete Autos nicht als Avatar in der Aktion auftauchen. const dvs = homeVehicles.filter(v => deal.vehicles.includes(v.id)); const heroVeh = dvs[0]; const heroImage = deal.image || (heroVeh && heroVeh.image); const isWeekendDeal = !!(deal.isWeekend && deal.weekend); // v2.2.41 — Aktion-Slot prominenter zeigen: für Weekend-Aktionen // die nächste Slot-Periode (Pickup → Return) ausrechnen und im // Karten-Header neben dem Countdown anzeigen, damit User vor // dem Klick wissen, welcher Zeitraum gelockt wird. let slotPickup = null; let slotReturn = null; if (isWeekendDeal) { const w = deal.weekend; const now = new Date(); slotPickup = acnNextDateForDow(now, Number(w.pickupDay), Number(w.pickupHour) || 17); slotReturn = acnNextDateForDow(slotPickup, Number(w.returnDay), Number(w.returnHour) || 9); if (slotReturn.getTime() <= slotPickup.getTime()) { slotReturn.setDate(slotReturn.getDate() + 7); } } return (
{deal.isWeekend ? ( Weekend ) : ( −{deal.discount}% )} {deal.accent && Limited}
Aktion {String(idx + 1).padStart(2, "0")}

{deal.title}

{deal.subtitle && (

{deal.subtitle}

)}
Endet in
{/* v2.2.41 — Slot-Anzeige für Weekend-Aktionen. Macht für den User transparent, welcher Zeitraum beim Klick auf ein Fahrzeug fixiert wird. */} {isWeekendDeal && slotPickup && slotReturn && (
Nächster Aktion-Slot
Abholung {acnFmtSlot(slotPickup)}
Rückgabe {acnFmtSlot(slotReturn)}
Slot ist fixiert. Nur in dieser Periode buchbar.
)} {dvs.length > 0 && (
Verfügbare Fahrzeuge
{dvs.map(v => { // v2.2.45 — Streichpreis-Berechnung: // - Bei %-Rabatt-Aktionen: alter Tagespreis → neuer Tagespreis (1:1 Vergleich) // - Bei Weekend-Pauschale: Tagespreis × Anzahl Tage des Slots // ergibt den „normal gerechneten" Vergleichswert. Streichpreis // wird nur gerendert, wenn das Pauschal-Angebot tatsächlich // günstiger ist. Sonst gar kein Vergleich (Pauschale = Pauschale). let oldP, newP, perLabel; if (isWeekendDeal) { const vp = (deal.weekend.vehiclePrices && typeof deal.weekend.vehiclePrices === "object") ? deal.weekend.vehiclePrices : {}; const wPrice = vp[v.id] && Number(vp[v.id]) > 0 ? Number(vp[v.id]) : (deal.weekend.price > 0 ? Number(deal.weekend.price) : 0); newP = wPrice > 0 ? wPrice : Math.round(v.pricePerDay * (1 - (Number(deal.discount) || 0) / 100)); perLabel = "Pauschale"; // Slot-Dauer in Tagen (Aufrunden — angefangener Tag zählt voll). let slotDays = 0; if (slotPickup && slotReturn) { const ms = slotReturn.getTime() - slotPickup.getTime(); slotDays = Math.max(1, Math.ceil(ms / (1000 * 60 * 60 * 24))); } oldP = slotDays > 0 ? v.pricePerDay * slotDays : 0; } else { oldP = v.pricePerDay; newP = Math.round(oldP * (1 - (Number(deal.discount) || 0) / 100)); perLabel = "/Tag"; } // v2.2.19 — Klick → Buchung mit Aktion + Auto vorbelegt const handleClick = () => onDealBook && onDealBook(deal, v); return (
{ if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleClick(); } }} title={`${v.brand} ${v.model} mit dieser Aktion buchen`} >
{v.brand}
{v.model}
{/* v2.2.45 — Streichpreis nur dann anzeigen, wenn die Pauschale (oder %-Aktion) tatsächlich günstiger ist als der „normal gerechnete" Vergleichspreis. */} {oldP > newP && ( CHF {chf(oldP)} )} CHF {chf(newP)} {perLabel}
); })}
)} {/* v2.2.20 — „Jetzt sichern"-Button entfernt: jede Auto-Reihe ist klickbar (data-clickable) und führt direkt zur Buchung mit Aktion + Auto vorbelegt — der CTA war redundant. */}
); })}
{/* v2.2.18 — Vehicle-Karten-Grid bleibt unten (Naim's Wunsch) */} {dealVehicles.length > 0 && window.VehicleCard && (
{dealVehicles.slice(0, 6).map((v) => { const matchingDeal = deals.find(d => Array.isArray(d.vehicles) && d.vehicles.includes(v.id) ) || null; return ( { // v2.2.19 — Wenn das Auto in einer Aktion ist: // Buchung mit Aktion + Auto vorbelegt. // v2.2.23 — Bugfix: Klick auf eine Tier-Pille (3h / 6h / // 1 Tag) leitete bei Weekend-Aktion-Fahrzeugen automatisch // in den Weekend-Booking-Flow um (Datum gelockt aufs // Wochenende, Tier-Wahl ging verloren). Naim's Wunsch: // Weekend-Modus NUR via expliziten Weekend-Button oder // Weekend-Datum. Wenn der User explizit einen Kurz-Tier // (3h / 6h / 24h) gewählt hat, respektieren wir das und // gehen den normalen Buchungs-Flow — die Aktion/Weekend // wird nicht aufgezwungen. const isWeekendDeal = !!(matchingDeal && matchingDeal.isWeekend); const pickedShortTier = tier && tier > 0 && tier < 168; if (matchingDeal && onDealBook && !(isWeekendDeal && pickedShortTier)) { onDealBook(matchingDeal, veh); } else { onBook(veh, tier); } }} onOpen={(veh) => onOpen && onOpen(veh, matchingDeal)} contact={contact} infoCardSettings={boot && boot.infoCard} /> ); })}
)}
)} {/* v2.2.41 — ALLE FAHRZEUGE — Flotte-Page wurde entfernt, deshalb listet die Startseite jetzt alle Fahrzeuge in einem dedizierten Grid. Diese Sektion ersetzt die alte Flotte-Seite (Naim-Wunsch v2.2.41). ID „alle-fahrzeuge" ist Sprung-Ziel für die „Flotte ansehen"- und „Fahrzeug wählen"-Buttons im Hero und Why-Block. v2.2.42 — Filter: Autos in einer Discount-Aktion erscheinen NICHT mehr im Grid (sind oben in der Aktion-Sektion sichtbar — Duplikat vermeiden). Autos in einer Package-Aktion (z.B. Weekend-Slot) bleiben im Grid, kriegen aber einen klickbaren Hinweis-Streifen unter der Karte, der zur Aktion-Sektion scrollt. So sind sie für Mo-Do (außerhalb des Aktion-Slots) zum normalen Tier-Preis buchbar. */} {(() => { const allVehiclesFiltered = vehicles.filter(v => { const d = vehicleDealMap[v.id]; if (d && d.dealType === 'discount') return false; return true; }); if (!(allVehiclesFiltered.length > 0 && window.VehicleCard)) return null; return (
Unsere Flotte

Alle Fahrzeuge auf einen Blick

{allVehiclesFiltered.length} {allVehiclesFiltered.length === 1 ? "Fahrzeug" : "Fahrzeuge"} sofort buchbar — Sportwagen, Luxuslimousinen und SUVs aus der Schweiz. Wählen Sie Ihr Wunsch-Modell und buchen Sie in wenigen Klicks.

{allVehiclesFiltered.map((v) => { const matchingDeal = vehicleDealMap[v.id] || null; // v2.2.42 — Package-Hinweis-Info: nur wenn Auto in // einer Package-Aktion ist (Weekend-Slot etc.). let pkgHint = null; if (matchingDeal && matchingDeal.dealType === 'package' && matchingDeal.weekend) { const w = matchingDeal.weekend; const vp = (w.vehiclePrices && typeof w.vehiclePrices === 'object') ? w.vehiclePrices : {}; const price = vp[v.id] && Number(vp[v.id]) > 0 ? Number(vp[v.id]) : (Number(w.price) > 0 ? Number(w.price) : 0); const now = new Date(); const pickup = acnNextDateForDow(now, Number(w.pickupDay), Number(w.pickupHour) || 17); const ret = acnNextDateForDow(pickup, Number(w.returnDay), Number(w.returnHour) || 9); if (ret.getTime() <= pickup.getTime()) ret.setDate(ret.getDate() + 7); pkgHint = { price, pickup, ret }; } return (
{ // v2.2.44 — Untere Flotte: Default = normaler Tier-Buchungs-Flow. // Package-Deals (Weekend-Slot) werden hier NICHT mehr automatisch // erzwungen. Wer den Deal will, klickt oben auf die Aktion-Karte // (oder unten auf den Hint-Streifen). Discount-Aktionen werden // ohnehin ausgefiltert (kommen hier gar nicht vor — siehe // allVehiclesFiltered oben). onBook(veh, tier); }} onOpen={(veh) => onOpen && onOpen(veh, matchingDeal ? Object.assign({}, matchingDeal, { __hint: true }) : null)} contact={contact} infoCardSettings={boot && boot.infoCard} /> {pkgHint && ( )}
); })}
); })()} {/* SCHNELL RECHNEN — eingebetteter Kalkulator-Block */}
Schnell rechnen

Was kostet meine Wunsch-Miete?

Wählen Sie Fahrzeug und Mietdauer — der Preis erscheint sofort, transparent und inklusive MwSt. Smart-Rabatte (Wochenende, längere Miete) werden automatisch angewendet.

{ onBook({ id: vid }); }} />
{/* Marquee brands */}
{["PORSCHE", "MERCEDES-BENZ", "BMW M", "AUDI RS", "LAMBORGHINI", "ROLLS-ROYCE", "MASERATI", "BENTLEY", "PORSCHE", "MERCEDES-BENZ", "BMW M", "AUDI RS", "LAMBORGHINI", "ROLLS-ROYCE", "MASERATI", "BENTLEY" ].map((b, i) => (
{b}
))}
{/* v2.2.5 — Why-Sektion: Bento-Grid mit großen Icons & Hover-Animationen */}
Warum ACN-Cars

Premium-Mieten, spürbar besser.

Vier Versprechen, die wir täglich einhalten — von Aufbereitung über Preise bis 24/7-Service.

{/* v2.2.20 — Why-Cards lesen Title + Text aus boot.whyCards (Backend-editierbar). Icons + Reihenfolge + Karten-Style bleiben hardcoded. */} {whyCard(0).enabled && (
{whyCard(0).title}
{whyCard(0).text}
40+ handselektierte Modelle
)} {whyCard(1).enabled && (
{whyCard(1).title}
{whyCard(1).text}
)} {whyCard(2).enabled && (
{whyCard(2).title}
{whyCard(2).text}
)} {whyCard(3).enabled && (
{whyCard(3).title}
{whyCard(3).text}
)}
{ratingValue}
aus {ratingCount} Bewertungen
Hunderte zufriedene Kunden — vom Wochenend-Mieter bis zum Stamm-Konzern-Account.
Bereit?
Jetzt unverbindlich anfragen
{/* v2.2.21 — Antwort-Versprechen realistischer („30 Min" war zu optimistisch). */}
Antwort innerhalb von 2 Stunden — werktags meist schneller.
{/* v2.2.41 — Flotte-Page komplett entfernt. Button scrollt jetzt zur Fahrzeug-Grid-Sektion auf der Startseite (id="alle-fahrzeuge"), statt auf eine separate Flotte-Seite zu navigieren. */}
); } // v2.2.6 — Hero-Stats: Bento-Card-Reihe mit Icons + Hover-Glow + Animation der Zahl // v2.2.14 — Rating + Anzahl kommen jetzt aus dem Backend (boot.rating / // boot.ratingCount); werden hier als Default für die letzte Stat // verwendet (falls boot.heroStats nicht überschrieben wurde). function HeroStats({ stats, rating, ratingCount }) { const r = (rating != null && String(rating).trim() !== "") ? String(rating).trim() : "4.9"; const rc = (ratingCount != null && String(ratingCount).trim() !== "") ? String(ratingCount).trim() : "327"; const defaults = [ { value: "5", label: "Standorte in der Schweiz" }, { value: "40+", label: "Premium-Fahrzeuge" }, { value: "24/7", label: "Lieferung & Abholung" }, { value: r + " ★", label: "Google-Bewertung (" + rc + ")" }, ]; const list = (stats && stats.length > 0 ? stats : defaults) .filter(s => (s.value && String(s.value).trim() !== "") || (s.label && String(s.label).trim() !== "")); // Icon-Heuristik: anhand des Labels oder Values das passende Icon wählen const pickIcon = (s) => { const t = ((s.label || "") + " " + (s.value || "")).toLowerCase(); if (t.includes("★") || t.includes("bewert") || t.includes("rating") || t.includes("google")) return "star"; if (t.includes("standort") || t.includes("location") || t.includes("filiale") || t.includes("ort")) return "map"; if (t.includes("24/7") || t.includes("liefer") || t.includes("zeit") || t.includes("std")) return "clock"; if (t.includes("fahrzeug") || t.includes("auto") || t.includes("flotte") || t.includes("car")) return "car"; if (t.includes("kund") || t.includes("client")) return "users"; if (t.includes("jahr") || t.includes("erfahr")) return "shield"; return "sparkle"; }; return (
{list.map((s, i) => { const ic = pickIcon(s); return (
{ic === "star" && } {ic === "map" && } {ic === "clock" && } {ic === "car" && } {ic === "users" && } {ic === "shield" && } {ic === "sparkle" && }
{s.value}
{s.label}
); })}
); } // v2.2.42 — QuickBookingBar entfernt: Datums-Eingaben wurden nie an die // Buchungsseite weitergegeben (reine Deko), der Button dupliziert // „Jetzt buchen". Naim's Wunsch: weniger Reibung für den Kunden. // Inline-Kalkulator auf der Startseite — schnelle Schätzung mit Stunden-Buckets function InlineCalc({ vehicles, onBook }) { const buckets = [ { hours: 1, label: "1 Std" }, { 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" }, ]; const [vid, setVid] = React.useState(vehicles[0] ? vehicles[0].id : ""); const [hours, setHours] = React.useState(24); const [extraKm, setExtraKm] = React.useState(0); const v = vehicles.find(x => x.id === vid) || vehicles[0]; const boot = (window.ACN_DATA && window.ACN_DATA.boot) || {}; const vatRate = (typeof boot.vatRate === "number" ? boot.vatRate : 8.1) / 100; if (!v) return null; const days = Math.max(1, Math.ceil(hours / 24)); const pricePerHour = v.pricePerHour || 0; const baseDay = v.pricePerDay * days; const baseHour = pricePerHour > 0 ? pricePerHour * hours : 0; // v2.2.22 — Tier-Schwellen-Logik: Tier-Slot gilt als Obergrenze. // tier_3h = Pauschale für 1-3h // tier_6h = Pauschale für 4-6h // tier_24h = Pauschale für 7-24h // tier_1w = nur bei genau 168h (alte Logik) // Ist ein Slot nicht gesetzt, fällt es auf den nächst-höheren gesetzten Slot. const tp = v.tierPrices || {}; let tierPrice = 0; let tierSlotHours = 0; if (hours <= 24) { const shortSlots = [3, 6, 24]; for (const sh of shortSlots) { if (hours <= sh) { const p = tp[String(sh)]; if (p && Number(p) > 0) { tierPrice = Number(p); tierSlotHours = sh; break; } } } } if (tierPrice === 0 && hours === 168) { const p = tp["168"]; if (p && Number(p) > 0) { tierPrice = Number(p); tierSlotHours = 168; } } let priceMode = "day"; let subtotal = baseDay; if (tierPrice > 0) { priceMode = "tier"; subtotal = tierPrice; } else if (pricePerHour > 0 && baseHour > 0 && baseHour < baseDay) { priceMode = "hour"; subtotal = baseHour; } // Aktion-Hinweis: oldPrice → Ersparnis vs. Original-Tagespreis const hasDeal = (typeof v.dealPct === "number" && v.dealPct > 0) || (v.oldPrice && v.oldPrice > v.pricePerDay); const dealPct = hasDeal ? (typeof v.dealPct === "number" && v.dealPct > 0 ? v.dealPct : Math.round((1 - v.pricePerDay / v.oldPrice) * 100)) : 0; const oldSubtotal = priceMode === "day" && v.oldPrice ? v.oldPrice * days : null; // Inkl.-km für gewählte Dauer const kmIncluded = window.ACN_kmIncludedFor ? window.ACN_kmIncludedFor(v, hours) : 0; const kmExtraPrice = parseFloat(v.kmExtraPrice || 1.5); const extraKmCost = Math.round(parseInt(extraKm || 0, 10) * kmExtraPrice); // Preise inkl. MwSt — Brutto = subtotal + extra-km, MwSt wird daraus extrahiert. const total = subtotal + extraKmCost; const net = Math.round(total / (1 + vatRate)); const vat = total - net; return (
Live-Schätzung
{window.DurationSlider ? : }
Schnellauswahl
{buckets.map(b => ( ))}
{/* Inkl. km + Extra-km */} {window.KmBlock && (
setExtraKm(Math.max(0, parseInt(n || 0, 10)))} compact />
)}
{priceMode === "tier" ? `Pauschale ${tierSlotHours === 3 ? "bis 3 Std" : tierSlotHours === 6 ? "bis 6 Std" : tierSlotHours === 24 ? "Tag" : tierSlotHours === 168 ? "Woche" : `${tierSlotHours} Std`}` : priceMode === "hour" ? `${hours} Std × CHF ${chf(pricePerHour)}` : `${days} × CHF ${chf(v.pricePerDay)}`} CHF {chf(subtotal)}
{extraKm > 0 && (
Extra-km ({extraKm} × CHF {kmExtraPrice.toFixed(2).replace('.', ',')}) CHF {chf(extraKmCost)}
)} {hasDeal && oldSubtotal && oldSubtotal > subtotal && (
Aktion −{dealPct}% CHF {chf(oldSubtotal)}
)}
davon MwSt CHF {chf(vat)}
Gesamt (inkl. MwSt) CHF {chf(total)}
{priceMode === "hour" && (
Stundentarif aktiv (günstiger als Tagespauschale).
)}
Smart-Rabatte (Wochenende, lange Miete) werden im Buchungsformular angezeigt.
); } function DealCard({ deal, vehicles, onBook }) { // v2.2.4 — Featured-Image bevorzugen, Fallback erstes Fahrzeug const dealVehicles = vehicles.filter(v => deal.vehicles.includes(v.id)); const main = dealVehicles[0]; const heroImage = deal.image || main?.image; // Wochenend-Pauschale: Min/Max für „Pauschale ab CHF X" let weekendFrom = null; if (deal.isWeekend && deal.weekend) { const prices = []; if (deal.weekend.vehiclePrices && typeof deal.weekend.vehiclePrices === "object") { Object.values(deal.weekend.vehiclePrices).forEach(p => { if (p > 0) prices.push(p); }); } if (prices.length === 0 && deal.weekend.price > 0) prices.push(deal.weekend.price); if (prices.length > 0) weekendFrom = Math.min(...prices); } return (
{deal.isWeekend ? ( Weekend-Pauschale ) : ( −{deal.discount}% )} {deal.accent && Limited}
{deal.title}
{deal.subtitle &&
{deal.subtitle}
}
Endet in
{weekendFrom != null ? (
Pauschale ab CHF {chf(weekendFrom)}
) : dealVehicles.length > 0 ? (
{dealVehicles.length} Fahrzeug{dealVehicles.length === 1 ? "" : "e"} in der Aktion
) :
}
); } window.HomePage = HomePage;