/* global React, Icon, chf */ // ACN-Cars · Pages: Deals, Calculator, Admin // v2.2.14 — DealsPage Premium-Magazin-Layout: Back-Bar, hellerer Hero (Cream // + Gold), breite Aktion-Karten (Bild links, Story rechts), klare // Typo, sichtbarer Countdown, prominenter „Jetzt sichern"-CTA. // v2.2.15 — Fahrzeug-Reihen in den Magazin-Karten klickbar (öffnen Detailseite). // v2.2.16 — Klick auf Fahrzeug-Reihe übergibt jetzt zusätzlich die Aktion an // onOpen(v, deal), damit die Detailseite den Aktion-Banner und // Aktion-Preise anzeigen kann. Weekend-Deals zeigen jetzt korrekt // die Wochenend-Pauschale (statt eines −%-Rabatts auf den Tagespreis). function DealsPage({ deals, vehicles, onBook, onOpen, onHome, onFleet }) { const totalDeals = deals.length; const maxPct = deals.reduce((m, d) => Math.max(m, Number(d.discount) || 0), 0); return (
{/* Premium Back-Bar */}
{onFleet && ( )}
{/* Magazin-Hero — Cream + Gold, kompakt, lesbar */}
Aktionen & Deals

Kurz & exklusiv.
Solange verfügbar.

Handverlesene Aktionen auf unsere Premium-Flotte. Jede Aktion ist zeitlich begrenzt — sichern Sie sich Ihren Wagen, bevor der Countdown abläuft.

{totalDeals} aktive Aktion{totalDeals === 1 ? "" : "en"}
{maxPct > 0 && (
−{maxPct}% Top-Rabatt
)}
Limitiert
{/* Aktion-Karten — Bild links, Story rechts (Magazin-Stil) */}
{deals.length === 0 && (
Aktuell keine aktiven Aktionen — schauen Sie bald wieder vorbei.
)} {deals.map((deal, idx) => { const dealVehicles = vehicles.filter(v => deal.vehicles.includes(v.id)); const heroVeh = dealVehicles[0]; return (
−{deal.discount}% {deal.accent && Limited}
Aktion {String(idx + 1).padStart(2, "0")}

{deal.title}

{deal.subtitle}

Endet in
{dealVehicles.length > 0 && (
Verfügbare Fahrzeuge
{dealVehicles.map(v => { // v2.2.16 — Weekend-Deals: per-Vehicle-Pauschale, sonst Prozent-Rabatt. // v2.2.32 — Strikethrough vergleicht Pauschal mit (Tagespreis × Wochenend-Tage), // nicht mit dem Tagespreis allein (sonst sieht es so aus als wäre die // Pauschal teurer als der Tagespreis). const isWeekendDeal = !!(deal.isWeekend && deal.weekend); const oldP = v.pricePerDay; let newP, perLabel, oldDisplay; 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(oldP * (1 - (Number(deal.discount) || 0) / 100)); perLabel = "Pauschale"; // Anzahl Wochenend-Tage aus Pickup/Return-Day berechnen const pd = Number(deal.weekend.pickupDay); const rd = Number(deal.weekend.returnDay); const wDays = (Number.isFinite(pd) && Number.isFinite(rd)) ? ((rd - pd + 7) % 7) + 1 : 0; oldDisplay = wDays > 0 ? oldP * wDays : 0; } else { newP = Math.round(oldP * (1 - (Number(deal.discount) || 0) / 100)); perLabel = "/Tag"; oldDisplay = oldP; } // Strikethrough nur zeigen wenn echte Ersparnis vorhanden const showOldPrice = oldDisplay > newP; const clickable = typeof onOpen === "function"; // v2.2.16 — Aktion mitgeben, damit DetailPage den Aktions-Banner zeigt. const handleOpen = () => clickable && onOpen(v, deal); return (
{ if (clickable && (e.key === "Enter" || e.key === " ")) { e.preventDefault(); handleOpen(); } }} title={clickable ? `${v.brand} ${v.model} ansehen` : undefined} >
{v.brand}
{v.model}
{showOldPrice && ( CHF {chf(oldDisplay)} )} CHF {chf(newP)} {perLabel}
{clickable && ( )}
); })}
)}
); })}
); } // ---- Standalone Calculator (mit Stunden-Buckets, inkl. MwSt) ---- function CalcPage({ vehicles, addons, boot, onHome, onFleet }) { 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: 72, label: "3 Tage" }, { hours: 168, label: "1 Woche" }, ]; const vatRate = (boot && typeof boot.vatRate === "number" ? boot.vatRate : 8.1) / 100; const [hours, setHours] = React.useState(24); const [vehicleId, setVehicleId] = React.useState(vehicles[0].id); const [picks, setPicks] = React.useState(addons.filter(a => a.default).map(a => a.id)); const [extraKm, setExtraKm] = React.useState(0); const v = vehicles.find(x => x.id === vehicleId) || vehicles[0]; const togg = id => setPicks(p => p.includes(id) ? p.filter(x => x !== id) : [...p, id]); 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; let priceMode = "day"; let base = baseDay; if (pricePerHour > 0 && baseHour > 0 && baseHour < baseDay) { priceMode = "hour"; base = baseHour; } const ad = picks.reduce((s, id) => { const a = addons.find(x => x.id === id); if (!a) return s; return s + (a.per === "Tag" ? a.price * days : a.price); }, 0); // Inkl.-km für gewählte Dauer + Extra-km 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); // Inkl. MwSt: total = brutto, MwSt herausrechnen. const total = base + ad + extraKmCost; const net = Math.round(total / (1 + vatRate)); const vat = total - net; 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; return (
Preis-Kalkulator

Sehen Sie den Preis sofort.

{window.DurationSlider ? : }
Schnellauswahl: {buckets.map(b => ( ))}
{/* Inkl. km + Extra-km */} {window.KmBlock && (
setExtraKm(Math.max(0, parseInt(n || 0, 10)))} />
)}
Zusatzleistungen
{addons.filter(a => !a.locked).map(a => { const active = picks.includes(a.id); return (
togg(a.id)} style={{ display: "flex", alignItems: "center", gap: 12, cursor: "pointer", padding: "10px 14px", borderRadius: "var(--r-md)", border: "1px solid " + (active ? "var(--ink)" : "var(--line)"), background: active ? "var(--bg-sunken)" : "transparent", }}>
{active && }
{a.label} {a.price === 0 ? "Inkl." : `CHF ${chf(a.price)}/${a.per}`}
); })}
Live-Berechnung
{priceMode === "hour" ? `${hours} Std × CHF ${chf(pricePerHour)}` : `${days} × CHF ${chf(v.pricePerDay)}`} CHF {chf(base)}
{hasDeal && priceMode === "day" && v.oldPrice && (
Aktion −{dealPct}% CHF {chf(v.oldPrice * days)}
)}
ZusatzleistungenCHF {chf(ad)}
{kmIncluded > 0 && (
Inkl. {kmIncluded} km
)} {extraKm > 0 && (
Extra-km ({extraKm} × CHF {kmExtraPrice.toFixed(2).replace('.', ',')}) CHF {chf(extraKmCost)}
)}
davon MwSt {(vatRate * 100).toFixed(1).replace(/\.0$/, "")}% CHF {chf(vat)}
Gesamt (inkl. MwSt) CHF {chf(total)}
{priceMode === "hour" ? `CHF ${chf(Math.round(total / hours))} pro Stunde (effektiv)` : `CHF ${chf(Math.round(total / days))} pro Tag (effektiv)`}
{priceMode === "hour" && (
Stundentarif aktiv (günstiger als Tagespauschale).
)}
); } // ---- Admin Concept ---- function AdminPage({ vehicles, deals, addons }) { const [tab, setTab] = React.useState("vehicles"); return (
Admin · Konzept

Backend für Vermieter.

Mockup des WordPress-Plugin-Backends. Im fertigen Plugin liegt diese Oberfläche unter WP-Admin → ACN-Cars.

{tab === "vehicles" && } {tab === "bookings" && } {tab === "deals" && } {tab === "addons" && } {tab === "availability" && } {tab === "appearance" && } {tab === "shortcode" && }
); } function AdminHeader({ title, action }) { return (

{title}

{action}
); } function AdminVehicles({ vehicles }) { return (
Neues Fahrzeug} /> {vehicles.map(v => ( ))}
ModellKategoriePreis/TagStatus
{v.brand} {v.model}
{v.year} · ID {v.id}
{v.category} CHF {chf(v.pricePerDay)}{v.oldPrice && Aktion} {v.deal ? "Im Deal" : "Aktiv"}
); } function AdminBookings() { const rows = [ { id: "B-2401", name: "Sandra Keller", car: "Porsche 911 GT3", from: "02. Mai", to: "05. Mai", total: 3870, status: "Bestätigt" }, { id: "B-2402", name: "Marco Bianchi", car: "Mercedes S 580", from: "03. Mai", to: "10. Mai", total: 4830, status: "Anzahlung offen" }, { id: "B-2403", name: "Luca Hofer", car: "Audi RS6 Avant", from: "04. Mai", to: "06. Mai", total: 1440, status: "Bestätigt" }, { id: "B-2404", name: "Nina Roth", car: "BMW i7", from: "10. Mai", to: "14. Mai", total: 2600, status: "Anfrage" }, ]; return (
Export CSV} /> {rows.map(r => ( ))}
NrKundeFahrzeugZeitraumTotalStatus
{r.id} {r.name} {r.car} {r.from} – {r.to} CHF {chf(r.total)} {r.status}
); } function AdminDeals({ deals, vehicles }) { return (
Neue Aktion} />
{deals.map(d => (
−{d.discount}%
{d.title}
{d.subtitle} · {d.vehicles.length} Fahrzeuge
))}
); } function AdminAddons({ addons }) { return (
Neue Leistung} /> {addons.map(a => ( ))}
BezeichnungPreisAbrechnungStandard
{a.label} {a.price === 0 ? "Inkl." : `CHF ${chf(a.price)}`} {a.per}
); } function AdminAvailability({ vehicles }) { const days = Array.from({ length: 14 }, (_, i) => i); return (
{days.map(d => )} {vehicles.slice(0, 6).map((v, i) => ( {days.map(d => { const booked = (i + d) % 5 === 0 || (i + d) % 7 === 0; return ; })} ))}
Fahrzeug{d + 1}
{v.brand} {v.model}
Verfügbar Gebucht
); } function AdminAppearance() { return (
Akzentfarbe
{["#A8916F", "#0066CC", "#1F8A4C", "#B23838", "#8C1D40"].map(c => (
))}
Modus
Logo
ACN-CARS
); } function AdminShortcode() { return (

Bestehender Shortcode bleibt unverändert. Einfach in jede WordPress-Seite einfügen:

[acn-automiete]
Erweiterte Parameter (optional)
[acn-automiete view="fleet"] Nur Fahrzeugübersicht
[acn-automiete view="booking" vehicle="p911-gt3"] Direktbuchung Fahrzeug
[acn-automiete view="deals"] Aktionsseite
); } // ============================================================ // v2.2.6 — ComparePage: Bis zu 3 Fahrzeuge nebeneinander vergleichen // Ersetzt die alte CalcPage als Haupt-Service-Seite. Fahrzeug-Auswahl // per Klick, Specs/Preise/Tier-Pauschalen und Inkl.-km im direkten Vergleich. // ============================================================ function ComparePage({ vehicles, onHome, onFleet, onBook }) { const initial = vehicles.slice(0, Math.min(3, vehicles.length)).map(v => v.id); const [picked, setPicked] = React.useState(initial); const togglePick = (id) => { setPicked(p => { if (p.includes(id)) return p.filter(x => x !== id); if (p.length >= 3) return [...p.slice(1), id]; // ältesten verdrängen return [...p, id]; }); }; const cmp = picked.map(id => vehicles.find(v => v.id === id)).filter(Boolean); // Helpers const tierFor = (v, h) => { if (!v || !v.tierPrices) return null; const x = v.tierPrices[String(h)]; return (x && x > 0) ? x : null; }; const kmFor = (v, h) => { if (window.ACN_kmIncludedFor) return window.ACN_kmIncludedFor(v, h); if (v && v.kmIncluded && v.kmIncluded[String(h)]) return parseInt(v.kmIncluded[String(h)], 10); return null; }; // Best-Marker pro Zeile (niedrigster Wert / höchste Specs hervorheben) const bestOf = (vals, mode) => { const nums = vals.map(x => (typeof x === "number" ? x : (parseFloat(x) || null))); const valid = nums.filter(n => n != null); if (valid.length < 2) return -1; const target = mode === "min" ? Math.min(...valid) : Math.max(...valid); return nums.findIndex(n => n === target); }; return (
{/* Back-Bar */}
{onFleet && ( )}
Vergleichen

Bis zu drei Fahrzeuge im direkten Vergleich.

Specs, Pauschalpreise und inkludierte Kilometer auf einen Blick. Wählen Sie unten die Fahrzeuge, die Sie vergleichen möchten.

{/* Vehicle-Picker (Chips) */}
{vehicles.map(v => { const active = picked.includes(v.id); return ( ); })}
{cmp.length < 2 ? (
Wählen Sie mindestens 2 Fahrzeuge oben aus, um sie zu vergleichen.
) : (
{/* Header-Reihe: Fahrzeug-Karten mit Bild, Marke/Modell und Buchen-Button */}
{cmp.map(v => (
{v.brand}
{v.model}
))}
{/* Preis-Reihe (CHF/Tag) */} {(() => { const vals = cmp.map(v => v.pricePerDay || 0); const best = bestOf(vals, "min"); return (
Preis pro Tag
{cmp.map((v, i) => (
CHF {chf(v.pricePerDay)} {i === best && Günstigster}
))}
); })()} {/* Tier-Pauschalen */} {[ { h: 3, l: "Pauschale 3 Std." }, { h: 6, l: "Pauschale 6 Std." }, { h: 24, l: "Pauschale 1 Tag" }, { h: 168, l: "Pauschale 1 Woche" }, ].map(t => { const vals = cmp.map(v => tierFor(v, t.h)); if (vals.every(x => x == null)) return null; const best = bestOf(vals, "min"); return (
{t.l}
{cmp.map((v, i) => { const x = vals[i]; return (
{x ? <>CHF {chf(x)}{i === best && vals.filter(Boolean).length > 1 && Günstigster} : }
); })}
); })} {/* Inkl. km pro Tag */} {(() => { const vals = cmp.map(v => kmFor(v, 24)); if (vals.every(x => x == null)) return null; const best = bestOf(vals, "max"); return (
Inkl. km / Tag
{cmp.map((v, i) => (
{vals[i] != null ? {vals[i]} km : }
))}
); })()} {/* Specs */} {[ { k: "power", l: "Leistung", fmt: (x) => x ? `${x} PS` : "—", mode: "max" }, { k: "transmission", l: "Getriebe", fmt: (x) => x || "—", mode: null }, { k: "fuel", l: "Antrieb", fmt: (x) => x || "—", mode: null }, { k: "seats", l: "Plätze", fmt: (x) => x ? `${x}` : "—", mode: "max" }, { k: "topSpeed", l: "Höchstgeschw.", fmt: (x) => x ? `${x} km/h` : "—", mode: "max" }, { k: "accel", l: "0–100 km/h", fmt: (x) => x ? `${x} s` : "—", mode: "min" }, ].map(spec => { const raws = cmp.map(v => v[spec.k]); if (raws.every(x => x == null || x === "" || x === 0)) return null; const numericFor = spec.mode ? raws.map(x => (typeof x === "number" ? x : (parseFloat(x) || null))) : raws; const best = spec.mode ? bestOf(numericFor, spec.mode) : -1; return (
{spec.l}
{cmp.map((v, i) => (
{spec.fmt(v[spec.k])}
))}
); })} {/* Mindestmietdauer */} {(() => { const vals = cmp.map(v => parseInt(v.minHours || (v.minDays ? v.minDays * 24 : 0), 10) || 0); if (vals.every(x => x === 0)) return null; return (
Mindestmietdauer
{cmp.map((v, i) => (
{vals[i] > 0 ? `${vals[i]} Std.` : "—"}
))}
); })()}
)}
); } Object.assign(window, { DealsPage, CalcPage, ComparePage, AdminPage });