Anpassung Design

This commit is contained in:
2026-05-05 12:30:45 +02:00
parent bec1c8725f
commit 4a4517c514
35 changed files with 2990 additions and 1339 deletions
+34 -29
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+18 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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';
+2 -2
View File
@@ -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
View File
@@ -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;
}
}
+4 -13
View File
@@ -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;
}
}
+73
View File
@@ -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
View File
@@ -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',
];
}
+55
View File
@@ -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>
+44
View File
@@ -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
View File
@@ -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>
+58
View File
@@ -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
View File
@@ -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>
+55
View File
@@ -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>
+5 -3
View File
@@ -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
View File
@@ -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>
+55
View File
@@ -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>
+44
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+42
View File
@@ -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>
+28
View File
@@ -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>
+36
View File
@@ -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>
+17
View File
@@ -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>
+31
View File
@@ -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>
+29
View File
@@ -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>
+42
View File
@@ -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>
+29
View File
@@ -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>
+9
View File
@@ -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>
+59
View File
@@ -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>
+41
View File
@@ -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>
+137
View File
@@ -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>