/* 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}
{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}
>
{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} />
| Modell | Kategorie | Preis/Tag | Status | |
{vehicles.map(v => (
|
|
{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} />
| Nr | Kunde | Fahrzeug | Zeitraum | Total | Status |
{rows.map(r => (
| {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} />
| Bezeichnung | Preis | Abrechnung | Standard |
{addons.map(a => (
| {a.label} |
{a.price === 0 ? "Inkl." : `CHF ${chf(a.price)}`} |
{a.per} |
|
))}
);
}
function AdminAvailability({ vehicles }) {
const days = Array.from({ length: 14 }, (_, i) => i);
return (
| Fahrzeug |
{days.map(d => {d + 1} | )}
{vehicles.slice(0, 6).map((v, i) => (
| {v.brand} {v.model} |
{days.map(d => {
const booked = (i + d) % 5 === 0 || (i + d) % 7 === 0;
return
| ;
})}
))}
Verfügbar
Gebucht
);
}
function AdminAppearance() {
return (
Akzentfarbe
{["#A8916F", "#0066CC", "#1F8A4C", "#B23838", "#8C1D40"].map(c => (
))}
);
}
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 => (
))}
{/* 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 });