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 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` - 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 - einen Verwaltungsbereich für Anfragen, Buchungen, Kunden, Kalender, Rechnungen und Einstellungen
- MySQL-Unterstützung mit Tabellenpräfix `fb_` sowie JSON-Fallback - 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. - Ein Miettag entspricht immer einer Übernachtung.
- Beispiel: `Montag bis Dienstag = 1 Miettag` - 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` - Zahlungsarten: `Rechnung / Überweisung` und `PayPal`
- Öffentliche Eingaben sind zunächst Buchungsanfragen und werden erst nach Bestätigung verbindlich. - Ö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 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) => const formatCurrency = (cents) =>
new Intl.NumberFormat('de-DE', { new Intl.NumberFormat('de-DE', {
@@ -26,17 +29,54 @@ forms.forEach((form) => {
const startInput = form.querySelector('[data-booking-start]'); const startInput = form.querySelector('[data-booking-start]');
const endInput = form.querySelector('[data-booking-end]'); const endInput = form.querySelector('[data-booking-end]');
const daysOutput = form.querySelector('[data-summary-days]'); const daysOutput = form.querySelector('[data-summary-days]');
const rateOutput = form.querySelector('[data-summary-rate]');
const totalOutput = form.querySelector('[data-summary-total]'); const totalOutput = form.querySelector('[data-summary-total]');
const rateInput = form.querySelector('input[name="price_per_day_cents"]'); 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 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 render = () => {
const rentalDays = calculateRentalDays(startInput?.value, endInput?.value); const rentalDays = calculateRentalDays(startInput?.value, endInput?.value);
const rate = Number(rateInput?.value || defaultRate); const rate = syncRate();
if (!rentalDays || rate < 0) { if (!rentalDays || rate < 0) {
if (daysOutput) daysOutput.textContent = 'Noch nicht gewählt'; 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; return;
} }
@@ -44,6 +84,10 @@ forms.forEach((form) => {
daysOutput.textContent = `${rentalDays} ${rentalDays === 1 ? 'Miettag' : 'Miettage'}`; daysOutput.textContent = `${rentalDays} ${rentalDays === 1 ? 'Miettag' : 'Miettage'}`;
} }
if (rateOutput) {
rateOutput.textContent = formatCurrency(rate);
}
if (totalOutput) { if (totalOutput) {
totalOutput.textContent = formatCurrency(rentalDays * rate); totalOutput.textContent = formatCurrency(rentalDays * rate);
} }
@@ -52,5 +96,29 @@ forms.forEach((form) => {
startInput?.addEventListener('input', render); startInput?.addEventListener('input', render);
endInput?.addEventListener('input', render); endInput?.addEventListener('input', render);
rateInput?.addEventListener('input', render); rateInput?.addEventListener('input', render);
deliveryModeInput?.addEventListener('change', render);
deliveryZoneInput?.addEventListener('change', render);
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); 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 { .header-actions {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -618,6 +737,136 @@ p {
gap: 0.9rem; 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, .availability-card,
.stack-item { .stack-item {
display: flex; display: flex;
@@ -1064,21 +1313,90 @@ tbody tr:last-child td {
@media (max-width: 900px) { @media (max-width: 900px) {
.header-inner { .header-inner {
gap: 0.85rem;
}
.site-header-admin .header-inner {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
} }
.site-nav, .site-header-admin .site-nav,
.header-actions { .site-header-admin .header-actions {
justify-content: center; 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, .faq-grid,
.legal-section, .legal-section,
.trust-grid, .trust-grid,
.form-grid, .form-grid,
.form-grid-two, .form-grid-two,
.calendar-grid { .calendar-grid,
.public-calendar-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -1123,9 +1441,14 @@ tbody tr:last-child td {
flex-direction: column; flex-direction: column;
} }
.topbar {
display: none;
}
.button-primary, .button-primary,
.button-secondary, .button-secondary,
.ghost-button { .ghost-button,
.contact-chip {
width: 100%; width: 100%;
} }
@@ -1144,10 +1467,30 @@ tbody tr:last-child td {
.availability-card, .availability-card,
.stack-item, .stack-item,
.calendar-entry,
.form-section-header, .form-section-header,
.hero-panel-top, .hero-panel-top,
.hero-panel-bottom { .hero-panel-bottom {
flex-direction: column; flex-direction: column;
align-items: flex-start; 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', 'email' => 'hallo@fotobox-moments.local',
'phone' => '+49 170 1234567', 'phone' => '+49 170 1234567',
'website' => 'https://fotobox-moments.local', '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', 'response_time' => 'Antwort meist innerhalb von 24 Stunden',
'pickup_window' => 'Abholung ab 17:00 Uhr', 'pickup_window' => 'Abholung ab 17:00 Uhr',
'return_window' => 'Rückgabe bis 13:00 Uhr', 'return_window' => 'Rückgabe bis 13:00 Uhr',
@@ -42,7 +42,29 @@ return [
'pricing' => [ 'pricing' => [
'default_day_rate_cents' => 9999, 'default_day_rate_cents' => 9999,
'currency' => 'EUR', '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' => [ 'admin' => [
'username' => '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. 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. 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`. 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. 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. 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); 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 public function getDashboardStats(): array
{ {
$bookings = $this->getBookings(); $bookings = $this->getBookings();
@@ -303,6 +376,7 @@ final class BookingService
'payment_status' => 'unpaid', 'payment_status' => 'unpaid',
'payment_method' => 'invoice_transfer', 'payment_method' => 'invoice_transfer',
'delivery_mode' => 'self_pickup', '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 private function normalizeBookingInput(array $input, bool $adminMode): array
{ {
$customerName = trim((string) ($input['customer_name'] ?? '')); $customerName = trim((string) ($input['customer_name'] ?? ''));
@@ -351,11 +439,11 @@ final class BookingService
$endDate = trim((string) ($input['end_date'] ?? '')); $endDate = trim((string) ($input['end_date'] ?? ''));
$paymentMethod = trim((string) ($input['payment_method'] ?? 'invoice_transfer')); $paymentMethod = trim((string) ($input['payment_method'] ?? 'invoice_transfer'));
$deliveryMode = trim((string) ($input['delivery_mode'] ?? 'self_pickup')); $deliveryMode = trim((string) ($input['delivery_mode'] ?? 'self_pickup'));
$deliveryZone = trim((string) ($input['delivery_zone'] ?? 'self_pickup'));
$notesCustomer = trim((string) ($input['notes_customer'] ?? '')); $notesCustomer = trim((string) ($input['notes_customer'] ?? ''));
$internalNotes = trim((string) ($input['internal_notes'] ?? '')); $internalNotes = trim((string) ($input['internal_notes'] ?? ''));
$status = trim((string) ($input['status'] ?? ($adminMode ? 'confirmed' : 'requested'))); $status = trim((string) ($input['status'] ?? ($adminMode ? 'confirmed' : 'requested')));
$paymentStatus = trim((string) ($input['payment_status'] ?? 'unpaid')); $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'; $privacyAccepted = (string) ($input['privacy_accepted'] ?? '') === '1';
$termsAccepted = (string) ($input['terms_accepted'] ?? '') === '1'; $termsAccepted = (string) ($input['terms_accepted'] ?? '') === '1';
@@ -408,11 +496,8 @@ final class BookingService
'paypal' => 'PayPal', 'paypal' => 'PayPal',
]; ];
$deliveryLabels = [ $deliveryLabels = $this->getDeliveryModeOptions();
'self_pickup' => 'Selbstabholung', $deliveryZones = $this->getDeliveryZoneOptions();
'delivery_setup' => 'Lieferung und Aufbau',
'on_site_support' => 'Lieferung, Aufbau und Vor-Ort-Betreuung',
];
if (!array_key_exists($paymentMethod, $paymentLabels)) { if (!array_key_exists($paymentMethod, $paymentLabels)) {
throw new RuntimeException('Die gewählte Zahlungsart ist ungültig.'); 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.'); 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) { if ($pricePerDay < 0) {
throw new RuntimeException('Der Tagespreis ist ungültig.'); throw new RuntimeException('Der Tagespreis ist ungültig.');
} }
@@ -443,6 +540,8 @@ final class BookingService
'payment_method_label' => $paymentLabels[$paymentMethod], 'payment_method_label' => $paymentLabels[$paymentMethod],
'delivery_mode' => $deliveryMode, 'delivery_mode' => $deliveryMode,
'delivery_mode_label' => $deliveryLabels[$deliveryMode], 'delivery_mode_label' => $deliveryLabels[$deliveryMode],
'delivery_zone' => $deliveryZone,
'delivery_zone_label' => $deliveryZones[$deliveryZone]['label'],
'notes_customer' => $notesCustomer, 'notes_customer' => $notesCustomer,
'internal_notes' => $internalNotes, 'internal_notes' => $internalNotes,
'status' => $status, 'status' => $status,
@@ -472,6 +571,8 @@ final class BookingService
'payment_method_label' => $payload['payment_method_label'], 'payment_method_label' => $payload['payment_method_label'],
'delivery_mode' => $payload['delivery_mode'], 'delivery_mode' => $payload['delivery_mode'],
'delivery_mode_label' => $payload['delivery_mode_label'], 'delivery_mode_label' => $payload['delivery_mode_label'],
'delivery_zone' => $payload['delivery_zone'],
'delivery_zone_label' => $payload['delivery_zone_label'],
'start_date' => $payload['start_date'], 'start_date' => $payload['start_date'],
'end_date' => $payload['end_date'], 'end_date' => $payload['end_date'],
'total_days' => $payload['total_days'], 'total_days' => $payload['total_days'],
@@ -562,6 +663,46 @@ final class BookingService
return $prefix . '_' . strtolower(bin2hex(random_bytes(6))); 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 private function formatMonthLabel(string $monthKey): string
{ {
[$year, $month] = explode('-', $monthKey) + [null, null]; [$year, $month] = explode('-', $monthKey) + [null, null];
+2 -1
View File
@@ -64,6 +64,7 @@ function basePath(): string
'/admin/login', '/admin/login',
'/admin/logout', '/admin/logout',
'/admin', '/admin',
'/fotobox',
'/leistungen', '/leistungen',
'/preise', '/preise',
'/verfuegbarkeit', '/verfuegbarkeit',
@@ -185,7 +186,7 @@ function h(string $value): string
function formatCurrency(int $cents): 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 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.', 'metaDescription' => 'DSLR-Kamera, Studioblitz, Softbox, WLAN-Download und professioneller Lieferumfang für Ihre Fotobox-Miete.',
'pageKey' => 'leistungen', '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' => [ '/preise' => [
'view' => 'pages/preise', 'view' => 'pages/preise',
'pageTitle' => 'Preise und Mietlogik', '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', 'pageKey' => 'preise',
], ],
'/verfuegbarkeit' => [ '/verfuegbarkeit' => [
@@ -404,6 +410,7 @@ function renderPublicPage(array $route, BookingService $bookingService, array $c
{ {
$company = $config['company']; $company = $config['company'];
$bookings = $bookingService->getHighlightedBookings(); $bookings = $bookingService->getHighlightedBookings();
$availabilityBookings = $bookingService->getPublicAvailabilityBookings();
$currentView = (string) $route['view']; $currentView = (string) $route['view'];
render($currentView, [ render($currentView, [
@@ -413,10 +420,14 @@ function renderPublicPage(array $route, BookingService $bookingService, array $c
'config' => $config, 'config' => $config,
'company' => $company, 'company' => $company,
'dayRate' => $config['pricing']['default_day_rate_cents'], 'dayRate' => $config['pricing']['default_day_rate_cents'],
'deliveryModeOptions' => $bookingService->getDeliveryModeOptions(),
'deliveryZoneOptions' => $bookingService->getDeliveryZoneOptions(),
'flashSuccess' => flash('success'), 'flashSuccess' => flash('success'),
'flashError' => flash('error'), 'flashError' => flash('error'),
'old' => flash('old') ?? [], 'old' => flash('old') ?? [],
'bookings' => $bookings, 'bookings' => $bookings,
'availabilityBookings' => $availabilityBookings,
'availabilityCalendarMonths' => $bookingService->getPublicCalendarMonths(4),
'trustFacts' => publicTrustFacts($config), 'trustFacts' => publicTrustFacts($config),
'featureCards' => publicFeatureCards(), 'featureCards' => publicFeatureCards(),
'processSteps' => publicProcessSteps($config), 'processSteps' => publicProcessSteps($config),
@@ -547,6 +558,8 @@ function renderAdminCreate(BookingService $bookingService): void
'flashError' => flash('error'), 'flashError' => flash('error'),
'old' => flash('admin_old') ?? [], 'old' => flash('admin_old') ?? [],
'defaults' => $bookingService->getAdminDefaults(), 'defaults' => $bookingService->getAdminDefaults(),
'deliveryModeOptions' => $bookingService->getDeliveryModeOptions(),
'deliveryZoneOptions' => $bookingService->getDeliveryZoneOptions(),
]); ]);
} }
@@ -574,13 +587,14 @@ function renderAdminOrder(BookingService $bookingService): void
function publicTrustFacts(array $config): array function publicTrustFacts(array $config): array
{ {
$company = $config['company']; $rates = $config['pricing']['delivery_rates'];
return [ return [
['label' => 'Preis', 'value' => $config['pricing']['label']], ['label' => 'Abholung', 'value' => formatCurrency((int) $rates['self_pickup']['price_cents']) . ' pro Miettag'],
['label' => 'Mietlogik', 'value' => '1 Miettag = 1 Übernachtung'], ['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' => '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 [ return [
[ [
'title' => 'Professionelle Bildqualität', 'title' => 'Spiegelreflexkamera & Studioblitz',
'text' => 'DSLR-Kamera, Studioblitz und Softbox sorgen für klare, helle Fotos bei wechselnden Lichtverhältnissen.', 'text' => 'DSLR-Kamera, Bildschirm und große Softbox sorgen für helle, hochwertige Fotos bei jedem Anlass.',
], ],
[ [
'title' => 'Direkter Download aufs Handy', 'title' => 'WLAN-Download direkt vor Ort',
'text' => 'Ihre Gäste können Bilder vor Ort per WLAN laden. Nach dem Event erhalten Sie zusätzlich die komplette Galerie digital.', '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', 'title' => 'Schnell aufgebaut, leicht bedient',
'text' => 'Sie entscheiden zwischen Selbstabholung, Lieferung mit Aufbau oder einem Rundum-Service mit Vor-Ort-Betreuung.', 'text' => 'Die Fotobox ist in wenigen Minuten einsatzbereit und wird bei der Übergabe kurz erklärt.',
], ],
[ [
'title' => 'Saubere Verwaltung im Hintergrund', 'title' => 'Lieferung, Abholung oder Betreuung',
'text' => 'Anfragen, Kundendaten, Rechnungen und Zahlungsstatus werden in einem Verwaltungsbereich zentral gepflegt.', '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 [ return [
[ [
'title' => 'Zeitraum wählen', 'title' => 'Wunschtermin anfragen',
'text' => 'Sie wählen Abholtag und Rückgabetag. Montag bis Dienstag zählt als 1 Miettag.', 'text' => 'Sie wählen Abholtag und Rückgabetag. Montag bis Dienstag zählt als 1 Miettag.',
], ],
[ [
'title' => 'Leistung festlegen', 'title' => 'Verfügbarkeit bestätigen lassen',
'text' => 'Selbstabholung, Lieferung oder Betreuung vor Ort werden passend zu Ihrem Event gewählt.', 'text' => 'Wir prüfen den Termin und melden uns in der Regel innerhalb von 24 Stunden zurück.',
], ],
[ [
'title' => 'Anfrage absenden', 'title' => 'Fotobox abholen oder liefern lassen',
'text' => 'Wir prüfen Verfügbarkeit, erfassen Ihre Daten und bestätigen den Auftrag persönlich.', 'text' => 'Sie wählen Selbstabholung oder Lieferung mit Aufbau passend zu Ihrem Event.',
], ],
[ [
'title' => 'Feiern und Bilder erhalten', '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 function publicPricingExamples(array $config): array
{ {
$label = $config['pricing']['label']; $rates = $config['pricing']['delivery_rates'];
return [ return [
['title' => 'Montag bis Dienstag', 'text' => '1 Miettag · ' . $label], ['title' => 'Abholung', 'text' => formatCurrency((int) $rates['self_pickup']['price_cents']) . ' pro Miettag'],
['title' => 'Freitag bis Sonntag', 'text' => '2 Miettage · 199,98 €'], ['title' => 'Lieferung nach Hannover', 'text' => formatCurrency((int) $rates['hannover']['price_cents']) . ' pro Miettag'],
['title' => 'Buchungsanfrage', 'text' => 'Noch kein Sofortvertrag. Verbindlich erst nach Bestätigung.'], ['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 function publicOccasionCards(): array
{ {
return [ return [
['title' => 'Hochzeiten', 'text' => 'Für Erinnerungen mit ruhiger Technik und hochwertigem Licht.'], ['title' => 'Hochzeiten', 'text' => 'Für emotionale Erinnerungen und eine entspannte Feier mit Ihren Gästen.'],
['title' => 'Geburtstage', 'text' => 'Einfach zu bedienen und schnell einsatzbereit.'], ['title' => 'Geburtstage', 'text' => 'Einfach zu bedienen und schnell einsatzbereit für jede Party.'],
['title' => 'Firmenfeiern', 'text' => 'Mit Rechnung, klarer Planung und sauberer Abwicklung.'], ['title' => 'Firmenfeiern', 'text' => 'Mit klarer Abwicklung, Rechnung und professionellem Auftritt.'],
['title' => 'Jubiläen und Vereinsfeste', 'text' => 'Für Veranstaltungen mit vielen Gästen und wenig Zeitverlust.'], ['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.', '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?', 'question' => 'Was kostet die Fotobox je nach Zielort?',
'answer' => 'Nein. Sie senden zunächst eine Buchungsanfrage. Verbindlich wird der Auftrag erst nach unserer Bestätigung.', '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?', 'question' => 'Welche Zahlungsarten sind möglich?',
@@ -672,13 +691,17 @@ function publicFaqItems(array $config): array
'question' => 'Wie laufen Abholung und Rückgabe ab?', '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.', '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?', '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.', '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?', 'question' => 'Kann ich auch auf Rechnung zahlen?',
'answer' => 'Ja. Im Verwaltungsprozess können Rechnungen mit vollständigen Kundendaten erzeugt und als PDF bereitgestellt werden.', '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 function publicServiceModules(): array
{ {
return [ return [
['title' => 'Technikpaket', 'items' => ['DSLR-Kamera', 'Studioblitz mit Softbox', 'Bedienbildschirm', 'WLAN-Fotofreigabe']], ['title' => 'Im Preis enthalten', 'items' => ['Fotobox mit Stativ', 'Spiegelreflexkamera', 'Studioblitz mit Softbox', 'Alle Bilder digital']],
['title' => 'Eventbetrieb', 'items' => ['Schneller Aufbau', 'Intuitive Bedienung', 'Digitale Galerie', 'Saubere Rückgabeplanung']], ['title' => 'Das macht es einfach', 'items' => ['Kurze Einweisung bei der Übergabe', 'Schneller Aufbau', 'WLAN-Download vor Ort', 'Einfache Bedienung per Knopfdruck']],
['title' => 'Kaufmännische Abwicklung', 'items' => ['Anfrageerfassung', 'Rechnungsstellung', 'Zahlungsstatus', 'Admin-Verwaltung']], ['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 function publicServiceStandards(array $config): array
{ {
$company = $config['company']; $company = $config['company'];
$rates = $config['pricing']['delivery_rates'];
return [ return [
'Klare Preisangabe mit ' . $config['pricing']['label'], 'Selbstabholung ab ' . formatCurrency((int) $rates['self_pickup']['price_cents']) . ' pro Miettag',
'Direkt sichtbare Kontaktwege: ' . $company['phone'] . ' und ' . $company['email'], 'Lieferung nach Hannover ab ' . formatCurrency((int) $rates['hannover']['price_cents']) . ' pro Miettag',
'Pflichtseiten für Impressum, Datenschutz und Mietbedingungen', 'WLAN-Download direkt auf das Handy Ihrer Gäste',
'Barrierearme Formulare mit eindeutigen Beschriftungen und Fehlermeldungen', '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 function publicBookingChecklist(array $config): array
{ {
return [ return [
'Startdatum und Rückgabedatum bereithalten', 'Wunschtermin auswählen',
'Lieferart auswählen: Selbstabholung, Lieferung oder Betreuung', 'Leistungsart, Liefergebiet und Zahlungsart festlegen',
'Rechnungsdaten und Veranstaltungsort eintragen', 'Kontaktdaten und Veranstaltungsort eintragen',
'Datenschutz und Mietbedingungen vor dem Absenden bestätigen', 'Anfrage unverbindlich absenden',
]; ];
} }
+27 -10
View File
@@ -1,4 +1,10 @@
<section class="admin-section narrow-section"> <section class="admin-section narrow-section">
<?php
$priceRates = [];
foreach ($deliveryZoneOptions as $zoneKey => $zone) {
$priceRates[$zoneKey] = (int) $zone['price_cents'];
}
?>
<div class="section-header"> <div class="section-header">
<div> <div>
<p class="eyebrow">Manuelle Buchung</p> <p class="eyebrow">Manuelle Buchung</p>
@@ -14,8 +20,9 @@
<div class="flash flash-error"><?= h((string) $flashError) ?></div> <div class="flash flash-error"><?= h((string) $flashError) ?></div>
<?php endif; ?> <?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() ?> <?= 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">
<div class="form-section-header"> <div class="form-section-header">
<span class="form-step">Verwaltung</span> <span class="form-step">Verwaltung</span>
@@ -67,10 +74,6 @@
<span>Rückgabedatum</span> <span>Rückgabedatum</span>
<input type="date" name="end_date" data-booking-end value="<?= h((string) ($old['end_date'] ?? '')) ?>" required> <input type="date" name="end_date" data-booking-end value="<?= h((string) ($old['end_date'] ?? '')) ?>" required>
</label> </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> <label>
<span>Status</span> <span>Status</span>
<select name="status"> <select name="status">
@@ -90,10 +93,20 @@
</label> </label>
<label> <label>
<span>Lieferart</span> <span>Lieferart</span>
<select name="delivery_mode"> <select name="delivery_mode" data-delivery-mode>
<option value="self_pickup" <?= selected((string) ($old['delivery_mode'] ?? $defaults['delivery_mode']), 'self_pickup') ?>>Selbstabholung</option> <?php foreach ($deliveryModeOptions as $value => $label): ?>
<option value="delivery_setup" <?= selected((string) ($old['delivery_mode'] ?? ''), 'delivery_setup') ?>>Lieferung und Aufbau</option> <option value="<?= h($value) ?>" <?= selected((string) ($old['delivery_mode'] ?? $defaults['delivery_mode']), $value) ?>><?= h($label) ?></option>
<option value="on_site_support" <?= selected((string) ($old['delivery_mode'] ?? ''), 'on_site_support') ?>>Lieferung, Aufbau und Vor-Ort-Betreuung</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> </select>
</label> </label>
<label> <label>
@@ -112,6 +125,7 @@
<textarea name="internal_notes" rows="4"><?= h((string) ($old['internal_notes'] ?? '')) ?></textarea> <textarea name="internal_notes" rows="4"><?= h((string) ($old['internal_notes'] ?? '')) ?></textarea>
</label> </label>
</div> </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>
<div class="booking-summary-card"> <div class="booking-summary-card">
@@ -119,6 +133,10 @@
<span>Mietdauer</span> <span>Mietdauer</span>
<strong data-summary-days>Noch nicht gewählt</strong> <strong data-summary-days>Noch nicht gewählt</strong>
</div> </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"> <div class="summary-line summary-line-total">
<span>Gesamtpreis</span> <span>Gesamtpreis</span>
<strong data-summary-total><?= h(formatCurrency((int) $defaults['price_per_day_cents'])) ?></strong> <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> <button type="submit" class="button-primary">Buchung speichern</button>
</form> </form>
</section> </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>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>Miettage</dt><dd><?= h((string) $booking['total_days']) ?></dd></div>
<div><dt>Leistung</dt><dd><?= h($booking['delivery_mode_label']) ?></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>Zahlungsart</dt><dd><?= h($booking['payment_method_label']) ?></dd></div>
<div><dt>Gesamt</dt><dd><?= h(formatCurrency((int) $booking['subtotal_cents'])) ?></dd></div> <div><dt>Gesamt</dt><dd><?= h(formatCurrency((int) $booking['subtotal_cents'])) ?></dd></div>
</dl> </dl>
@@ -106,4 +107,3 @@
</article> </article>
</div> </div>
</section> </section>
+28 -28
View File
@@ -1,15 +1,14 @@
<section class="hero"> <section class="hero">
<div class="hero-copy"> <div class="hero-copy">
<p class="eyebrow">Professionelle Fotobox-Vermietung</p> <p class="eyebrow">Fotobox-Verleih für <?= h($company['service_area']) ?></p>
<h1>Fotobox mieten für Hochzeiten, Geburtstage und Firmenfeiern.</h1> <h1>Fotobox mieten für Hochzeit, Geburtstag und Firmenfeier.</h1>
<p class="hero-text"> <p class="hero-text">
Hochwertige Technik, klare Preislogik pro Miettag und ein Buchungsablauf, Professionelle Fotobox mit Spiegelreflexkamera, Studioblitz und digitaler Bildübergabe.
der auch kaufmännisch sauber funktioniert. Anfrage senden, Bestätigung erhalten, Lieferung oder Selbstabholung möglich. Schon ab <?= h(formatCurrency((int) $dayRate)) ?> pro Miettag bei Selbstabholung.
Bilder digital bekommen.
</p> </p>
<div class="hero-actions"> <div class="hero-actions">
<a class="button-primary" href="<?= h(url('buchen')) ?>">Verfügbarkeit prüfen</a> <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>
<div class="trust-grid"> <div class="trust-grid">
<?php foreach ($trustFacts as $fact): ?> <?php foreach ($trustFacts as $fact): ?>
@@ -23,8 +22,8 @@
<aside class="hero-panel"> <aside class="hero-panel">
<div class="hero-panel-top"> <div class="hero-panel-top">
<span>Service mit Struktur</span> <span>Beliebt für Hochzeiten, Geburtstage und Firmenfeiern</span>
<strong>Vom ersten Termin bis zur Rechnung</strong> <strong>Professionelle Fotos mit wenig Aufwand</strong>
</div> </div>
<div class="device-stage"> <div class="device-stage">
<div class="device-glow"></div> <div class="device-glow"></div>
@@ -48,11 +47,11 @@
</div> </div>
<div class="hero-panel-bottom"> <div class="hero-panel-bottom">
<div> <div>
<span>Abholung</span> <span>Mietbeginn</span>
<strong><?= h($company['pickup_window']) ?></strong> <strong><?= h($company['pickup_window']) ?></strong>
</div> </div>
<div> <div>
<span>Rückgabe</span> <span>Mietende</span>
<strong><?= h($company['return_window']) ?></strong> <strong><?= h($company['return_window']) ?></strong>
</div> </div>
</div> </div>
@@ -61,11 +60,11 @@
<section class="section section-tight"> <section class="section section-tight">
<div class="section-heading"> <div class="section-heading">
<p class="eyebrow">Warum diese Seite anders aufgebaut ist</p> <p class="eyebrow">Warum unsere Fotobox</p>
<h2>Kein Party-Prospekt, sondern eine ruhige Buchungsseite für einen echten Mietservice.</h2> <h2>Klare Leistungen. Klare Preise. Klare Abläufe.</h2>
<p> <p>
Die Agenten-Recherche hat klar gezeigt: Kundenfreundlich ist eine verständliche Service-Seite Sie sehen sofort, was enthalten ist, wie ein Miettag berechnet wird
mit Preis, Ablauf, Verfügbarkeit und einem Verwaltungsprozess im Hintergrund. und wie Ihre Anfrage abläuft. So planen Sie Ihr Event ohne unnötige Rückfragen und ohne Technikstress.
</p> </p>
</div> </div>
<div class="feature-card-grid"> <div class="feature-card-grid">
@@ -81,7 +80,7 @@
<section class="section split-section"> <section class="section split-section">
<div class="content-card"> <div class="content-card">
<p class="eyebrow">Ablauf</p> <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"> <ol class="step-list">
<?php foreach ($processSteps as $index => $step): ?> <?php foreach ($processSteps as $index => $step): ?>
<li> <li>
@@ -95,8 +94,8 @@
</ol> </ol>
</div> </div>
<div class="content-card editorial-card"> <div class="content-card editorial-card">
<p class="eyebrow">Standards</p> <p class="eyebrow">Auf einen Blick</p>
<h2>Kommerziell gedacht, nicht nur hübsch.</h2> <h2>Alles, was für eine entspannte Buchung wichtig ist.</h2>
<ul class="check-list"> <ul class="check-list">
<?php foreach ($serviceStandards as $standard): ?> <?php foreach ($serviceStandards as $standard): ?>
<li><?= h($standard) ?></li> <li><?= h($standard) ?></li>
@@ -107,8 +106,8 @@
<section class="section"> <section class="section">
<div class="section-heading"> <div class="section-heading">
<p class="eyebrow">Leistungsmodule</p> <p class="eyebrow">Leistungen</p>
<h2>Technik, Eventbetrieb und Verwaltung greifen ineinander.</h2> <h2>Alles drin für eine Fotobox, die sofort einsatzbereit ist.</h2>
</div> </div>
<div class="module-grid"> <div class="module-grid">
<?php foreach ($serviceModules as $module): ?> <?php foreach ($serviceModules as $module): ?>
@@ -127,7 +126,7 @@
<section class="section"> <section class="section">
<div class="section-heading"> <div class="section-heading">
<p class="eyebrow">Anlässe</p> <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>
<div class="occasion-grid"> <div class="occasion-grid">
<?php foreach ($occasionCards as $occasion): ?> <?php foreach ($occasionCards as $occasion): ?>
@@ -142,8 +141,8 @@
<section class="section split-section"> <section class="section split-section">
<div class="content-card"> <div class="content-card">
<p class="eyebrow">Verfügbarkeit</p> <p class="eyebrow">Verfügbarkeit</p>
<h2>Aktuell geblockte oder bestätigte Zeiträume</h2> <h2>Bereits reservierte Termine</h2>
<p>Die Übersicht stammt direkt aus dem Verwaltungssystem und zeigt belegte Termine.</p> <p>Hier sehen Sie, welche Zeiträume aktuell angefragt, reserviert oder bereits bestätigt sind.</p>
<div class="availability-list"> <div class="availability-list">
<?php if ($bookings === []): ?> <?php if ($bookings === []): ?>
<article class="availability-card"> <article class="availability-card">
@@ -154,24 +153,25 @@
<?php foreach ($bookings as $booking): ?> <?php foreach ($bookings as $booking): ?>
<article class="availability-card"> <article class="availability-card">
<div> <div>
<strong><?= h($booking['reference']) ?></strong> <strong><?= h(formatDate($booking['start_date'])) ?> bis <?= h(formatDate($booking['end_date'])) ?></strong>
<span><?= h(formatDate($booking['start_date'])) ?> bis <?= h(formatDate($booking['end_date'])) ?></span> <span><?= h((string) ($booking['delivery_zone_label'] ?: $booking['delivery_mode_label'])) ?></span>
</div> </div>
<span class="<?= h(statusPillClass((string) $booking['status'])) ?>"><?= h($booking['status_label']) ?></span> <span class="<?= h(statusPillClass((string) $booking['status'])) ?>"><?= h($booking['status_label']) ?></span>
</article> </article>
<?php endforeach; ?> <?php endforeach; ?>
</div> </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>
<div class="content-card emphasis-card"> <div class="content-card emphasis-card">
<p class="eyebrow">Nächster Schritt</p> <p class="eyebrow">Jetzt anfragen</p>
<h2>In wenigen Minuten zur Anfrage</h2> <h2>Unverbindlich Verfügbarkeit anfragen</h2>
<ul class="check-list"> <ul class="check-list">
<?php foreach ($bookingChecklist as $item): ?> <?php foreach ($bookingChecklist as $item): ?>
<li><?= h($item) ?></li> <li><?= h($item) ?></li>
<?php endforeach; ?> <?php endforeach; ?>
</ul> </ul>
<p>Lieferpreise richten sich nach dem Zielort: Hannover, Region Hannover oder Hameln, Braunschweig, Hildesheim und Celle.</p>
<div class="pricing-example-list"> <div class="pricing-example-list">
<?php foreach ($pricingExamples as $example): ?> <?php foreach ($pricingExamples as $example): ?>
<article> <article>
@@ -180,6 +180,6 @@
</article> </article>
<?php endforeach; ?> <?php endforeach; ?>
</div> </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> </div>
</section> </section>
+33 -8
View File
@@ -2,16 +2,15 @@
$app = appConfig(); $app = appConfig();
$company = $app['company']; $company = $app['company'];
$metaTitle = isset($pageTitle) ? $pageTitle . ' | ' . $company['name'] : $company['name']; $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/'); $isAdminArea = str_contains($viewPath, '/admin/');
$styleVersion = is_file(dirname(__DIR__) . '/assets/styles.css') ? (string) filemtime(dirname(__DIR__) . '/assets/styles.css') : '1'; $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'; $scriptVersion = is_file(dirname(__DIR__) . '/assets/app.js') ? (string) filemtime(dirname(__DIR__) . '/assets/app.js') : '1';
$currentPath = currentPath(); $currentPath = currentPath();
$publicNav = [ $publicNav = [
['label' => 'Leistungen', 'path' => '/leistungen'], ['label' => 'Fotobox', 'path' => '/fotobox', 'activePaths' => ['/fotobox', '/leistungen']],
['label' => 'Preise', 'path' => '/preise'], ['label' => 'Preise', 'path' => '/preise'],
['label' => 'Verfügbarkeit', 'path' => '/verfuegbarkeit'],
['label' => 'Ablauf', 'path' => '/ablauf'], ['label' => 'Ablauf', 'path' => '/ablauf'],
['label' => 'FAQ', 'path' => '/faq'], ['label' => 'FAQ', 'path' => '/faq'],
['label' => 'Kontakt', 'path' => '/kontakt'], ['label' => 'Kontakt', 'path' => '/kontakt'],
@@ -78,14 +77,40 @@ $adminNav = [
</form> </form>
</div> </div>
<?php else: ?> <?php else: ?>
<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"> <nav class="site-nav site-nav-public" aria-label="Hauptnavigation">
<?php foreach ($publicNav as $item): ?> <?php foreach ($publicNav as $item): ?>
<a class="<?= $currentPath === $item['path'] ? 'is-active' : '' ?>" href="<?= h(url($item['path'])) ?>"><?= h($item['label']) ?></a> <?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; ?> <?php endforeach; ?>
</nav> </nav>
<div class="header-actions"> <div class="menu-meta">
<a class="button-secondary" href="<?= h(url('kontakt')) ?>">Kontakt</a> <span><?= h($company['service_area']) ?></span>
<a class="button-primary" href="<?= h(url('buchen')) ?>">Buchungsanfrage</a> <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> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
@@ -112,7 +137,7 @@ $adminNav = [
</div> </div>
<div> <div>
<h3>Seiten</h3> <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('preise')) ?>">Preise</a>
<a href="<?= h(url('buchen')) ?>">Buchen</a> <a href="<?= h(url('buchen')) ?>">Buchen</a>
<a href="<?= h(url('faq')) ?>">FAQ</a> <a href="<?= h(url('faq')) ?>">FAQ</a>
+8 -8
View File
@@ -1,7 +1,7 @@
<section class="page-hero"> <section class="page-hero">
<p class="eyebrow">Ablauf</p> <p class="eyebrow">Ablauf</p>
<h1>Von der Anfrage bis zur Rückgabe klar geführt.</h1> <h1>So einfach mieten Sie Ihre Fotobox</h1>
<p>Die Seite ist so gebaut, dass Privatkunden und Firmenkunden denselben klaren Ablauf erleben.</p> <p>Von der Anfrage bis zur Rückgabe: In wenigen Schritten zu Ihrer Fotobox.</p>
</section> </section>
<section class="section"> <section class="section">
@@ -22,21 +22,21 @@
<section class="section split-section"> <section class="section split-section">
<article class="content-card"> <article class="content-card">
<h2>Abholung und Rückgabe</h2> <h2>Abholung oder Lieferung</h2>
<ul class="check-list"> <ul class="check-list">
<li><?= h($company['pickup_window']) ?></li> <li><?= h($company['pickup_window']) ?></li>
<li><?= h($company['return_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>Lieferung und Aufbau können im Anfrageprozess gewählt werden.</li>
<li>Der Mietzeitraum wird immer über Übernachtungen berechnet.</li> <li>Der Mietzeitraum wird immer über Übernachtungen berechnet.</li>
</ul> </ul>
</article> </article>
<article class="content-card"> <article class="content-card">
<h2>Verwaltung im Hintergrund</h2> <h2>Was nach Ihrer Anfrage passiert</h2>
<ul class="check-list"> <ul class="check-list">
<li>Anfragen werden im Backend geprüft und bestätigt.</li> <li>Wir prüfen die Verfügbarkeit Ihres Wunschtermins.</li>
<li>Für bestätigte Aufträge können Rechnungen mit Kundendaten erstellt werden.</li> <li>Sie erhalten eine Rückmeldung zum Termin und zum weiteren Ablauf.</li>
<li>Zahlungsstatus und interne Notizen bleiben jederzeit nachvollziehbar.</li> <li>Auf Wunsch erstellen wir eine Rechnung mit Ihren Kundendaten.</li>
</ul> </ul>
</article> </article>
</section> </section>
+7 -5
View File
@@ -1,15 +1,18 @@
<section class="page-hero"> <section class="page-hero">
<p class="eyebrow">Buchungsanfrage</p> <p class="eyebrow">Buchungsanfrage</p>
<h1>Fotobox jetzt anfragen.</h1> <h1>Verfügbarkeit Ihrer Fotobox prüfen</h1>
<p> <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> </p>
</section> </section>
<section class="section split-section"> <section class="section split-section">
<article class="content-card emphasis-card"> <article class="content-card emphasis-card">
<p class="eyebrow">Vor dem Absenden</p> <p class="eyebrow">In 2 Minuten zur Anfrage</p>
<h2>Was wir für eine saubere Bearbeitung brauchen</h2> <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"> <ul class="check-list">
<?php foreach ($bookingChecklist as $item): ?> <?php foreach ($bookingChecklist as $item): ?>
<li><?= h($item) ?></li> <li><?= h($item) ?></li>
@@ -25,4 +28,3 @@
<?php require dirname(__DIR__) . '/partials/public-booking-form.php'; ?> <?php require dirname(__DIR__) . '/partials/public-booking-form.php'; ?>
</article> </article>
</section> </section>
+2 -3
View File
@@ -1,7 +1,7 @@
<section class="page-hero"> <section class="page-hero">
<p class="eyebrow">FAQ</p> <p class="eyebrow">FAQ</p>
<h1>Häufige Fragen vor der Anfrage.</h1> <h1>Häufige Fragen zur Fotobox-Miete</h1>
<p>Hier finden Sie die Punkte, die in Vermietung, Zahlung und Rückgabe am häufigsten geklärt werden müssen.</p> <p>Hier finden Sie Antworten zu Mietdauer, Zahlung, Lieferung, Rückgabe und digitaler Bildübergabe.</p>
</section> </section>
<section class="section"> <section class="section">
@@ -14,4 +14,3 @@
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
</section> </section>
+4 -5
View File
@@ -1,7 +1,7 @@
<section class="page-hero"> <section class="page-hero">
<p class="eyebrow">Kontakt</p> <p class="eyebrow">Kontakt</p>
<h1>Direkt erreichbar für Fragen zu Termin, Lieferung und Rechnung.</h1> <h1>Wir beraten Sie gerne persönlich.</h1>
<p>Wenn Sie vor der Anfrage noch etwas abstimmen möchten, erreichen Sie uns über die folgenden Kontaktwege.</p> <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>
<section class="section split-section"> <section class="section split-section">
@@ -16,14 +16,13 @@
</div> </div>
</article> </article>
<article class="content-card emphasis-card"> <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"> <ul class="check-list">
<li>Prüfung von Wunschterminen</li> <li>Prüfung von Wunschterminen</li>
<li>Lieferung, Aufbau und regionale Einsatzorte</li> <li>Lieferung, Aufbau und regionale Einsatzorte</li>
<li>Fragen zu Rechnung, Zahlungsart und Mietdauer</li> <li>Fragen zu Rechnung, Zahlungsart und Mietdauer</li>
<li>Abstimmung von Firmenveranstaltungen und Sonderfällen</li> <li>Abstimmung von Firmenveranstaltungen und Sonderfällen</li>
</ul> </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> </article>
</section> </section>
+9 -9
View File
@@ -1,9 +1,10 @@
<section class="page-hero"> <section class="page-hero">
<p class="eyebrow">Leistungen & Ausstattung</p> <p class="eyebrow">Fotobox & Leistungen</p>
<h1>Eine Fotobox, die technisch überzeugt und organisatorisch mitdenkt.</h1> <h1>Professionelle Fotobox für Hochzeit, Geburtstag und Firmenfeier.</h1>
<p> <p>
Diese Seite zeigt nicht nur die Technik, sondern den gesamten Service: Hochwertige Bilder, einfache Bedienung und eine klare Abwicklung.
Bildqualität, Bedienbarkeit, Logistik, digitale Übergabe und die kaufmännische 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> </p>
</section> </section>
@@ -33,10 +34,9 @@
<section class="section cta-band"> <section class="section cta-band">
<div> <div>
<p class="eyebrow">Buchung</p> <p class="eyebrow">Wunschtermin</p>
<h2>Sie wissen schon, was Sie brauchen?</h2> <h2>Prüfen Sie direkt die Verfügbarkeit Ihrer Fotobox.</h2>
<p>Dann prüfen Sie direkt Ihren Zeitraum und senden Sie Ihre Anfrage digital.</p> <p>Senden Sie uns Ihre Anfrage unverbindlich online. Wir melden uns zeitnah zurück.</p>
</div> </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> </section>
+1 -2
View File
@@ -15,7 +15,7 @@
</article> </article>
<article class="legal-card"> <article class="legal-card">
<h2>3. Zahlung</h2> <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>
<article class="legal-card"> <article class="legal-card">
<h2>4. Übergabe und Rückgabe</h2> <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> <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> </article>
</section> </section>
+29 -19
View File
@@ -1,36 +1,47 @@
<section class="page-hero"> <section class="page-hero">
<p class="eyebrow">Preise & Mietlogik</p> <p class="eyebrow">Preise & Mietlogik</p>
<h1>Klare Preise ohne versteckte Logik.</h1> <h1>Fotobox-Preise auf einen Blick</h1>
<p> <p>
Der Standardpreis beträgt <strong><?= h(formatCurrency((int) $dayRate)) ?></strong> pro Miettag. <strong>Abholung ab <?= h(formatCurrency((int) $dayRate)) ?> pro Miettag.</strong>
Ein Miettag entspricht immer einer Übernachtung. Lieferpreise richten sich nach dem Zielort. Ein Miettag entspricht immer einer Übernachtung.
</p> </p>
</section> </section>
<section class="section split-section"> <section class="section split-section">
<article class="content-card emphasis-card"> <article class="content-card emphasis-card">
<p class="eyebrow">Grundpreis</p> <p class="eyebrow">Preis je Liefergebiet</p>
<h2><?= h(formatCurrency((int) $dayRate)) ?> pro Miettag</h2> <h2>Abholung oder Lieferung nach Region</h2>
<p>Montag bis Dienstag = 1 Miettag. Freitag bis Sonntag = 2 Miettage.</p> <p>Ein Miettag entspricht einer Übernachtung. Montag bis Dienstag = 1 Miettag. Freitag bis Sonntag = 2 Miettage.</p>
<ul class="check-list"> <ul class="check-list">
<li>Technikpaket mit DSLR-Kamera, Blitz und Softbox</li> <?php foreach ($pricingExamples as $example): ?>
<li>Digitale Bildübergabe inklusive</li> <li><strong><?= h($example['title']) ?>:</strong> <?= h($example['text']) ?></li>
<li>Zahlung per Rechnung, Überweisung oder PayPal</li> <?php endforeach; ?>
<li>Verbindlichkeit erst nach Bestätigung Ihrer Anfrage</li>
</ul> </ul>
</article> </article>
<article class="content-card"> <article class="content-card">
<p class="eyebrow">Preisbeispiele</p> <p class="eyebrow">Was im Preis enthalten ist</p>
<div class="pricing-example-list"> <div class="pricing-example-list">
<?php foreach ($pricingExamples as $example): ?>
<article> <article>
<strong><?= h($example['title']) ?></strong> <strong>Technik</strong>
<span><?= h($example['text']) ?></span> <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> </article>
<?php endforeach; ?>
</div> </div>
<p class="small-note"> <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> </p>
</article> </article>
</section> </section>
@@ -39,7 +50,7 @@
<div class="section-heading"> <div class="section-heading">
<p class="eyebrow">Zahlung</p> <p class="eyebrow">Zahlung</p>
<h2>Rechnung, Überweisung oder PayPal</h2> <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>
<div class="trust-grid"> <div class="trust-grid">
<article class="trust-card"> <article class="trust-card">
@@ -48,7 +59,7 @@
</article> </article>
<article class="trust-card"> <article class="trust-card">
<span>PayPal</span> <span>PayPal</span>
<strong>Als Zahlungsart auswählbar</strong> <strong>Direkt in der Anfrage auswählbar</strong>
</article> </article>
<article class="trust-card"> <article class="trust-card">
<span>Steuerhinweis</span> <span>Steuerhinweis</span>
@@ -56,4 +67,3 @@
</article> </article>
</div> </div>
</section> </section>
+82 -13
View File
@@ -1,27 +1,91 @@
<section class="page-hero"> <section class="page-hero">
<p class="eyebrow">Verfügbarkeit</p> <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> <p>
Die Übersicht zeigt aktuelle Belegungen aus dem Verwaltungssystem. Hier sehen Sie bereits belegte Termine im Monatskalender.
Für Ihren Wunschtermin senden Sie am besten direkt eine Anfrage. Ist Ihr Wunschtermin noch frei, senden Sie uns direkt Ihre unverbindliche Anfrage.
</p> </p>
</section> </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"> <section class="section split-section">
<article class="content-card"> <article class="content-card">
<h2>Aktuelle Belegung</h2> <h2>Aktuelle Belegung</h2>
<div class="availability-list"> <div class="availability-list">
<?php if ($bookings === []): ?> <?php if ($availabilityBookings === []): ?>
<article class="availability-card"> <article class="availability-card">
<strong>Momentan gibt es keine festen Einträge.</strong> <strong>Momentan gibt es keine festen Einträge.</strong>
<span>Ihre Anfrage kann direkt neu aufgenommen werden.</span> <span>Ihre Anfrage kann direkt neu aufgenommen werden.</span>
</article> </article>
<?php endif; ?> <?php endif; ?>
<?php foreach ($bookings as $booking): ?> <?php foreach ($availabilityBookings as $booking): ?>
<article class="availability-card"> <article class="availability-card">
<div> <div>
<strong><?= h($booking['reference']) ?></strong> <strong><?= h(formatDate($booking['start_date'])) ?> bis <?= h(formatDate($booking['end_date'])) ?></strong>
<span><?= h(formatDate($booking['start_date'])) ?> bis <?= h(formatDate($booking['end_date'])) ?></span> <span><?= h((string) ($booking['delivery_zone_label'] ?: $booking['delivery_mode_label'])) ?> · <?= h((string) $booking['total_days']) ?> <?= $booking['total_days'] === 1 ? 'Miettag' : 'Miettage' ?></span>
</div> </div>
<span class="<?= h(statusPillClass((string) $booking['status'])) ?>"><?= h($booking['status_label']) ?></span> <span class="<?= h(statusPillClass((string) $booking['status'])) ?>"><?= h($booking['status_label']) ?></span>
</article> </article>
@@ -29,13 +93,18 @@
</div> </div>
</article> </article>
<article class="content-card emphasis-card"> <article class="content-card emphasis-card">
<h2>Direkt zur Anfrage</h2> <h2>Jetzt unverbindlich anfragen</h2>
<ul class="check-list"> <ul class="check-list">
<li>Zeitraum nach Übernachtungen wählen</li> <li>Wunschtermin auswählen</li>
<li>Lieferart und Zahlungsart festlegen</li> <li>Leistungsart, Liefergebiet und Zahlungsart festlegen</li>
<li>Kundendaten für Rechnung und Rückfragen erfassen</li> <li>Kontaktdaten und Veranstaltungsort eintragen</li>
</ul> </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> </article>
</section> </section>
+30 -10
View File
@@ -1,5 +1,9 @@
<?php <?php
$oldData = is_array($old ?? null) ? $old : []; $oldData = is_array($old ?? null) ? $old : [];
$priceRates = [];
foreach ($deliveryZoneOptions as $zoneKey => $zone) {
$priceRates[$zoneKey] = (int) $zone['price_cents'];
}
?> ?>
<div class="booking-form-shell"> <div class="booking-form-shell">
<?php if (!empty($flashSuccess)): ?> <?php if (!empty($flashSuccess)): ?>
@@ -10,7 +14,7 @@ $oldData = is_array($old ?? null) ? $old : [];
<div class="flash flash-error"><?= h((string) $flashError) ?></div> <div class="flash flash-error"><?= h((string) $flashError) ?></div>
<?php endif; ?> <?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() ?> <?= csrfField() ?>
<input type="hidden" name="price_per_day_cents" value="<?= h((string) $dayRate) ?>"> <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">
<div class="form-section-header"> <div class="form-section-header">
<span class="form-step">Schritt 2</span> <span class="form-step">Schritt 2</span>
<h3>Leistung und Zahlung festlegen</h3> <h3>Paket und Zahlung wählen</h3>
</div> </div>
<div class="form-grid form-grid-two"> <div class="form-grid form-grid-two">
<label> <label>
<span>Leistungsart</span> <span>Leistungsart</span>
<select name="delivery_mode"> <select name="delivery_mode" data-delivery-mode>
<option value="self_pickup" <?= selected((string) ($oldData['delivery_mode'] ?? 'self_pickup'), 'self_pickup') ?>>Selbstabholung</option> <?php foreach ($deliveryModeOptions as $value => $label): ?>
<option value="delivery_setup" <?= selected((string) ($oldData['delivery_mode'] ?? ''), 'delivery_setup') ?>>Lieferung und Aufbau</option> <option value="<?= h($value) ?>" <?= selected((string) ($oldData['delivery_mode'] ?? 'self_pickup'), $value) ?>><?= h($label) ?></option>
<option value="on_site_support" <?= selected((string) ($oldData['delivery_mode'] ?? ''), 'on_site_support') ?>>Lieferung, Aufbau und Vor-Ort-Betreuung</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> </select>
</label> </label>
<label> <label>
@@ -54,12 +68,18 @@ $oldData = is_array($old ?? null) ? $old : [];
</select> </select>
</label> </label>
</div> </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>
<div class="form-section"> <div class="form-section">
<div class="form-section-header"> <div class="form-section-header">
<span class="form-step">Schritt 3</span> <span class="form-step">Schritt 3</span>
<h3>Kundendaten erfassen</h3> <h3>Kontaktdaten und Veranstaltungsort</h3>
</div> </div>
<div class="form-grid"> <div class="form-grid">
<label> <label>
@@ -112,7 +132,7 @@ $oldData = is_array($old ?? null) ? $old : [];
</div> </div>
<div class="summary-line"> <div class="summary-line">
<span>Preis pro Miettag</span> <span>Preis pro Miettag</span>
<strong><?= h(formatCurrency((int) $dayRate)) ?></strong> <strong data-summary-rate><?= h(formatCurrency((int) $dayRate)) ?></strong>
</div> </div>
<div class="summary-line summary-line-total"> <div class="summary-line summary-line-total">
<span>Voraussichtlicher Gesamtpreis</span> <span>Voraussichtlicher Gesamtpreis</span>
@@ -131,7 +151,7 @@ $oldData = is_array($old ?? null) ? $old : [];
</label> </label>
</div> </div>
<button type="submit" class="button-primary button-block">Buchungsanfrage senden</button> <button type="submit" class="button-primary button-block">Buchungsanfrage unverbindlich senden</button>
<p class="form-note">Keine Sofortabbuchung. Ihr Auftrag wird erst nach persönlicher Bestätigung verbindlich.</p> <p class="form-note">Sie erhalten schnell eine Rückmeldung zur Verfügbarkeit und zum weiteren Ablauf.</p>
</form> </form>
</div> </div>