Anpassung Design
This commit is contained in:
@@ -1,75 +1,80 @@
|
||||
# Fotobox-Webseite
|
||||
|
||||
Diese Anwendung stellt eine komplette Vermietungsseite fuer eine Fotobox bereit. Sie enthaelt:
|
||||
Diese Anwendung stellt eine mehrseitige deutsche Vermietungsseite für eine Fotobox bereit. Sie enthält:
|
||||
|
||||
- eine neu aufgebaute Landingpage auf Basis der Inhalte von `https://ctb-it.de/fotobox/`
|
||||
- eine oeffentliche Buchungsanfrage mit zwei Terminen, Live-Preisberechnung und Zahlungsart-Auswahl
|
||||
- einen Admin-Bereich fuer Anfragen, manuelle Kundenbestellungen, Statuspflege und Rechnungs-PDFs
|
||||
- eine MySQL-faehige Datenhaltung mit JSON-Fallback
|
||||
- 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
|
||||
- einen Verwaltungsbereich für Anfragen, Buchungen, Kunden, Kalender, Rechnungen und Einstellungen
|
||||
- MySQL-Unterstützung mit Tabellenpräfix `fb_` sowie JSON-Fallback
|
||||
|
||||
## Starten
|
||||
|
||||
Die Anwendung benoetigt nur PHP 8.3 oder neuer.
|
||||
Die Anwendung benötigt PHP `8.3` oder neuer.
|
||||
|
||||
Für den lokalen Start sollte die Website mit dem Router-Skript gestartet werden:
|
||||
|
||||
```bash
|
||||
php -S 127.0.0.1:8000
|
||||
php -S 127.0.0.1:8000 router.php
|
||||
```
|
||||
|
||||
Danach ist die Seite unter `http://127.0.0.1:8000` erreichbar.
|
||||
|
||||
Wenn die Anwendung unter einem Unterordner laeuft, kannst du zusaetzlich `FOTOBOX_BASE_PATH` setzen, z. B.:
|
||||
Wenn die Anwendung hinter einem Proxy oder in einem Unterordner läuft, kann zusätzlich `FOTOBOX_BASE_PATH` gesetzt werden:
|
||||
|
||||
```bash
|
||||
FOTOBOX_BASE_PATH=/fotobox php -S 127.0.0.1:8000
|
||||
FOTOBOX_BASE_PATH=/proxy/8000 php -S 127.0.0.1:8000 router.php
|
||||
```
|
||||
|
||||
## Admin-Zugang
|
||||
|
||||
- Benutzername: `admin`
|
||||
- Passwort: Standardmaessig `fotobox-admin`
|
||||
- Passwort: standardmäßig `fotobox-admin`
|
||||
|
||||
Falls du das Passwort aendern willst, setze die Umgebungsvariable `FOTOBOX_ADMIN_PASSWORD`.
|
||||
Falls du das Passwort ändern willst, setze die Umgebungsvariable `FOTOBOX_ADMIN_PASSWORD`.
|
||||
|
||||
## Datenhaltung
|
||||
|
||||
Standardmaessig nutzt die App JSON-Dateien:
|
||||
Standardmäßig nutzt die App die Dateien:
|
||||
|
||||
- `storage/bookings.json`
|
||||
- `storage/invoices.json`
|
||||
|
||||
Sobald du `mysql.local.php` mit echten Zugangsdaten befuellst, `enabled => true` setzt und optional ein `table_prefix` definierst, schaltet die App automatisch auf MySQL um.
|
||||
Sobald `mysql.local.php` mit echten Zugangsdaten befüllt ist und `enabled => true` gesetzt wurde, schaltet die App automatisch auf MySQL um.
|
||||
|
||||
## MySQL vorbereiten
|
||||
|
||||
Im Repository liegt als Vorlage:
|
||||
Im Repository liegen als Vorlage:
|
||||
|
||||
- `mysql.local.php.example`
|
||||
- `docs/mysql-schema.sql`
|
||||
|
||||
Lokal liegt ausserhalb des Git-Trackings:
|
||||
Lokal außerhalb des Git-Trackings liegt:
|
||||
|
||||
- `mysql.local.php`
|
||||
|
||||
Die Datei `mysql.local.php` ist bereits in `.gitignore` ausgeschlossen und kann von dir mit echten Zugangsdaten befuellt werden.
|
||||
Standardmaessig verwendet die App das Prefix `fb_`, also z. B. `fb_bookings` und `fb_invoices`.
|
||||
Die Datei `mysql.local.php` ist bereits in `.gitignore` ausgeschlossen. Standardmäßig verwendet die App das Präfix `fb_`, also zum Beispiel `fb_bookings` und `fb_invoices`.
|
||||
|
||||
## Wichtige Annahmen
|
||||
## Wichtige Regeln im System
|
||||
|
||||
- Mietbeginn und Mietende werden inklusiv berechnet.
|
||||
- Standardpreis: `99,99 EUR` pro Kalendertag.
|
||||
- Zahlungsarten: `Rechnung / Ueberweisung` und `PayPal`.
|
||||
- Die PayPal-Auswahl ist im Prozess und in der Verwaltung abgebildet; fuer einen echten Live-Payment-Flow brauchst du spaeter zusaetzliche API-Zugangsdaten.
|
||||
- Ein Miettag entspricht immer einer Übernachtung.
|
||||
- Beispiel: `Montag bis Dienstag = 1 Miettag`
|
||||
- Der Standardpreis beträgt `99,99 €` pro Miettag.
|
||||
- Zahlungsarten: `Rechnung / Überweisung` und `PayPal`
|
||||
- Öffentliche Eingaben sind zunächst Buchungsanfragen und werden erst nach Bestätigung verbindlich.
|
||||
|
||||
## Verwaltung
|
||||
|
||||
Im Admin-Bereich kannst du:
|
||||
Im Verwaltungsbereich können aktuell folgende Aufgaben erledigt werden:
|
||||
|
||||
- neue Kundenbestellungen manuell anlegen
|
||||
- Anfragen bestaetigen oder stornieren
|
||||
- Zahlungsstatus pflegen
|
||||
- Rechnungen mit Kundendaten erzeugen
|
||||
- Rechnungen als PDF oeffnen
|
||||
- offene Anfragen prüfen
|
||||
- Buchungen manuell für Kunden anlegen
|
||||
- Kalender und belegte Zeiträume einsehen
|
||||
- Kundenhistorien aus Aufträgen ableiten
|
||||
- Rechnungen mit Kundendaten erzeugen und als PDF öffnen
|
||||
- Zahlungs- und Buchungsstatus pflegen
|
||||
- aktiven Speicher-Treiber und das Tabellenpräfix prüfen
|
||||
|
||||
## Tests
|
||||
|
||||
Eine kurze Checkliste liegt in [docs/manual-test.md](docs/manual-test.md).
|
||||
Eine kurze Checkliste liegt in [docs/manual-test.md](/config/workspace/fotobox-webspite/docs/manual-test.md:1).
|
||||
|
||||
+16
-10
@@ -6,14 +6,20 @@ const formatCurrency = (cents) =>
|
||||
currency: 'EUR',
|
||||
}).format(cents / 100);
|
||||
|
||||
const calculateDays = (start, end) => {
|
||||
const calculateRentalDays = (start, end) => {
|
||||
if (!start || !end) return null;
|
||||
|
||||
const startDate = new Date(start);
|
||||
const endDate = new Date(end);
|
||||
if (Number.isNaN(startDate.valueOf()) || Number.isNaN(endDate.valueOf())) return null;
|
||||
|
||||
if (Number.isNaN(startDate.valueOf()) || Number.isNaN(endDate.valueOf())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const milliseconds = endDate.getTime() - startDate.getTime();
|
||||
const days = Math.floor(milliseconds / 86400000) + 1;
|
||||
return days > 0 ? days : null;
|
||||
const rentalDays = Math.floor(milliseconds / 86400000);
|
||||
|
||||
return rentalDays > 0 ? rentalDays : null;
|
||||
};
|
||||
|
||||
forms.forEach((form) => {
|
||||
@@ -22,24 +28,24 @@ forms.forEach((form) => {
|
||||
const daysOutput = form.querySelector('[data-summary-days]');
|
||||
const totalOutput = form.querySelector('[data-summary-total]');
|
||||
const rateInput = form.querySelector('input[name="price_per_day_cents"]');
|
||||
const defaultRate = Number(form.dataset.dayRate || 9999);
|
||||
const defaultRate = Number(form.dataset.dayRate || rateInput?.value || 9999);
|
||||
|
||||
const render = () => {
|
||||
const days = calculateDays(startInput?.value, endInput?.value);
|
||||
const rentalDays = calculateRentalDays(startInput?.value, endInput?.value);
|
||||
const rate = Number(rateInput?.value || defaultRate);
|
||||
|
||||
if (!days || rate < 0) {
|
||||
if (daysOutput) daysOutput.textContent = 'Noch nicht gewaehlt';
|
||||
if (!rentalDays || rate < 0) {
|
||||
if (daysOutput) daysOutput.textContent = 'Noch nicht gewählt';
|
||||
if (totalOutput) totalOutput.textContent = formatCurrency(defaultRate);
|
||||
return;
|
||||
}
|
||||
|
||||
if (daysOutput) {
|
||||
daysOutput.textContent = `${days} ${days === 1 ? 'Tag' : 'Tage'}`;
|
||||
daysOutput.textContent = `${rentalDays} ${rentalDays === 1 ? 'Miettag' : 'Miettage'}`;
|
||||
}
|
||||
|
||||
if (totalOutput) {
|
||||
totalOutput.textContent = formatCurrency(days * rate);
|
||||
totalOutput.textContent = formatCurrency(rentalDays * rate);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+933
-687
File diff suppressed because it is too large
Load Diff
+18
-3
@@ -5,15 +5,20 @@ declare(strict_types=1);
|
||||
return [
|
||||
'app' => [
|
||||
'base_path' => getenv('FOTOBOX_BASE_PATH') ?: '',
|
||||
'locale' => 'de_DE',
|
||||
],
|
||||
'company' => [
|
||||
'name' => 'Fotobox Moments',
|
||||
'tagline' => 'Fotobox-Vermietung fuer Hochzeiten, Geburtstage und Firmenevents',
|
||||
'tagline' => 'Fotobox-Vermietung für Hochzeiten, Geburtstage und Firmenveranstaltungen',
|
||||
'email' => 'hallo@fotobox-moments.local',
|
||||
'phone' => '+49 170 1234567',
|
||||
'website' => 'https://fotobox-moments.local',
|
||||
'service_area' => 'Musterstadt und Umgebung',
|
||||
'response_time' => 'Antwort meist innerhalb von 24 Stunden',
|
||||
'pickup_window' => 'Abholung ab 17:00 Uhr',
|
||||
'return_window' => 'Rückgabe bis 13:00 Uhr',
|
||||
'address' => [
|
||||
'street' => 'Musterstrasse 12',
|
||||
'street' => 'Musterstraße 12',
|
||||
'postal_code' => '12345',
|
||||
'city' => 'Musterstadt',
|
||||
],
|
||||
@@ -23,11 +28,21 @@ return [
|
||||
'bic' => 'DEMOXXX',
|
||||
'bank_name' => 'Musterbank',
|
||||
],
|
||||
'tax_notice' => 'Gemaess Paragraph 19 UStG wird keine Umsatzsteuer berechnet.',
|
||||
'legal' => [
|
||||
'owner' => 'Fotobox Moments',
|
||||
'representative' => 'Fotobox Moments',
|
||||
'vat_id' => '',
|
||||
'register_court' => '',
|
||||
'register_number' => '',
|
||||
'privacy_contact' => 'Datenschutzanfragen bitte an hallo@fotobox-moments.local richten.',
|
||||
'dispute_notice' => 'Wir sind nicht bereit und nicht verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.',
|
||||
],
|
||||
'tax_notice' => 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.',
|
||||
],
|
||||
'pricing' => [
|
||||
'default_day_rate_cents' => 9999,
|
||||
'currency' => 'EUR',
|
||||
'label' => '99,99 € pro Miettag',
|
||||
],
|
||||
'admin' => [
|
||||
'username' => 'admin',
|
||||
|
||||
+9
-8
@@ -1,10 +1,11 @@
|
||||
# Manuelle Tests
|
||||
|
||||
1. Startseite unter `/` oeffnen und pruefen, ob Hero, Buchungsformular, FAQ und Verfuegbarkeitsliste sichtbar sind.
|
||||
2. Im Buchungsformular Start- und Enddatum setzen und kontrollieren, ob Mietdauer und Gesamtpreis automatisch mit `99,99 EUR` pro Kalendertag berechnet werden.
|
||||
3. Eine Anfrage absenden und sicherstellen, dass sie in `storage/bookings.json` erscheint oder bei aktivierter MySQL-Verbindung in `fotobox_bookings`.
|
||||
4. `/admin` oeffnen, mit `admin` und dem konfigurierten Passwort anmelden und die neue Anfrage im Dashboard sehen.
|
||||
5. Im Admin-Bereich eine manuelle Bestellung fuer einen Kunden anlegen und pruefen, ob Konflikte bei bereits geblockten Zeitraeumen erkannt werden.
|
||||
6. Eine bestehende Buchung oeffnen, Status und Zahlungsstatus anpassen und speichern.
|
||||
7. Fuer eine Buchung eine Rechnung erzeugen und kontrollieren, ob eine Rechnungsnummer sowie ein PDF unter `/admin/invoice/pdf?id=...` abrufbar sind.
|
||||
8. Optional `mysql.local.php` aktivieren und nach erneutem Start pruefen, ob die Datenbanktabellen automatisch angelegt werden.
|
||||
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.
|
||||
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.
|
||||
7. Einen Auftrag öffnen, Status und Zahlungsstatus ändern und speichern.
|
||||
8. Für einen bestätigten Auftrag eine Rechnung erzeugen und prüfen, ob das PDF unter `/admin/invoice/pdf?id=...` geöffnet werden kann.
|
||||
9. Optional `mysql.local.php` aktivieren und nach erneutem Start prüfen, ob die Tabellen automatisch angelegt werden.
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$requestUri = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
|
||||
$uri = $requestUri;
|
||||
$basePath = trim((string) getenv('FOTOBOX_BASE_PATH'));
|
||||
|
||||
if ($basePath !== '' && $basePath !== '/') {
|
||||
$basePath = '/' . trim($basePath, '/');
|
||||
|
||||
if (str_starts_with($uri, $basePath)) {
|
||||
$uri = substr($uri, strlen($basePath)) ?: '/';
|
||||
}
|
||||
}
|
||||
|
||||
$resolvedFile = __DIR__ . '/' . ltrim($uri, '/');
|
||||
if ($uri !== '/' && is_file($resolvedFile)) {
|
||||
if ($uri === $requestUri) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$extension = strtolower(pathinfo($resolvedFile, PATHINFO_EXTENSION));
|
||||
$mimeTypes = [
|
||||
'css' => 'text/css; charset=UTF-8',
|
||||
'js' => 'application/javascript; charset=UTF-8',
|
||||
'svg' => 'image/svg+xml',
|
||||
'png' => 'image/png',
|
||||
'jpg' => 'image/jpeg',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'webp' => 'image/webp',
|
||||
];
|
||||
|
||||
header('Content-Type: ' . ($mimeTypes[$extension] ?? 'application/octet-stream'));
|
||||
header('Content-Length: ' . (string) filesize($resolvedFile));
|
||||
readfile($resolvedFile);
|
||||
return true;
|
||||
}
|
||||
|
||||
require __DIR__ . '/index.php';
|
||||
@@ -36,7 +36,7 @@ final class JsonRepository implements RecordRepositoryInterface
|
||||
{
|
||||
$handle = fopen($this->filePath, 'c+');
|
||||
if ($handle === false) {
|
||||
throw new RuntimeException('Datenspeicher kann nicht geoeffnet werden.');
|
||||
throw new RuntimeException('Datenspeicher kann nicht geöffnet werden.');
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -90,7 +90,7 @@ final class JsonRepository implements RecordRepositoryInterface
|
||||
|
||||
$decoded = json_decode($content, true);
|
||||
if (!is_array($decoded)) {
|
||||
throw new RuntimeException('Datenspeicher ist ungueltig.');
|
||||
throw new RuntimeException('Datenspeicher ist ungültig.');
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
|
||||
+143
-22
@@ -55,11 +55,11 @@ final class BookingService
|
||||
$paymentStatus = (string) ($input['payment_status'] ?? $booking['payment_status']);
|
||||
|
||||
if (!array_key_exists($status, $this->getStatusOptions())) {
|
||||
throw new RuntimeException('Der gewaehlte Status ist ungueltig.');
|
||||
throw new RuntimeException('Der gewählte Status ist ungültig.');
|
||||
}
|
||||
|
||||
if (!array_key_exists($paymentStatus, $this->getPaymentStatusOptions())) {
|
||||
throw new RuntimeException('Der gewaehlte Zahlungsstatus ist ungueltig.');
|
||||
throw new RuntimeException('Der gewählte Zahlungsstatus ist ungültig.');
|
||||
}
|
||||
|
||||
if ($this->isBlockingStatus($status)) {
|
||||
@@ -88,7 +88,7 @@ final class BookingService
|
||||
}
|
||||
|
||||
if (!in_array($booking['status'], ['reserved', 'confirmed', 'completed'], true)) {
|
||||
throw new RuntimeException('Fuer diesen Auftrag kann noch keine Rechnung erstellt werden.');
|
||||
throw new RuntimeException('Für diesen Auftrag kann noch keine Rechnung erstellt werden.');
|
||||
}
|
||||
|
||||
if (!empty($booking['invoice_id'])) {
|
||||
@@ -104,7 +104,7 @@ final class BookingService
|
||||
|
||||
$lineItems = [
|
||||
[
|
||||
'label' => 'Fotobox-Miete ' . formatDate($booking['start_date']) . ' bis ' . formatDate($booking['end_date']),
|
||||
'label' => 'Fotobox-Miete ' . formatDate($booking['start_date']) . ' bis ' . formatDate($booking['end_date']) . ' (' . $booking['total_days'] . ' Miettag' . ($booking['total_days'] === 1 ? '' : 'e') . ')',
|
||||
'quantity' => $booking['total_days'],
|
||||
'unit_price_cents' => $booking['price_per_day_cents'],
|
||||
'total_cents' => $booking['subtotal_cents'],
|
||||
@@ -128,7 +128,7 @@ final class BookingService
|
||||
'line_items' => $lineItems,
|
||||
'subtotal_cents' => $booking['subtotal_cents'],
|
||||
'total_cents' => $booking['subtotal_cents'],
|
||||
'notes' => trim((string) ($input['invoice_notes'] ?? 'Vielen Dank fuer deinen Auftrag.')),
|
||||
'notes' => trim((string) ($input['invoice_notes'] ?? 'Vielen Dank für deinen Auftrag.')),
|
||||
];
|
||||
|
||||
array_unshift($records, $invoice);
|
||||
@@ -139,7 +139,7 @@ final class BookingService
|
||||
$this->bookingRepository->transaction(function (array &$records) use ($bookingId, $invoice): void {
|
||||
$index = $this->findBookingIndex($records, $bookingId);
|
||||
if ($index === null) {
|
||||
throw new RuntimeException('Der Auftrag wurde beim Verknuepfen der Rechnung nicht gefunden.');
|
||||
throw new RuntimeException('Der Auftrag wurde beim Verknüpfen der Rechnung nicht gefunden.');
|
||||
}
|
||||
|
||||
$records[$index]['invoice_id'] = $invoice['id'];
|
||||
@@ -169,6 +169,25 @@ final class BookingService
|
||||
return $invoices;
|
||||
}
|
||||
|
||||
public function getBookingsByStatuses(array $statuses): array
|
||||
{
|
||||
$bookings = array_values(array_filter(
|
||||
$this->getBookings(),
|
||||
static fn(array $booking): bool => in_array((string) $booking['status'], $statuses, true)
|
||||
));
|
||||
|
||||
usort($bookings, static function (array $a, array $b): int {
|
||||
$dateComparison = strcmp($a['start_date'], $b['start_date']);
|
||||
if ($dateComparison !== 0) {
|
||||
return $dateComparison;
|
||||
}
|
||||
|
||||
return strcmp($a['created_at'], $b['created_at']);
|
||||
});
|
||||
|
||||
return $bookings;
|
||||
}
|
||||
|
||||
public function getHighlightedBookings(): array
|
||||
{
|
||||
$bookings = array_values(array_filter(
|
||||
@@ -217,6 +236,65 @@ final class BookingService
|
||||
];
|
||||
}
|
||||
|
||||
public function getCalendarGroups(): array
|
||||
{
|
||||
$groups = [];
|
||||
|
||||
foreach ($this->getBookingsByStatuses(['requested', 'reserved', 'confirmed', 'completed']) as $booking) {
|
||||
$monthKey = substr((string) $booking['start_date'], 0, 7);
|
||||
$label = $this->formatMonthLabel($monthKey);
|
||||
|
||||
$groups[$label][] = $booking;
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
public function getCustomers(): array
|
||||
{
|
||||
$customers = [];
|
||||
|
||||
foreach ($this->getBookings() as $booking) {
|
||||
$customer = $booking['customer'];
|
||||
$key = strtolower(trim((string) $customer['email'])) . '|' . strtolower(trim((string) $customer['phone']));
|
||||
|
||||
if (!isset($customers[$key])) {
|
||||
$customers[$key] = [
|
||||
'name' => $customer['name'],
|
||||
'company' => $customer['company'],
|
||||
'email' => $customer['email'],
|
||||
'phone' => $customer['phone'],
|
||||
'city' => $customer['city'],
|
||||
'booking_count' => 0,
|
||||
'revenue_cents' => 0,
|
||||
'last_booking_date' => $booking['start_date'],
|
||||
'last_reference' => $booking['reference'],
|
||||
'last_status_label' => $booking['status_label'],
|
||||
];
|
||||
}
|
||||
|
||||
$customers[$key]['booking_count']++;
|
||||
$customers[$key]['revenue_cents'] += (int) $booking['subtotal_cents'];
|
||||
|
||||
if ($booking['start_date'] >= $customers[$key]['last_booking_date']) {
|
||||
$customers[$key]['last_booking_date'] = $booking['start_date'];
|
||||
$customers[$key]['last_reference'] = $booking['reference'];
|
||||
$customers[$key]['last_status_label'] = $booking['status_label'];
|
||||
}
|
||||
}
|
||||
|
||||
usort($customers, static function (array $a, array $b): int {
|
||||
$nameComparison = strcmp($a['name'], $b['name']);
|
||||
if ($nameComparison !== 0) {
|
||||
return $nameComparison;
|
||||
}
|
||||
|
||||
return strcmp($a['email'], $b['email']);
|
||||
});
|
||||
|
||||
return $customers;
|
||||
}
|
||||
|
||||
public function getAdminDefaults(): array
|
||||
{
|
||||
return [
|
||||
@@ -243,7 +321,7 @@ final class BookingService
|
||||
return [
|
||||
'requested' => 'Neue Anfrage',
|
||||
'reserved' => 'Reserviert',
|
||||
'confirmed' => 'Bestaetigt',
|
||||
'confirmed' => 'Bestätigt',
|
||||
'completed' => 'Abgeschlossen',
|
||||
'cancelled' => 'Storniert',
|
||||
];
|
||||
@@ -278,16 +356,18 @@ final class BookingService
|
||||
$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';
|
||||
|
||||
foreach ([
|
||||
'Name' => $customerName,
|
||||
'E-Mail' => $email,
|
||||
'Telefon' => $phone,
|
||||
'Strasse' => $street,
|
||||
'Straße' => $street,
|
||||
'PLZ' => $postalCode,
|
||||
'Ort' => $city,
|
||||
'Startdatum' => $startDate,
|
||||
'Enddatum' => $endDate,
|
||||
'Abholdatum' => $startDate,
|
||||
'Rückgabedatum' => $endDate,
|
||||
] as $label => $value) {
|
||||
if ($value === '') {
|
||||
throw new RuntimeException($label . ' ist ein Pflichtfeld.');
|
||||
@@ -295,28 +375,36 @@ final class BookingService
|
||||
}
|
||||
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
throw new RuntimeException('Bitte gib eine gueltige E-Mail-Adresse an.');
|
||||
throw new RuntimeException('Bitte gib eine gültige E-Mail-Adresse an.');
|
||||
}
|
||||
|
||||
if (!$adminMode && !$privacyAccepted) {
|
||||
throw new RuntimeException('Bitte bestätige die Datenschutzerklärung.');
|
||||
}
|
||||
|
||||
if (!$adminMode && !$termsAccepted) {
|
||||
throw new RuntimeException('Bitte bestätige die Mietbedingungen.');
|
||||
}
|
||||
|
||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) {
|
||||
throw new RuntimeException('Bitte waehle gueltige Mietdaten aus.');
|
||||
throw new RuntimeException('Bitte wähle gültige Mietdaten aus.');
|
||||
}
|
||||
|
||||
$totalDays = $this->calculateRentalDays($startDate, $endDate);
|
||||
if ($totalDays < 1) {
|
||||
throw new RuntimeException('Das Mietende darf nicht vor dem Mietbeginn liegen.');
|
||||
throw new RuntimeException('Die Rückgabe muss nach der Abholung liegen. Ein Miettag entspricht zum Beispiel Montag auf Dienstag.');
|
||||
}
|
||||
|
||||
if (!array_key_exists($status, $this->getStatusOptions())) {
|
||||
throw new RuntimeException('Der Buchungsstatus ist ungueltig.');
|
||||
throw new RuntimeException('Der Buchungsstatus ist ungültig.');
|
||||
}
|
||||
|
||||
if (!array_key_exists($paymentStatus, $this->getPaymentStatusOptions())) {
|
||||
throw new RuntimeException('Der Zahlungsstatus ist ungueltig.');
|
||||
throw new RuntimeException('Der Zahlungsstatus ist ungültig.');
|
||||
}
|
||||
|
||||
$paymentLabels = [
|
||||
'invoice_transfer' => 'Ueberweisung auf Rechnung',
|
||||
'invoice_transfer' => 'Rechnung / Überweisung',
|
||||
'paypal' => 'PayPal',
|
||||
];
|
||||
|
||||
@@ -327,15 +415,15 @@ final class BookingService
|
||||
];
|
||||
|
||||
if (!array_key_exists($paymentMethod, $paymentLabels)) {
|
||||
throw new RuntimeException('Die gewaehlte Zahlungsart ist ungueltig.');
|
||||
throw new RuntimeException('Die gewählte Zahlungsart ist ungültig.');
|
||||
}
|
||||
|
||||
if (!array_key_exists($deliveryMode, $deliveryLabels)) {
|
||||
throw new RuntimeException('Die gewaehlte Lieferart ist ungueltig.');
|
||||
throw new RuntimeException('Die gewählte Lieferart ist ungültig.');
|
||||
}
|
||||
|
||||
if ($pricePerDay < 0) {
|
||||
throw new RuntimeException('Der Tagespreis ist ungueltig.');
|
||||
throw new RuntimeException('Der Tagespreis ist ungültig.');
|
||||
}
|
||||
|
||||
return [
|
||||
@@ -363,6 +451,8 @@ final class BookingService
|
||||
'payment_status_label' => $this->getPaymentStatusOptions()[$paymentStatus],
|
||||
'price_per_day_cents' => $pricePerDay,
|
||||
'subtotal_cents' => $totalDays * $pricePerDay,
|
||||
'privacy_accepted' => $privacyAccepted,
|
||||
'terms_accepted' => $termsAccepted,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -392,6 +482,11 @@ final class BookingService
|
||||
'invoice_id' => null,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
'customer_consents' => [
|
||||
'privacy_accepted' => $payload['privacy_accepted'],
|
||||
'terms_accepted' => $payload['terms_accepted'],
|
||||
'recorded_at' => $now,
|
||||
],
|
||||
'customer' => [
|
||||
'name' => $payload['customer_name'],
|
||||
'company' => $payload['company'],
|
||||
@@ -417,9 +512,9 @@ final class BookingService
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($startDate <= $record['end_date'] && $endDate >= $record['start_date']) {
|
||||
if ($startDate < $record['end_date'] && $endDate > $record['start_date']) {
|
||||
throw new RuntimeException(
|
||||
'Die Fotobox ist im gewaehlten Zeitraum bereits blockiert. Bitte waehle einen anderen Termin.'
|
||||
'Die Fotobox ist im gewählten Zeitraum bereits blockiert. Bitte wähle einen anderen Termin.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -430,7 +525,7 @@ final class BookingService
|
||||
$start = new DateTimeImmutable($startDate);
|
||||
$end = new DateTimeImmutable($endDate);
|
||||
|
||||
return (int) $start->diff($end)->format('%r%a') + 1;
|
||||
return (int) $start->diff($end)->format('%r%a');
|
||||
}
|
||||
|
||||
private function nextInvoiceNumber(array $records): string
|
||||
@@ -466,4 +561,30 @@ final class BookingService
|
||||
{
|
||||
return $prefix . '_' . strtolower(bin2hex(random_bytes(6)));
|
||||
}
|
||||
|
||||
private function formatMonthLabel(string $monthKey): string
|
||||
{
|
||||
[$year, $month] = explode('-', $monthKey) + [null, null];
|
||||
$monthNumber = (int) $month;
|
||||
$monthLabels = [
|
||||
1 => 'Januar',
|
||||
2 => 'Februar',
|
||||
3 => 'März',
|
||||
4 => 'April',
|
||||
5 => 'Mai',
|
||||
6 => 'Juni',
|
||||
7 => 'Juli',
|
||||
8 => 'August',
|
||||
9 => 'September',
|
||||
10 => 'Oktober',
|
||||
11 => 'November',
|
||||
12 => 'Dezember',
|
||||
];
|
||||
|
||||
if (!isset($monthLabels[$monthNumber]) || $year === null) {
|
||||
return $monthKey;
|
||||
}
|
||||
|
||||
return $monthLabels[$monthNumber] . ' ' . $year;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ final class InvoicePdfService
|
||||
'',
|
||||
'Rechnung: ' . $invoice['invoice_number'],
|
||||
'Rechnungsdatum: ' . formatDate($invoice['issue_date']),
|
||||
'Faellig bis: ' . formatDate($invoice['due_date']),
|
||||
'Fällig bis: ' . formatDate($invoice['due_date']),
|
||||
'',
|
||||
'Rechnung an:',
|
||||
$customer['name'],
|
||||
@@ -107,21 +107,12 @@ final class InvoicePdfService
|
||||
|
||||
private function escapePdfText(string $value): string
|
||||
{
|
||||
$value = str_replace('€', 'EUR', $value);
|
||||
$value = iconv('UTF-8', 'Windows-1252//TRANSLIT', $value) ?: $value;
|
||||
$value = str_replace('\\', '\\\\', $value);
|
||||
$value = str_replace('(', '\\(', $value);
|
||||
$value = str_replace(')', '\\)', $value);
|
||||
|
||||
$replacements = [
|
||||
'ä' => 'ae',
|
||||
'ö' => 'oe',
|
||||
'ü' => 'ue',
|
||||
'Ä' => 'Ae',
|
||||
'Ö' => 'Oe',
|
||||
'Ü' => 'Ue',
|
||||
'ß' => 'ss',
|
||||
'€' => 'EUR',
|
||||
];
|
||||
|
||||
return strtr($value, $replacements);
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,22 @@ function render(string $view, array $data = []): void
|
||||
require dirname(__DIR__, 1) . '/../views/layout.php';
|
||||
}
|
||||
|
||||
function currentPath(): string
|
||||
{
|
||||
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
|
||||
$basePath = basePath();
|
||||
|
||||
if ($basePath !== '' && str_starts_with($path, $basePath)) {
|
||||
$path = substr($path, strlen($basePath)) ?: '/';
|
||||
}
|
||||
|
||||
if ($path === '') {
|
||||
return '/';
|
||||
}
|
||||
|
||||
return '/' . ltrim($path, '/');
|
||||
}
|
||||
|
||||
function basePath(): string
|
||||
{
|
||||
$configured = trim((string) (appConfig()['app']['base_path'] ?? ''));
|
||||
@@ -39,9 +55,25 @@ function basePath(): string
|
||||
'/admin/invoice/pdf',
|
||||
'/admin/create',
|
||||
'/admin/order',
|
||||
'/admin/anfragen',
|
||||
'/admin/buchungen',
|
||||
'/admin/kalender',
|
||||
'/admin/kunden',
|
||||
'/admin/rechnungen',
|
||||
'/admin/einstellungen',
|
||||
'/admin/login',
|
||||
'/admin/logout',
|
||||
'/admin',
|
||||
'/leistungen',
|
||||
'/preise',
|
||||
'/verfuegbarkeit',
|
||||
'/buchen',
|
||||
'/ablauf',
|
||||
'/faq',
|
||||
'/kontakt',
|
||||
'/impressum',
|
||||
'/datenschutz',
|
||||
'/mietbedingungen',
|
||||
'/book',
|
||||
'/assets/styles.css',
|
||||
'/assets/app.js',
|
||||
@@ -91,6 +123,11 @@ function redirect(string $path): void
|
||||
exit;
|
||||
}
|
||||
|
||||
function isCurrentPath(string $path): bool
|
||||
{
|
||||
return currentPath() === $path;
|
||||
}
|
||||
|
||||
function flash(string $key, mixed $value = null): mixed
|
||||
{
|
||||
if (func_num_args() === 2) {
|
||||
@@ -104,6 +141,30 @@ function flash(string $key, mixed $value = null): mixed
|
||||
return $stored;
|
||||
}
|
||||
|
||||
function csrfToken(): string
|
||||
{
|
||||
if (!isset($_SESSION['csrf_token']) || !is_string($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
return $_SESSION['csrf_token'];
|
||||
}
|
||||
|
||||
function csrfField(): string
|
||||
{
|
||||
return '<input type="hidden" name="_csrf" value="' . h(csrfToken()) . '">';
|
||||
}
|
||||
|
||||
function verifyCsrfToken(): void
|
||||
{
|
||||
$token = (string) ($_POST['_csrf'] ?? '');
|
||||
$sessionToken = (string) ($_SESSION['csrf_token'] ?? '');
|
||||
|
||||
if ($token === '' || $sessionToken === '' || !hash_equals($sessionToken, $token)) {
|
||||
throw new RuntimeException('Die Anfrage konnte aus Sicherheitsgründen nicht verarbeitet werden. Bitte lade die Seite neu.');
|
||||
}
|
||||
}
|
||||
|
||||
function isAdminAuthenticated(): bool
|
||||
{
|
||||
return (bool) ($_SESSION['admin_authenticated'] ?? false);
|
||||
@@ -141,3 +202,15 @@ function formatDate(string $date): string
|
||||
|
||||
return $dateTime->format('d.m.Y');
|
||||
}
|
||||
|
||||
function statusPillClass(string $status): string
|
||||
{
|
||||
return match ($status) {
|
||||
'requested' => 'status-pill status-requested',
|
||||
'reserved' => 'status-pill status-reserved',
|
||||
'confirmed' => 'status-pill status-confirmed',
|
||||
'completed', 'paid' => 'status-pill status-completed',
|
||||
'cancelled', 'refunded' => 'status-pill status-cancelled',
|
||||
default => 'status-pill',
|
||||
};
|
||||
}
|
||||
|
||||
+450
-56
@@ -9,20 +9,38 @@ require __DIR__ . '/Repository/MySqlJsonRepository.php';
|
||||
require __DIR__ . '/Services/BookingService.php';
|
||||
require __DIR__ . '/Services/InvoicePdfService.php';
|
||||
|
||||
session_start();
|
||||
bootstrapSession();
|
||||
|
||||
function bootstrapSession(): void
|
||||
{
|
||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||
return;
|
||||
}
|
||||
|
||||
ini_set('session.use_strict_mode', '1');
|
||||
ini_set('session.cookie_httponly', '1');
|
||||
|
||||
session_set_cookie_params([
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
|
||||
session_start();
|
||||
}
|
||||
|
||||
function runApplication(): void
|
||||
{
|
||||
$config = require dirname(__DIR__) . '/config.php';
|
||||
setAppConfig($config);
|
||||
[$bookingRepository, $invoiceRepository, $runtime] = resolveRepositories($config);
|
||||
setAppConfig($config + ['runtime' => $runtime]);
|
||||
|
||||
[$bookingRepository, $invoiceRepository] = resolveRepositories($config);
|
||||
sendSecurityHeaders();
|
||||
|
||||
$bookingService = new BookingService($bookingRepository, $invoiceRepository, $config);
|
||||
$invoicePdfService = new InvoicePdfService($config);
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
$path = requestPath();
|
||||
$path = currentPath();
|
||||
|
||||
if (serveStaticAssetIfRequested($path)) {
|
||||
return;
|
||||
@@ -50,27 +68,26 @@ function runApplication(): void
|
||||
}
|
||||
|
||||
if (str_starts_with($path, '/admin')) {
|
||||
handleAdminRequest($path, $method, $bookingService);
|
||||
handleAdminRequest($path, $method, $bookingService, $runtime);
|
||||
return;
|
||||
}
|
||||
|
||||
renderHome($bookingService, $config);
|
||||
$route = publicRoutes()[$path] ?? null;
|
||||
if ($route !== null) {
|
||||
renderPublicPage($route, $bookingService, $config);
|
||||
return;
|
||||
}
|
||||
|
||||
renderNotFound($bookingService, $config);
|
||||
}
|
||||
|
||||
function requestPath(): string
|
||||
function sendSecurityHeaders(): void
|
||||
{
|
||||
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
|
||||
$basePath = basePath();
|
||||
|
||||
if ($basePath !== '' && str_starts_with($path, $basePath)) {
|
||||
$path = substr($path, strlen($basePath)) ?: '/';
|
||||
}
|
||||
|
||||
if ($path === '') {
|
||||
return '/';
|
||||
}
|
||||
|
||||
return '/' . ltrim($path, '/');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('X-Frame-Options: SAMEORIGIN');
|
||||
header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||
header('Permissions-Policy: camera=(), geolocation=(), microphone=()');
|
||||
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self';");
|
||||
}
|
||||
|
||||
function serveStaticAssetIfRequested(string $path): bool
|
||||
@@ -106,12 +123,14 @@ function serveStaticAssetIfRequested(string $path): bool
|
||||
|
||||
function resolveRepositories(array $config): array
|
||||
{
|
||||
$defaultPrefix = (string) ($config['database']['table_prefix'] ?? '');
|
||||
$databaseFile = $config['database']['credentials_file'];
|
||||
|
||||
if (file_exists($databaseFile)) {
|
||||
$databaseConfig = require $databaseFile;
|
||||
if (is_array($databaseConfig) && ($databaseConfig['enabled'] ?? false) === true) {
|
||||
try {
|
||||
$tablePrefix = (string) ($databaseConfig['table_prefix'] ?? $config['database']['table_prefix'] ?? '');
|
||||
$tablePrefix = (string) ($databaseConfig['table_prefix'] ?? $defaultPrefix);
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4',
|
||||
$databaseConfig['host'],
|
||||
@@ -127,9 +146,29 @@ function resolveRepositories(array $config): array
|
||||
return [
|
||||
new MySqlJsonRepository($pdo, resolveTableName($tablePrefix, $config['database']['tables']['bookings'])),
|
||||
new MySqlJsonRepository($pdo, resolveTableName($tablePrefix, $config['database']['tables']['invoices'])),
|
||||
[
|
||||
'storage_driver' => 'MySQL',
|
||||
'table_prefix' => $tablePrefix,
|
||||
'database_host' => (string) $databaseConfig['host'],
|
||||
'database_name' => (string) $databaseConfig['database'],
|
||||
'database_enabled' => true,
|
||||
],
|
||||
];
|
||||
} catch (Throwable $exception) {
|
||||
error_log('MySQL-Verbindung fehlgeschlagen, JSON-Fallback aktiv: ' . $exception->getMessage());
|
||||
|
||||
return [
|
||||
new JsonRepository($config['storage']['bookings']),
|
||||
new JsonRepository($config['storage']['invoices']),
|
||||
[
|
||||
'storage_driver' => 'JSON-Fallback',
|
||||
'table_prefix' => (string) ($databaseConfig['table_prefix'] ?? $defaultPrefix),
|
||||
'database_host' => (string) ($databaseConfig['host'] ?? ''),
|
||||
'database_name' => (string) ($databaseConfig['database'] ?? ''),
|
||||
'database_enabled' => true,
|
||||
'fallback_reason' => 'MySQL-Verbindung fehlgeschlagen',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,6 +176,13 @@ function resolveRepositories(array $config): array
|
||||
return [
|
||||
new JsonRepository($config['storage']['bookings']),
|
||||
new JsonRepository($config['storage']['invoices']),
|
||||
[
|
||||
'storage_driver' => 'JSON',
|
||||
'table_prefix' => $defaultPrefix,
|
||||
'database_host' => '',
|
||||
'database_name' => '',
|
||||
'database_enabled' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -148,40 +194,66 @@ function resolveTableName(string $prefix, string $table): string
|
||||
function handlePublicBooking(BookingService $bookingService): void
|
||||
{
|
||||
try {
|
||||
verifyCsrfToken();
|
||||
$bookingService->createPublicBooking($_POST);
|
||||
flash('success', 'Deine Anfrage wurde gespeichert. Wir melden uns zeitnah mit der Bestaetigung und allen Details.');
|
||||
flash('success', 'Ihre Buchungsanfrage wurde gespeichert. Wir melden uns zeitnah mit der Bestätigung und allen weiteren Details.');
|
||||
} catch (Throwable $exception) {
|
||||
flash('error', $exception->getMessage());
|
||||
flash('old', $_POST);
|
||||
}
|
||||
|
||||
redirect('/');
|
||||
redirect('/buchen');
|
||||
}
|
||||
|
||||
function handleAdminLogin(array $adminConfig): void
|
||||
{
|
||||
try {
|
||||
verifyCsrfToken();
|
||||
} catch (Throwable $exception) {
|
||||
flash('error', $exception->getMessage());
|
||||
redirect('/admin/login');
|
||||
}
|
||||
|
||||
$username = trim((string) ($_POST['username'] ?? ''));
|
||||
$password = (string) ($_POST['password'] ?? '');
|
||||
|
||||
if ($username === $adminConfig['username'] && hash_equals($adminConfig['password'], $password)) {
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['admin_authenticated'] = true;
|
||||
flash('success', 'Admin-Bereich geoeffnet.');
|
||||
} else {
|
||||
flash('error', 'Die Admin-Zugangsdaten sind nicht korrekt.');
|
||||
flash('success', 'Der Verwaltungsbereich wurde geöffnet.');
|
||||
redirect('/admin');
|
||||
}
|
||||
|
||||
redirect('/admin');
|
||||
flash('error', 'Die Zugangsdaten sind nicht korrekt.');
|
||||
redirect('/admin/login');
|
||||
}
|
||||
|
||||
function handleAdminLogout(): void
|
||||
{
|
||||
try {
|
||||
verifyCsrfToken();
|
||||
} catch (Throwable $exception) {
|
||||
flash('error', $exception->getMessage());
|
||||
redirect('/admin');
|
||||
}
|
||||
|
||||
unset($_SESSION['admin_authenticated']);
|
||||
flash('success', 'Du wurdest aus dem Admin-Bereich abgemeldet.');
|
||||
redirect('/admin');
|
||||
session_regenerate_id(true);
|
||||
flash('success', 'Sie wurden aus dem Verwaltungsbereich abgemeldet.');
|
||||
redirect('/admin/login');
|
||||
}
|
||||
|
||||
function handleAdminRequest(string $path, string $method, BookingService $bookingService): void
|
||||
function handleAdminRequest(string $path, string $method, BookingService $bookingService, array $runtime): void
|
||||
{
|
||||
if ($path === '/admin/login') {
|
||||
if (isAdminAuthenticated()) {
|
||||
redirect('/admin');
|
||||
}
|
||||
|
||||
renderAdminLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAdminAuthenticated()) {
|
||||
renderAdminLogin();
|
||||
return;
|
||||
@@ -189,18 +261,20 @@ function handleAdminRequest(string $path, string $method, BookingService $bookin
|
||||
|
||||
if ($method === 'POST' && $path === '/admin/create') {
|
||||
try {
|
||||
verifyCsrfToken();
|
||||
$bookingService->createAdminBooking($_POST);
|
||||
flash('success', 'Die Bestellung wurde fuer den Kunden angelegt.');
|
||||
flash('success', 'Die Bestellung wurde für den Kunden angelegt.');
|
||||
redirect('/admin/buchungen');
|
||||
} catch (Throwable $exception) {
|
||||
flash('error', $exception->getMessage());
|
||||
flash('admin_old', $_POST);
|
||||
redirect('/admin/create');
|
||||
}
|
||||
|
||||
redirect('/admin/create');
|
||||
}
|
||||
|
||||
if ($method === 'POST' && $path === '/admin/order/update') {
|
||||
try {
|
||||
verifyCsrfToken();
|
||||
$bookingService->updateBooking((string) ($_POST['booking_id'] ?? ''), $_POST);
|
||||
flash('success', 'Der Auftrag wurde aktualisiert.');
|
||||
} catch (Throwable $exception) {
|
||||
@@ -212,6 +286,7 @@ function handleAdminRequest(string $path, string $method, BookingService $bookin
|
||||
|
||||
if ($method === 'POST' && $path === '/admin/order/invoice') {
|
||||
try {
|
||||
verifyCsrfToken();
|
||||
$invoiceId = $bookingService->createInvoiceForBooking((string) ($_POST['booking_id'] ?? ''), $_POST);
|
||||
flash('success', 'Die Rechnung wurde erstellt.');
|
||||
redirect('admin/order?id=' . urlencode((string) ($_POST['booking_id'] ?? '')) . '&invoice=' . urlencode($invoiceId));
|
||||
@@ -221,17 +296,18 @@ function handleAdminRequest(string $path, string $method, BookingService $bookin
|
||||
}
|
||||
}
|
||||
|
||||
if ($path === '/admin/create') {
|
||||
renderAdminCreate($bookingService);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($path === '/admin/order') {
|
||||
renderAdminOrder($bookingService);
|
||||
return;
|
||||
}
|
||||
|
||||
renderAdminDashboard($bookingService);
|
||||
match ($path) {
|
||||
'/admin' => renderAdminDashboard($bookingService),
|
||||
'/admin/create' => renderAdminCreate($bookingService),
|
||||
'/admin/anfragen' => renderAdminRequests($bookingService),
|
||||
'/admin/buchungen' => renderAdminBookings($bookingService),
|
||||
'/admin/kalender' => renderAdminCalendar($bookingService),
|
||||
'/admin/kunden' => renderAdminCustomers($bookingService),
|
||||
'/admin/rechnungen' => renderAdminInvoices($bookingService),
|
||||
'/admin/einstellungen' => renderAdminSettings($runtime),
|
||||
'/admin/order' => renderAdminOrder($bookingService),
|
||||
default => renderAdminDashboard($bookingService),
|
||||
};
|
||||
}
|
||||
|
||||
function handleInvoicePdf(BookingService $bookingService, InvoicePdfService $invoicePdfService): void
|
||||
@@ -246,51 +322,227 @@ function handleInvoicePdf(BookingService $bookingService, InvoicePdfService $inv
|
||||
}
|
||||
|
||||
$pdf = $invoicePdfService->render($invoice);
|
||||
|
||||
header('Content-Type: application/pdf');
|
||||
header('Content-Disposition: inline; filename="' . $invoice['invoice_number'] . '.pdf"');
|
||||
header('Content-Length: ' . strlen($pdf));
|
||||
|
||||
header('Content-Length: ' . (string) strlen($pdf));
|
||||
echo $pdf;
|
||||
}
|
||||
|
||||
function renderHome(BookingService $bookingService, array $config): void
|
||||
function publicRoutes(): array
|
||||
{
|
||||
render('home', [
|
||||
'pageTitle' => 'Fotobox mieten',
|
||||
return [
|
||||
'/' => [
|
||||
'view' => 'home',
|
||||
'pageTitle' => 'Fotobox mieten für Hochzeiten, Geburtstage und Firmenfeiern',
|
||||
'metaDescription' => 'Professionelle Fotobox-Vermietung mit klarer Preislogik, Buchungsanfrage, Lieferung oder Abholung und digitaler Bildübergabe.',
|
||||
'pageKey' => 'home',
|
||||
],
|
||||
'/leistungen' => [
|
||||
'view' => 'pages/leistungen',
|
||||
'pageTitle' => 'Leistungen und Ausstattung',
|
||||
'metaDescription' => 'DSLR-Kamera, Studioblitz, Softbox, WLAN-Download und professioneller Lieferumfang für Ihre Fotobox-Miete.',
|
||||
'pageKey' => 'leistungen',
|
||||
],
|
||||
'/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.',
|
||||
'pageKey' => 'preise',
|
||||
],
|
||||
'/verfuegbarkeit' => [
|
||||
'view' => 'pages/verfuegbarkeit',
|
||||
'pageTitle' => 'Verfügbarkeit prüfen',
|
||||
'metaDescription' => 'Aktuelle Belegung, geblockte Zeiträume und der direkte Einstieg in Ihre Buchungsanfrage.',
|
||||
'pageKey' => 'verfuegbarkeit',
|
||||
],
|
||||
'/buchen' => [
|
||||
'view' => 'pages/buchen',
|
||||
'pageTitle' => 'Buchungsanfrage stellen',
|
||||
'metaDescription' => 'Zeitraum wählen, Leistung festlegen und Ihre Fotobox-Anfrage in wenigen Minuten senden.',
|
||||
'pageKey' => 'buchen',
|
||||
],
|
||||
'/ablauf' => [
|
||||
'view' => 'pages/ablauf',
|
||||
'pageTitle' => 'Ablauf Ihrer Miete',
|
||||
'metaDescription' => 'Von der Anfrage über Lieferung oder Abholung bis zur Rückgabe und Bildübergabe: so läuft die Fotobox-Miete ab.',
|
||||
'pageKey' => 'ablauf',
|
||||
],
|
||||
'/faq' => [
|
||||
'view' => 'pages/faq',
|
||||
'pageTitle' => 'Häufige Fragen',
|
||||
'metaDescription' => 'Antworten zu Mietdauer, Verfügbarkeit, Zahlung, Rückgabe, Bildübergabe und technischer Unterstützung.',
|
||||
'pageKey' => 'faq',
|
||||
],
|
||||
'/kontakt' => [
|
||||
'view' => 'pages/kontakt',
|
||||
'pageTitle' => 'Kontakt',
|
||||
'metaDescription' => 'Kontaktieren Sie Fotobox Moments für Fragen zu Verfügbarkeit, Lieferung, Rechnungen und individuellen Anforderungen.',
|
||||
'pageKey' => 'kontakt',
|
||||
],
|
||||
'/impressum' => [
|
||||
'view' => 'pages/impressum',
|
||||
'pageTitle' => 'Impressum',
|
||||
'metaDescription' => 'Anbieterkennzeichnung und Kontaktangaben für die Fotobox-Vermietung.',
|
||||
'pageKey' => 'impressum',
|
||||
],
|
||||
'/datenschutz' => [
|
||||
'view' => 'pages/datenschutz',
|
||||
'pageTitle' => 'Datenschutzerklärung',
|
||||
'metaDescription' => 'Informationen zur Verarbeitung personenbezogener Daten auf der Fotobox-Website und im Buchungsprozess.',
|
||||
'pageKey' => 'datenschutz',
|
||||
],
|
||||
'/mietbedingungen' => [
|
||||
'view' => 'pages/mietbedingungen',
|
||||
'pageTitle' => 'Mietbedingungen',
|
||||
'metaDescription' => 'Rahmenbedingungen für Anfrage, Bestätigung, Zahlung, Rückgabe und Haftung bei der Fotobox-Miete.',
|
||||
'pageKey' => 'mietbedingungen',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function renderPublicPage(array $route, BookingService $bookingService, array $config): void
|
||||
{
|
||||
$company = $config['company'];
|
||||
$bookings = $bookingService->getHighlightedBookings();
|
||||
$currentView = (string) $route['view'];
|
||||
|
||||
render($currentView, [
|
||||
'pageTitle' => $route['pageTitle'],
|
||||
'metaDescription' => $route['metaDescription'],
|
||||
'pageKey' => $route['pageKey'],
|
||||
'config' => $config,
|
||||
'company' => $company,
|
||||
'dayRate' => $config['pricing']['default_day_rate_cents'],
|
||||
'flashSuccess' => flash('success'),
|
||||
'flashError' => flash('error'),
|
||||
'old' => flash('old') ?? [],
|
||||
'bookings' => $bookingService->getHighlightedBookings(),
|
||||
'bookings' => $bookings,
|
||||
'trustFacts' => publicTrustFacts($config),
|
||||
'featureCards' => publicFeatureCards(),
|
||||
'processSteps' => publicProcessSteps($config),
|
||||
'pricingExamples' => publicPricingExamples($config),
|
||||
'occasionCards' => publicOccasionCards(),
|
||||
'faqItems' => publicFaqItems($config),
|
||||
'serviceModules' => publicServiceModules(),
|
||||
'serviceStandards' => publicServiceStandards($config),
|
||||
'bookingChecklist' => publicBookingChecklist($config),
|
||||
]);
|
||||
}
|
||||
|
||||
function renderNotFound(BookingService $bookingService, array $config): void
|
||||
{
|
||||
http_response_code(404);
|
||||
renderPublicPage([
|
||||
'view' => 'pages/not-found',
|
||||
'pageTitle' => 'Seite nicht gefunden',
|
||||
'metaDescription' => 'Die angeforderte Seite konnte nicht gefunden werden.',
|
||||
'pageKey' => 'not-found',
|
||||
], $bookingService, $config);
|
||||
}
|
||||
|
||||
function renderAdminLogin(): void
|
||||
{
|
||||
render('admin/login', [
|
||||
'pageTitle' => 'Admin Login',
|
||||
'pageTitle' => 'Verwaltungszugang',
|
||||
'metaDescription' => 'Login für die interne Verwaltung der Fotobox-Anfragen und Buchungen.',
|
||||
'pageKey' => 'admin-login',
|
||||
'flashSuccess' => flash('success'),
|
||||
'flashError' => flash('error'),
|
||||
'defaultUsername' => appConfig()['admin']['username'] ?? 'admin',
|
||||
]);
|
||||
}
|
||||
|
||||
function renderAdminDashboard(BookingService $bookingService): void
|
||||
{
|
||||
render('admin/dashboard', [
|
||||
'pageTitle' => 'Admin Dashboard',
|
||||
'pageTitle' => 'Dashboard',
|
||||
'pageKey' => 'admin-dashboard',
|
||||
'flashSuccess' => flash('success'),
|
||||
'flashError' => flash('error'),
|
||||
'stats' => $bookingService->getDashboardStats(),
|
||||
'bookings' => array_slice($bookingService->getBookings(), 0, 8),
|
||||
'requests' => array_slice($bookingService->getBookingsByStatuses(['requested', 'reserved']), 0, 6),
|
||||
'invoices' => array_slice($bookingService->getInvoices(), 0, 6),
|
||||
'upcomingBookings' => array_slice($bookingService->getBookingsByStatuses(['requested', 'reserved', 'confirmed']), 0, 6),
|
||||
]);
|
||||
}
|
||||
|
||||
function renderAdminRequests(BookingService $bookingService): void
|
||||
{
|
||||
render('admin/requests', [
|
||||
'pageTitle' => 'Anfragen',
|
||||
'pageKey' => 'admin-requests',
|
||||
'flashSuccess' => flash('success'),
|
||||
'flashError' => flash('error'),
|
||||
'requests' => $bookingService->getBookingsByStatuses(['requested', 'reserved']),
|
||||
]);
|
||||
}
|
||||
|
||||
function renderAdminBookings(BookingService $bookingService): void
|
||||
{
|
||||
render('admin/bookings', [
|
||||
'pageTitle' => 'Buchungen',
|
||||
'pageKey' => 'admin-bookings',
|
||||
'flashSuccess' => flash('success'),
|
||||
'flashError' => flash('error'),
|
||||
'bookings' => $bookingService->getBookings(),
|
||||
]);
|
||||
}
|
||||
|
||||
function renderAdminCalendar(BookingService $bookingService): void
|
||||
{
|
||||
render('admin/calendar', [
|
||||
'pageTitle' => 'Kalender',
|
||||
'pageKey' => 'admin-calendar',
|
||||
'flashSuccess' => flash('success'),
|
||||
'flashError' => flash('error'),
|
||||
'calendarGroups' => $bookingService->getCalendarGroups(),
|
||||
]);
|
||||
}
|
||||
|
||||
function renderAdminCustomers(BookingService $bookingService): void
|
||||
{
|
||||
render('admin/customers', [
|
||||
'pageTitle' => 'Kunden',
|
||||
'pageKey' => 'admin-customers',
|
||||
'flashSuccess' => flash('success'),
|
||||
'flashError' => flash('error'),
|
||||
'customers' => $bookingService->getCustomers(),
|
||||
]);
|
||||
}
|
||||
|
||||
function renderAdminInvoices(BookingService $bookingService): void
|
||||
{
|
||||
render('admin/invoices', [
|
||||
'pageTitle' => 'Rechnungen',
|
||||
'pageKey' => 'admin-invoices',
|
||||
'flashSuccess' => flash('success'),
|
||||
'flashError' => flash('error'),
|
||||
'invoices' => $bookingService->getInvoices(),
|
||||
]);
|
||||
}
|
||||
|
||||
function renderAdminSettings(array $runtime): void
|
||||
{
|
||||
$config = appConfig();
|
||||
|
||||
render('admin/settings', [
|
||||
'pageTitle' => 'Einstellungen',
|
||||
'pageKey' => 'admin-settings',
|
||||
'flashSuccess' => flash('success'),
|
||||
'flashError' => flash('error'),
|
||||
'runtime' => $runtime,
|
||||
'company' => $config['company'],
|
||||
'pricing' => $config['pricing'],
|
||||
'database' => $config['database'],
|
||||
]);
|
||||
}
|
||||
|
||||
function renderAdminCreate(BookingService $bookingService): void
|
||||
{
|
||||
render('admin/create', [
|
||||
'pageTitle' => 'Kundenbestellung anlegen',
|
||||
'pageTitle' => 'Manuelle Buchung',
|
||||
'pageKey' => 'admin-create',
|
||||
'flashSuccess' => flash('success'),
|
||||
'flashError' => flash('error'),
|
||||
'old' => flash('admin_old') ?? [],
|
||||
@@ -304,13 +556,13 @@ function renderAdminOrder(BookingService $bookingService): void
|
||||
$booking = $bookingService->findBooking($bookingId);
|
||||
|
||||
if ($booking === null) {
|
||||
http_response_code(404);
|
||||
echo 'Auftrag nicht gefunden.';
|
||||
return;
|
||||
flash('error', 'Der gewünschte Auftrag wurde nicht gefunden.');
|
||||
redirect('/admin/buchungen');
|
||||
}
|
||||
|
||||
render('admin/order', [
|
||||
'pageTitle' => 'Auftrag ' . $booking['reference'],
|
||||
'pageKey' => 'admin-order',
|
||||
'flashSuccess' => flash('success'),
|
||||
'flashError' => flash('error'),
|
||||
'booking' => $booking,
|
||||
@@ -319,3 +571,145 @@ function renderAdminOrder(BookingService $bookingService): void
|
||||
'paymentOptions' => $bookingService->getPaymentStatusOptions(),
|
||||
]);
|
||||
}
|
||||
|
||||
function publicTrustFacts(array $config): array
|
||||
{
|
||||
$company = $config['company'];
|
||||
|
||||
return [
|
||||
['label' => 'Preis', 'value' => $config['pricing']['label']],
|
||||
['label' => 'Mietlogik', 'value' => '1 Miettag = 1 Übernachtung'],
|
||||
['label' => 'Zahlung', 'value' => 'Rechnung, Überweisung oder PayPal'],
|
||||
['label' => 'Servicefenster', 'value' => $company['pickup_window'] . ' / ' . $company['return_window']],
|
||||
];
|
||||
}
|
||||
|
||||
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' => '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' => 'Lieferung oder Selbstabholung',
|
||||
'text' => 'Sie entscheiden zwischen Selbstabholung, Lieferung mit Aufbau oder einem Rundum-Service mit Vor-Ort-Betreuung.',
|
||||
],
|
||||
[
|
||||
'title' => 'Saubere Verwaltung im Hintergrund',
|
||||
'text' => 'Anfragen, Kundendaten, Rechnungen und Zahlungsstatus werden in einem Verwaltungsbereich zentral gepflegt.',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function publicProcessSteps(array $config): array
|
||||
{
|
||||
$company = $config['company'];
|
||||
|
||||
return [
|
||||
[
|
||||
'title' => 'Zeitraum wählen',
|
||||
'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' => 'Anfrage absenden',
|
||||
'text' => 'Wir prüfen Verfügbarkeit, erfassen Ihre Daten und bestätigen den Auftrag persönlich.',
|
||||
],
|
||||
[
|
||||
'title' => 'Feiern und Bilder erhalten',
|
||||
'text' => 'Die Fotobox steht rechtzeitig bereit. Die Rückgabe erfolgt bis ' . $company['return_window'] . '.',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function publicPricingExamples(array $config): array
|
||||
{
|
||||
$label = $config['pricing']['label'];
|
||||
|
||||
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.'],
|
||||
];
|
||||
}
|
||||
|
||||
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.'],
|
||||
];
|
||||
}
|
||||
|
||||
function publicFaqItems(array $config): array
|
||||
{
|
||||
$company = $config['company'];
|
||||
|
||||
return [
|
||||
[
|
||||
'question' => 'Wie wird ein Miettag berechnet?',
|
||||
'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' => 'Welche Zahlungsarten sind möglich?',
|
||||
'answer' => 'Sie können per Rechnung / Überweisung oder per PayPal zahlen. Die gewünschte Zahlungsart wird bereits in der Anfrage abgefragt.',
|
||||
],
|
||||
[
|
||||
'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' => '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.',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
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']],
|
||||
];
|
||||
}
|
||||
|
||||
function publicServiceStandards(array $config): array
|
||||
{
|
||||
$company = $config['company'];
|
||||
|
||||
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',
|
||||
];
|
||||
}
|
||||
|
||||
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',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<section class="admin-section">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<p class="eyebrow">Buchungen</p>
|
||||
<h1>Alle Aufträge im Überblick</h1>
|
||||
</div>
|
||||
<a class="button-primary" href="<?= h(url('admin/create')) ?>">Neuen Auftrag anlegen</a>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($flashSuccess)): ?>
|
||||
<div class="flash flash-success"><?= h((string) $flashSuccess) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($flashError)): ?>
|
||||
<div class="flash flash-error"><?= h((string) $flashError) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="table-card">
|
||||
<div class="table-card-header">
|
||||
<h2>Auftragsliste</h2>
|
||||
<span><?= h((string) count($bookings)) ?> Einträge</span>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Referenz</th>
|
||||
<th>Kunde</th>
|
||||
<th>Zeitraum</th>
|
||||
<th>Status</th>
|
||||
<th>Zahlung</th>
|
||||
<th>Preis</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($bookings as $booking): ?>
|
||||
<tr>
|
||||
<td><a href="<?= h(url('admin/order?id=' . urlencode($booking['id']))) ?>"><?= h($booking['reference']) ?></a></td>
|
||||
<td><?= h($booking['customer']['name']) ?></td>
|
||||
<td><?= h(formatDate($booking['start_date'])) ?> bis <?= h(formatDate($booking['end_date'])) ?></td>
|
||||
<td><span class="<?= h(statusPillClass((string) $booking['status'])) ?>"><?= h($booking['status_label']) ?></span></td>
|
||||
<td><?= h($booking['payment_status_label']) ?></td>
|
||||
<td><?= h(formatCurrency((int) $booking['subtotal_cents'])) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($bookings === []): ?>
|
||||
<tr>
|
||||
<td colspan="6">Noch keine Buchungen vorhanden.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<section class="admin-section">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<p class="eyebrow">Kalender</p>
|
||||
<h1>Zeiträume und Belegung nach Monaten</h1>
|
||||
</div>
|
||||
<a class="button-secondary" href="<?= h(url('admin/buchungen')) ?>">Zur Auftragsliste</a>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($flashSuccess)): ?>
|
||||
<div class="flash flash-success"><?= h((string) $flashSuccess) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($flashError)): ?>
|
||||
<div class="flash flash-error"><?= h((string) $flashError) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="calendar-grid">
|
||||
<?php foreach ($calendarGroups as $label => $entries): ?>
|
||||
<article class="table-card">
|
||||
<div class="table-card-header">
|
||||
<h2><?= h((string) $label) ?></h2>
|
||||
<span><?= h((string) count($entries)) ?> Termine</span>
|
||||
</div>
|
||||
<div class="stack-list">
|
||||
<?php foreach ($entries as $booking): ?>
|
||||
<article class="stack-item">
|
||||
<div>
|
||||
<strong><?= h($booking['reference']) ?></strong>
|
||||
<span><?= h($booking['customer']['name']) ?> · <?= h(formatDate($booking['start_date'])) ?> bis <?= h(formatDate($booking['end_date'])) ?></span>
|
||||
</div>
|
||||
<span class="<?= h(statusPillClass((string) $booking['status'])) ?>"><?= h($booking['status_label']) ?></span>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($calendarGroups === []): ?>
|
||||
<article class="table-card">
|
||||
<p>Aktuell sind keine Termine im Kalender hinterlegt.</p>
|
||||
</article>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
+109
-98
@@ -2,9 +2,9 @@
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<p class="eyebrow">Manuelle Buchung</p>
|
||||
<h1>Bestellung fuer Kunden anlegen</h1>
|
||||
<h1>Buchung oder Auftrag für Kunden anlegen</h1>
|
||||
</div>
|
||||
<a class="button-secondary" href="<?= h(url('admin')) ?>">Zurueck zum Dashboard</a>
|
||||
<a class="button-secondary" href="<?= h(url('admin/buchungen')) ?>">Zurück zu den Buchungen</a>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($flashSuccess)): ?>
|
||||
@@ -15,106 +15,117 @@
|
||||
<?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']) ?>">
|
||||
<div class="form-grid">
|
||||
<label>
|
||||
<span>Name</span>
|
||||
<input type="text" name="customer_name" value="<?= h((string) ($old['customer_name'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Firma</span>
|
||||
<input type="text" name="company" value="<?= h((string) ($old['company'] ?? '')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>E-Mail</span>
|
||||
<input type="email" name="email" value="<?= h((string) ($old['email'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Telefon</span>
|
||||
<input type="text" name="phone" value="<?= h((string) ($old['phone'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Strasse</span>
|
||||
<input type="text" name="street" value="<?= h((string) ($old['street'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>PLZ</span>
|
||||
<input type="text" name="postal_code" value="<?= h((string) ($old['postal_code'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Ort</span>
|
||||
<input type="text" name="city" value="<?= h((string) ($old['city'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Anlass</span>
|
||||
<input type="text" name="event_type" value="<?= h((string) ($old['event_type'] ?? '')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>Veranstaltungsort</span>
|
||||
<input type="text" name="event_location" value="<?= h((string) ($old['event_location'] ?? '')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>Mietbeginn</span>
|
||||
<input type="date" name="start_date" data-booking-start value="<?= h((string) ($old['start_date'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Mietende</span>
|
||||
<input type="date" name="end_date" data-booking-end value="<?= h((string) ($old['end_date'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Tagespreis 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">
|
||||
<option value="confirmed" <?= selected((string) ($old['status'] ?? $defaults['status']), 'confirmed') ?>>Bestaetigt</option>
|
||||
<option value="reserved" <?= selected((string) ($old['status'] ?? ''), 'reserved') ?>>Reserviert</option>
|
||||
<option value="requested" <?= selected((string) ($old['status'] ?? ''), 'requested') ?>>Neue Anfrage</option>
|
||||
<option value="cancelled" <?= selected((string) ($old['status'] ?? ''), 'cancelled') ?>>Storniert</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Zahlungsstatus</span>
|
||||
<select name="payment_status">
|
||||
<option value="unpaid" <?= selected((string) ($old['payment_status'] ?? $defaults['payment_status']), 'unpaid') ?>>Offen</option>
|
||||
<option value="paid" <?= selected((string) ($old['payment_status'] ?? ''), 'paid') ?>>Bezahlt</option>
|
||||
<option value="refunded" <?= selected((string) ($old['payment_status'] ?? ''), 'refunded') ?>>Erstattet</option>
|
||||
</select>
|
||||
</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>
|
||||
</label>
|
||||
<label>
|
||||
<span>Zahlungsart</span>
|
||||
<select name="payment_method">
|
||||
<option value="invoice_transfer" <?= selected((string) ($old['payment_method'] ?? $defaults['payment_method']), 'invoice_transfer') ?>>Rechnung / Ueberweisung</option>
|
||||
<option value="paypal" <?= selected((string) ($old['payment_method'] ?? ''), 'paypal') ?>>PayPal</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
<span>Hinweis fuer Kunden</span>
|
||||
<textarea name="notes_customer" rows="4"><?= h((string) ($old['notes_customer'] ?? '')) ?></textarea>
|
||||
</label>
|
||||
<label>
|
||||
<span>Interne Notiz</span>
|
||||
<textarea name="internal_notes" rows="4"><?= h((string) ($old['internal_notes'] ?? '')) ?></textarea>
|
||||
</label>
|
||||
<div class="price-summary">
|
||||
<div>
|
||||
<span>Mietdauer</span>
|
||||
<strong data-summary-days>Noch nicht gewaehlt</strong>
|
||||
<?= csrfField() ?>
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
<span class="form-step">Verwaltung</span>
|
||||
<h3>Kundendaten und Zeitraum</h3>
|
||||
</div>
|
||||
<div>
|
||||
<p class="form-help">Auch intern gilt: Montag bis Dienstag zählt als 1 Miettag.</p>
|
||||
<div class="form-grid">
|
||||
<label>
|
||||
<span>Name</span>
|
||||
<input type="text" name="customer_name" value="<?= h((string) ($old['customer_name'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Firma</span>
|
||||
<input type="text" name="company" value="<?= h((string) ($old['company'] ?? '')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>E-Mail</span>
|
||||
<input type="email" name="email" value="<?= h((string) ($old['email'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Telefon</span>
|
||||
<input type="text" name="phone" value="<?= h((string) ($old['phone'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Straße</span>
|
||||
<input type="text" name="street" value="<?= h((string) ($old['street'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>PLZ</span>
|
||||
<input type="text" name="postal_code" value="<?= h((string) ($old['postal_code'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Ort</span>
|
||||
<input type="text" name="city" value="<?= h((string) ($old['city'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Anlass</span>
|
||||
<input type="text" name="event_type" value="<?= h((string) ($old['event_type'] ?? '')) ?>">
|
||||
</label>
|
||||
<label class="form-grid-span">
|
||||
<span>Veranstaltungsort</span>
|
||||
<input type="text" name="event_location" value="<?= h((string) ($old['event_location'] ?? '')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>Abholdatum</span>
|
||||
<input type="date" name="start_date" data-booking-start value="<?= h((string) ($old['start_date'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<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">
|
||||
<option value="confirmed" <?= selected((string) ($old['status'] ?? $defaults['status']), 'confirmed') ?>>Bestätigt</option>
|
||||
<option value="reserved" <?= selected((string) ($old['status'] ?? ''), 'reserved') ?>>Reserviert</option>
|
||||
<option value="requested" <?= selected((string) ($old['status'] ?? ''), 'requested') ?>>Neue Anfrage</option>
|
||||
<option value="cancelled" <?= selected((string) ($old['status'] ?? ''), 'cancelled') ?>>Storniert</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Zahlungsstatus</span>
|
||||
<select name="payment_status">
|
||||
<option value="unpaid" <?= selected((string) ($old['payment_status'] ?? $defaults['payment_status']), 'unpaid') ?>>Offen</option>
|
||||
<option value="paid" <?= selected((string) ($old['payment_status'] ?? ''), 'paid') ?>>Bezahlt</option>
|
||||
<option value="refunded" <?= selected((string) ($old['payment_status'] ?? ''), 'refunded') ?>>Erstattet</option>
|
||||
</select>
|
||||
</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>
|
||||
</label>
|
||||
<label>
|
||||
<span>Zahlungsart</span>
|
||||
<select name="payment_method">
|
||||
<option value="invoice_transfer" <?= selected((string) ($old['payment_method'] ?? $defaults['payment_method']), 'invoice_transfer') ?>>Rechnung / Überweisung</option>
|
||||
<option value="paypal" <?= selected((string) ($old['payment_method'] ?? ''), 'paypal') ?>>PayPal</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="form-grid-span">
|
||||
<span>Hinweis für Kunden</span>
|
||||
<textarea name="notes_customer" rows="4"><?= h((string) ($old['notes_customer'] ?? '')) ?></textarea>
|
||||
</label>
|
||||
<label class="form-grid-span">
|
||||
<span>Interne Notiz</span>
|
||||
<textarea name="internal_notes" rows="4"><?= h((string) ($old['internal_notes'] ?? '')) ?></textarea>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="booking-summary-card">
|
||||
<div class="summary-line">
|
||||
<span>Mietdauer</span>
|
||||
<strong data-summary-days>Noch nicht gewählt</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>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="button-primary">Bestellung speichern</button>
|
||||
|
||||
<button type="submit" class="button-primary">Buchung speichern</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<section class="admin-section">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<p class="eyebrow">Kunden</p>
|
||||
<h1>Kundenhistorie aus allen Aufträgen</h1>
|
||||
</div>
|
||||
<a class="button-secondary" href="<?= h(url('admin/create')) ?>">Neuen Kundenauftrag anlegen</a>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($flashSuccess)): ?>
|
||||
<div class="flash flash-success"><?= h((string) $flashSuccess) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($flashError)): ?>
|
||||
<div class="flash flash-error"><?= h((string) $flashError) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="table-card">
|
||||
<div class="table-card-header">
|
||||
<h2>Kundenübersicht</h2>
|
||||
<span><?= h((string) count($customers)) ?> Kunden</span>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Kontakt</th>
|
||||
<th>Ort</th>
|
||||
<th>Aufträge</th>
|
||||
<th>Umsatz</th>
|
||||
<th>Letzter Auftrag</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($customers as $customer): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<strong><?= h($customer['name']) ?></strong><br>
|
||||
<span><?= h($customer['company'] ?: '-') ?></span>
|
||||
</td>
|
||||
<td><?= h($customer['email']) ?><br><?= h($customer['phone']) ?></td>
|
||||
<td><?= h($customer['city']) ?></td>
|
||||
<td><?= h((string) $customer['booking_count']) ?></td>
|
||||
<td><?= h(formatCurrency((int) $customer['revenue_cents'])) ?></td>
|
||||
<td><?= h($customer['last_reference']) ?> · <?= h(formatDate($customer['last_booking_date'])) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($customers === []): ?>
|
||||
<tr>
|
||||
<td colspan="6">Es wurden noch keine Kunden gespeichert.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
+67
-46
@@ -2,9 +2,12 @@
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<p class="eyebrow">Dashboard</p>
|
||||
<h1>Anfragen, Buchungen und Rechnungen</h1>
|
||||
<h1>Verwaltung für Anfragen, Buchungen und Rechnungen</h1>
|
||||
</div>
|
||||
<div class="section-actions">
|
||||
<a class="button-secondary" href="<?= h(url('admin/anfragen')) ?>">Offene Anfragen</a>
|
||||
<a class="button-primary" href="<?= h(url('admin/create')) ?>">Buchung für Kunden anlegen</a>
|
||||
</div>
|
||||
<a class="button-primary" href="<?= h(url('admin/create')) ?>">Bestellung fuer Kunden anlegen</a>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($flashSuccess)): ?>
|
||||
@@ -16,7 +19,7 @@
|
||||
|
||||
<div class="stats-grid">
|
||||
<article class="stat-card">
|
||||
<span>Alle Auftraege</span>
|
||||
<span>Alle Aufträge</span>
|
||||
<strong><?= h((string) $stats['bookings_total']) ?></strong>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
@@ -24,7 +27,7 @@
|
||||
<strong><?= h((string) $stats['open_requests']) ?></strong>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<span>Bestaetigte Buchungen</span>
|
||||
<span>Bestätigte Buchungen</span>
|
||||
<strong><?= h((string) $stats['confirmed_bookings']) ?></strong>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
@@ -44,8 +47,8 @@
|
||||
<div class="admin-grid">
|
||||
<section class="table-card">
|
||||
<div class="table-card-header">
|
||||
<h2>Auftraege</h2>
|
||||
<span><?= h((string) count($bookings)) ?> Eintraege</span>
|
||||
<h2>Offene oder reservierte Anfragen</h2>
|
||||
<a href="<?= h(url('admin/anfragen')) ?>">Alle ansehen</a>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
@@ -53,26 +56,22 @@
|
||||
<tr>
|
||||
<th>Referenz</th>
|
||||
<th>Kunde</th>
|
||||
<th>Termin</th>
|
||||
<th>Zeitraum</th>
|
||||
<th>Status</th>
|
||||
<th>Zahlung</th>
|
||||
<th>Preis</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($bookings as $booking): ?>
|
||||
<?php foreach ($requests as $booking): ?>
|
||||
<tr>
|
||||
<td><a href="<?= h(url('admin/order?id=' . urlencode($booking['id']))) ?>"><?= h($booking['reference']) ?></a></td>
|
||||
<td><?= h($booking['customer']['name']) ?></td>
|
||||
<td><?= h(formatDate($booking['start_date'])) ?> - <?= h(formatDate($booking['end_date'])) ?></td>
|
||||
<td><?= h($booking['status_label']) ?></td>
|
||||
<td><?= h($booking['payment_status_label']) ?></td>
|
||||
<td><?= h(formatCurrency((int) $booking['subtotal_cents'])) ?></td>
|
||||
<td><?= h(formatDate($booking['start_date'])) ?> bis <?= h(formatDate($booking['end_date'])) ?></td>
|
||||
<td><span class="<?= h(statusPillClass((string) $booking['status'])) ?>"><?= h($booking['status_label']) ?></span></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($bookings === []): ?>
|
||||
<?php if ($requests === []): ?>
|
||||
<tr>
|
||||
<td colspan="6">Noch keine Auftraege vorhanden.</td>
|
||||
<td colspan="4">Aktuell gibt es keine offenen Anfragen.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
@@ -82,38 +81,60 @@
|
||||
|
||||
<section class="table-card">
|
||||
<div class="table-card-header">
|
||||
<h2>Rechnungen</h2>
|
||||
<span><?= h((string) count($invoices)) ?> Eintraege</span>
|
||||
<h2>Nächste Einsätze</h2>
|
||||
<a href="<?= h(url('admin/kalender')) ?>">Zum Kalender</a>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nummer</th>
|
||||
<th>Auftrag</th>
|
||||
<th>Faellig</th>
|
||||
<th>Zahlungsart</th>
|
||||
<th>Gesamt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($invoices as $invoice): ?>
|
||||
<tr>
|
||||
<td><a href="<?= h(url('admin/invoice/pdf?id=' . urlencode($invoice['id']))) ?>" target="_blank"><?= h($invoice['invoice_number']) ?></a></td>
|
||||
<td><?= h($invoice['booking_id']) ?></td>
|
||||
<td><?= h(formatDate($invoice['due_date'])) ?></td>
|
||||
<td><?= h($invoice['payment_method_label']) ?></td>
|
||||
<td><?= h(formatCurrency((int) $invoice['total_cents'])) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($invoices === []): ?>
|
||||
<tr>
|
||||
<td colspan="5">Noch keine Rechnungen erstellt.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="stack-list">
|
||||
<?php foreach ($upcomingBookings as $booking): ?>
|
||||
<article class="stack-item">
|
||||
<div>
|
||||
<strong><?= h($booking['reference']) ?></strong>
|
||||
<span><?= h($booking['customer']['name']) ?> · <?= h(formatDate($booking['start_date'])) ?> bis <?= h(formatDate($booking['end_date'])) ?></span>
|
||||
</div>
|
||||
<span class="<?= h(statusPillClass((string) $booking['status'])) ?>"><?= h($booking['status_label']) ?></span>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($upcomingBookings === []): ?>
|
||||
<p class="empty-state">Noch keine Termine geplant.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="table-card">
|
||||
<div class="table-card-header">
|
||||
<h2>Zuletzt erstellte Rechnungen</h2>
|
||||
<a href="<?= h(url('admin/rechnungen')) ?>">Alle Rechnungen</a>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nummer</th>
|
||||
<th>Auftrag</th>
|
||||
<th>Fällig</th>
|
||||
<th>Zahlungsart</th>
|
||||
<th>Gesamt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($invoices as $invoice): ?>
|
||||
<tr>
|
||||
<td><a href="<?= h(url('admin/invoice/pdf?id=' . urlencode($invoice['id']))) ?>" target="_blank"><?= h($invoice['invoice_number']) ?></a></td>
|
||||
<td><?= h($invoice['booking_id']) ?></td>
|
||||
<td><?= h(formatDate($invoice['due_date'])) ?></td>
|
||||
<td><?= h($invoice['payment_method_label']) ?></td>
|
||||
<td><?= h(formatCurrency((int) $invoice['total_cents'])) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($invoices === []): ?>
|
||||
<tr>
|
||||
<td colspan="5">Noch keine Rechnungen vorhanden.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<section class="admin-section">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<p class="eyebrow">Rechnungen</p>
|
||||
<h1>Erstellte Rechnungen und Zahlungsstand</h1>
|
||||
</div>
|
||||
<a class="button-secondary" href="<?= h(url('admin/buchungen')) ?>">Zu den Buchungen</a>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($flashSuccess)): ?>
|
||||
<div class="flash flash-success"><?= h((string) $flashSuccess) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($flashError)): ?>
|
||||
<div class="flash flash-error"><?= h((string) $flashError) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="table-card">
|
||||
<div class="table-card-header">
|
||||
<h2>Rechnungsliste</h2>
|
||||
<span><?= h((string) count($invoices)) ?> Einträge</span>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rechnung</th>
|
||||
<th>Auftrag</th>
|
||||
<th>Ausgestellt</th>
|
||||
<th>Fällig</th>
|
||||
<th>Zahlungsart</th>
|
||||
<th>Gesamt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($invoices as $invoice): ?>
|
||||
<tr>
|
||||
<td><a href="<?= h(url('admin/invoice/pdf?id=' . urlencode($invoice['id']))) ?>" target="_blank"><?= h($invoice['invoice_number']) ?></a></td>
|
||||
<td><?= h($invoice['booking_id']) ?></td>
|
||||
<td><?= h(formatDate($invoice['issue_date'])) ?></td>
|
||||
<td><?= h(formatDate($invoice['due_date'])) ?></td>
|
||||
<td><?= h($invoice['payment_method_label']) ?></td>
|
||||
<td><?= h(formatCurrency((int) $invoice['total_cents'])) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($invoices === []): ?>
|
||||
<tr>
|
||||
<td colspan="6">Noch keine Rechnungen vorhanden.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<section class="admin-login-section">
|
||||
<div class="admin-login-card">
|
||||
<p class="eyebrow">Verwaltung</p>
|
||||
<h1>Admin-Login</h1>
|
||||
<p>Hier verwaltest du Anfragen, Kundenbestellungen und Rechnungen.</p>
|
||||
<h1>Interner Zugang</h1>
|
||||
<p>Hier verwalten Sie Anfragen, Buchungen, Kunden und Rechnungen.</p>
|
||||
|
||||
<?php if (!empty($flashSuccess)): ?>
|
||||
<div class="flash flash-success"><?= h((string) $flashSuccess) ?></div>
|
||||
@@ -12,9 +12,10 @@
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="<?= h(url('admin/login')) ?>" class="stack-form">
|
||||
<?= csrfField() ?>
|
||||
<label>
|
||||
<span>Benutzername</span>
|
||||
<input type="text" name="username" value="admin" required>
|
||||
<input type="text" name="username" value="<?= h((string) $defaultUsername) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Passwort</span>
|
||||
@@ -24,3 +25,4 @@
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
+14
-10
@@ -4,7 +4,7 @@
|
||||
<p class="eyebrow">Auftragsdetail</p>
|
||||
<h1><?= h($booking['reference']) ?></h1>
|
||||
</div>
|
||||
<a class="button-secondary" href="<?= h(url('admin')) ?>">Zurueck zum Dashboard</a>
|
||||
<a class="button-secondary" href="<?= h(url('admin/buchungen')) ?>">Zurück zur Übersicht</a>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($flashSuccess)): ?>
|
||||
@@ -24,8 +24,9 @@
|
||||
<div><dt>Telefon</dt><dd><?= h($booking['customer']['phone']) ?></dd></div>
|
||||
<div><dt>Adresse</dt><dd><?= h($booking['customer']['street']) ?>, <?= h($booking['customer']['postal_code']) ?> <?= h($booking['customer']['city']) ?></dd></div>
|
||||
<div><dt>Anlass</dt><dd><?= h($booking['customer']['event_type'] ?: '-') ?></dd></div>
|
||||
<div><dt>Ort</dt><dd><?= h($booking['customer']['event_location'] ?: '-') ?></dd></div>
|
||||
<div><dt>Veranstaltungsort</dt><dd><?= h($booking['customer']['event_location'] ?: '-') ?></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>Leistung</dt><dd><?= h($booking['delivery_mode_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>
|
||||
@@ -35,6 +36,7 @@
|
||||
<article class="table-card">
|
||||
<h2>Verwaltung</h2>
|
||||
<form method="post" action="<?= h(url('admin/order/update')) ?>" class="stack-form">
|
||||
<?= csrfField() ?>
|
||||
<input type="hidden" name="booking_id" value="<?= h($booking['id']) ?>">
|
||||
<label>
|
||||
<span>Status</span>
|
||||
@@ -60,7 +62,7 @@
|
||||
<span>Interne Notiz</span>
|
||||
<textarea name="internal_notes" rows="4"><?= h((string) $booking['internal_notes']) ?></textarea>
|
||||
</label>
|
||||
<button type="submit" class="button-primary">Aenderungen speichern</button>
|
||||
<button type="submit" class="button-primary">Änderungen speichern</button>
|
||||
</form>
|
||||
</article>
|
||||
</div>
|
||||
@@ -69,31 +71,32 @@
|
||||
<article class="table-card">
|
||||
<h2>Rechnung</h2>
|
||||
<?php if ($invoice !== null): ?>
|
||||
<p>Rechnung <strong><?= h($invoice['invoice_number']) ?></strong> ist erstellt.</p>
|
||||
<p>Rechnung <strong><?= h($invoice['invoice_number']) ?></strong> wurde bereits erzeugt.</p>
|
||||
<ul class="check-list compact-list">
|
||||
<li>Faellig am <?= h(formatDate($invoice['due_date'])) ?></li>
|
||||
<li>Fällig am <?= h(formatDate($invoice['due_date'])) ?></li>
|
||||
<li>Gesamtbetrag <?= h(formatCurrency((int) $invoice['total_cents'])) ?></li>
|
||||
<li>Zahlungsart <?= h($invoice['payment_method_label']) ?></li>
|
||||
</ul>
|
||||
<a class="button-secondary" href="<?= h(url('admin/invoice/pdf?id=' . urlencode($invoice['id']))) ?>" target="_blank">PDF oeffnen</a>
|
||||
<a class="button-secondary" href="<?= h(url('admin/invoice/pdf?id=' . urlencode($invoice['id']))) ?>" target="_blank">PDF öffnen</a>
|
||||
<?php else: ?>
|
||||
<p>Fuer diesen Auftrag wurde noch keine Rechnung erstellt.</p>
|
||||
<p>Für diesen Auftrag wurde noch keine Rechnung erstellt.</p>
|
||||
<form method="post" action="<?= h(url('admin/order/invoice')) ?>" class="stack-form">
|
||||
<?= csrfField() ?>
|
||||
<input type="hidden" name="booking_id" value="<?= h($booking['id']) ?>">
|
||||
<label>
|
||||
<span>Faelligkeitsdatum</span>
|
||||
<span>Fälligkeitsdatum</span>
|
||||
<input type="date" name="due_date">
|
||||
</label>
|
||||
<label>
|
||||
<span>Rechnungsnotiz</span>
|
||||
<textarea name="invoice_notes" rows="4">Vielen Dank fuer deinen Auftrag.</textarea>
|
||||
<textarea name="invoice_notes" rows="4">Vielen Dank für Ihren Auftrag.</textarea>
|
||||
</label>
|
||||
<button type="submit" class="button-primary">Rechnung erstellen</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
<article class="table-card">
|
||||
<h2>Systeminfos</h2>
|
||||
<h2>Systeminformationen</h2>
|
||||
<dl class="detail-list">
|
||||
<div><dt>ID</dt><dd><?= h($booking['id']) ?></dd></div>
|
||||
<div><dt>Quelle</dt><dd><?= h($booking['source']) ?></dd></div>
|
||||
@@ -103,3 +106,4 @@
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<section class="admin-section">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<p class="eyebrow">Anfragen</p>
|
||||
<h1>Offene und reservierte Anfragen</h1>
|
||||
</div>
|
||||
<a class="button-primary" href="<?= h(url('admin/create')) ?>">Manuelle Buchung anlegen</a>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($flashSuccess)): ?>
|
||||
<div class="flash flash-success"><?= h((string) $flashSuccess) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($flashError)): ?>
|
||||
<div class="flash flash-error"><?= h((string) $flashError) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="table-card">
|
||||
<div class="table-card-header">
|
||||
<h2>Zu bearbeitende Einträge</h2>
|
||||
<span><?= h((string) count($requests)) ?> Einträge</span>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Referenz</th>
|
||||
<th>Kunde</th>
|
||||
<th>Zeitraum</th>
|
||||
<th>Leistung</th>
|
||||
<th>Status</th>
|
||||
<th>Preis</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($requests as $booking): ?>
|
||||
<tr>
|
||||
<td><a href="<?= h(url('admin/order?id=' . urlencode($booking['id']))) ?>"><?= h($booking['reference']) ?></a></td>
|
||||
<td><?= h($booking['customer']['name']) ?></td>
|
||||
<td><?= h(formatDate($booking['start_date'])) ?> bis <?= h(formatDate($booking['end_date'])) ?></td>
|
||||
<td><?= h($booking['delivery_mode_label']) ?></td>
|
||||
<td><span class="<?= h(statusPillClass((string) $booking['status'])) ?>"><?= h($booking['status_label']) ?></span></td>
|
||||
<td><?= h(formatCurrency((int) $booking['subtotal_cents'])) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($requests === []): ?>
|
||||
<tr>
|
||||
<td colspan="6">Keine offenen Anfragen vorhanden.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<section class="admin-section narrow-section">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<p class="eyebrow">Einstellungen</p>
|
||||
<h1>Aktiver Systemstand</h1>
|
||||
</div>
|
||||
<a class="button-secondary" href="<?= h(url('admin')) ?>">Zurück zum Dashboard</a>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($flashSuccess)): ?>
|
||||
<div class="flash flash-success"><?= h((string) $flashSuccess) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($flashError)): ?>
|
||||
<div class="flash flash-error"><?= h((string) $flashError) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="detail-grid">
|
||||
<article class="table-card">
|
||||
<h2>Unternehmen & Preise</h2>
|
||||
<dl class="detail-list">
|
||||
<div><dt>Firma</dt><dd><?= h($company['name']) ?></dd></div>
|
||||
<div><dt>Servicegebiet</dt><dd><?= h($company['service_area']) ?></dd></div>
|
||||
<div><dt>Preis pro Miettag</dt><dd><?= h(formatCurrency((int) $pricing['default_day_rate_cents'])) ?></dd></div>
|
||||
<div><dt>Kontakt</dt><dd><?= h($company['email']) ?> · <?= h($company['phone']) ?></dd></div>
|
||||
<div><dt>Abholung</dt><dd><?= h($company['pickup_window']) ?></dd></div>
|
||||
<div><dt>Rückgabe</dt><dd><?= h($company['return_window']) ?></dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<article class="table-card">
|
||||
<h2>Datenhaltung</h2>
|
||||
<dl class="detail-list">
|
||||
<div><dt>Aktiver Treiber</dt><dd><?= h($runtime['storage_driver']) ?></dd></div>
|
||||
<div><dt>Tabellenpräfix</dt><dd><?= h((string) $runtime['table_prefix']) ?></dd></div>
|
||||
<div><dt>Datenbank aktiviert</dt><dd><?= h(!empty($runtime['database_enabled']) ? 'Ja' : 'Nein') ?></dd></div>
|
||||
<div><dt>Host</dt><dd><?= h((string) ($runtime['database_host'] ?: '-')) ?></dd></div>
|
||||
<div><dt>Datenbank</dt><dd><?= h((string) ($runtime['database_name'] ?: '-')) ?></dd></div>
|
||||
<?php if (!empty($runtime['fallback_reason'])): ?>
|
||||
<div><dt>Hinweis</dt><dd><?= h($runtime['fallback_reason']) ?></dd></div>
|
||||
<?php endif; ?>
|
||||
</dl>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
+156
-330
@@ -1,359 +1,185 @@
|
||||
<?php
|
||||
$dayRate = $config['pricing']['default_day_rate_cents'];
|
||||
$company = $config['company'];
|
||||
?>
|
||||
<section class="hero-section">
|
||||
<section class="hero">
|
||||
<div class="hero-copy">
|
||||
<p class="eyebrow">Fotobox-Vermietung neu gedacht</p>
|
||||
<h1>Fotobox mieten. Online anfragen. Fotos direkt aufs Handy.</h1>
|
||||
<p class="eyebrow">Professionelle Fotobox-Vermietung</p>
|
||||
<h1>Fotobox mieten für Hochzeiten, Geburtstage und Firmenfeiern.</h1>
|
||||
<p class="hero-text">
|
||||
Fuer Hochzeiten, Geburtstage, Firmenfeiern und Jubilaeen: hochwertige Fotobox-Technik,
|
||||
einfache Bedienung, flexible Lieferung und ein klarer Buchungsprozess ohne Shop-Chaos.
|
||||
Hochwertige Technik, klare Preislogik pro Miettag und ein Buchungsablauf,
|
||||
der auch kaufmännisch sauber funktioniert. Anfrage senden, Bestätigung erhalten,
|
||||
Bilder digital bekommen.
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a class="button-primary" href="#buchung">Verfuegbarkeit pruefen</a>
|
||||
<a class="button-secondary" href="#ablauf">So funktioniert die Miete</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>
|
||||
</div>
|
||||
<div class="hero-highlight-grid">
|
||||
<article class="hero-highlight-card">
|
||||
<span>Technik</span>
|
||||
<strong>DSLR, Blitz und Softbox</strong>
|
||||
</article>
|
||||
<article class="hero-highlight-card">
|
||||
<span>Sharing</span>
|
||||
<strong>WLAN-Download direkt vor Ort</strong>
|
||||
</article>
|
||||
<article class="hero-highlight-card">
|
||||
<span>Logistik</span>
|
||||
<strong>Abholung oder Lieferung</strong>
|
||||
</article>
|
||||
<article class="hero-highlight-card">
|
||||
<span>Preis</span>
|
||||
<strong>99,99 EUR pro Kalendertag</strong>
|
||||
</article>
|
||||
<div class="trust-grid">
|
||||
<?php foreach ($trustFacts as $fact): ?>
|
||||
<article class="trust-card">
|
||||
<span><?= h($fact['label']) ?></span>
|
||||
<strong><?= h($fact['value']) ?></strong>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-card">
|
||||
<div class="hero-card-panel hero-card-panel-top">
|
||||
<span>Produktionsreif fuer dein Event</span>
|
||||
<strong>einsatzklar in wenigen Minuten</strong>
|
||||
|
||||
<aside class="hero-panel">
|
||||
<div class="hero-panel-top">
|
||||
<span>Service mit Struktur</span>
|
||||
<strong>Vom ersten Termin bis zur Rechnung</strong>
|
||||
</div>
|
||||
<div class="hero-card-visual">
|
||||
<div class="device-plinth"></div>
|
||||
<div class="photo-strip photo-strip-left">
|
||||
<div class="device-stage">
|
||||
<div class="device-glow"></div>
|
||||
<div class="device-card device-card-left">
|
||||
<span>DSLR</span>
|
||||
<strong>Scharfe Bilder</strong>
|
||||
</div>
|
||||
<div class="device-card device-card-right">
|
||||
<span>WLAN</span>
|
||||
<span>Live</span>
|
||||
<span>Shots</span>
|
||||
<strong>Direkt aufs Handy</strong>
|
||||
</div>
|
||||
<div class="camera-tower">
|
||||
<div class="camera-head">
|
||||
<div class="camera-lens"></div>
|
||||
<div class="camera-flash"></div>
|
||||
<div class="device-illustration">
|
||||
<div class="device-head">
|
||||
<div class="device-lens"></div>
|
||||
<div class="device-flash"></div>
|
||||
</div>
|
||||
<div class="camera-stand"></div>
|
||||
<div class="camera-base"></div>
|
||||
</div>
|
||||
<div class="photo-strip photo-strip-right">
|
||||
<span>Reel</span>
|
||||
<span>Share</span>
|
||||
<span>Smile</span>
|
||||
<div class="device-neck"></div>
|
||||
<div class="device-body"></div>
|
||||
<div class="device-base"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-card-panel">
|
||||
<span>Digitale Galerie inklusive</span>
|
||||
<strong>Rueckgabe bis 13:00 Uhr</strong>
|
||||
<div class="hero-panel-bottom">
|
||||
<div>
|
||||
<span>Abholung</span>
|
||||
<strong><?= h($company['pickup_window']) ?></strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Rückgabe</span>
|
||||
<strong><?= h($company['return_window']) ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<section class="trust-bar">
|
||||
<article>
|
||||
<span>Tagessatz</span>
|
||||
<strong>99,99 EUR / Tag</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>Bilduebergabe</span>
|
||||
<strong>alle Fotos digital</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>Zahlung</span>
|
||||
<strong>Rechnung oder PayPal</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>Verfuegbarkeit</span>
|
||||
<strong>online und verwaltbar</strong>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="feature-strip" id="leistungen">
|
||||
<article>
|
||||
<h2>Technik, die nicht zickt</h2>
|
||||
<p>Spiegelreflexkamera, Bildschirm und Studioblitz mit Softbox sorgen fuer helle, scharfe Bilder bei jedem Anlass.</p>
|
||||
</article>
|
||||
<article>
|
||||
<h2>Direkt aufs Handy</h2>
|
||||
<p>Per WLAN koennen deine Gaeste ihre Fotos sofort laden und teilen. Nach dem Event gibt es alle Bilder digital.</p>
|
||||
</article>
|
||||
<article>
|
||||
<h2>Flexible Logistik</h2>
|
||||
<p>Selbst abholen, liefern lassen oder auf Wunsch mit Aufbau und Vor-Ort-Unterstuetzung buchen.</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="content-grid" id="ablauf">
|
||||
<div class="content-block">
|
||||
<p class="eyebrow">Ablauf</p>
|
||||
<h2>So laeuft die Miete ab</h2>
|
||||
<ol class="step-list">
|
||||
<li>
|
||||
<strong><span class="step-number">1</span> Zeitraum waehlen</strong>
|
||||
<span>Du waehlst Mietbeginn und Mietende und siehst sofort die voraussichtlichen Kosten.</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong><span class="step-number">2</span> Leistung festlegen</strong>
|
||||
<span>Selbstabholung, Lieferung mit Aufbau oder Vor-Ort-Betreuung passend zu deinem Event.</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong><span class="step-number">3</span> Anfrage absenden</strong>
|
||||
<span>Wir speichern alle Kundendaten und bereiten auf Wunsch direkt die Rechnungsabwicklung vor.</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong><span class="step-number">4</span> Fotos geniessen</strong>
|
||||
<span>Am Eventtag steht die Box bereit und danach bekommst du alle Bilder digital zur Weitergabe.</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="content-block equipment-block">
|
||||
<p class="eyebrow">Ausstattung</p>
|
||||
<h2>Alles drin fuer einen reibungslosen Party-Hit</h2>
|
||||
<ul class="check-list">
|
||||
<li>Spiegelreflexkamera fuer gestochen scharfe Aufnahmen</li>
|
||||
<li>Bildschirm mit einfacher Bedienung per Touch</li>
|
||||
<li>Studioblitz mit grosser Softbox fuer gleichmaessiges Licht</li>
|
||||
<li>Digitale Uebergabe aller Fotos nach dem Event</li>
|
||||
<li>Optionaler Hintergrund und Betreuung vor Ort</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="pricing-panel">
|
||||
<div>
|
||||
<p class="eyebrow">Preis und Logistik</p>
|
||||
<h2>Transparent statt versteckt</h2>
|
||||
<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>
|
||||
Standardmaessig berechnen wir <strong><?= h(formatCurrency($dayRate)) ?></strong> pro Kalendertag.
|
||||
Mietbeginn und Mietende zaehlen beide mit. Selbstabholung spart Zeit in der Abstimmung,
|
||||
Lieferung und Aufbau machen es vor Ort noch entspannter.
|
||||
Die Agenten-Recherche hat klar gezeigt: Kundenfreundlich ist eine verständliche Service-Seite
|
||||
mit Preis, Ablauf, Verfügbarkeit und einem Verwaltungsprozess im Hintergrund.
|
||||
</p>
|
||||
</div>
|
||||
<div class="pricing-aside">
|
||||
<div>
|
||||
<span>Tagespreis</span>
|
||||
<strong><?= h(formatCurrency($dayRate)) ?></strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Zahlungsarten</span>
|
||||
<strong>Rechnung / Ueberweisung und PayPal</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Servicefenster</span>
|
||||
<strong>Abholung ab 17:00 Uhr, Rueckgabe bis 13:00 Uhr</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="occasion-section">
|
||||
<div class="section-heading">
|
||||
<p class="eyebrow">Anlaesse</p>
|
||||
<h2>Passend fuer kleine Feiern und grosse Events</h2>
|
||||
</div>
|
||||
<div class="occasion-grid">
|
||||
<article><strong>Hochzeiten</strong><span>emotionale Erinnerungen, direkt teilbar</span></article>
|
||||
<article><strong>Geburtstage</strong><span>einfacher Aufbau, unkomplizierter Spass</span></article>
|
||||
<article><strong>Firmenfeiern</strong><span>sauberer Ablauf mit Rechnung und Verwaltung</span></article>
|
||||
<article><strong>Jubilaeen</strong><span>hochwertige Bilder ohne Fotostress</span></article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="availability-section">
|
||||
<div>
|
||||
<p class="eyebrow">Online-Verfuegbarkeit</p>
|
||||
<h2>Bereits reservierte Zeitraeume</h2>
|
||||
<p>Diese Liste zeigt geblockte oder bestaetigte Termine aus dem Verwaltungssystem.</p>
|
||||
</div>
|
||||
<div class="availability-list">
|
||||
<?php if ($bookings === []): ?>
|
||||
<article class="availability-card availability-card-empty">
|
||||
<strong>Aktuell sind keine festen Reservierungen hinterlegt.</strong>
|
||||
<span>Du kannst direkt eine neue Anfrage senden.</span>
|
||||
</article>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($bookings as $booking): ?>
|
||||
<article class="availability-card">
|
||||
<strong><?= h($booking['reference']) ?></strong>
|
||||
<span><?= h(formatDate($booking['start_date'])) ?> bis <?= h(formatDate($booking['end_date'])) ?></span>
|
||||
<small><?= h($booking['status_label']) ?></small>
|
||||
<div class="feature-card-grid">
|
||||
<?php foreach ($featureCards as $card): ?>
|
||||
<article class="feature-card">
|
||||
<h3><?= h($card['title']) ?></h3>
|
||||
<p><?= h($card['text']) ?></p>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="booking-section" id="buchung">
|
||||
<div class="booking-copy">
|
||||
<p class="eyebrow">Buchungsanfrage</p>
|
||||
<h2>Fotobox jetzt anfragen</h2>
|
||||
<p>
|
||||
Zwei Termine auswaehlen, Wunschleistung festlegen und Kundendaten hinterlegen.
|
||||
Die Verwaltung kann deine Anfrage danach direkt bestaetigen, als Kundenbestellung uebernehmen und eine Rechnung erzeugen.
|
||||
</p>
|
||||
<div class="booking-info-card">
|
||||
<strong>Kontakt fuer Rueckfragen</strong>
|
||||
<span><?= h($company['email']) ?></span>
|
||||
<span><?= h($company['phone']) ?></span>
|
||||
<section class="section split-section">
|
||||
<div class="content-card">
|
||||
<p class="eyebrow">Ablauf</p>
|
||||
<h2>So läuft Ihre Anfrage ab</h2>
|
||||
<ol class="step-list">
|
||||
<?php foreach ($processSteps as $index => $step): ?>
|
||||
<li>
|
||||
<span class="step-number"><?= h((string) ($index + 1)) ?></span>
|
||||
<div>
|
||||
<strong><?= h($step['title']) ?></strong>
|
||||
<p><?= h($step['text']) ?></p>
|
||||
</div>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="content-card editorial-card">
|
||||
<p class="eyebrow">Standards</p>
|
||||
<h2>Kommerziell gedacht, nicht nur hübsch.</h2>
|
||||
<ul class="check-list">
|
||||
<?php foreach ($serviceStandards as $standard): ?>
|
||||
<li><?= h($standard) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-heading">
|
||||
<p class="eyebrow">Leistungsmodule</p>
|
||||
<h2>Technik, Eventbetrieb und Verwaltung greifen ineinander.</h2>
|
||||
</div>
|
||||
<div class="module-grid">
|
||||
<?php foreach ($serviceModules as $module): ?>
|
||||
<article class="module-card">
|
||||
<h3><?= h($module['title']) ?></h3>
|
||||
<ul class="check-list compact-list">
|
||||
<?php foreach ($module['items'] as $item): ?>
|
||||
<li><?= h($item) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-heading">
|
||||
<p class="eyebrow">Anlässe</p>
|
||||
<h2>Für Privatfeiern und professionelle Events geeignet.</h2>
|
||||
</div>
|
||||
<div class="occasion-grid">
|
||||
<?php foreach ($occasionCards as $occasion): ?>
|
||||
<article class="occasion-card">
|
||||
<strong><?= h($occasion['title']) ?></strong>
|
||||
<p><?= h($occasion['text']) ?></p>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
<div class="availability-list">
|
||||
<?php if ($bookings === []): ?>
|
||||
<article class="availability-card">
|
||||
<strong>Aktuell ist noch kein Zeitraum blockiert.</strong>
|
||||
<span>Sie können direkt eine neue Buchungsanfrage stellen.</span>
|
||||
</article>
|
||||
<?php endif; ?>
|
||||
<?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>
|
||||
</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>
|
||||
</div>
|
||||
<div class="booking-form-card">
|
||||
<?php if (!empty($flashSuccess)): ?>
|
||||
<div class="flash flash-success"><?= h((string) $flashSuccess) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($flashError)): ?>
|
||||
<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) ?>">
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
<span class="form-step">Schritt 1</span>
|
||||
<h3>Zeitraum waehlen</h3>
|
||||
</div>
|
||||
<div class="form-grid form-grid-two">
|
||||
<label>
|
||||
<span>Mietbeginn</span>
|
||||
<input type="date" name="start_date" data-booking-start value="<?= h((string) ($old['start_date'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Mietende</span>
|
||||
<input type="date" name="end_date" data-booking-end value="<?= h((string) ($old['end_date'] ?? '')) ?>" required>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
<span class="form-step">Schritt 2</span>
|
||||
<h3>Leistung und Zahlung</h3>
|
||||
</div>
|
||||
<div class="form-grid form-grid-two">
|
||||
<label>
|
||||
<span>Lieferart</span>
|
||||
<select name="delivery_mode">
|
||||
<option value="self_pickup" <?= selected((string) ($old['delivery_mode'] ?? 'self_pickup'), '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>
|
||||
</label>
|
||||
<label>
|
||||
<span>Zahlungsart</span>
|
||||
<select name="payment_method">
|
||||
<option value="invoice_transfer" <?= selected((string) ($old['payment_method'] ?? 'invoice_transfer'), 'invoice_transfer') ?>>Rechnung / Ueberweisung</option>
|
||||
<option value="paypal" <?= selected((string) ($old['payment_method'] ?? ''), 'paypal') ?>>PayPal</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
<span class="form-step">Schritt 3</span>
|
||||
<h3>Kontaktdaten</h3>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<label>
|
||||
<span>Name</span>
|
||||
<input type="text" name="customer_name" value="<?= h((string) ($old['customer_name'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Firma</span>
|
||||
<input type="text" name="company" value="<?= h((string) ($old['company'] ?? '')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>E-Mail</span>
|
||||
<input type="email" name="email" value="<?= h((string) ($old['email'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Telefon</span>
|
||||
<input type="text" name="phone" value="<?= h((string) ($old['phone'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Strasse</span>
|
||||
<input type="text" name="street" value="<?= h((string) ($old['street'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>PLZ</span>
|
||||
<input type="text" name="postal_code" value="<?= h((string) ($old['postal_code'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Ort</span>
|
||||
<input type="text" name="city" value="<?= h((string) ($old['city'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Anlass</span>
|
||||
<input type="text" name="event_type" value="<?= h((string) ($old['event_type'] ?? '')) ?>" placeholder="z. B. Hochzeit oder Sommerfest">
|
||||
</label>
|
||||
<label class="field-full">
|
||||
<span>Veranstaltungsort</span>
|
||||
<input type="text" name="event_location" value="<?= h((string) ($old['event_location'] ?? '')) ?>" placeholder="Location oder Stadt">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label>
|
||||
<span>Nachricht</span>
|
||||
<textarea name="notes_customer" rows="4" placeholder="Sonderwuensche, Lieferdetails oder Aufbauhinweise"><?= h((string) ($old['notes_customer'] ?? '')) ?></textarea>
|
||||
</label>
|
||||
|
||||
<div class="price-summary">
|
||||
<div>
|
||||
<span>Mietdauer</span>
|
||||
<strong data-summary-days>Noch nicht gewaehlt</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Tagespreis</span>
|
||||
<strong><?= h(formatCurrency($dayRate)) ?></strong>
|
||||
</div>
|
||||
<div class="price-summary-total">
|
||||
<span>Gesamtpreis</span>
|
||||
<strong data-summary-total><?= h(formatCurrency($dayRate)) ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="button-primary button-block">Buchungsanfrage senden</button>
|
||||
<p class="form-note">Keine Sofortabbuchung. Der Verwalter bestaetigt den Termin, pflegt die Buchung und kann direkt eine Rechnung erzeugen.</p>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="faq-section" id="faq">
|
||||
<div>
|
||||
<p class="eyebrow">FAQ</p>
|
||||
<h2>Wichtige Fragen auf einen Blick</h2>
|
||||
</div>
|
||||
<div class="faq-list">
|
||||
<article>
|
||||
<h3>Wie schnell ist die Fotobox einsatzbereit?</h3>
|
||||
<p>Durch den einfachen Aufbau und die kurze Einweisung ist die Box in wenigen Minuten startklar.</p>
|
||||
</article>
|
||||
<article>
|
||||
<h3>Bekommen wir alle Fotos?</h3>
|
||||
<p>Ja. Alle Bilder werden nach dem Event digital zur Verfuegung gestellt. Auf Wunsch koennen Gaeste sie schon vor Ort aufs Handy laden.</p>
|
||||
</article>
|
||||
<article>
|
||||
<h3>Kann ich auch Lieferung und Betreuung buchen?</h3>
|
||||
<p>Ja. Du kannst zwischen Selbstabholung, Lieferung mit Aufbau oder zusaetzlicher Vor-Ort-Unterstuetzung waehlen.</p>
|
||||
</article>
|
||||
<article>
|
||||
<h3>Ist PayPal schon moeglich?</h3>
|
||||
<p>Ja, PayPal ist als Zahlungsart im Ablauf vorgesehen. Sobald du echte MySQL- und PayPal-Daten hinterlegst, laesst sich der operative Betrieb direkt anbinden.</p>
|
||||
</article>
|
||||
<div class="content-card emphasis-card">
|
||||
<p class="eyebrow">Nächster Schritt</p>
|
||||
<h2>In wenigen Minuten zur Anfrage</h2>
|
||||
<ul class="check-list">
|
||||
<?php foreach ($bookingChecklist as $item): ?>
|
||||
<li><?= h($item) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<div class="pricing-example-list">
|
||||
<?php foreach ($pricingExamples as $example): ?>
|
||||
<article>
|
||||
<strong><?= h($example['title']) ?></strong>
|
||||
<span><?= h($example['text']) ?></span>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<a class="button-primary button-block" href="<?= h(url('buchen')) ?>">Zur Buchungsanfrage</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
+106
-22
@@ -1,8 +1,31 @@
|
||||
<?php
|
||||
$metaTitle = isset($pageTitle) ? $pageTitle . ' | Fotobox Moments' : 'Fotobox Moments';
|
||||
$app = appConfig();
|
||||
$company = $app['company'];
|
||||
$metaTitle = isset($pageTitle) ? $pageTitle . ' | ' . $company['name'] : $company['name'];
|
||||
$metaDescription = $metaDescription ?? 'Professionelle Fotobox-Vermietung mit klarer Buchungsanfrage und Verwaltungsbereich.';
|
||||
$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' => 'Preise', 'path' => '/preise'],
|
||||
['label' => 'Verfügbarkeit', 'path' => '/verfuegbarkeit'],
|
||||
['label' => 'Ablauf', 'path' => '/ablauf'],
|
||||
['label' => 'FAQ', 'path' => '/faq'],
|
||||
['label' => 'Kontakt', 'path' => '/kontakt'],
|
||||
];
|
||||
|
||||
$adminNav = [
|
||||
['label' => 'Dashboard', 'path' => '/admin'],
|
||||
['label' => 'Anfragen', 'path' => '/admin/anfragen'],
|
||||
['label' => 'Buchungen', 'path' => '/admin/buchungen'],
|
||||
['label' => 'Kalender', 'path' => '/admin/kalender'],
|
||||
['label' => 'Kunden', 'path' => '/admin/kunden'],
|
||||
['label' => 'Rechnungen', 'path' => '/admin/rechnungen'],
|
||||
['label' => 'Einstellungen', 'path' => '/admin/einstellungen'],
|
||||
];
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
@@ -10,39 +33,100 @@ $scriptVersion = is_file(dirname(__DIR__) . '/assets/app.js') ? (string) filemti
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= h($metaTitle) ?></title>
|
||||
<meta name="description" content="Fotobox mieten mit einfacher Online-Anfrage, flexibler Lieferung und kompletter Admin-Verwaltung.">
|
||||
<meta name="description" content="<?= h($metaDescription) ?>">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="<?= h($metaTitle) ?>">
|
||||
<meta property="og:description" content="<?= h($metaDescription) ?>">
|
||||
<meta property="og:url" content="<?= h($company['website']) ?>">
|
||||
<link rel="stylesheet" href="<?= h(asset('assets/styles.css')) ?>?v=<?= h($styleVersion) ?>">
|
||||
<script defer src="<?= h(asset('assets/app.js')) ?>?v=<?= h($scriptVersion) ?>"></script>
|
||||
</head>
|
||||
<body class="<?= $isAdminArea ? 'theme-admin' : 'theme-public' ?>">
|
||||
<div class="page-shell">
|
||||
<header class="site-header">
|
||||
<a class="brand" href="<?= h($isAdminArea ? url('admin') : url('/')) ?>">
|
||||
<span class="brand-mark">FM</span>
|
||||
<span>
|
||||
<strong>Fotobox Moments</strong>
|
||||
<small>Vermietung & Verwaltung</small>
|
||||
</span>
|
||||
</a>
|
||||
<nav class="site-nav">
|
||||
<?php if (!$isAdminArea): ?>
|
||||
<div class="topbar">
|
||||
<div class="topbar-inner">
|
||||
<span><?= h($company['service_area']) ?></span>
|
||||
<span><?= h($company['response_time']) ?></span>
|
||||
<a href="mailto:<?= h($company['email']) ?>"><?= h($company['email']) ?></a>
|
||||
<a href="tel:<?= h($company['phone']) ?>"><?= h($company['phone']) ?></a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<header class="site-header <?= $isAdminArea ? 'site-header-admin' : 'site-header-public' ?>">
|
||||
<div class="header-inner">
|
||||
<a class="brand" href="<?= h($isAdminArea ? url('admin') : url('/')) ?>">
|
||||
<span class="brand-mark">FM</span>
|
||||
<span class="brand-copy">
|
||||
<strong><?= h($company['name']) ?></strong>
|
||||
<small><?= h($isAdminArea ? 'Verwaltung & Buchungen' : $company['tagline']) ?></small>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<?php if ($isAdminArea): ?>
|
||||
<a href="<?= h(url('admin')) ?>">Dashboard</a>
|
||||
<a href="<?= h(url('admin/create')) ?>">Bestellung anlegen</a>
|
||||
<form method="post" action="<?= h(url('admin/logout')) ?>">
|
||||
<button type="submit" class="ghost-button">Logout</button>
|
||||
</form>
|
||||
<nav class="site-nav site-nav-admin" aria-label="Admin-Navigation">
|
||||
<?php foreach ($adminNav as $item): ?>
|
||||
<a class="<?= str_starts_with($currentPath, $item['path']) && ($item['path'] !== '/admin' || $currentPath === '/admin') ? '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('admin/create')) ?>">Manuelle Buchung</a>
|
||||
<form method="post" action="<?= h(url('admin/logout')) ?>">
|
||||
<?= csrfField() ?>
|
||||
<button type="submit" class="ghost-button">Abmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<a href="#leistungen">Leistungen</a>
|
||||
<a href="#ablauf">Ablauf</a>
|
||||
<a href="#faq">FAQ</a>
|
||||
<a class="primary-link" href="#buchung">Verfuegbarkeit pruefen</a>
|
||||
<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>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<main class="site-main">
|
||||
<?php require $viewPath; ?>
|
||||
</main>
|
||||
|
||||
<?php if (!$isAdminArea): ?>
|
||||
<footer class="site-footer">
|
||||
<div class="footer-grid">
|
||||
<div>
|
||||
<h2><?= h($company['name']) ?></h2>
|
||||
<p><?= h($company['tagline']) ?></p>
|
||||
<p><?= h($company['service_area']) ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Kontakt</h3>
|
||||
<a href="mailto:<?= h($company['email']) ?>"><?= h($company['email']) ?></a>
|
||||
<a href="tel:<?= h($company['phone']) ?>"><?= h($company['phone']) ?></a>
|
||||
<span><?= h($company['address']['street']) ?></span>
|
||||
<span><?= h($company['address']['postal_code'] . ' ' . $company['address']['city']) ?></span>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Seiten</h3>
|
||||
<a href="<?= h(url('leistungen')) ?>">Leistungen</a>
|
||||
<a href="<?= h(url('preise')) ?>">Preise</a>
|
||||
<a href="<?= h(url('buchen')) ?>">Buchen</a>
|
||||
<a href="<?= h(url('faq')) ?>">FAQ</a>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Rechtliches</h3>
|
||||
<a href="<?= h(url('impressum')) ?>">Impressum</a>
|
||||
<a href="<?= h(url('datenschutz')) ?>">Datenschutz</a>
|
||||
<a href="<?= h(url('mietbedingungen')) ?>">Mietbedingungen</a>
|
||||
<a href="<?= h(url('admin/login')) ?>">Verwaltung</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="content-card">
|
||||
<ol class="step-list large-step-list">
|
||||
<?php foreach ($processSteps as $index => $step): ?>
|
||||
<li>
|
||||
<span class="step-number"><?= h((string) ($index + 1)) ?></span>
|
||||
<div>
|
||||
<strong><?= h($step['title']) ?></strong>
|
||||
<p><?= h($step['text']) ?></p>
|
||||
</div>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section split-section">
|
||||
<article class="content-card">
|
||||
<h2>Abholung und Rückgabe</h2>
|
||||
<ul class="check-list">
|
||||
<li><?= h($company['pickup_window']) ?></li>
|
||||
<li><?= h($company['return_window']) ?></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>
|
||||
<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>
|
||||
</ul>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<section class="page-hero">
|
||||
<p class="eyebrow">Buchungsanfrage</p>
|
||||
<h1>Fotobox jetzt anfragen.</h1>
|
||||
<p>
|
||||
Wählen Sie Ihren Zeitraum, legen Sie Leistungsart und Zahlungsart fest und senden Sie Ihre Anfrage direkt an die Verwaltung.
|
||||
</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>
|
||||
<ul class="check-list">
|
||||
<?php foreach ($bookingChecklist as $item): ?>
|
||||
<li><?= h($item) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<div class="contact-card">
|
||||
<strong>Rückfragen</strong>
|
||||
<a href="mailto:<?= h($company['email']) ?>"><?= h($company['email']) ?></a>
|
||||
<a href="tel:<?= h($company['phone']) ?>"><?= h($company['phone']) ?></a>
|
||||
</div>
|
||||
</article>
|
||||
<article class="content-card booking-card">
|
||||
<?php require dirname(__DIR__) . '/partials/public-booking-form.php'; ?>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<section class="page-hero">
|
||||
<p class="eyebrow">Datenschutz</p>
|
||||
<h1>Datenschutzerklärung</h1>
|
||||
<p>Die Hinweise bilden den aktuellen Funktionsumfang dieser Website ab. Vor dem Produktivstart sollten sie rechtlich geprüft und mit Ihren echten Unternehmensdaten ergänzt werden.</p>
|
||||
</section>
|
||||
|
||||
<section class="section legal-section">
|
||||
<article class="legal-card">
|
||||
<h2>1. Verantwortlicher</h2>
|
||||
<p><?= h($company['name']) ?></p>
|
||||
<p><?= h($company['address']['street']) ?>, <?= h($company['address']['postal_code'] . ' ' . $company['address']['city']) ?></p>
|
||||
<p>E-Mail: <a href="mailto:<?= h($company['email']) ?>"><?= h($company['email']) ?></a></p>
|
||||
<p><?= h($company['legal']['privacy_contact']) ?></p>
|
||||
</article>
|
||||
|
||||
<article class="legal-card">
|
||||
<h2>2. Zweck der Datenverarbeitung</h2>
|
||||
<p>Wir verarbeiten die im Buchungsformular eingegebenen Daten, um Ihre Anfrage zu prüfen, mit Ihnen Kontakt aufzunehmen, eine mögliche Buchung zu verwalten und bei Bedarf eine Rechnung zu erstellen.</p>
|
||||
<p>Dazu gehören insbesondere Name, Kontaktdaten, Rechnungsadresse, Veranstaltungsort, gewünschter Zeitraum, gewählte Leistungsart und Zahlungsart.</p>
|
||||
</article>
|
||||
|
||||
<article class="legal-card">
|
||||
<h2>3. Speicherdauer</h2>
|
||||
<p>Wir speichern Anfragen und daraus entstehende Buchungen nur so lange, wie es für die Bearbeitung, Vertragserfüllung, Nachweise und gesetzliche Aufbewahrungspflichten erforderlich ist.</p>
|
||||
</article>
|
||||
|
||||
<article class="legal-card">
|
||||
<h2>4. Ihre Rechte</h2>
|
||||
<p>Sie haben das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung sowie auf Widerspruch und Datenübertragbarkeit, soweit die gesetzlichen Voraussetzungen vorliegen.</p>
|
||||
</article>
|
||||
<article class="legal-card">
|
||||
<h2>5. Technische Hinweise</h2>
|
||||
<p>Diese Website verarbeitet Formulardaten serverseitig. Tracking- oder Drittanbieter-Dienste sind im aktuellen Stand nicht eingebunden. Sollten später Analyse- oder Zahlungsdienste ergänzt werden, muss diese Erklärung entsprechend erweitert werden.</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="faq-grid">
|
||||
<?php foreach ($faqItems as $item): ?>
|
||||
<article class="faq-card">
|
||||
<h2><?= h($item['question']) ?></h2>
|
||||
<p><?= h($item['answer']) ?></p>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<section class="page-hero">
|
||||
<p class="eyebrow">Impressum</p>
|
||||
<h1>Anbieterkennzeichnung</h1>
|
||||
<p>Bitte prüfen Sie die Angaben vor einem Live-Betrieb und ergänzen Sie fehlende Unternehmensdaten bei Bedarf.</p>
|
||||
</section>
|
||||
|
||||
<section class="section legal-section">
|
||||
<article class="legal-card">
|
||||
<h2>Angaben gemäß § 5 DDG</h2>
|
||||
<p><?= h($company['legal']['owner']) ?></p>
|
||||
<p><?= h($company['address']['street']) ?></p>
|
||||
<p><?= h($company['address']['postal_code'] . ' ' . $company['address']['city']) ?></p>
|
||||
<p>E-Mail: <a href="mailto:<?= h($company['email']) ?>"><?= h($company['email']) ?></a></p>
|
||||
<p>Telefon: <a href="tel:<?= h($company['phone']) ?>"><?= h($company['phone']) ?></a></p>
|
||||
<p>Website: <a href="<?= h($company['website']) ?>"><?= h($company['website']) ?></a></p>
|
||||
</article>
|
||||
|
||||
<article class="legal-card">
|
||||
<h2>Verantwortlich für den Inhalt</h2>
|
||||
<p><?= h($company['legal']['representative']) ?></p>
|
||||
<?php if ($company['legal']['vat_id'] !== ''): ?>
|
||||
<p>Umsatzsteuer-ID: <?= h($company['legal']['vat_id']) ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if ($company['legal']['register_court'] !== '' || $company['legal']['register_number'] !== ''): ?>
|
||||
<p>Registergericht: <?= h($company['legal']['register_court']) ?></p>
|
||||
<p>Registernummer: <?= h($company['legal']['register_number']) ?></p>
|
||||
<?php endif; ?>
|
||||
<p><?= h($company['legal']['dispute_notice']) ?></p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<section class="section split-section">
|
||||
<article class="content-card">
|
||||
<h2>Kontaktmöglichkeiten</h2>
|
||||
<div class="contact-card">
|
||||
<strong><?= h($company['name']) ?></strong>
|
||||
<a href="mailto:<?= h($company['email']) ?>"><?= h($company['email']) ?></a>
|
||||
<a href="tel:<?= h($company['phone']) ?>"><?= h($company['phone']) ?></a>
|
||||
<span><?= h($company['address']['street']) ?></span>
|
||||
<span><?= h($company['address']['postal_code'] . ' ' . $company['address']['city']) ?></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="content-card emphasis-card">
|
||||
<h2>Was wir schnell beantworten können</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>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<section class="page-hero">
|
||||
<p class="eyebrow">Leistungen & Ausstattung</p>
|
||||
<h1>Eine Fotobox, die technisch überzeugt und organisatorisch mitdenkt.</h1>
|
||||
<p>
|
||||
Diese Seite zeigt nicht nur die Technik, sondern den gesamten Service:
|
||||
Bildqualität, Bedienbarkeit, Logistik, digitale Übergabe und die kaufmännische Abwicklung.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="feature-card-grid">
|
||||
<?php foreach ($featureCards as $card): ?>
|
||||
<article class="feature-card">
|
||||
<h2><?= h($card['title']) ?></h2>
|
||||
<p><?= h($card['text']) ?></p>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section split-section">
|
||||
<?php foreach ($serviceModules as $module): ?>
|
||||
<article class="content-card">
|
||||
<h2><?= h($module['title']) ?></h2>
|
||||
<ul class="check-list">
|
||||
<?php foreach ($module['items'] as $item): ?>
|
||||
<li><?= h($item) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
<a class="button-primary" href="<?= h(url('buchen')) ?>">Jetzt anfragen</a>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<section class="page-hero">
|
||||
<p class="eyebrow">Mietbedingungen</p>
|
||||
<h1>Rahmenbedingungen für Anfrage, Bestätigung und Rückgabe.</h1>
|
||||
<p>Die Bedingungen sind als solide Grundlage für die Website angelegt und sollten vor einem verbindlichen Live-Betrieb fachlich geprüft werden.</p>
|
||||
</section>
|
||||
|
||||
<section class="section legal-section">
|
||||
<article class="legal-card">
|
||||
<h2>1. Anfrage und Bestätigung</h2>
|
||||
<p>Die Online-Eingabe stellt zunächst eine Buchungsanfrage dar. Ein Vertrag kommt erst zustande, wenn die Anfrage von uns bestätigt wird.</p>
|
||||
</article>
|
||||
<article class="legal-card">
|
||||
<h2>2. Mietdauer</h2>
|
||||
<p>Ein Miettag entspricht einer Übernachtung. Montag bis Dienstag zählt als 1 Miettag. Die Rückgabe muss nach dem Abholdatum liegen.</p>
|
||||
</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>
|
||||
</article>
|
||||
<article class="legal-card">
|
||||
<h2>4. Übergabe und Rückgabe</h2>
|
||||
<p><?= h($company['pickup_window']) ?>. <?= h($company['return_window']) ?>. Abweichungen und Zusatzleistungen werden individuell vereinbart.</p>
|
||||
</article>
|
||||
<article class="legal-card">
|
||||
<h2>5. Schäden und Ausfälle</h2>
|
||||
<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>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<section class="page-hero">
|
||||
<p class="eyebrow">404</p>
|
||||
<h1>Diese Seite wurde nicht gefunden.</h1>
|
||||
<p>Bitte wechseln Sie zurück zur Startseite oder direkt zur Buchungsanfrage.</p>
|
||||
<div class="hero-actions">
|
||||
<a class="button-primary" href="<?= h(url('/')) ?>">Zur Startseite</a>
|
||||
<a class="button-secondary" href="<?= h(url('buchen')) ?>">Zur Anfrage</a>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,59 @@
|
||||
<section class="page-hero">
|
||||
<p class="eyebrow">Preise & Mietlogik</p>
|
||||
<h1>Klare Preise ohne versteckte Logik.</h1>
|
||||
<p>
|
||||
Der Standardpreis beträgt <strong><?= h(formatCurrency((int) $dayRate)) ?></strong> pro Miettag.
|
||||
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>
|
||||
<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>
|
||||
</ul>
|
||||
</article>
|
||||
<article class="content-card">
|
||||
<p class="eyebrow">Preisbeispiele</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; ?>
|
||||
</div>
|
||||
<p class="small-note">
|
||||
Lieferung, Aufbau oder Vor-Ort-Betreuung werden im Anfrageprozess passend zum Anlass abgestimmt.
|
||||
</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<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>
|
||||
</div>
|
||||
<div class="trust-grid">
|
||||
<article class="trust-card">
|
||||
<span>Überweisung</span>
|
||||
<strong>Rechnung mit Kundendaten</strong>
|
||||
</article>
|
||||
<article class="trust-card">
|
||||
<span>PayPal</span>
|
||||
<strong>Als Zahlungsart auswählbar</strong>
|
||||
</article>
|
||||
<article class="trust-card">
|
||||
<span>Steuerhinweis</span>
|
||||
<strong><?= h($company['tax_notice']) ?></strong>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<section class="page-hero">
|
||||
<p class="eyebrow">Verfügbarkeit</p>
|
||||
<h1>Geblockte und bereits bestätigte Zeiträume im Blick.</h1>
|
||||
<p>
|
||||
Die Übersicht zeigt aktuelle Belegungen aus dem Verwaltungssystem.
|
||||
Für Ihren Wunschtermin senden Sie am besten direkt eine Anfrage.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="section split-section">
|
||||
<article class="content-card">
|
||||
<h2>Aktuelle Belegung</h2>
|
||||
<div class="availability-list">
|
||||
<?php if ($bookings === []): ?>
|
||||
<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): ?>
|
||||
<article class="availability-card">
|
||||
<div>
|
||||
<strong><?= h($booking['reference']) ?></strong>
|
||||
<span><?= h(formatDate($booking['start_date'])) ?> bis <?= h(formatDate($booking['end_date'])) ?></span>
|
||||
</div>
|
||||
<span class="<?= h(statusPillClass((string) $booking['status'])) ?>"><?= h($booking['status_label']) ?></span>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</article>
|
||||
<article class="content-card emphasis-card">
|
||||
<h2>Direkt zur Anfrage</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>
|
||||
</ul>
|
||||
<a class="button-primary button-block" href="<?= h(url('buchen')) ?>">Jetzt Termin anfragen</a>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
$oldData = is_array($old ?? null) ? $old : [];
|
||||
?>
|
||||
<div class="booking-form-shell">
|
||||
<?php if (!empty($flashSuccess)): ?>
|
||||
<div class="flash flash-success"><?= h((string) $flashSuccess) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($flashError)): ?>
|
||||
<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) ?>">
|
||||
<?= csrfField() ?>
|
||||
<input type="hidden" name="price_per_day_cents" value="<?= h((string) $dayRate) ?>">
|
||||
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
<span class="form-step">Schritt 1</span>
|
||||
<h3>Mietzeitraum wählen</h3>
|
||||
</div>
|
||||
<p class="form-help">Ein Miettag entspricht immer einer Übernachtung. Montag bis Dienstag zählt als 1 Miettag.</p>
|
||||
<div class="form-grid form-grid-two">
|
||||
<label>
|
||||
<span>Abholdatum</span>
|
||||
<input type="date" name="start_date" data-booking-start value="<?= h((string) ($oldData['start_date'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Rückgabedatum</span>
|
||||
<input type="date" name="end_date" data-booking-end value="<?= h((string) ($oldData['end_date'] ?? '')) ?>" required>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
<span class="form-step">Schritt 2</span>
|
||||
<h3>Leistung und Zahlung festlegen</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>
|
||||
</label>
|
||||
<label>
|
||||
<span>Zahlungsart</span>
|
||||
<select name="payment_method">
|
||||
<option value="invoice_transfer" <?= selected((string) ($oldData['payment_method'] ?? 'invoice_transfer'), 'invoice_transfer') ?>>Rechnung / Überweisung</option>
|
||||
<option value="paypal" <?= selected((string) ($oldData['payment_method'] ?? ''), 'paypal') ?>>PayPal</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
<span class="form-step">Schritt 3</span>
|
||||
<h3>Kundendaten erfassen</h3>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<label>
|
||||
<span>Name</span>
|
||||
<input type="text" name="customer_name" value="<?= h((string) ($oldData['customer_name'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Firma</span>
|
||||
<input type="text" name="company" value="<?= h((string) ($oldData['company'] ?? '')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>E-Mail</span>
|
||||
<input type="email" name="email" value="<?= h((string) ($oldData['email'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Telefon</span>
|
||||
<input type="text" name="phone" value="<?= h((string) ($oldData['phone'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Straße</span>
|
||||
<input type="text" name="street" value="<?= h((string) ($oldData['street'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>PLZ</span>
|
||||
<input type="text" name="postal_code" value="<?= h((string) ($oldData['postal_code'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Ort</span>
|
||||
<input type="text" name="city" value="<?= h((string) ($oldData['city'] ?? '')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Anlass</span>
|
||||
<input type="text" name="event_type" value="<?= h((string) ($oldData['event_type'] ?? '')) ?>" placeholder="z. B. Hochzeit oder Firmenfeier">
|
||||
</label>
|
||||
<label class="form-grid-span">
|
||||
<span>Veranstaltungsort</span>
|
||||
<input type="text" name="event_location" value="<?= h((string) ($oldData['event_location'] ?? '')) ?>" placeholder="Location, Stadt oder Lieferadresse">
|
||||
</label>
|
||||
<label class="form-grid-span">
|
||||
<span>Hinweis für Ihre Anfrage</span>
|
||||
<textarea name="notes_customer" rows="4" placeholder="Lieferdetails, Aufbauwunsch oder besondere Anforderungen"><?= h((string) ($oldData['notes_customer'] ?? '')) ?></textarea>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="booking-summary-card">
|
||||
<div class="summary-line">
|
||||
<span>Mietdauer</span>
|
||||
<strong data-summary-days>Noch nicht gewählt</strong>
|
||||
</div>
|
||||
<div class="summary-line">
|
||||
<span>Preis pro Miettag</span>
|
||||
<strong><?= h(formatCurrency((int) $dayRate)) ?></strong>
|
||||
</div>
|
||||
<div class="summary-line summary-line-total">
|
||||
<span>Voraussichtlicher Gesamtpreis</span>
|
||||
<strong data-summary-total><?= h(formatCurrency((int) $dayRate)) ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="consent-stack">
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="privacy_accepted" value="1" <?= !empty($oldData['privacy_accepted']) ? 'checked' : '' ?> required>
|
||||
<span>Ich habe die <a href="<?= h(url('datenschutz')) ?>">Datenschutzerklärung</a> gelesen.</span>
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="terms_accepted" value="1" <?= !empty($oldData['terms_accepted']) ? 'checked' : '' ?> required>
|
||||
<span>Ich bestätige die <a href="<?= h(url('mietbedingungen')) ?>">Mietbedingungen</a> und die Preislogik pro Miettag.</span>
|
||||
</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>
|
||||
</form>
|
||||
</div>
|
||||
Reference in New Issue
Block a user