Buchungskalender eingefügt

This commit is contained in:
2026-05-05 19:18:05 +02:00
parent 4a4517c514
commit c1f07343e3
21 changed files with 931 additions and 187 deletions
+5 -2
View File
@@ -4,7 +4,7 @@ Diese Anwendung stellt eine mehrseitige deutsche Vermietungsseite für eine Foto
- eine komplett neu aufgebaute öffentliche Website mit den Seiten `Leistungen`, `Preise`, `Verfügbarkeit`, `Buchen`, `Ablauf`, `FAQ`, `Kontakt`, `Impressum`, `Datenschutz` und `Mietbedingungen`
- eine Buchungsanfrage mit Nachtlogik: `Montag bis Dienstag = 1 Miettag`
- Live-Preisberechnung mit `99,99 €` pro Miettag
- Live-Preisberechnung je nach Abholung oder Liefergebiet
- einen Verwaltungsbereich für Anfragen, Buchungen, Kunden, Kalender, Rechnungen und Einstellungen
- MySQL-Unterstützung mit Tabellenpräfix `fb_` sowie JSON-Fallback
@@ -59,7 +59,10 @@ Die Datei `mysql.local.php` ist bereits in `.gitignore` ausgeschlossen. Standard
- Ein Miettag entspricht immer einer Übernachtung.
- Beispiel: `Montag bis Dienstag = 1 Miettag`
- Der Standardpreis beträgt `99,99 €` pro Miettag.
- Selbstabholung kostet `99,99 €` pro Miettag.
- Lieferung nach Hannover kostet `199,99 €` pro Miettag.
- Lieferung in die Region Hannover kostet `249,99 €` pro Miettag.
- Lieferung nach Hameln, Braunschweig, Hildesheim oder Celle kostet `299,99 €` pro Miettag.
- Zahlungsarten: `Rechnung / Überweisung` und `PayPal`
- Öffentliche Eingaben sind zunächst Buchungsanfragen und werden erst nach Bestätigung verbindlich.
+70 -2
View File
@@ -1,4 +1,7 @@
const forms = document.querySelectorAll('.booking-form');
const navToggle = document.querySelector('[data-nav-toggle]');
const navMenu = document.querySelector('[data-nav-menu]');
const navLinks = document.querySelectorAll('.site-nav-public a');
const formatCurrency = (cents) =>
new Intl.NumberFormat('de-DE', {
@@ -26,17 +29,54 @@ forms.forEach((form) => {
const startInput = form.querySelector('[data-booking-start]');
const endInput = form.querySelector('[data-booking-end]');
const daysOutput = form.querySelector('[data-summary-days]');
const rateOutput = form.querySelector('[data-summary-rate]');
const totalOutput = form.querySelector('[data-summary-total]');
const rateInput = form.querySelector('input[name="price_per_day_cents"]');
const deliveryModeInput = form.querySelector('[data-delivery-mode]');
const deliveryZoneInput = form.querySelector('[data-delivery-zone]');
const defaultRate = Number(form.dataset.dayRate || rateInput?.value || 9999);
const priceRates = (() => {
try {
return JSON.parse(form.dataset.priceRates || '{}');
} catch {
return {};
}
})();
const resolveRate = () => {
if (deliveryModeInput?.value === 'self_pickup') {
return Number(priceRates.self_pickup || defaultRate);
}
return Number(priceRates[deliveryZoneInput?.value || ''] || defaultRate);
};
const syncRate = () => {
if (deliveryZoneInput) {
if (deliveryModeInput?.value === 'self_pickup') {
deliveryZoneInput.value = 'self_pickup';
} else if (deliveryZoneInput.value === 'self_pickup') {
deliveryZoneInput.value = 'hannover';
}
}
const rate = resolveRate();
if (rateInput) {
rateInput.value = String(rate);
}
return rate;
};
const render = () => {
const rentalDays = calculateRentalDays(startInput?.value, endInput?.value);
const rate = Number(rateInput?.value || defaultRate);
const rate = syncRate();
if (!rentalDays || rate < 0) {
if (daysOutput) daysOutput.textContent = 'Noch nicht gewählt';
if (totalOutput) totalOutput.textContent = formatCurrency(defaultRate);
if (rateOutput) rateOutput.textContent = formatCurrency(rate);
if (totalOutput) totalOutput.textContent = formatCurrency(rate);
return;
}
@@ -44,6 +84,10 @@ forms.forEach((form) => {
daysOutput.textContent = `${rentalDays} ${rentalDays === 1 ? 'Miettag' : 'Miettage'}`;
}
if (rateOutput) {
rateOutput.textContent = formatCurrency(rate);
}
if (totalOutput) {
totalOutput.textContent = formatCurrency(rentalDays * rate);
}
@@ -52,5 +96,29 @@ forms.forEach((form) => {
startInput?.addEventListener('input', render);
endInput?.addEventListener('input', render);
rateInput?.addEventListener('input', render);
deliveryModeInput?.addEventListener('change', render);
deliveryZoneInput?.addEventListener('change', render);
render();
});
if (navToggle && navMenu) {
const closeMenu = () => {
navToggle.setAttribute('aria-expanded', 'false');
navMenu.classList.remove('is-open');
};
navToggle.addEventListener('click', () => {
const isOpen = navMenu.classList.toggle('is-open');
navToggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
});
navLinks.forEach((link) => {
link.addEventListener('click', closeMenu);
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
closeMenu();
}
});
}
+347 -4
View File
@@ -175,6 +175,125 @@ textarea {
transform: translateY(-1px);
}
.public-header-shell {
display: flex;
align-items: center;
gap: 1rem;
margin-left: auto;
}
.public-header-controls {
display: flex;
align-items: center;
gap: 1.25rem;
}
.site-nav-public {
gap: 1.2rem;
}
.site-nav-public a {
position: relative;
padding: 0.35rem 0;
border-radius: 0;
background: transparent;
color: var(--text-soft);
font-weight: 500;
}
.site-nav-public a::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: -0.35rem;
height: 2px;
border-radius: 999px;
background: linear-gradient(90deg, var(--accent) 0%, #d4a081 100%);
opacity: 0;
transform: scaleX(0.55);
transition: opacity 0.2s ease, transform 0.2s ease;
}
.site-nav-public a:hover,
.site-nav-public a:focus-visible,
.site-nav-public a.is-active {
background: transparent;
color: var(--text);
transform: none;
}
.site-nav-public a:hover::after,
.site-nav-public a:focus-visible::after,
.site-nav-public a.is-active::after {
opacity: 1;
transform: scaleX(1);
}
.header-actions-public {
gap: 0.8rem;
}
.menu-meta {
display: none;
gap: 0.35rem;
color: var(--text-soft);
font-size: 0.92rem;
}
.menu-meta a {
color: var(--accent-strong);
text-decoration: none;
}
.contact-chip {
display: inline-grid;
gap: 0.1rem;
padding: 0.75rem 1rem;
border-radius: 999px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.74);
text-decoration: none;
line-height: 1.25;
}
.contact-chip span {
font-size: 0.72rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-soft);
}
.contact-chip strong {
font-size: 0.95rem;
color: var(--text);
}
.nav-toggle {
display: none;
align-items: center;
gap: 0.75rem;
padding: 0.85rem 1rem;
border-radius: 999px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.76);
color: var(--text);
cursor: pointer;
}
.nav-toggle-box {
display: inline-grid;
gap: 0.24rem;
}
.nav-toggle-box span {
display: block;
width: 1rem;
height: 2px;
border-radius: 999px;
background: currentColor;
}
.header-actions {
display: flex;
align-items: center;
@@ -618,6 +737,136 @@ p {
gap: 0.9rem;
}
.calendar-legend,
.public-calendar-grid,
.calendar-weekdays,
.calendar-days,
.calendar-entry-list {
display: grid;
}
.calendar-legend {
grid-auto-flow: column;
justify-content: start;
gap: 0.75rem;
margin-bottom: 1rem;
}
.public-calendar-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.calendar-month-card {
padding: 1.35rem;
border-radius: var(--radius-xl);
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.78);
box-shadow: var(--shadow-card);
}
.calendar-month-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1rem;
}
.calendar-month-header h2 {
margin: 0;
font-size: 1.45rem;
}
.calendar-month-header span,
.calendar-weekdays span,
.calendar-day-state {
color: var(--text-soft);
font-size: 0.88rem;
}
.calendar-weekdays,
.calendar-days {
grid-template-columns: repeat(7, minmax(0, 1fr));
gap: 0.45rem;
}
.calendar-weekdays {
margin-bottom: 0.45rem;
text-align: center;
}
.calendar-day {
min-height: 4.6rem;
padding: 0.55rem 0.45rem;
border-radius: var(--radius-sm);
border: 1px solid var(--line);
background: rgba(247, 243, 236, 0.9);
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 0.35rem;
}
.calendar-day-empty {
border-style: dashed;
background: rgba(255, 255, 255, 0.34);
}
.calendar-day.is-today {
border-color: rgba(183, 106, 71, 0.42);
box-shadow: inset 0 0 0 1px rgba(183, 106, 71, 0.18);
}
.calendar-day.is-booked {
color: var(--text);
}
.calendar-day-requested {
background: rgba(183, 106, 71, 0.14);
border-color: rgba(183, 106, 71, 0.24);
}
.calendar-day-reserved {
background: rgba(102, 87, 74, 0.14);
border-color: rgba(102, 87, 74, 0.22);
}
.calendar-day-confirmed {
background: rgba(77, 105, 91, 0.14);
border-color: rgba(77, 105, 91, 0.22);
}
.calendar-day-number {
font-weight: 700;
font-size: 1rem;
}
.calendar-entry-list {
gap: 0.75rem;
margin-top: 1rem;
}
.calendar-entry {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.9rem;
padding: 0.85rem 0.95rem;
border-radius: var(--radius-md);
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.58);
}
.calendar-entry strong {
display: block;
}
.calendar-entry span {
color: var(--text-soft);
font-size: 0.92rem;
}
.availability-card,
.stack-item {
display: flex;
@@ -1064,21 +1313,90 @@ tbody tr:last-child td {
@media (max-width: 900px) {
.header-inner {
gap: 0.85rem;
}
.site-header-admin .header-inner {
flex-direction: column;
align-items: stretch;
}
.site-nav,
.header-actions {
.site-header-admin .site-nav,
.site-header-admin .header-actions {
justify-content: center;
}
.public-header-shell {
position: relative;
margin-left: 0;
}
.nav-toggle {
display: inline-flex;
}
.public-header-controls {
position: absolute;
top: calc(100% + 0.85rem);
right: 0;
width: min(24rem, calc(100vw - 2rem));
padding: 1rem;
border-radius: var(--radius-lg);
border: 1px solid var(--line);
background: rgba(255, 251, 246, 0.98);
box-shadow: var(--shadow-soft);
display: none;
flex-direction: column;
align-items: stretch;
gap: 1rem;
}
.public-header-controls.is-open {
display: flex;
}
.site-nav-public,
.menu-meta,
.header-actions-public {
width: 100%;
align-items: stretch;
}
.site-nav-public {
display: flex;
flex-direction: column;
gap: 0;
}
.menu-meta {
display: grid;
padding-top: 0.15rem;
}
.header-actions-public {
flex-direction: column;
}
.site-nav-public a {
padding: 0.95rem 0;
border-bottom: 1px solid var(--line);
}
.site-nav-public a::after {
display: none;
}
.site-nav-public a:last-child {
border-bottom: 0;
}
.faq-grid,
.legal-section,
.trust-grid,
.form-grid,
.form-grid-two,
.calendar-grid {
.calendar-grid,
.public-calendar-grid {
grid-template-columns: 1fr;
}
@@ -1123,9 +1441,14 @@ tbody tr:last-child td {
flex-direction: column;
}
.topbar {
display: none;
}
.button-primary,
.button-secondary,
.ghost-button {
.ghost-button,
.contact-chip {
width: 100%;
}
@@ -1144,10 +1467,30 @@ tbody tr:last-child td {
.availability-card,
.stack-item,
.calendar-entry,
.form-section-header,
.hero-panel-top,
.hero-panel-bottom {
flex-direction: column;
align-items: flex-start;
}
.calendar-weekdays,
.calendar-days {
gap: 0.3rem;
}
.calendar-day {
min-height: 4rem;
padding: 0.45rem 0.35rem;
}
.calendar-legend {
grid-auto-flow: row;
}
.public-header-shell {
width: 100%;
justify-content: flex-end;
}
}
+24 -2
View File
@@ -13,7 +13,7 @@ return [
'email' => 'hallo@fotobox-moments.local',
'phone' => '+49 170 1234567',
'website' => 'https://fotobox-moments.local',
'service_area' => 'Musterstadt und Umgebung',
'service_area' => 'Hannover, Region Hannover, Hameln, Braunschweig, Hildesheim und Celle',
'response_time' => 'Antwort meist innerhalb von 24 Stunden',
'pickup_window' => 'Abholung ab 17:00 Uhr',
'return_window' => 'Rückgabe bis 13:00 Uhr',
@@ -42,7 +42,29 @@ return [
'pricing' => [
'default_day_rate_cents' => 9999,
'currency' => 'EUR',
'label' => '99,99 € pro Miettag',
'label' => 'Abholung ab 99,99 € pro Miettag',
'delivery_rates' => [
'self_pickup' => [
'label' => 'Selbstabholung',
'description' => 'Abholung in Hannover',
'price_cents' => 9999,
],
'hannover' => [
'label' => 'Lieferung nach Hannover',
'description' => 'Lieferung und Aufbau im Stadtgebiet Hannover',
'price_cents' => 19999,
],
'region_hannover' => [
'label' => 'Lieferung in die Region Hannover',
'description' => 'Lieferung und Aufbau in die Region Hannover',
'price_cents' => 24999,
],
'extended_region' => [
'label' => 'Lieferung nach Hameln, Braunschweig, Hildesheim oder Celle',
'description' => 'Lieferung und Aufbau in Hameln, Braunschweig, Hildesheim oder Celle',
'price_cents' => 29999,
],
],
],
'admin' => [
'username' => 'admin',
+1 -1
View File
@@ -2,7 +2,7 @@
1. Startseite unter `/` öffnen und prüfen, ob Hero, Leistungsblöcke, Verfügbarkeitsvorschau und der Link zur Buchungsanfrage sichtbar sind.
2. Die Seite `/buchen` öffnen und kontrollieren, ob die Zusammenfassung `Montag bis Dienstag = 1 Miettag` korrekt berechnet.
3. Im Buchungsformular einen Zeitraum wählen und prüfen, ob der Gesamtpreis mit `99,99 €` pro Miettag berechnet wird.
3. Im Buchungsformular die Lieferart wechseln und prüfen, ob die Preisberechnung sauber umschaltet: `99,99 €` für Selbstabholung, `199,99 €` für Hannover, `249,99 €` für Region Hannover und `299,99 €` für Hameln, Braunschweig, Hildesheim oder Celle.
4. Eine Anfrage absenden und sicherstellen, dass sie in `storage/bookings.json` erscheint oder bei aktiver MySQL-Verbindung in `fb_bookings`.
5. `/admin/login` öffnen, mit `admin` und dem konfigurierten Passwort anmelden und das Dashboard prüfen.
6. Im Admin-Bereich eine manuelle Buchung anlegen und kontrollieren, ob Terminüberschneidungen erkannt werden.
+147 -6
View File
@@ -200,6 +200,79 @@ final class BookingService
return array_slice($bookings, 0, 4);
}
public function getPublicAvailabilityBookings(): array
{
$today = gmdate('Y-m-d');
return array_values(array_filter(
$this->getBookingsByStatuses(['requested', 'reserved', 'confirmed']),
static fn(array $booking): bool => (string) $booking['end_date'] >= $today
));
}
public function getPublicCalendarMonths(int $monthCount = 4): array
{
$months = [];
$monthCount = max(1, $monthCount);
$today = gmdate('Y-m-d');
$bookings = $this->getPublicAvailabilityBookings();
$monthCursor = (new DateTimeImmutable('first day of this month'))->setTime(0, 0);
for ($index = 0; $index < $monthCount; $index++) {
$monthStart = $monthCursor->modify('+' . $index . ' month');
$monthEnd = $monthStart->modify('first day of next month');
$monthKey = $monthStart->format('Y-m');
$entries = array_values(array_filter(
$bookings,
fn(array $booking): bool => $this->bookingIntersectsRange(
$booking,
$monthStart->format('Y-m-d'),
$monthEnd->format('Y-m-d')
)
));
$days = [];
for ($blank = 1; $blank < (int) $monthStart->format('N'); $blank++) {
$days[] = ['is_padding' => true];
}
$dayCursor = $monthStart;
while ($dayCursor < $monthEnd) {
$date = $dayCursor->format('Y-m-d');
$dayBooking = $this->findBookingForDate($entries, $date);
$days[] = [
'is_padding' => false,
'date' => $date,
'day' => (int) $dayCursor->format('j'),
'is_today' => $date === $today,
'is_booked' => $dayBooking !== null,
'status' => $dayBooking['status'] ?? '',
'status_label' => $dayBooking['status_label'] ?? '',
];
$dayCursor = $dayCursor->modify('+1 day');
}
while (count($days) % 7 !== 0) {
$days[] = ['is_padding' => true];
}
$months[] = [
'label' => $this->formatMonthLabel($monthKey),
'month_key' => $monthKey,
'entry_count' => count($entries),
'days' => $days,
'entries' => array_map(
fn(array $booking): array => $this->buildPublicCalendarEntry($booking),
$entries
),
];
}
return $months;
}
public function getDashboardStats(): array
{
$bookings = $this->getBookings();
@@ -303,6 +376,7 @@ final class BookingService
'payment_status' => 'unpaid',
'payment_method' => 'invoice_transfer',
'delivery_mode' => 'self_pickup',
'delivery_zone' => 'self_pickup',
];
}
@@ -336,6 +410,20 @@ final class BookingService
];
}
public function getDeliveryModeOptions(): array
{
return [
'self_pickup' => 'Selbstabholung',
'delivery_setup' => 'Lieferung und Aufbau',
'on_site_support' => 'Lieferung, Aufbau und Vor-Ort-Betreuung',
];
}
public function getDeliveryZoneOptions(): array
{
return $this->config['pricing']['delivery_rates'] ?? [];
}
private function normalizeBookingInput(array $input, bool $adminMode): array
{
$customerName = trim((string) ($input['customer_name'] ?? ''));
@@ -351,11 +439,11 @@ final class BookingService
$endDate = trim((string) ($input['end_date'] ?? ''));
$paymentMethod = trim((string) ($input['payment_method'] ?? 'invoice_transfer'));
$deliveryMode = trim((string) ($input['delivery_mode'] ?? 'self_pickup'));
$deliveryZone = trim((string) ($input['delivery_zone'] ?? 'self_pickup'));
$notesCustomer = trim((string) ($input['notes_customer'] ?? ''));
$internalNotes = trim((string) ($input['internal_notes'] ?? ''));
$status = trim((string) ($input['status'] ?? ($adminMode ? 'confirmed' : 'requested')));
$paymentStatus = trim((string) ($input['payment_status'] ?? 'unpaid'));
$pricePerDay = (int) ($input['price_per_day_cents'] ?? $this->config['pricing']['default_day_rate_cents']);
$privacyAccepted = (string) ($input['privacy_accepted'] ?? '') === '1';
$termsAccepted = (string) ($input['terms_accepted'] ?? '') === '1';
@@ -408,11 +496,8 @@ final class BookingService
'paypal' => 'PayPal',
];
$deliveryLabels = [
'self_pickup' => 'Selbstabholung',
'delivery_setup' => 'Lieferung und Aufbau',
'on_site_support' => 'Lieferung, Aufbau und Vor-Ort-Betreuung',
];
$deliveryLabels = $this->getDeliveryModeOptions();
$deliveryZones = $this->getDeliveryZoneOptions();
if (!array_key_exists($paymentMethod, $paymentLabels)) {
throw new RuntimeException('Die gewählte Zahlungsart ist ungültig.');
@@ -422,6 +507,18 @@ final class BookingService
throw new RuntimeException('Die gewählte Lieferart ist ungültig.');
}
if (!array_key_exists($deliveryZone, $deliveryZones)) {
throw new RuntimeException('Bitte wähle ein gültiges Liefergebiet aus.');
}
if ($deliveryMode === 'self_pickup') {
$deliveryZone = 'self_pickup';
} elseif ($deliveryZone === 'self_pickup') {
throw new RuntimeException('Bitte wähle für die Lieferung ein passendes Liefergebiet aus.');
}
$pricePerDay = $this->resolveRateForSelection($deliveryMode, $deliveryZone);
if ($pricePerDay < 0) {
throw new RuntimeException('Der Tagespreis ist ungültig.');
}
@@ -443,6 +540,8 @@ final class BookingService
'payment_method_label' => $paymentLabels[$paymentMethod],
'delivery_mode' => $deliveryMode,
'delivery_mode_label' => $deliveryLabels[$deliveryMode],
'delivery_zone' => $deliveryZone,
'delivery_zone_label' => $deliveryZones[$deliveryZone]['label'],
'notes_customer' => $notesCustomer,
'internal_notes' => $internalNotes,
'status' => $status,
@@ -472,6 +571,8 @@ final class BookingService
'payment_method_label' => $payload['payment_method_label'],
'delivery_mode' => $payload['delivery_mode'],
'delivery_mode_label' => $payload['delivery_mode_label'],
'delivery_zone' => $payload['delivery_zone'],
'delivery_zone_label' => $payload['delivery_zone_label'],
'start_date' => $payload['start_date'],
'end_date' => $payload['end_date'],
'total_days' => $payload['total_days'],
@@ -562,6 +663,46 @@ final class BookingService
return $prefix . '_' . strtolower(bin2hex(random_bytes(6)));
}
private function resolveRateForSelection(string $deliveryMode, string $deliveryZone): int
{
$zones = $this->getDeliveryZoneOptions();
if ($deliveryMode === 'self_pickup') {
return (int) ($zones['self_pickup']['price_cents'] ?? $this->config['pricing']['default_day_rate_cents']);
}
return (int) ($zones[$deliveryZone]['price_cents'] ?? $this->config['pricing']['default_day_rate_cents']);
}
private function bookingIntersectsRange(array $booking, string $rangeStart, string $rangeEnd): bool
{
return $booking['start_date'] < $rangeEnd && $booking['end_date'] > $rangeStart;
}
private function findBookingForDate(array $bookings, string $date): ?array
{
foreach ($bookings as $booking) {
if ($booking['start_date'] <= $date && $booking['end_date'] > $date) {
return $booking;
}
}
return null;
}
private function buildPublicCalendarEntry(array $booking): array
{
$deliveryLabel = (string) ($booking['delivery_zone_label'] ?: $booking['delivery_mode_label']);
return [
'date_label' => formatDate($booking['start_date']) . ' bis ' . formatDate($booking['end_date']),
'status' => $booking['status'],
'status_label' => $booking['status_label'],
'delivery_label' => $deliveryLabel,
'day_count_label' => $booking['total_days'] . ' Miettag' . ($booking['total_days'] === 1 ? '' : 'e'),
];
}
private function formatMonthLabel(string $monthKey): string
{
[$year, $month] = explode('-', $monthKey) + [null, null];
+2 -1
View File
@@ -64,6 +64,7 @@ function basePath(): string
'/admin/login',
'/admin/logout',
'/admin',
'/fotobox',
'/leistungen',
'/preise',
'/verfuegbarkeit',
@@ -185,7 +186,7 @@ function h(string $value): string
function formatCurrency(int $cents): string
{
return number_format($cents / 100, 2, ',', '.') . ' EUR';
return number_format($cents / 100, 2, ',', '.') . ' ';
}
function selected(string $current, string $expected): string
+68 -42
View File
@@ -343,10 +343,16 @@ function publicRoutes(): array
'metaDescription' => 'DSLR-Kamera, Studioblitz, Softbox, WLAN-Download und professioneller Lieferumfang für Ihre Fotobox-Miete.',
'pageKey' => 'leistungen',
],
'/fotobox' => [
'view' => 'pages/leistungen',
'pageTitle' => 'Fotobox und Ausstattung',
'metaDescription' => 'DSLR-Kamera, Studioblitz, Softbox, WLAN-Download und professioneller Lieferumfang für Ihre Fotobox-Miete.',
'pageKey' => 'fotobox',
],
'/preise' => [
'view' => 'pages/preise',
'pageTitle' => 'Preise und Mietlogik',
'metaDescription' => '99,99 € pro Miettag. Ein Miettag entspricht einer Übernachtung. Klare Preise, transparente Leistungen und feste Zahlungsarten.',
'metaDescription' => 'Selbstabholung ab 99,99 € pro Miettag, Lieferung je nach Zielort. Ein Miettag entspricht einer Übernachtung.',
'pageKey' => 'preise',
],
'/verfuegbarkeit' => [
@@ -404,6 +410,7 @@ function renderPublicPage(array $route, BookingService $bookingService, array $c
{
$company = $config['company'];
$bookings = $bookingService->getHighlightedBookings();
$availabilityBookings = $bookingService->getPublicAvailabilityBookings();
$currentView = (string) $route['view'];
render($currentView, [
@@ -413,10 +420,14 @@ function renderPublicPage(array $route, BookingService $bookingService, array $c
'config' => $config,
'company' => $company,
'dayRate' => $config['pricing']['default_day_rate_cents'],
'deliveryModeOptions' => $bookingService->getDeliveryModeOptions(),
'deliveryZoneOptions' => $bookingService->getDeliveryZoneOptions(),
'flashSuccess' => flash('success'),
'flashError' => flash('error'),
'old' => flash('old') ?? [],
'bookings' => $bookings,
'availabilityBookings' => $availabilityBookings,
'availabilityCalendarMonths' => $bookingService->getPublicCalendarMonths(4),
'trustFacts' => publicTrustFacts($config),
'featureCards' => publicFeatureCards(),
'processSteps' => publicProcessSteps($config),
@@ -547,6 +558,8 @@ function renderAdminCreate(BookingService $bookingService): void
'flashError' => flash('error'),
'old' => flash('admin_old') ?? [],
'defaults' => $bookingService->getAdminDefaults(),
'deliveryModeOptions' => $bookingService->getDeliveryModeOptions(),
'deliveryZoneOptions' => $bookingService->getDeliveryZoneOptions(),
]);
}
@@ -574,13 +587,14 @@ function renderAdminOrder(BookingService $bookingService): void
function publicTrustFacts(array $config): array
{
$company = $config['company'];
$rates = $config['pricing']['delivery_rates'];
return [
['label' => 'Preis', 'value' => $config['pricing']['label']],
['label' => 'Mietlogik', 'value' => '1 Miettag = 1 Übernachtung'],
['label' => 'Abholung', 'value' => formatCurrency((int) $rates['self_pickup']['price_cents']) . ' pro Miettag'],
['label' => 'Lieferung Hannover', 'value' => formatCurrency((int) $rates['hannover']['price_cents']) . ' pro Miettag'],
['label' => 'Mietdauer', 'value' => '1 Miettag = 1 Übernachtung'],
['label' => 'Zahlung', 'value' => 'Rechnung, Überweisung oder PayPal'],
['label' => 'Servicefenster', 'value' => $company['pickup_window'] . ' / ' . $company['return_window']],
['label' => 'Bilder', 'value' => 'Digitale Bilder inklusive'],
];
}
@@ -588,20 +602,20 @@ function publicFeatureCards(): array
{
return [
[
'title' => 'Professionelle Bildqualität',
'text' => 'DSLR-Kamera, Studioblitz und Softbox sorgen für klare, helle Fotos bei wechselnden Lichtverhältnissen.',
'title' => 'Spiegelreflexkamera & Studioblitz',
'text' => 'DSLR-Kamera, Bildschirm und große Softbox sorgen für helle, hochwertige Fotos bei jedem Anlass.',
],
[
'title' => 'Direkter Download aufs Handy',
'text' => 'Ihre Gäste können Bilder vor Ort per WLAN laden. Nach dem Event erhalten Sie zusätzlich die komplette Galerie digital.',
'title' => 'WLAN-Download direkt vor Ort',
'text' => 'Ihre Gäste können Fotos direkt auf das Handy laden und teilen. Nach dem Event erhalten Sie zusätzlich alle Bilder digital.',
],
[
'title' => 'Lieferung oder Selbstabholung',
'text' => 'Sie entscheiden zwischen Selbstabholung, Lieferung mit Aufbau oder einem Rundum-Service mit Vor-Ort-Betreuung.',
'title' => 'Schnell aufgebaut, leicht bedient',
'text' => 'Die Fotobox ist in wenigen Minuten einsatzbereit und wird bei der Übergabe kurz erklärt.',
],
[
'title' => 'Saubere Verwaltung im Hintergrund',
'text' => 'Anfragen, Kundendaten, Rechnungen und Zahlungsstatus werden in einem Verwaltungsbereich zentral gepflegt.',
'title' => 'Lieferung, Abholung oder Betreuung',
'text' => 'Sie entscheiden zwischen Selbstabholung, Lieferung mit Aufbau oder Vor-Ort-Unterstützung für Ihr Event.',
],
];
}
@@ -612,42 +626,43 @@ function publicProcessSteps(array $config): array
return [
[
'title' => 'Zeitraum wählen',
'title' => 'Wunschtermin anfragen',
'text' => 'Sie wählen Abholtag und Rückgabetag. Montag bis Dienstag zählt als 1 Miettag.',
],
[
'title' => 'Leistung festlegen',
'text' => 'Selbstabholung, Lieferung oder Betreuung vor Ort werden passend zu Ihrem Event gewählt.',
'title' => 'Verfügbarkeit bestätigen lassen',
'text' => 'Wir prüfen den Termin und melden uns in der Regel innerhalb von 24 Stunden zurück.',
],
[
'title' => 'Anfrage absenden',
'text' => 'Wir prüfen Verfügbarkeit, erfassen Ihre Daten und bestätigen den Auftrag persönlich.',
'title' => 'Fotobox abholen oder liefern lassen',
'text' => 'Sie wählen Selbstabholung oder Lieferung mit Aufbau passend zu Ihrem Event.',
],
[
'title' => 'Feiern und Bilder erhalten',
'text' => 'Die Fotobox steht rechtzeitig bereit. Die Rückgabe erfolgt bis ' . $company['return_window'] . '.',
'text' => 'Die Fotobox steht rechtzeitig bereit und alle Bilder erhalten Sie anschließend digital.',
],
];
}
function publicPricingExamples(array $config): array
{
$label = $config['pricing']['label'];
$rates = $config['pricing']['delivery_rates'];
return [
['title' => 'Montag bis Dienstag', 'text' => '1 Miettag · ' . $label],
['title' => 'Freitag bis Sonntag', 'text' => '2 Miettage · 199,98 €'],
['title' => 'Buchungsanfrage', 'text' => 'Noch kein Sofortvertrag. Verbindlich erst nach Bestätigung.'],
['title' => 'Abholung', 'text' => formatCurrency((int) $rates['self_pickup']['price_cents']) . ' pro Miettag'],
['title' => 'Lieferung nach Hannover', 'text' => formatCurrency((int) $rates['hannover']['price_cents']) . ' pro Miettag'],
['title' => 'Region Hannover', 'text' => formatCurrency((int) $rates['region_hannover']['price_cents']) . ' pro Miettag'],
['title' => 'Hameln, Braunschweig, Hildesheim oder Celle', 'text' => formatCurrency((int) $rates['extended_region']['price_cents']) . ' pro Miettag'],
];
}
function publicOccasionCards(): array
{
return [
['title' => 'Hochzeiten', 'text' => 'Für Erinnerungen mit ruhiger Technik und hochwertigem Licht.'],
['title' => 'Geburtstage', 'text' => 'Einfach zu bedienen und schnell einsatzbereit.'],
['title' => 'Firmenfeiern', 'text' => 'Mit Rechnung, klarer Planung und sauberer Abwicklung.'],
['title' => 'Jubiläen und Vereinsfeste', 'text' => 'Für Veranstaltungen mit vielen Gästen und wenig Zeitverlust.'],
['title' => 'Hochzeiten', 'text' => 'Für emotionale Erinnerungen und eine entspannte Feier mit Ihren Gästen.'],
['title' => 'Geburtstage', 'text' => 'Einfach zu bedienen und schnell einsatzbereit für jede Party.'],
['title' => 'Firmenfeiern', 'text' => 'Mit klarer Abwicklung, Rechnung und professionellem Auftritt.'],
['title' => 'Jubiläen und Vereinsfeste', 'text' => 'Ideal für Veranstaltungen mit vielen Gästen und unkompliziertem Ablauf.'],
];
}
@@ -661,8 +676,12 @@ function publicFaqItems(array $config): array
'answer' => 'Ein Miettag entspricht immer einer Übernachtung. Montag bis Dienstag ist also 1 Miettag, Freitag bis Sonntag sind 2 Miettage.',
],
[
'question' => 'Ist die Online-Anfrage direkt verbindlich?',
'answer' => 'Nein. Sie senden zunächst eine Buchungsanfrage. Verbindlich wird der Auftrag erst nach unserer Bestätigung.',
'question' => 'Was kostet die Fotobox je nach Zielort?',
'answer' => 'Selbstabholung kostet ' . formatCurrency((int) $config['pricing']['delivery_rates']['self_pickup']['price_cents']) . ' pro Miettag. Lieferung nach Hannover kostet ' . formatCurrency((int) $config['pricing']['delivery_rates']['hannover']['price_cents']) . ', in die Region Hannover ' . formatCurrency((int) $config['pricing']['delivery_rates']['region_hannover']['price_cents']) . ' und nach Hameln, Braunschweig, Hildesheim oder Celle ' . formatCurrency((int) $config['pricing']['delivery_rates']['extended_region']['price_cents']) . ' pro Miettag.',
],
[
'question' => 'Wann erhalte ich eine Rückmeldung?',
'answer' => 'In der Regel erhalten Sie innerhalb von 24 Stunden eine Rückmeldung zur Verfügbarkeit und zum weiteren Ablauf.',
],
[
'question' => 'Welche Zahlungsarten sind möglich?',
@@ -672,13 +691,17 @@ function publicFaqItems(array $config): array
'question' => 'Wie laufen Abholung und Rückgabe ab?',
'answer' => 'Standardmäßig gilt ' . $company['pickup_window'] . '. Die Rückgabe erfolgt bis ' . $company['return_window'] . '. Lieferung und Aufbau sind ebenfalls möglich.',
],
[
'question' => 'Wie schnell ist die Fotobox einsatzbereit?',
'answer' => 'Nach Aufbau und Stromversorgung ist die Fotobox in wenigen Minuten startklar. Bei der Übergabe erhalten Sie zudem eine kurze Einweisung.',
],
[
'question' => 'Wann erhalten wir die Fotos?',
'answer' => 'Die Bilder können vor Ort per WLAN geteilt werden. Zusätzlich erhalten Sie nach dem Event alle Fotos digital gesammelt.',
],
[
'question' => 'Gibt es eine Rechnung mit Kundendaten?',
'answer' => 'Ja. Im Verwaltungsprozess können Rechnungen mit vollständigen Kundendaten erzeugt und als PDF bereitgestellt werden.',
'question' => 'Kann ich auch auf Rechnung zahlen?',
'answer' => 'Ja. Auf Wunsch erhalten Sie eine Rechnung mit vollständigen Kundendaten. Alternativ ist auch PayPal möglich.',
],
];
}
@@ -686,30 +709,33 @@ function publicFaqItems(array $config): array
function publicServiceModules(): array
{
return [
['title' => 'Technikpaket', 'items' => ['DSLR-Kamera', 'Studioblitz mit Softbox', 'Bedienbildschirm', 'WLAN-Fotofreigabe']],
['title' => 'Eventbetrieb', 'items' => ['Schneller Aufbau', 'Intuitive Bedienung', 'Digitale Galerie', 'Saubere Rückgabeplanung']],
['title' => 'Kaufmännische Abwicklung', 'items' => ['Anfrageerfassung', 'Rechnungsstellung', 'Zahlungsstatus', 'Admin-Verwaltung']],
['title' => 'Im Preis enthalten', 'items' => ['Fotobox mit Stativ', 'Spiegelreflexkamera', 'Studioblitz mit Softbox', 'Alle Bilder digital']],
['title' => 'Das macht es einfach', 'items' => ['Kurze Einweisung bei der Übergabe', 'Schneller Aufbau', 'WLAN-Download vor Ort', 'Einfache Bedienung per Knopfdruck']],
['title' => 'Auf Wunsch zusätzlich', 'items' => ['Lieferung und Aufbau', 'Vor-Ort-Betreuung', 'Rechnung für Firmenkunden', 'Persönliche Rücksprache zum Ablauf']],
];
}
function publicServiceStandards(array $config): array
{
$company = $config['company'];
$rates = $config['pricing']['delivery_rates'];
return [
'Klare Preisangabe mit ' . $config['pricing']['label'],
'Direkt sichtbare Kontaktwege: ' . $company['phone'] . ' und ' . $company['email'],
'Pflichtseiten für Impressum, Datenschutz und Mietbedingungen',
'Barrierearme Formulare mit eindeutigen Beschriftungen und Fehlermeldungen',
'Selbstabholung ab ' . formatCurrency((int) $rates['self_pickup']['price_cents']) . ' pro Miettag',
'Lieferung nach Hannover ab ' . formatCurrency((int) $rates['hannover']['price_cents']) . ' pro Miettag',
'WLAN-Download direkt auf das Handy Ihrer Gäste',
'Kurze Einweisung bei der Übergabe',
$company['pickup_window'] . ' · ' . $company['return_window'],
'Rückmeldung in der Regel innerhalb von 24 Stunden',
];
}
function publicBookingChecklist(array $config): array
{
return [
'Startdatum und Rückgabedatum bereithalten',
'Lieferart auswählen: Selbstabholung, Lieferung oder Betreuung',
'Rechnungsdaten und Veranstaltungsort eintragen',
'Datenschutz und Mietbedingungen vor dem Absenden bestätigen',
'Wunschtermin auswählen',
'Leistungsart, Liefergebiet und Zahlungsart festlegen',
'Kontaktdaten und Veranstaltungsort eintragen',
'Anfrage unverbindlich absenden',
];
}
+27 -10
View File
@@ -1,4 +1,10 @@
<section class="admin-section narrow-section">
<?php
$priceRates = [];
foreach ($deliveryZoneOptions as $zoneKey => $zone) {
$priceRates[$zoneKey] = (int) $zone['price_cents'];
}
?>
<div class="section-header">
<div>
<p class="eyebrow">Manuelle Buchung</p>
@@ -14,8 +20,9 @@
<div class="flash flash-error"><?= h((string) $flashError) ?></div>
<?php endif; ?>
<form method="post" action="<?= h(url('admin/create')) ?>" class="booking-form admin-form" data-day-rate="<?= h((string) $defaults['price_per_day_cents']) ?>">
<form method="post" action="<?= h(url('admin/create')) ?>" class="booking-form admin-form" data-day-rate="<?= h((string) $defaults['price_per_day_cents']) ?>" data-price-rates="<?= h((string) json_encode($priceRates, JSON_THROW_ON_ERROR)) ?>">
<?= csrfField() ?>
<input type="hidden" name="price_per_day_cents" value="<?= h((string) ($old['price_per_day_cents'] ?? $defaults['price_per_day_cents'])) ?>">
<div class="form-section">
<div class="form-section-header">
<span class="form-step">Verwaltung</span>
@@ -67,10 +74,6 @@
<span>Rückgabedatum</span>
<input type="date" name="end_date" data-booking-end value="<?= h((string) ($old['end_date'] ?? '')) ?>" required>
</label>
<label>
<span>Preis pro Miettag in Cent</span>
<input type="number" name="price_per_day_cents" min="0" value="<?= h((string) ($old['price_per_day_cents'] ?? $defaults['price_per_day_cents'])) ?>" required>
</label>
<label>
<span>Status</span>
<select name="status">
@@ -90,10 +93,20 @@
</label>
<label>
<span>Lieferart</span>
<select name="delivery_mode">
<option value="self_pickup" <?= selected((string) ($old['delivery_mode'] ?? $defaults['delivery_mode']), 'self_pickup') ?>>Selbstabholung</option>
<option value="delivery_setup" <?= selected((string) ($old['delivery_mode'] ?? ''), 'delivery_setup') ?>>Lieferung und Aufbau</option>
<option value="on_site_support" <?= selected((string) ($old['delivery_mode'] ?? ''), 'on_site_support') ?>>Lieferung, Aufbau und Vor-Ort-Betreuung</option>
<select name="delivery_mode" data-delivery-mode>
<?php foreach ($deliveryModeOptions as $value => $label): ?>
<option value="<?= h($value) ?>" <?= selected((string) ($old['delivery_mode'] ?? $defaults['delivery_mode']), $value) ?>><?= h($label) ?></option>
<?php endforeach; ?>
</select>
</label>
<label>
<span>Liefergebiet</span>
<select name="delivery_zone" data-delivery-zone>
<?php foreach ($deliveryZoneOptions as $value => $zone): ?>
<option value="<?= h($value) ?>" <?= selected((string) ($old['delivery_zone'] ?? $defaults['delivery_zone']), $value) ?>>
<?= h($zone['label']) ?> · <?= h(formatCurrency((int) $zone['price_cents'])) ?>
</option>
<?php endforeach; ?>
</select>
</label>
<label>
@@ -112,6 +125,7 @@
<textarea name="internal_notes" rows="4"><?= h((string) ($old['internal_notes'] ?? '')) ?></textarea>
</label>
</div>
<p class="form-help">Preislogik: Abholung <?= h(formatCurrency((int) $deliveryZoneOptions['self_pickup']['price_cents'])) ?>, Hannover <?= h(formatCurrency((int) $deliveryZoneOptions['hannover']['price_cents'])) ?>, Region Hannover <?= h(formatCurrency((int) $deliveryZoneOptions['region_hannover']['price_cents'])) ?>, Hameln/Braunschweig/Hildesheim/Celle <?= h(formatCurrency((int) $deliveryZoneOptions['extended_region']['price_cents'])) ?> pro Miettag.</p>
</div>
<div class="booking-summary-card">
@@ -119,6 +133,10 @@
<span>Mietdauer</span>
<strong data-summary-days>Noch nicht gewählt</strong>
</div>
<div class="summary-line">
<span>Preis pro Miettag</span>
<strong data-summary-rate><?= h(formatCurrency((int) ($old['price_per_day_cents'] ?? $defaults['price_per_day_cents']))) ?></strong>
</div>
<div class="summary-line summary-line-total">
<span>Gesamtpreis</span>
<strong data-summary-total><?= h(formatCurrency((int) $defaults['price_per_day_cents'])) ?></strong>
@@ -128,4 +146,3 @@
<button type="submit" class="button-primary">Buchung speichern</button>
</form>
</section>
+1 -1
View File
@@ -28,6 +28,7 @@
<div><dt>Mietzeitraum</dt><dd><?= h(formatDate($booking['start_date'])) ?> bis <?= h(formatDate($booking['end_date'])) ?></dd></div>
<div><dt>Miettage</dt><dd><?= h((string) $booking['total_days']) ?></dd></div>
<div><dt>Leistung</dt><dd><?= h($booking['delivery_mode_label']) ?></dd></div>
<div><dt>Liefergebiet</dt><dd><?= h($booking['delivery_zone_label'] ?? '-') ?></dd></div>
<div><dt>Zahlungsart</dt><dd><?= h($booking['payment_method_label']) ?></dd></div>
<div><dt>Gesamt</dt><dd><?= h(formatCurrency((int) $booking['subtotal_cents'])) ?></dd></div>
</dl>
@@ -106,4 +107,3 @@
</article>
</div>
</section>
+28 -28
View File
@@ -1,15 +1,14 @@
<section class="hero">
<div class="hero-copy">
<p class="eyebrow">Professionelle Fotobox-Vermietung</p>
<h1>Fotobox mieten für Hochzeiten, Geburtstage und Firmenfeiern.</h1>
<p class="eyebrow">Fotobox-Verleih für <?= h($company['service_area']) ?></p>
<h1>Fotobox mieten für Hochzeit, Geburtstag und Firmenfeier.</h1>
<p class="hero-text">
Hochwertige Technik, klare Preislogik pro Miettag und ein Buchungsablauf,
der auch kaufmännisch sauber funktioniert. Anfrage senden, Bestätigung erhalten,
Bilder digital bekommen.
Professionelle Fotobox mit Spiegelreflexkamera, Studioblitz und digitaler Bildübergabe.
Lieferung oder Selbstabholung möglich. Schon ab <?= h(formatCurrency((int) $dayRate)) ?> pro Miettag bei Selbstabholung.
</p>
<div class="hero-actions">
<a class="button-primary" href="<?= h(url('buchen')) ?>">Verfügbarkeit prüfen</a>
<a class="button-secondary" href="<?= h(url('leistungen')) ?>">Leistungen ansehen</a>
<a class="button-secondary" href="<?= h(url('preise')) ?>">Preise ansehen</a>
</div>
<div class="trust-grid">
<?php foreach ($trustFacts as $fact): ?>
@@ -23,8 +22,8 @@
<aside class="hero-panel">
<div class="hero-panel-top">
<span>Service mit Struktur</span>
<strong>Vom ersten Termin bis zur Rechnung</strong>
<span>Beliebt für Hochzeiten, Geburtstage und Firmenfeiern</span>
<strong>Professionelle Fotos mit wenig Aufwand</strong>
</div>
<div class="device-stage">
<div class="device-glow"></div>
@@ -48,11 +47,11 @@
</div>
<div class="hero-panel-bottom">
<div>
<span>Abholung</span>
<span>Mietbeginn</span>
<strong><?= h($company['pickup_window']) ?></strong>
</div>
<div>
<span>Rückgabe</span>
<span>Mietende</span>
<strong><?= h($company['return_window']) ?></strong>
</div>
</div>
@@ -61,11 +60,11 @@
<section class="section section-tight">
<div class="section-heading">
<p class="eyebrow">Warum diese Seite anders aufgebaut ist</p>
<h2>Kein Party-Prospekt, sondern eine ruhige Buchungsseite für einen echten Mietservice.</h2>
<p class="eyebrow">Warum unsere Fotobox</p>
<h2>Klare Leistungen. Klare Preise. Klare Abläufe.</h2>
<p>
Die Agenten-Recherche hat klar gezeigt: Kundenfreundlich ist eine verständliche Service-Seite
mit Preis, Ablauf, Verfügbarkeit und einem Verwaltungsprozess im Hintergrund.
Sie sehen sofort, was enthalten ist, wie ein Miettag berechnet wird
und wie Ihre Anfrage abläuft. So planen Sie Ihr Event ohne unnötige Rückfragen und ohne Technikstress.
</p>
</div>
<div class="feature-card-grid">
@@ -81,7 +80,7 @@
<section class="section split-section">
<div class="content-card">
<p class="eyebrow">Ablauf</p>
<h2>So läuft Ihre Anfrage ab</h2>
<h2>So einfach mieten Sie die Fotobox</h2>
<ol class="step-list">
<?php foreach ($processSteps as $index => $step): ?>
<li>
@@ -95,8 +94,8 @@
</ol>
</div>
<div class="content-card editorial-card">
<p class="eyebrow">Standards</p>
<h2>Kommerziell gedacht, nicht nur hübsch.</h2>
<p class="eyebrow">Auf einen Blick</p>
<h2>Alles, was für eine entspannte Buchung wichtig ist.</h2>
<ul class="check-list">
<?php foreach ($serviceStandards as $standard): ?>
<li><?= h($standard) ?></li>
@@ -107,8 +106,8 @@
<section class="section">
<div class="section-heading">
<p class="eyebrow">Leistungsmodule</p>
<h2>Technik, Eventbetrieb und Verwaltung greifen ineinander.</h2>
<p class="eyebrow">Leistungen</p>
<h2>Alles drin für eine Fotobox, die sofort einsatzbereit ist.</h2>
</div>
<div class="module-grid">
<?php foreach ($serviceModules as $module): ?>
@@ -127,7 +126,7 @@
<section class="section">
<div class="section-heading">
<p class="eyebrow">Anlässe</p>
<h2>Für Privatfeiern und professionelle Events geeignet.</h2>
<h2>Die passende Fotobox für Ihr Event in <?= h($company['service_area']) ?>.</h2>
</div>
<div class="occasion-grid">
<?php foreach ($occasionCards as $occasion): ?>
@@ -142,8 +141,8 @@
<section class="section split-section">
<div class="content-card">
<p class="eyebrow">Verfügbarkeit</p>
<h2>Aktuell geblockte oder bestätigte Zeiträume</h2>
<p>Die Übersicht stammt direkt aus dem Verwaltungssystem und zeigt belegte Termine.</p>
<h2>Bereits reservierte Termine</h2>
<p>Hier sehen Sie, welche Zeiträume aktuell angefragt, reserviert oder bereits bestätigt sind.</p>
<div class="availability-list">
<?php if ($bookings === []): ?>
<article class="availability-card">
@@ -154,24 +153,25 @@
<?php foreach ($bookings as $booking): ?>
<article class="availability-card">
<div>
<strong><?= h($booking['reference']) ?></strong>
<span><?= h(formatDate($booking['start_date'])) ?> bis <?= h(formatDate($booking['end_date'])) ?></span>
<strong><?= h(formatDate($booking['start_date'])) ?> bis <?= h(formatDate($booking['end_date'])) ?></strong>
<span><?= h((string) ($booking['delivery_zone_label'] ?: $booking['delivery_mode_label'])) ?></span>
</div>
<span class="<?= h(statusPillClass((string) $booking['status'])) ?>"><?= h($booking['status_label']) ?></span>
</article>
<?php endforeach; ?>
</div>
<a class="button-secondary" href="<?= h(url('verfuegbarkeit')) ?>">Gesamte Verfügbarkeit ansehen</a>
<a class="button-secondary" href="<?= h(url('verfuegbarkeit')) ?>">Wunschtermin prüfen</a>
</div>
<div class="content-card emphasis-card">
<p class="eyebrow">Nächster Schritt</p>
<h2>In wenigen Minuten zur Anfrage</h2>
<p class="eyebrow">Jetzt anfragen</p>
<h2>Unverbindlich Verfügbarkeit anfragen</h2>
<ul class="check-list">
<?php foreach ($bookingChecklist as $item): ?>
<li><?= h($item) ?></li>
<?php endforeach; ?>
</ul>
<p>Lieferpreise richten sich nach dem Zielort: Hannover, Region Hannover oder Hameln, Braunschweig, Hildesheim und Celle.</p>
<div class="pricing-example-list">
<?php foreach ($pricingExamples as $example): ?>
<article>
@@ -180,6 +180,6 @@
</article>
<?php endforeach; ?>
</div>
<a class="button-primary button-block" href="<?= h(url('buchen')) ?>">Zur Buchungsanfrage</a>
<a class="button-primary button-block" href="<?= h(url('buchen')) ?>">Wunschtermin anfragen</a>
</div>
</section>
+37 -12
View File
@@ -2,16 +2,15 @@
$app = appConfig();
$company = $app['company'];
$metaTitle = isset($pageTitle) ? $pageTitle . ' | ' . $company['name'] : $company['name'];
$metaDescription = $metaDescription ?? 'Professionelle Fotobox-Vermietung mit klarer Buchungsanfrage und Verwaltungsbereich.';
$metaDescription = $metaDescription ?? 'Professionelle Fotobox-Vermietung mit klaren Preisen, einfacher Anfrage und digitaler Bildübergabe.';
$isAdminArea = str_contains($viewPath, '/admin/');
$styleVersion = is_file(dirname(__DIR__) . '/assets/styles.css') ? (string) filemtime(dirname(__DIR__) . '/assets/styles.css') : '1';
$scriptVersion = is_file(dirname(__DIR__) . '/assets/app.js') ? (string) filemtime(dirname(__DIR__) . '/assets/app.js') : '1';
$currentPath = currentPath();
$publicNav = [
['label' => 'Leistungen', 'path' => '/leistungen'],
['label' => 'Fotobox', 'path' => '/fotobox', 'activePaths' => ['/fotobox', '/leistungen']],
['label' => 'Preise', 'path' => '/preise'],
['label' => 'Verfügbarkeit', 'path' => '/verfuegbarkeit'],
['label' => 'Ablauf', 'path' => '/ablauf'],
['label' => 'FAQ', 'path' => '/faq'],
['label' => 'Kontakt', 'path' => '/kontakt'],
@@ -78,14 +77,40 @@ $adminNav = [
</form>
</div>
<?php else: ?>
<nav class="site-nav site-nav-public" aria-label="Hauptnavigation">
<?php foreach ($publicNav as $item): ?>
<a class="<?= $currentPath === $item['path'] ? 'is-active' : '' ?>" href="<?= h(url($item['path'])) ?>"><?= h($item['label']) ?></a>
<?php endforeach; ?>
</nav>
<div class="header-actions">
<a class="button-secondary" href="<?= h(url('kontakt')) ?>">Kontakt</a>
<a class="button-primary" href="<?= h(url('buchen')) ?>">Buchungsanfrage</a>
<div class="public-header-shell">
<button
type="button"
class="nav-toggle"
data-nav-toggle
aria-expanded="false"
aria-controls="public-navigation"
>
<span class="nav-toggle-box" aria-hidden="true">
<span></span>
<span></span>
<span></span>
</span>
<span>Menü</span>
</button>
<div class="public-header-controls" id="public-navigation" data-nav-menu>
<nav class="site-nav site-nav-public" aria-label="Hauptnavigation">
<?php foreach ($publicNav as $item): ?>
<?php $isActive = in_array($currentPath, $item['activePaths'] ?? [$item['path']], true); ?>
<a class="<?= $isActive ? 'is-active' : '' ?>" href="<?= h(url($item['path'])) ?>"><?= h($item['label']) ?></a>
<?php endforeach; ?>
</nav>
<div class="menu-meta">
<span><?= h($company['service_area']) ?></span>
<a href="mailto:<?= h($company['email']) ?>"><?= h($company['email']) ?></a>
</div>
<div class="header-actions header-actions-public">
<a class="contact-chip" href="tel:<?= h($company['phone']) ?>">
<span>Telefon</span>
<strong><?= h($company['phone']) ?></strong>
</a>
<a class="button-primary" href="<?= h(url('verfuegbarkeit')) ?>">Verfügbarkeit prüfen</a>
</div>
</div>
</div>
<?php endif; ?>
</div>
@@ -112,7 +137,7 @@ $adminNav = [
</div>
<div>
<h3>Seiten</h3>
<a href="<?= h(url('leistungen')) ?>">Leistungen</a>
<a href="<?= h(url('fotobox')) ?>">Fotobox</a>
<a href="<?= h(url('preise')) ?>">Preise</a>
<a href="<?= h(url('buchen')) ?>">Buchen</a>
<a href="<?= h(url('faq')) ?>">FAQ</a>
+8 -8
View File
@@ -1,7 +1,7 @@
<section class="page-hero">
<p class="eyebrow">Ablauf</p>
<h1>Von der Anfrage bis zur Rückgabe klar geführt.</h1>
<p>Die Seite ist so gebaut, dass Privatkunden und Firmenkunden denselben klaren Ablauf erleben.</p>
<h1>So einfach mieten Sie Ihre Fotobox</h1>
<p>Von der Anfrage bis zur Rückgabe: In wenigen Schritten zu Ihrer Fotobox.</p>
</section>
<section class="section">
@@ -22,21 +22,21 @@
<section class="section split-section">
<article class="content-card">
<h2>Abholung und Rückgabe</h2>
<h2>Abholung oder Lieferung</h2>
<ul class="check-list">
<li><?= h($company['pickup_window']) ?></li>
<li><?= h($company['return_window']) ?></li>
<li>Bei der Abholung erhalten Sie eine kurze Einweisung in die Fotobox.</li>
<li>Lieferung und Aufbau können im Anfrageprozess gewählt werden.</li>
<li>Der Mietzeitraum wird immer über Übernachtungen berechnet.</li>
</ul>
</article>
<article class="content-card">
<h2>Verwaltung im Hintergrund</h2>
<h2>Was nach Ihrer Anfrage passiert</h2>
<ul class="check-list">
<li>Anfragen werden im Backend geprüft und bestätigt.</li>
<li>Für bestätigte Aufträge können Rechnungen mit Kundendaten erstellt werden.</li>
<li>Zahlungsstatus und interne Notizen bleiben jederzeit nachvollziehbar.</li>
<li>Wir prüfen die Verfügbarkeit Ihres Wunschtermins.</li>
<li>Sie erhalten eine Rückmeldung zum Termin und zum weiteren Ablauf.</li>
<li>Auf Wunsch erstellen wir eine Rechnung mit Ihren Kundendaten.</li>
</ul>
</article>
</section>
+7 -5
View File
@@ -1,15 +1,18 @@
<section class="page-hero">
<p class="eyebrow">Buchungsanfrage</p>
<h1>Fotobox jetzt anfragen.</h1>
<h1>Verfügbarkeit Ihrer Fotobox prüfen</h1>
<p>
Wählen Sie Ihren Zeitraum, legen Sie Leistungsart und Zahlungsart fest und senden Sie Ihre Anfrage direkt an die Verwaltung.
Wählen Sie Abhol- und Rückgabetag und senden Sie uns Ihre unverbindliche Anfrage.
Ein Miettag entspricht immer einer Übernachtung.
Selbstabholung kostet <?= h(formatCurrency((int) $dayRate)) ?> pro Miettag, Lieferungen werden je nach Zielort berechnet.
</p>
</section>
<section class="section split-section">
<article class="content-card emphasis-card">
<p class="eyebrow">Vor dem Absenden</p>
<h2>Was wir für eine saubere Bearbeitung brauchen</h2>
<p class="eyebrow">In 2 Minuten zur Anfrage</p>
<h2>Diese Angaben benötigen wir für Ihre Anfrage</h2>
<p>Mit Ihren Angaben prüfen wir den Termin, erfassen die Rechnungsdaten und melden uns zeitnah mit einer Bestätigung oder Rückfrage.</p>
<ul class="check-list">
<?php foreach ($bookingChecklist as $item): ?>
<li><?= h($item) ?></li>
@@ -25,4 +28,3 @@
<?php require dirname(__DIR__) . '/partials/public-booking-form.php'; ?>
</article>
</section>
+2 -3
View File
@@ -1,7 +1,7 @@
<section class="page-hero">
<p class="eyebrow">FAQ</p>
<h1>Häufige Fragen vor der Anfrage.</h1>
<p>Hier finden Sie die Punkte, die in Vermietung, Zahlung und Rückgabe am häufigsten geklärt werden müssen.</p>
<h1>Häufige Fragen zur Fotobox-Miete</h1>
<p>Hier finden Sie Antworten zu Mietdauer, Zahlung, Lieferung, Rückgabe und digitaler Bildübergabe.</p>
</section>
<section class="section">
@@ -14,4 +14,3 @@
<?php endforeach; ?>
</div>
</section>
+4 -5
View File
@@ -1,7 +1,7 @@
<section class="page-hero">
<p class="eyebrow">Kontakt</p>
<h1>Direkt erreichbar für Fragen zu Termin, Lieferung und Rechnung.</h1>
<p>Wenn Sie vor der Anfrage noch etwas abstimmen möchten, erreichen Sie uns über die folgenden Kontaktwege.</p>
<h1>Wir beraten Sie gerne persönlich.</h1>
<p>Wenn Sie vor Ihrer Anfrage noch Fragen zu Termin, Lieferung oder Zahlungsart haben, erreichen Sie uns direkt über die folgenden Kontaktwege.</p>
</section>
<section class="section split-section">
@@ -16,14 +16,13 @@
</div>
</article>
<article class="content-card emphasis-card">
<h2>Was wir schnell beantworten können</h2>
<h2>Wobei wir Sie unterstützen</h2>
<ul class="check-list">
<li>Prüfung von Wunschterminen</li>
<li>Lieferung, Aufbau und regionale Einsatzorte</li>
<li>Fragen zu Rechnung, Zahlungsart und Mietdauer</li>
<li>Abstimmung von Firmenveranstaltungen und Sonderfällen</li>
</ul>
<a class="button-primary" href="<?= h(url('buchen')) ?>">Zur Buchungsanfrage</a>
<a class="button-primary" href="<?= h(url('buchen')) ?>">Jetzt unverbindlich anfragen</a>
</article>
</section>
+9 -9
View File
@@ -1,9 +1,10 @@
<section class="page-hero">
<p class="eyebrow">Leistungen & Ausstattung</p>
<h1>Eine Fotobox, die technisch überzeugt und organisatorisch mitdenkt.</h1>
<p class="eyebrow">Fotobox & Leistungen</p>
<h1>Professionelle Fotobox für Hochzeit, Geburtstag und Firmenfeier.</h1>
<p>
Diese Seite zeigt nicht nur die Technik, sondern den gesamten Service:
Bildqualität, Bedienbarkeit, Logistik, digitale Übergabe und die kaufmännische Abwicklung.
Hochwertige Bilder, einfache Bedienung und eine klare Abwicklung.
Hier sehen Sie, was im Preis enthalten ist und welche Leistungen zusätzlich möglich sind.
Von der Spiegelreflexkamera bis zum WLAN-Download ist alles auf eine einfache Nutzung ausgelegt.
</p>
</section>
@@ -33,10 +34,9 @@
<section class="section cta-band">
<div>
<p class="eyebrow">Buchung</p>
<h2>Sie wissen schon, was Sie brauchen?</h2>
<p>Dann prüfen Sie direkt Ihren Zeitraum und senden Sie Ihre Anfrage digital.</p>
<p class="eyebrow">Wunschtermin</p>
<h2>Prüfen Sie direkt die Verfügbarkeit Ihrer Fotobox.</h2>
<p>Senden Sie uns Ihre Anfrage unverbindlich online. Wir melden uns zeitnah zurück.</p>
</div>
<a class="button-primary" href="<?= h(url('buchen')) ?>">Jetzt anfragen</a>
<a class="button-primary" href="<?= h(url('verfuegbarkeit')) ?>">Verfügbarkeit prüfen</a>
</section>
+1 -2
View File
@@ -15,7 +15,7 @@
</article>
<article class="legal-card">
<h2>3. Zahlung</h2>
<p>Zahlungen sind per Rechnung / Überweisung oder per PayPal möglich. Die gewählte Zahlungsart wird bei der Anfrage erfasst und kann im Verwaltungsprozess hinterlegt werden.</p>
<p>Zahlungen sind per Rechnung / Überweisung oder per PayPal möglich. Die gewünschte Zahlungsart wird direkt in Ihrer Anfrage angegeben.</p>
</article>
<article class="legal-card">
<h2>4. Übergabe und Rückgabe</h2>
@@ -26,4 +26,3 @@
<p>Bitte melden Sie Störungen oder Schäden unverzüglich. Für einen verbindlichen gewerblichen Einsatz sollten Haftungs- und Ausfallregelungen vor dem Live-Start individuell ergänzt werden.</p>
</article>
</section>
+31 -21
View File
@@ -1,36 +1,47 @@
<section class="page-hero">
<p class="eyebrow">Preise & Mietlogik</p>
<h1>Klare Preise ohne versteckte Logik.</h1>
<h1>Fotobox-Preise auf einen Blick</h1>
<p>
Der Standardpreis beträgt <strong><?= h(formatCurrency((int) $dayRate)) ?></strong> pro Miettag.
Ein Miettag entspricht immer einer Übernachtung.
<strong>Abholung ab <?= h(formatCurrency((int) $dayRate)) ?> pro Miettag.</strong>
Lieferpreise richten sich nach dem Zielort. Ein Miettag entspricht immer einer Übernachtung.
</p>
</section>
<section class="section split-section">
<article class="content-card emphasis-card">
<p class="eyebrow">Grundpreis</p>
<h2><?= h(formatCurrency((int) $dayRate)) ?> pro Miettag</h2>
<p>Montag bis Dienstag = 1 Miettag. Freitag bis Sonntag = 2 Miettage.</p>
<p class="eyebrow">Preis je Liefergebiet</p>
<h2>Abholung oder Lieferung nach Region</h2>
<p>Ein Miettag entspricht einer Übernachtung. Montag bis Dienstag = 1 Miettag. Freitag bis Sonntag = 2 Miettage.</p>
<ul class="check-list">
<li>Technikpaket mit DSLR-Kamera, Blitz und Softbox</li>
<li>Digitale Bildübergabe inklusive</li>
<li>Zahlung per Rechnung, Überweisung oder PayPal</li>
<li>Verbindlichkeit erst nach Bestätigung Ihrer Anfrage</li>
<?php foreach ($pricingExamples as $example): ?>
<li><strong><?= h($example['title']) ?>:</strong> <?= h($example['text']) ?></li>
<?php endforeach; ?>
</ul>
</article>
<article class="content-card">
<p class="eyebrow">Preisbeispiele</p>
<p class="eyebrow">Was im Preis enthalten ist</p>
<div class="pricing-example-list">
<?php foreach ($pricingExamples as $example): ?>
<article>
<strong><?= h($example['title']) ?></strong>
<span><?= h($example['text']) ?></span>
</article>
<?php endforeach; ?>
<article>
<strong>Technik</strong>
<span>Fotobox, Spiegelreflexkamera, Studioblitz und Softbox</span>
</article>
<article>
<strong>Bilder</strong>
<span>Alle Fotos digital inklusive</span>
</article>
<article>
<strong>Abwicklung</strong>
<span>Zahlung per Rechnung / Überweisung oder PayPal</span>
</article>
<article>
<strong>Anfrage</strong>
<span>Verbindlich wird Ihre Buchung erst nach unserer Bestätigung</span>
</article>
</div>
<p class="small-note">
Lieferung, Aufbau oder Vor-Ort-Betreuung werden im Anfrageprozess passend zum Anlass abgestimmt.
Hannover kostet <?= h(formatCurrency((int) $config['pricing']['delivery_rates']['hannover']['price_cents'])) ?>,
die Region Hannover <?= h(formatCurrency((int) $config['pricing']['delivery_rates']['region_hannover']['price_cents'])) ?> und
Hameln, Braunschweig, Hildesheim oder Celle <?= h(formatCurrency((int) $config['pricing']['delivery_rates']['extended_region']['price_cents'])) ?> pro Miettag.
</p>
</article>
</section>
@@ -39,7 +50,7 @@
<div class="section-heading">
<p class="eyebrow">Zahlung</p>
<h2>Rechnung, Überweisung oder PayPal</h2>
<p>Die gewünschte Zahlungsart wird bereits in der Anfrage hinterlegt und kann im Backend verwaltet werden.</p>
<p>Die gewünschte Zahlungsart geben Sie direkt in Ihrer Anfrage an. Auf Wunsch erhalten Sie eine Rechnung mit vollständigen Kundendaten.</p>
</div>
<div class="trust-grid">
<article class="trust-card">
@@ -48,7 +59,7 @@
</article>
<article class="trust-card">
<span>PayPal</span>
<strong>Als Zahlungsart auswählbar</strong>
<strong>Direkt in der Anfrage auswählbar</strong>
</article>
<article class="trust-card">
<span>Steuerhinweis</span>
@@ -56,4 +67,3 @@
</article>
</div>
</section>
+82 -13
View File
@@ -1,27 +1,91 @@
<section class="page-hero">
<p class="eyebrow">Verfügbarkeit</p>
<h1>Geblockte und bereits bestätigte Zeiträume im Blick.</h1>
<h1>Prüfen Sie, ob Ihr Wunschtermin noch frei ist.</h1>
<p>
Die Übersicht zeigt aktuelle Belegungen aus dem Verwaltungssystem.
Für Ihren Wunschtermin senden Sie am besten direkt eine Anfrage.
Hier sehen Sie bereits belegte Termine im Monatskalender.
Ist Ihr Wunschtermin noch frei, senden Sie uns direkt Ihre unverbindliche Anfrage.
</p>
</section>
<section class="section">
<div class="section-heading">
<p class="eyebrow">Buchungskalender</p>
<h2>Belegte Termine auf einen Blick</h2>
<p>Die Kalenderansicht zeigt angefragte, reservierte und bestätigte Zeiträume für die nächsten Monate.</p>
</div>
<div class="calendar-legend" aria-label="Legende zur Verfügbarkeit">
<span class="status-pill status-requested">Anfrage</span>
<span class="status-pill status-reserved">Reserviert</span>
<span class="status-pill status-confirmed">Bestätigt</span>
</div>
<div class="public-calendar-grid">
<?php foreach ($availabilityCalendarMonths as $month): ?>
<article class="calendar-month-card">
<div class="calendar-month-header">
<h2><?= h($month['label']) ?></h2>
<span><?= h((string) $month['entry_count']) ?> Termine</span>
</div>
<div class="calendar-weekdays" aria-hidden="true">
<span>Mo</span>
<span>Di</span>
<span>Mi</span>
<span>Do</span>
<span>Fr</span>
<span>Sa</span>
<span>So</span>
</div>
<div class="calendar-days">
<?php foreach ($month['days'] as $day): ?>
<?php if (!empty($day['is_padding'])): ?>
<div class="calendar-day calendar-day-empty" aria-hidden="true"></div>
<?php else: ?>
<div class="calendar-day<?= !empty($day['is_booked']) ? ' is-booked calendar-day-' . h((string) $day['status']) : '' ?><?= !empty($day['is_today']) ? ' is-today' : '' ?>">
<span class="calendar-day-number"><?= h((string) $day['day']) ?></span>
<?php if (!empty($day['is_booked'])): ?>
<span class="calendar-day-state"><?= h((string) $day['status_label']) ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endforeach; ?>
</div>
<div class="calendar-entry-list">
<?php if ($month['entries'] === []): ?>
<article class="calendar-entry">
<strong>Derzeit kein belegter Zeitraum.</strong>
<span>Dieser Monat ist aktuell noch frei.</span>
</article>
<?php else: ?>
<?php foreach ($month['entries'] as $entry): ?>
<article class="calendar-entry">
<div>
<strong><?= h($entry['date_label']) ?></strong>
<span><?= h($entry['delivery_label']) ?> · <?= h($entry['day_count_label']) ?></span>
</div>
<span class="<?= h(statusPillClass((string) $entry['status'])) ?>"><?= h($entry['status_label']) ?></span>
</article>
<?php endforeach; ?>
<?php endif; ?>
</div>
</article>
<?php endforeach; ?>
</div>
</section>
<section class="section split-section">
<article class="content-card">
<h2>Aktuelle Belegung</h2>
<div class="availability-list">
<?php if ($bookings === []): ?>
<?php if ($availabilityBookings === []): ?>
<article class="availability-card">
<strong>Momentan gibt es keine festen Einträge.</strong>
<span>Ihre Anfrage kann direkt neu aufgenommen werden.</span>
</article>
<?php endif; ?>
<?php foreach ($bookings as $booking): ?>
<?php foreach ($availabilityBookings as $booking): ?>
<article class="availability-card">
<div>
<strong><?= h($booking['reference']) ?></strong>
<span><?= h(formatDate($booking['start_date'])) ?> bis <?= h(formatDate($booking['end_date'])) ?></span>
<strong><?= h(formatDate($booking['start_date'])) ?> bis <?= h(formatDate($booking['end_date'])) ?></strong>
<span><?= h((string) ($booking['delivery_zone_label'] ?: $booking['delivery_mode_label'])) ?> · <?= h((string) $booking['total_days']) ?> <?= $booking['total_days'] === 1 ? 'Miettag' : 'Miettage' ?></span>
</div>
<span class="<?= h(statusPillClass((string) $booking['status'])) ?>"><?= h($booking['status_label']) ?></span>
</article>
@@ -29,13 +93,18 @@
</div>
</article>
<article class="content-card emphasis-card">
<h2>Direkt zur Anfrage</h2>
<h2>Jetzt unverbindlich anfragen</h2>
<ul class="check-list">
<li>Zeitraum nach Übernachtungen wählen</li>
<li>Lieferart und Zahlungsart festlegen</li>
<li>Kundendaten für Rechnung und Rückfragen erfassen</li>
<li>Wunschtermin auswählen</li>
<li>Leistungsart, Liefergebiet und Zahlungsart festlegen</li>
<li>Kontaktdaten und Veranstaltungsort eintragen</li>
</ul>
<a class="button-primary button-block" href="<?= h(url('buchen')) ?>">Jetzt Termin anfragen</a>
<p class="small-note">
Selbstabholung: <?= h(formatCurrency((int) $config['pricing']['delivery_rates']['self_pickup']['price_cents'])) ?> ·
Hannover: <?= h(formatCurrency((int) $config['pricing']['delivery_rates']['hannover']['price_cents'])) ?> ·
Region Hannover: <?= h(formatCurrency((int) $config['pricing']['delivery_rates']['region_hannover']['price_cents'])) ?> ·
Hameln, Braunschweig, Hildesheim oder Celle: <?= h(formatCurrency((int) $config['pricing']['delivery_rates']['extended_region']['price_cents'])) ?>
</p>
<a class="button-primary button-block" href="<?= h(url('buchen')) ?>">Wunschtermin anfragen</a>
</article>
</section>
+30 -10
View File
@@ -1,5 +1,9 @@
<?php
$oldData = is_array($old ?? null) ? $old : [];
$priceRates = [];
foreach ($deliveryZoneOptions as $zoneKey => $zone) {
$priceRates[$zoneKey] = (int) $zone['price_cents'];
}
?>
<div class="booking-form-shell">
<?php if (!empty($flashSuccess)): ?>
@@ -10,7 +14,7 @@ $oldData = is_array($old ?? null) ? $old : [];
<div class="flash flash-error"><?= h((string) $flashError) ?></div>
<?php endif; ?>
<form method="post" action="<?= h(url('book')) ?>" class="booking-form" data-day-rate="<?= h((string) $dayRate) ?>">
<form method="post" action="<?= h(url('book')) ?>" class="booking-form" data-day-rate="<?= h((string) $dayRate) ?>" data-price-rates="<?= h((string) json_encode($priceRates, JSON_THROW_ON_ERROR)) ?>">
<?= csrfField() ?>
<input type="hidden" name="price_per_day_cents" value="<?= h((string) $dayRate) ?>">
@@ -35,15 +39,25 @@ $oldData = is_array($old ?? null) ? $old : [];
<div class="form-section">
<div class="form-section-header">
<span class="form-step">Schritt 2</span>
<h3>Leistung und Zahlung festlegen</h3>
<h3>Paket und Zahlung wählen</h3>
</div>
<div class="form-grid form-grid-two">
<label>
<span>Leistungsart</span>
<select name="delivery_mode">
<option value="self_pickup" <?= selected((string) ($oldData['delivery_mode'] ?? 'self_pickup'), 'self_pickup') ?>>Selbstabholung</option>
<option value="delivery_setup" <?= selected((string) ($oldData['delivery_mode'] ?? ''), 'delivery_setup') ?>>Lieferung und Aufbau</option>
<option value="on_site_support" <?= selected((string) ($oldData['delivery_mode'] ?? ''), 'on_site_support') ?>>Lieferung, Aufbau und Vor-Ort-Betreuung</option>
<select name="delivery_mode" data-delivery-mode>
<?php foreach ($deliveryModeOptions as $value => $label): ?>
<option value="<?= h($value) ?>" <?= selected((string) ($oldData['delivery_mode'] ?? 'self_pickup'), $value) ?>><?= h($label) ?></option>
<?php endforeach; ?>
</select>
</label>
<label>
<span>Liefergebiet</span>
<select name="delivery_zone" data-delivery-zone>
<?php foreach ($deliveryZoneOptions as $value => $zone): ?>
<option value="<?= h($value) ?>" <?= selected((string) ($oldData['delivery_zone'] ?? 'self_pickup'), $value) ?>>
<?= h($zone['label']) ?> · <?= h(formatCurrency((int) $zone['price_cents'])) ?>
</option>
<?php endforeach; ?>
</select>
</label>
<label>
@@ -54,12 +68,18 @@ $oldData = is_array($old ?? null) ? $old : [];
</select>
</label>
</div>
<p class="form-help">
Abholung kostet <?= h(formatCurrency((int) $deliveryZoneOptions['self_pickup']['price_cents'])) ?> pro Miettag.
Lieferung nach Hannover kostet <?= h(formatCurrency((int) $deliveryZoneOptions['hannover']['price_cents'])) ?>,
in die Region Hannover <?= h(formatCurrency((int) $deliveryZoneOptions['region_hannover']['price_cents'])) ?> und
nach Hameln, Braunschweig, Hildesheim oder Celle <?= h(formatCurrency((int) $deliveryZoneOptions['extended_region']['price_cents'])) ?> pro Miettag.
</p>
</div>
<div class="form-section">
<div class="form-section-header">
<span class="form-step">Schritt 3</span>
<h3>Kundendaten erfassen</h3>
<h3>Kontaktdaten und Veranstaltungsort</h3>
</div>
<div class="form-grid">
<label>
@@ -112,7 +132,7 @@ $oldData = is_array($old ?? null) ? $old : [];
</div>
<div class="summary-line">
<span>Preis pro Miettag</span>
<strong><?= h(formatCurrency((int) $dayRate)) ?></strong>
<strong data-summary-rate><?= h(formatCurrency((int) $dayRate)) ?></strong>
</div>
<div class="summary-line summary-line-total">
<span>Voraussichtlicher Gesamtpreis</span>
@@ -131,7 +151,7 @@ $oldData = is_array($old ?? null) ? $old : [];
</label>
</div>
<button type="submit" class="button-primary button-block">Buchungsanfrage senden</button>
<p class="form-note">Keine Sofortabbuchung. Ihr Auftrag wird erst nach persönlicher Bestätigung verbindlich.</p>
<button type="submit" class="button-primary button-block">Buchungsanfrage unverbindlich senden</button>
<p class="form-note">Sie erhalten schnell eine Rückmeldung zur Verfügbarkeit und zum weiteren Ablauf.</p>
</form>
</div>