Jelajahi Sumber

forking feuerwehr-shop to psa-orderform, first draft of rebuild

Medowar 1 bulan lalu
induk
melakukan
452979a100

+ 30 - 46
README.md

@@ -1,58 +1,42 @@
-# Feuerwehr Vereinsshop - Setup
+# PSA-Bestellsystem Feuerwehr Freising
 
-## Voraussetzungen
-
-- PHP 8.x
-- Webserver
-- Schreibrechte auf `data/`
-
-## 1) `.htaccess` aktivieren
-
-Im Projektordner liegt die zentrale Datei unter:
-
-- `.htaccess`
-
-Ein separates `data/.htaccess` wird nicht mehr verwendet, da der Zugriff auf `/data` zentral in der Root-`.htaccess` gesperrt ist.
+## Zweck
 
-## 2) Globale Config in `config.php` einrichten
+Dieses Projekt ist ein internes Bestellsystem für persönliche Schutzausrüstung der Feuerwehr Freising.
 
-In `config.php` mindestens diese Werte anpassen:
+## Kernfunktionen
 
-- `SITE_NAME`
-- `SITE_URL`
-- `ADMIN_EMAIL` (Fallback-Empfänger)
-- `FROM_EMAIL`
-- `FROM_NAME`
-- Optional: `ORDER_PREFIX`, `RESERVATION_EXPIRY_DAYS`
-- `ORDER_HISTORY_COOKIE_SECRET` (Pflicht für signierte Browser-Bestellhistorie)
+- Produktübersicht mit Kategorien
+- Produktdetailseiten mit Größenwahl
+- Warenkorb ohne Mengensteuerung
+- Checkout mit Name, E-Mail, Organisation und Kommentar
+- Optionaler Bestätigungslink vor interner Weiterleitung
+- Adminbereich für Bestellungen, Produkte, Kategorien, Organisationen, Einstellungen, FAQ und Admins
+- Positionsbezogene Bearbeitung und Stornierung von Bestellungen
 
-Konfigurations-Referenz mit Zweck und Laufzeitnutzung jeder Konstante:
-
-- `docs/CONFIG_REFERENCE.md`
-
-## 3) Admins einrichten
-
-Admin-Logins und Admin-Benachrichtigungs-E-Mails werden aus `data/admins.json` geladen (nicht aus `ADMIN_USERS` in `config.php`).
-
-Details:
-
-- `docs/ADMIN_SYSTEM.md`
+## Voraussetzungen
 
-## 4) Browser-Bestellhistorie konfigurieren
+- PHP 8.x
+- Schreibrechte auf `data/`
+- funktionierende PHP-Mailzustellung für Bestellmails
 
-Für die Seite `Meine Bestellungen` wird eine signierte, browsergebundene Historie genutzt.
+## Wichtige Dateien
 
-- Setze in `config.php` einen eigenen Wert für `ORDER_HISTORY_COOKIE_SECRET`
-- Optionale Tuning-Werte:
-  - `ORDER_HISTORY_COOKIE_NAME`
-  - `ORDER_HISTORY_COOKIE_TTL_DAYS`
-  - `ORDER_HISTORY_MAX_IDS`
+- Konfiguration: `config.php`
+- Zentrale Logik: `includes/functions.php`
+- Bestellungen: `data/orders.json`
+- Organisationen: `data/organizations.json`
+- Systemeinstellungen: `data/settings.json`
+- Produkte: `data/products.json`
 
-Details zur Funktion und zum Sicherheitsmodell:
+## Einrichtung
 
-- `docs/ORDER_HISTORY.md`
+1. `config.php` prüfen und insbesondere `SITE_URL`, `FROM_EMAIL` und die Bestell-Voreinstellungen anpassen.
+2. Adminzugänge in `data/admins.json` pflegen.
+3. Empfängeradresse, Bestätigungspflicht und PDF-Anhang im Admin unter `Einstellungen` prüfen.
+4. Organisationen im Admin unter `Organisationen verwalten` pflegen.
 
-## 5) FAQ-Inhalte pflegen
+## Hinweise
 
-- FAQ-Inhalt wird aus `data/faq.json` geladen (`FAQ_FILE` in `config.php`).
-- Bearbeitung erfolgt im Admin-Bereich unter `admin/faq.php`.
+- Alte Reservierungs-, Backorder- und Bestellhistorien-Features werden nicht mehr verwendet.
+- Bestellungen werden nicht im Browser für Endnutzer gespeichert oder nachverfolgt.

+ 2 - 305
admin/backorders.php

@@ -1,308 +1,5 @@
 <?php
 require_once __DIR__ . '/../config.php';
-require_once __DIR__ . '/../includes/functions.php';
 
-// Check admin login
-if (!isset($_SESSION['admin_logged_in']) || !$_SESSION['admin_logged_in']) {
-    header('Location: login.php');
-    exit;
-}
-
-$pageTitle = 'Vorbestellungen verwalten';
-
-// Handle availability notification
-if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['notify_available'])) {
-    $reservationId = sanitize($_POST['reservation_id']);
-    $result = markBackorderAvailable($reservationId);
-    $message = $result['success'] ? 'Kunde wurde informiert, dass die Vorbestellung zur Abholung bereit ist.' : $result['message'];
-    $messageType = $result['success'] ? 'success' : 'error';
-}
-if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['mark_hidden'])) {
-    $reservationId = sanitize($_POST['reservation_id']);
-    $result = markReservationHidden($reservationId);
-    $message = $result['success'] ? 'Vorbestellung als Spam/Gelöscht markiert und ausgeblendet.' : $result['message'];
-    $messageType = $result['success'] ? 'success' : 'error';
-}
-
-$reservations = getReservations();
-$filter = isset($_GET['filter']) ? sanitize($_GET['filter']) : 'pending';
-$searchOrderNumber = isset($_GET['order_number']) ? sanitize($_GET['order_number']) : '';
-$showHidden = in_array($filter, ['all', 'hidden'], true);
-
-// Filter backorders
-$reservations = array_filter($reservations, function($r) {
-    return isset($r['type']) && $r['type'] === 'backorder';
-});
-
-if (!$showHidden) {
-    $reservations = array_filter($reservations, function($r) {
-        return !isReservationHidden($r);
-    });
-}
-
-if ($searchOrderNumber) {
-    $reservations = array_filter($reservations, function($r) use ($searchOrderNumber) {
-        return stripos($r['id'], $searchOrderNumber) !== false;
-    });
-} else {
-    switch ($filter) {
-        case 'pending':
-            $reservations = array_filter($reservations, function($r) {
-                return !isset($r['backorder_status']) || $r['backorder_status'] !== 'notified';
-            });
-            break;
-        case 'hidden':
-            $reservations = array_filter($reservations, function($r) {
-                return isReservationHidden($r);
-            });
-            break;
-    }
-}
-
-$reservations = array_reverse($reservations); // Newest first
-
-$canNotifyMap = [];
-foreach ($reservations as $reservation) {
-    if (isReservationHidden($reservation)) {
-        $canNotifyMap[$reservation['id']] = false;
-        continue;
-    }
-    $isNotified = isset($reservation['backorder_status']) && $reservation['backorder_status'] === 'notified';
-    $canNotifyMap[$reservation['id']] = !$isNotified && canFulfillReservationItems($reservation['items']);
-}
-
-$productsById = [];
-foreach (getProducts() as $product) {
-    $productsById[$product['id']] = $product;
-}
-
-$backorderSummary = [];
-foreach ($reservations as $reservation) {
-    foreach ($reservation['items'] as $item) {
-        $productId = $item['product_id'];
-        if (!isset($productsById[$productId])) {
-            continue;
-        }
-        $sizeLabel = isset($item['size']) && $item['size'] !== '' ? $item['size'] : '';
-        $key = $productId . '|' . $sizeLabel;
-        if (!isset($backorderSummary[$key])) {
-            $backorderSummary[$key] = [
-                'name' => $productsById[$productId]['name'],
-                'size' => $sizeLabel,
-                'quantity' => 0
-            ];
-        }
-        $backorderSummary[$key]['quantity'] += (int)$item['quantity'];
-    }
-}
-
-$summaryRows = array_values($backorderSummary);
-usort($summaryRows, function($a, $b) {
-    $nameCompare = strcasecmp($a['name'], $b['name']);
-    if ($nameCompare !== 0) {
-        return $nameCompare;
-    }
-    return strcasecmp($a['size'], $b['size']);
-});
-
-$summaryTotal = 0;
-foreach ($summaryRows as $row) {
-    $summaryTotal += $row['quantity'];
-}
-
-$bodyClass = 'admin-page';
-include __DIR__ . '/../includes/header.php';
-?>
-
-<div class="admin-header">
-    <h2>Vorbestellungen verwalten</h2>
-    <div>
-        <a href="index.php" class="btn btn-secondary">Zurück zum Dashboard</a>
-    </div>
-</div>
-
-<?php if (isset($message)): ?>
-    <div class="alert alert-<?php echo $messageType; ?>">
-        <?php echo htmlspecialchars($message); ?>
-    </div>
-<?php endif; ?>
-
-<div class="panel">
-    <form method="GET" style="display: flex; gap: 1rem; align-items: end; flex-wrap: wrap;">
-        <div style="flex: 1; min-width: 200px;">
-            <label for="order_number">Bestellnummer suchen:</label>
-            <input type="text" id="order_number" name="order_number" value="<?php echo htmlspecialchars($searchOrderNumber); ?>" placeholder="Bestellnummer">
-        </div>
-        <div>
-            <label for="filter">Filter:</label>
-            <select id="filter" name="filter">
-                <option value="pending" <?php echo $filter === 'pending' ? 'selected' : ''; ?>>Offen</option>
-                <option value="all" <?php echo $filter === 'all' ? 'selected' : ''; ?>>Alle</option>
-                <option value="hidden" <?php echo $filter === 'hidden' ? 'selected' : ''; ?>>Spam/Gelöscht</option>
-            </select>
-        </div>
-        <div>
-            <button type="submit" class="btn">Filtern</button>
-            <a href="backorders.php" class="btn btn-secondary">Zurücksetzen</a>
-        </div>
-    </form>
-</div>
-
-<?php if (!empty($summaryRows)): ?>
-    <div class="panel">
-        <h3>Übersicht Vorbestellte Artikel</h3>
-        <div class="table-responsive">
-        <table class="table-compact responsive-table">
-            <thead>
-                <tr>
-                    <th>Produkt</th>
-                    <th>Größe</th>
-                    <th>Menge</th>
-                </tr>
-            </thead>
-            <tbody>
-                <?php foreach ($summaryRows as $row): ?>
-                    <tr>
-                        <td data-label="Produkt"><?php echo htmlspecialchars($row['name']); ?></td>
-                        <td data-label="Größe"><?php echo $row['size'] !== '' ? htmlspecialchars($row['size']) : '—'; ?></td>
-                        <td data-label="Menge"><?php echo (int)$row['quantity']; ?></td>
-                    </tr>
-                <?php endforeach; ?>
-            </tbody>
-        </table>
-        </div>
-        <p class="mt-2"><strong>Gesamt:</strong> <?php echo (int)$summaryTotal; ?> Artikel</p>
-    </div>
-<?php endif; ?>
-
-<?php if (empty($reservations)): ?>
-    <div class="alert alert-info">
-        <p>Keine Vorbestellungen gefunden.</p>
-    </div>
-<?php else: ?>
-    <div class="table-responsive">
-        <table class="responsive-table">
-            <thead>
-                <tr>
-                    <th>Bestellnummer</th>
-                    <th>Kunde</th>
-                    <th>Artikel</th>
-                    <th>Erstellt</th>
-                    <th>Status</th>
-                    <th>Aktionen</th>
-                </tr>
-            </thead>
-            <tbody>
-            <?php foreach ($reservations as $reservation): ?>
-                <tr>
-                    <td data-label="Bestellnummer"><strong><?php echo htmlspecialchars($reservation['id']); ?></strong></td>
-                    <td data-label="Kunde"><?php echo htmlspecialchars($reservation['customer_name']); ?></td>
-                    <td data-label="Artikel">
-                        <?php
-                        $itemCount = 0;
-                        foreach ($reservation['items'] as $item) {
-                            $itemCount += $item['quantity'];
-                        }
-                        echo $itemCount . ' Artikel';
-                        ?>
-                    </td>
-                    <td data-label="Erstellt"><?php echo formatDate($reservation['created']); ?></td>
-                    <td data-label="Status">
-                        <?php
-                        if (isReservationHidden($reservation)) {
-                            echo '<span class="status status-hidden">Spam/Gelöscht</span>';
-                        } elseif (isset($reservation['backorder_status']) && $reservation['backorder_status'] === 'notified') {
-                            echo '<span class="status status-notified">Informiert</span>';
-                        } else {
-                            echo '<span class="status status-open">Offen</span>';
-                        }
-                        ?>
-                    </td>
-                    <td data-label="Aktionen">
-                        <?php if (!isReservationHidden($reservation) && (!isset($reservation['backorder_status']) || $reservation['backorder_status'] !== 'notified') && canFulfillReservationItems($reservation['items'])): ?>
-                            <form method="POST" style="display: inline;" onsubmit="return confirm('Kunden informieren, dass die Vorbestellung zur Abholung bereit ist?');">
-                                <input type="hidden" name="reservation_id" value="<?php echo htmlspecialchars($reservation['id']); ?>">
-                                <button type="submit" name="notify_available" class="btn btn-small">Abholung bereit</button>
-                            </form>
-                        <?php endif; ?>
-                        <?php if (!isReservationHidden($reservation)): ?>
-                            <form method="POST" style="display: inline;" onsubmit="return confirm('Vorbestellung als Spam/Gelöscht markieren? Die Vorbestellung wird überall ausgeblendet.');">
-                                <input type="hidden" name="reservation_id" value="<?php echo htmlspecialchars($reservation['id']); ?>">
-                                <button type="submit" name="mark_hidden" class="btn btn-secondary btn-small">Spam/Gelöscht</button>
-                            </form>
-                        <?php endif; ?>
-                        <button onclick="showDetails('<?php echo htmlspecialchars($reservation['id']); ?>')" class="btn btn-secondary btn-small">Details</button>
-                    </td>
-                </tr>
-            <?php endforeach; ?>
-            </tbody>
-        </table>
-    </div>
-<?php endif; ?>
-
-<!-- Details Modal -->
-<div id="detailsModal" class="modal">
-    <div class="modal-content">
-        <button onclick="closeDetails()" class="btn btn-small modal-close">Schließen</button>
-        <div id="detailsContent"></div>
-        <form method="POST" id="notifyForm" style="margin-top: 1.5rem; display: none;" onsubmit="return confirm('Kunden informieren, dass die Vorbestellung zur Abholung bereit ist?');">
-            <input type="hidden" name="reservation_id" id="notifyReservationId" value="">
-            <button type="submit" name="notify_available" class="btn">Abholung bereit</button>
-        </form>
-    </div>
-</div>
-
-<script>
-function showDetails(reservationId) {
-    const reservations = <?php echo json_encode(getReservations()); ?>;
-    const reservation = reservations.find(r => r.id === reservationId);
-    const canNotifyMap = <?php echo json_encode($canNotifyMap); ?>;
-    
-    if (!reservation) return;
-    
-    let itemsHtml = '<h3>Artikel:</h3><ul>';
-    reservation.items.forEach(item => {
-        const product = <?php echo json_encode(getProducts()); ?>.find(p => p.id == item.product_id);
-        if (product) {
-            let sizeInfo = '';
-            if (item.size && item.size !== '') {
-                sizeInfo = ` - Größe: ${item.size}`;
-            }
-            itemsHtml += `<li>${product.name}${sizeInfo} - Menge: ${item.quantity}</li>`;
-        }
-    });
-    itemsHtml += '</ul>';
-    
-    const isHidden = reservation.is_hidden === true;
-    const statusText = isHidden ? 'Spam/Gelöscht' : (reservation.backorder_status === 'notified' ? 'Informiert' : 'Offen');
-    const statusClass = isHidden ? 'status-hidden' : (reservation.backorder_status === 'notified' ? 'status-notified' : 'status-open');
-    const html = `
-        <h2>Vorbestellungsdetails</h2>
-        <p><strong>Bestellnummer:</strong> <strong class="order-highlight">${reservation.id}</strong></p>
-        <p><strong>Kunde:</strong> ${reservation.customer_name}</p>
-        <p><strong>E-Mail:</strong> ${reservation.customer_email}</p>
-        <p><strong>Erstellt:</strong> ${reservation.created}</p>
-        <p><strong>Gültig bis:</strong> ${reservation.expires}</p>
-        <p><strong>Status:</strong> <span class="status ${statusClass}">${statusText}</span></p>
-        ${itemsHtml}
-    `;
-    
-    document.getElementById('detailsContent').innerHTML = html;
-    const notifyForm = document.getElementById('notifyForm');
-    const notifyReservationId = document.getElementById('notifyReservationId');
-    if (canNotifyMap[reservation.id]) {
-        notifyReservationId.value = reservation.id;
-        notifyForm.style.display = 'block';
-    } else {
-        notifyReservationId.value = '';
-        notifyForm.style.display = 'none';
-    }
-    document.getElementById('detailsModal').style.display = 'flex';
-}
-
-function closeDetails() {
-    document.getElementById('detailsModal').style.display = 'none';
-}
-</script>
-
-<?php include __DIR__ . '/../includes/footer.php'; ?>
+header('Location: orders.php');
+exit;

+ 64 - 115
admin/index.php

@@ -2,47 +2,47 @@
 require_once __DIR__ . '/../config.php';
 require_once __DIR__ . '/../includes/functions.php';
 
-// Check admin login
-if (!isset($_SESSION['admin_logged_in']) || !$_SESSION['admin_logged_in']) {
+if (empty($_SESSION['admin_logged_in'])) {
     header('Location: login.php');
     exit;
 }
 
-$pageTitle = 'Admin Dashboard';
+expirePendingOrders();
 
-// Get statistics
+$pageTitle = 'Admin Dashboard';
+$orders = getOrders();
 $products = getProducts();
-$reservations = getReservations();
-
-// Expire old reservations
-expireOldReservations();
-$reservations = getReservations(); // Refresh after expiry
+$organizations = getOrganizations(false);
 
-$regularReservations = array_filter($reservations, function($r) {
-    return (!isset($r['type']) || $r['type'] !== 'backorder') && !isReservationHidden($r);
-});
-$backorderReservations = array_filter($reservations, function($r) {
-    return isset($r['type']) && $r['type'] === 'backorder' && !isReservationHidden($r);
-});
+$stats = [
+    'products' => count($products),
+    'organizations' => count($organizations),
+    'unconfirmed' => 0,
+    'open' => 0,
+    'partial' => 0,
+    'processed' => 0,
+    'cancelled' => 0,
+];
 
-$totalProducts = count($products);
-$totalStock = 0;
-foreach ($products as $product) {
-    $totalStock += getTotalStock($product);
+foreach ($orders as $order) {
+    if ($order['confirmation_status'] === 'pending') {
+        $stats['unconfirmed']++;
+    } elseif ($order['status'] === 'cancelled') {
+        $stats['cancelled']++;
+    } elseif ($order['status'] === 'processed') {
+        $stats['processed']++;
+    } elseif ($order['status'] === 'partial') {
+        $stats['partial']++;
+    } else {
+        $stats['open']++;
+    }
 }
-$openReservations = count(array_filter($regularReservations, function($r) {
-    return $r['status'] === 'open' && !$r['picked_up'];
-}));
-$pickedUpReservations = count(array_filter($regularReservations, function($r) {
-    return $r['picked_up'];
-}));
-$openRegularReservations = array_values(array_filter($regularReservations, function($r) {
-    return $r['status'] === 'open' && !$r['picked_up'];
-}));
-$openPreorders = array_values(array_filter($backorderReservations, function($r) {
-    return !isset($r['backorder_status']) || $r['backorder_status'] !== 'notified';
-}));
-$openBackorders = count($openPreorders);
+
+$recentOrders = $orders;
+usort($recentOrders, function ($left, $right) {
+    return strcmp($right['created_at'], $left['created_at']);
+});
+$recentOrders = array_slice($recentOrders, 0, 8);
 
 $bodyClass = 'admin-page';
 include __DIR__ . '/../includes/header.php';
@@ -51,13 +51,14 @@ include __DIR__ . '/../includes/header.php';
 <div class="admin-header">
     <h2>Admin Dashboard</h2>
     <div class="admin-dashboard-actions">
-        <a href="reservations.php" class="btn">Reservierungen</a>
-        <a href="backorders.php" class="btn">Vorbestellungen</a>
+        <a href="orders.php" class="btn">Bestellungen</a>
         <details class="admin-actions-dropdown">
             <summary class="btn btn-secondary">Verwaltung</summary>
             <div class="admin-actions-menu">
                 <a href="products.php">Produkte verwalten</a>
                 <a href="categories.php">Kategorien verwalten</a>
+                <a href="organizations.php">Organisationen verwalten</a>
+                <a href="settings.php">Einstellungen</a>
                 <a href="faq.php">FAQ bearbeiten</a>
                 <a href="admins.php">Admins verwalten</a>
                 <a href="login.php?logout=1">Abmelden</a>
@@ -69,112 +70,60 @@ include __DIR__ . '/../includes/header.php';
 <div class="admin-stats">
     <div class="stat-card">
         <h3>Produkte</h3>
-        <div class="stat-value"><?php echo $totalProducts; ?></div>
+        <div class="stat-value"><?php echo $stats['products']; ?></div>
+    </div>
+    <div class="stat-card">
+        <h3>Organisationen</h3>
+        <div class="stat-value"><?php echo $stats['organizations']; ?></div>
     </div>
-    
     <div class="stat-card">
-        <h3>Gesamtbestand</h3>
-        <div class="stat-value"><?php echo $totalStock; ?></div>
+        <h3>Unbestätigt</h3>
+        <div class="stat-value"><?php echo $stats['unconfirmed']; ?></div>
     </div>
-    
     <div class="stat-card">
-        <h3>Offene Reservierungen</h3>
-        <div class="stat-value"><?php echo $openReservations; ?></div>
+        <h3>Offen</h3>
+        <div class="stat-value"><?php echo $stats['open']; ?></div>
     </div>
-    
     <div class="stat-card">
-        <h3>Abgeholt</h3>
-        <div class="stat-value"><?php echo $pickedUpReservations; ?></div>
+        <h3>Teilweise bearbeitet</h3>
+        <div class="stat-value"><?php echo $stats['partial']; ?></div>
     </div>
-    
     <div class="stat-card">
-        <h3>Offene Vorbestellungen</h3>
-        <div class="stat-value"><?php echo $openBackorders; ?></div>
+        <h3>Bearbeitet</h3>
+        <div class="stat-value"><?php echo $stats['processed']; ?></div>
+    </div>
+    <div class="stat-card">
+        <h3>Storniert</h3>
+        <div class="stat-value"><?php echo $stats['cancelled']; ?></div>
     </div>
-    
 </div>
 
-<h3 style="margin-top: 2rem;">Offene Reservierungen</h3>
-<?php
-$openReservationsList = array_slice(array_reverse($openRegularReservations), 0, 5);
-if (empty($openReservationsList)):
-?>
-    <p>Keine offenen Reservierungen vorhanden.</p>
-<?php else: ?>
-    <div class="table-responsive">
-        <table class="responsive-table">
-            <thead>
-                <tr>
-                    <th>Bestellnummer</th>
-                    <th>Kunde</th>
-                    <th>Erstellt</th>
-                    <th>Status</th>
-                    <th>Aktionen</th>
-                </tr>
-            </thead>
-            <tbody>
-                <?php foreach ($openReservationsList as $reservation): ?>
-                    <tr>
-                        <td data-label="Bestellnummer"><strong><?php echo htmlspecialchars($reservation['id']); ?></strong></td>
-                        <td data-label="Kunde"><?php echo htmlspecialchars($reservation['customer_name']); ?></td>
-                        <td data-label="Erstellt"><?php echo formatDate($reservation['created']); ?></td>
-                        <td data-label="Status">
-                            <?php
-                            if ($reservation['picked_up']) {
-                                echo '<span class="status status-picked">Abgeholt</span>';
-                            } elseif ($reservation['status'] === 'expired') {
-                                echo '<span class="status status-expired">Abgelaufen</span>';
-                            } else {
-                                echo '<span class="status status-open">Offen</span>';
-                            }
-                            ?>
-                        </td>
-                        <td data-label="Aktionen">
-                            <a href="reservations.php?order_number=<?php echo urlencode($reservation['id']); ?>" class="btn btn-small">Details</a>
-                        </td>
-                    </tr>
-                <?php endforeach; ?>
-            </tbody>
-        </table>
-    </div>
-<?php endif; ?>
+<h3 style="margin-top: 2rem;">Letzte Bestellungen</h3>
 
-<h3 style="margin-top: 2rem;">Offene Vorbestellungen</h3>
-<?php
-$openPreordersList = array_slice(array_reverse($openPreorders), 0, 5);
-if (empty($openPreordersList)):
-?>
-    <p>Keine offenen Vorbestellungen vorhanden.</p>
+<?php if (empty($recentOrders)): ?>
+    <p>Keine Bestellungen vorhanden.</p>
 <?php else: ?>
     <div class="table-responsive">
         <table class="responsive-table">
             <thead>
                 <tr>
                     <th>Bestellnummer</th>
-                    <th>Kunde</th>
+                    <th>Name</th>
+                    <th>Organisation</th>
                     <th>Erstellt</th>
                     <th>Status</th>
                     <th>Aktionen</th>
                 </tr>
             </thead>
             <tbody>
-                <?php foreach ($openPreordersList as $reservation): ?>
+                <?php foreach ($recentOrders as $order): ?>
                     <tr>
-                        <td data-label="Bestellnummer"><strong><?php echo htmlspecialchars($reservation['id']); ?></strong></td>
-                        <td data-label="Kunde"><?php echo htmlspecialchars($reservation['customer_name']); ?></td>
-                        <td data-label="Erstellt"><?php echo formatDate($reservation['created']); ?></td>
-                        <td data-label="Status">
-                            <?php
-                            if (isset($reservation['backorder_status']) && $reservation['backorder_status'] === 'notified') {
-                                echo '<span class="status status-notified">Informiert</span>';
-                            } else {
-                                echo '<span class="status status-open">Offen</span>';
-                            }
-                            ?>
-                        </td>
-                        <td data-label="Aktionen">
-                            <a href="backorders.php?order_number=<?php echo urlencode($reservation['id']); ?>" class="btn btn-small">Details</a>
-                        </td>
+                        <td data-label="Bestellnummer"><strong><?php echo escape($order['id']); ?></strong></td>
+                        <td data-label="Name"><?php echo escape($order['customer_name']); ?></td>
+                        <td data-label="Organisation"><?php echo escape($order['organization_label']); ?></td>
+                        <td data-label="Erstellt"><?php echo escape(formatDate($order['created_at'])); ?></td>
+                        <td data-label="Status"><span class="status <?php echo escape(getOrderStatusClass($order)); ?>"><?php echo escape(getOrderStatusLabel($order)); ?></span></td>
+                        <td data-label="Aktionen"><a href="orders.php?order_id=<?php echo urlencode($order['id']); ?>" class="btn btn-small">Details</a></td>
                     </tr>
                 <?php endforeach; ?>
             </tbody>

+ 2 - 2
admin/login.php

@@ -39,13 +39,13 @@ if (isset($_SESSION['admin_logged_in']) && $_SESSION['admin_logged_in']) {
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>Admin Login - <?php echo SITE_NAME; ?></title>
+    <title>Admin Login - <?php echo escape(SITE_NAME); ?></title>
     <link rel="stylesheet" href="<?php echo SITE_URL; ?>/assets/css/style.css">
 </head>
 <body>
     <header>
         <div class="container">
-            <h1><?php echo SITE_NAME; ?> - Admin</h1>
+            <h1><?php echo escape(SITE_NAME); ?> - Admin</h1>
         </div>
     </header>
     <main>

+ 228 - 0
admin/orders.php

@@ -0,0 +1,228 @@
+<?php
+require_once __DIR__ . '/../config.php';
+require_once __DIR__ . '/../includes/functions.php';
+
+if (empty($_SESSION['admin_logged_in'])) {
+    header('Location: login.php');
+    exit;
+}
+
+expirePendingOrders();
+
+$pageTitle = 'Bestellungen';
+$message = '';
+$messageType = '';
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['toggle_item_processed'])) {
+    $result = toggleOrderItemProcessed($_POST['order_id'] ?? '', (int) ($_POST['item_index'] ?? -1));
+    $message = $result['success'] ? 'Position wurde aktualisiert.' : $result['message'];
+    $messageType = $result['success'] ? 'success' : 'error';
+}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['cancel_order'])) {
+    $adminUsername = $_SESSION['admin_username'] ?? '';
+    $result = cancelOrder($_POST['order_id'] ?? '', $adminUsername, $_POST['cancellation_reason'] ?? '');
+    $message = $result['success'] ? 'Bestellung wurde storniert.' : $result['message'];
+    $messageType = $result['success'] ? 'success' : 'error';
+}
+
+$orders = getOrders();
+usort($orders, function ($left, $right) {
+    return strcmp($right['created_at'], $left['created_at']);
+});
+
+$filter = trim((string) ($_GET['filter'] ?? 'all'));
+$searchOrderId = trim((string) ($_GET['order_id'] ?? ''));
+$selectedOrderId = trim((string) ($_GET['details'] ?? $searchOrderId));
+
+if ($searchOrderId !== '') {
+    $orders = array_values(array_filter($orders, function ($order) use ($searchOrderId) {
+        return stripos($order['id'], $searchOrderId) !== false;
+    }));
+}
+
+if ($filter !== 'all') {
+    $orders = array_values(array_filter($orders, function ($order) use ($filter) {
+        switch ($filter) {
+            case 'unconfirmed':
+                return $order['confirmation_status'] === 'pending';
+            case 'expired':
+                return $order['confirmation_status'] === 'expired';
+            case 'open':
+                return $order['confirmation_status'] !== 'pending' && $order['status'] === 'open';
+            case 'partial':
+                return $order['status'] === 'partial';
+            case 'processed':
+                return $order['status'] === 'processed';
+            case 'cancelled':
+                return $order['status'] === 'cancelled';
+        }
+        return true;
+    }));
+}
+
+$selectedOrder = $selectedOrderId !== '' ? getOrderById($selectedOrderId) : null;
+
+$bodyClass = 'admin-page';
+include __DIR__ . '/../includes/header.php';
+?>
+
+<div class="admin-header">
+    <h2>Bestellungen</h2>
+    <div>
+        <a href="index.php" class="btn btn-secondary">Zurück zum Dashboard</a>
+    </div>
+</div>
+
+<?php if ($message !== ''): ?>
+    <div class="alert alert-<?php echo escape($messageType); ?>">
+        <?php echo escape($message); ?>
+    </div>
+<?php endif; ?>
+
+<div class="panel">
+    <form method="GET" style="display: flex; gap: 1rem; align-items: end; flex-wrap: wrap;">
+        <div style="flex: 1; min-width: 220px;">
+            <label for="order_id">Bestellnummer suchen</label>
+            <input type="text" id="order_id" name="order_id" value="<?php echo escape($searchOrderId); ?>" placeholder="z. B. FWFS-2026-001">
+        </div>
+        <div>
+            <label for="filter">Filter</label>
+            <select id="filter" name="filter">
+                <option value="all" <?php echo $filter === 'all' ? 'selected' : ''; ?>>Alle</option>
+                <option value="unconfirmed" <?php echo $filter === 'unconfirmed' ? 'selected' : ''; ?>>Unbestätigt</option>
+                <option value="expired" <?php echo $filter === 'expired' ? 'selected' : ''; ?>>Bestätigung abgelaufen</option>
+                <option value="open" <?php echo $filter === 'open' ? 'selected' : ''; ?>>Offen</option>
+                <option value="partial" <?php echo $filter === 'partial' ? 'selected' : ''; ?>>Teilweise bearbeitet</option>
+                <option value="processed" <?php echo $filter === 'processed' ? 'selected' : ''; ?>>Bearbeitet</option>
+                <option value="cancelled" <?php echo $filter === 'cancelled' ? 'selected' : ''; ?>>Storniert</option>
+            </select>
+        </div>
+        <div>
+            <button type="submit" class="btn">Filtern</button>
+            <a href="orders.php" class="btn btn-secondary">Zurücksetzen</a>
+        </div>
+    </form>
+</div>
+
+<?php if (empty($orders)): ?>
+    <div class="alert alert-info">
+        <p>Keine Bestellungen gefunden.</p>
+    </div>
+<?php else: ?>
+    <div class="table-responsive">
+        <table class="responsive-table">
+            <thead>
+                <tr>
+                    <th>Bestellnummer</th>
+                    <th>Name</th>
+                    <th>Organisation</th>
+                    <th>Artikel</th>
+                    <th>Erstellt</th>
+                    <th>Status</th>
+                    <th>Aktionen</th>
+                </tr>
+            </thead>
+            <tbody>
+                <?php foreach ($orders as $order): ?>
+                    <tr>
+                        <td data-label="Bestellnummer"><strong><?php echo escape($order['id']); ?></strong></td>
+                        <td data-label="Name"><?php echo escape($order['customer_name']); ?></td>
+                        <td data-label="Organisation"><?php echo escape($order['organization_label']); ?></td>
+                        <td data-label="Artikel"><?php echo count($order['items']); ?></td>
+                        <td data-label="Erstellt"><?php echo escape(formatDate($order['created_at'])); ?></td>
+                        <td data-label="Status"><span class="status <?php echo escape(getOrderStatusClass($order)); ?>"><?php echo escape(getOrderStatusLabel($order)); ?></span></td>
+                        <td data-label="Aktionen">
+                            <a href="orders.php?details=<?php echo urlencode($order['id']); ?>" class="btn btn-small">Details</a>
+                        </td>
+                    </tr>
+                <?php endforeach; ?>
+            </tbody>
+        </table>
+    </div>
+<?php endif; ?>
+
+<?php if ($selectedOrder !== null): ?>
+    <div class="panel">
+        <h3>Bestellung <?php echo escape($selectedOrder['id']); ?></h3>
+        <p><strong>Status:</strong> <span class="status <?php echo escape(getOrderStatusClass($selectedOrder)); ?>"><?php echo escape(getOrderStatusLabel($selectedOrder)); ?></span></p>
+        <p><strong>Name:</strong> <?php echo escape($selectedOrder['customer_name']); ?></p>
+        <p><strong>E-Mail:</strong> <?php echo escape($selectedOrder['customer_email']); ?></p>
+        <p><strong>Organisation:</strong> <?php echo escape($selectedOrder['organization_label']); ?></p>
+        <p><strong>Erstellt:</strong> <?php echo escape(formatDate($selectedOrder['created_at'])); ?></p>
+        <?php if ($selectedOrder['confirmed_at'] !== ''): ?>
+            <p><strong>Bestätigt:</strong> <?php echo escape(formatDate($selectedOrder['confirmed_at'])); ?></p>
+        <?php endif; ?>
+        <?php if ($selectedOrder['confirmation_status'] === 'pending'): ?>
+            <p><strong>Bestätigung offen bis:</strong> <?php echo escape(formatDate($selectedOrder['confirmation_expires_at'])); ?></p>
+        <?php endif; ?>
+        <?php if ($selectedOrder['admin_notified_at'] !== ''): ?>
+            <p><strong>Intern weitergeleitet:</strong> <?php echo escape(formatDate($selectedOrder['admin_notified_at'])); ?></p>
+        <?php endif; ?>
+        <p><strong>Kommentar:</strong><br><?php echo $selectedOrder['comment'] !== '' ? nl2br(escape($selectedOrder['comment'])) : 'Kein Kommentar'; ?></p>
+
+        <?php if ($selectedOrder['status'] === 'cancelled'): ?>
+            <div class="alert alert-warning">
+                <p><strong>Storniert am:</strong> <?php echo escape(formatDate($selectedOrder['cancelled_at'])); ?></p>
+                <p><strong>Storniert durch:</strong> <?php echo escape($selectedOrder['cancelled_by']); ?></p>
+                <p><strong>Stornogrund:</strong><br><?php echo $selectedOrder['cancellation_reason'] !== '' ? nl2br(escape($selectedOrder['cancellation_reason'])) : 'Kein Grund angegeben'; ?></p>
+            </div>
+        <?php endif; ?>
+
+        <h4>Positionen</h4>
+        <div class="table-responsive">
+            <table class="responsive-table table-compact">
+                <thead>
+                    <tr>
+                        <th>Artikel</th>
+                        <th>Größe</th>
+                        <th>Lieferhinweis</th>
+                        <th>Bearbeitet</th>
+                        <th>Aktion</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    <?php foreach ($selectedOrder['items'] as $index => $item): ?>
+                        <tr>
+                            <td data-label="Artikel"><?php echo escape($item['product_name']); ?></td>
+                            <td data-label="Größe"><?php echo $item['size'] !== '' ? escape($item['size']) : '-'; ?></td>
+                            <td data-label="Lieferhinweis"><?php echo $item['availability_label'] !== '' ? escape($item['availability_label']) : '-'; ?></td>
+                            <td data-label="Bearbeitet">
+                                <span class="status <?php echo !empty($item['is_processed']) ? 'status-processed' : 'status-open'; ?>">
+                                    <?php echo !empty($item['is_processed']) ? 'Ja' : 'Nein'; ?>
+                                </span>
+                            </td>
+                            <td data-label="Aktion">
+                                <?php if ($selectedOrder['status'] !== 'cancelled' && $selectedOrder['confirmation_status'] !== 'pending' && $selectedOrder['confirmation_status'] !== 'expired'): ?>
+                                    <form method="POST">
+                                        <input type="hidden" name="order_id" value="<?php echo escape($selectedOrder['id']); ?>">
+                                        <input type="hidden" name="item_index" value="<?php echo (int) $index; ?>">
+                                        <button type="submit" name="toggle_item_processed" class="btn btn-small">
+                                            <?php echo !empty($item['is_processed']) ? 'Als offen markieren' : 'Als bearbeitet markieren'; ?>
+                                        </button>
+                                    </form>
+                                <?php else: ?>
+                                    -
+                                <?php endif; ?>
+                            </td>
+                        </tr>
+                    <?php endforeach; ?>
+                </tbody>
+            </table>
+        </div>
+
+        <?php if ($selectedOrder['status'] !== 'cancelled'): ?>
+            <h4>Bestellung stornieren</h4>
+            <form method="POST" onsubmit="return confirm('Bestellung wirklich stornieren?');">
+                <input type="hidden" name="order_id" value="<?php echo escape($selectedOrder['id']); ?>">
+                <div class="form-group">
+                    <label for="cancellation_reason">Stornogrund</label>
+                    <textarea id="cancellation_reason" name="cancellation_reason" rows="3" placeholder="Optionaler Grund"></textarea>
+                </div>
+                <button type="submit" name="cancel_order" class="btn">Bestellung stornieren</button>
+            </form>
+        <?php endif; ?>
+    </div>
+<?php endif; ?>
+
+<?php include __DIR__ . '/../includes/footer.php'; ?>

+ 175 - 0
admin/organizations.php

@@ -0,0 +1,175 @@
+<?php
+require_once __DIR__ . '/../config.php';
+require_once __DIR__ . '/../includes/functions.php';
+
+if (empty($_SESSION['admin_logged_in'])) {
+    header('Location: login.php');
+    exit;
+}
+
+$pageTitle = 'Organisationen verwalten';
+$message = '';
+$messageType = '';
+$organizations = getOrganizations(false);
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+    if (isset($_POST['add_organization'])) {
+        $label = normalizeOrganizationLabel($_POST['label'] ?? '');
+        $sortOrder = (int) ($_POST['sort_order'] ?? 0);
+        $active = isset($_POST['active']);
+
+        if (!isValidOrganizationLabel($label)) {
+            $message = 'Bitte einen Organisationsnamen mit maximal 120 Zeichen eingeben.';
+            $messageType = 'error';
+        } else {
+            $organizations[] = [
+                'id' => generateOrganizationIdFromLabel($label, $organizations),
+                'label' => $label,
+                'sort_order' => $sortOrder,
+                'active' => $active,
+            ];
+            saveOrganizations($organizations);
+            $message = 'Organisation wurde angelegt.';
+            $messageType = 'success';
+        }
+    }
+
+    if (isset($_POST['update_organization'])) {
+        $organizationId = normalizeOrganizationId($_POST['organization_id'] ?? '');
+        $label = normalizeOrganizationLabel($_POST['label'] ?? '');
+        $sortOrder = (int) ($_POST['sort_order'] ?? 0);
+        $active = isset($_POST['active']);
+        $updated = false;
+
+        if (!isValidOrganizationLabel($label)) {
+            $message = 'Bitte einen Organisationsnamen mit maximal 120 Zeichen eingeben.';
+            $messageType = 'error';
+        } else {
+            foreach ($organizations as &$organization) {
+                if ($organization['id'] !== $organizationId) {
+                    continue;
+                }
+
+                $organization['label'] = $label;
+                $organization['sort_order'] = $sortOrder;
+                $organization['active'] = $active;
+                $updated = true;
+                break;
+            }
+            unset($organization);
+
+            if ($updated) {
+                saveOrganizations($organizations);
+                $message = 'Organisation wurde aktualisiert.';
+                $messageType = 'success';
+            } else {
+                $message = 'Organisation nicht gefunden.';
+                $messageType = 'error';
+            }
+        }
+    }
+
+    if (isset($_POST['delete_organization'])) {
+        $organizationId = normalizeOrganizationId($_POST['organization_id'] ?? '');
+        $organizations = array_values(array_filter($organizations, function ($organization) use ($organizationId) {
+            return $organization['id'] !== $organizationId;
+        }));
+        saveOrganizations($organizations);
+        $message = 'Organisation wurde gelöscht.';
+        $messageType = 'success';
+    }
+
+    $organizations = getOrganizations(false);
+}
+
+$editingOrganization = isset($_GET['edit']) ? getOrganizationById($_GET['edit']) : null;
+
+$bodyClass = 'admin-page';
+include __DIR__ . '/../includes/header.php';
+?>
+
+<div class="admin-header">
+    <h2>Organisationen verwalten</h2>
+    <div>
+        <a href="index.php" class="btn btn-secondary">Zurück zum Dashboard</a>
+    </div>
+</div>
+
+<?php if ($message !== ''): ?>
+    <div class="alert alert-<?php echo escape($messageType); ?>">
+        <?php echo escape($message); ?>
+    </div>
+<?php endif; ?>
+
+<div class="panel" style="padding: 2rem;">
+    <h3><?php echo $editingOrganization ? 'Organisation bearbeiten' : 'Neue Organisation'; ?></h3>
+    <form method="POST">
+        <?php if ($editingOrganization): ?>
+            <input type="hidden" name="organization_id" value="<?php echo escape($editingOrganization['id']); ?>">
+        <?php endif; ?>
+
+        <div class="form-group">
+            <label for="label">Name *</label>
+            <input type="text" id="label" name="label" required maxlength="120" value="<?php echo escape($editingOrganization['label'] ?? ''); ?>">
+        </div>
+
+        <div class="form-group">
+            <label for="sort_order">Sortierung</label>
+            <input type="number" id="sort_order" name="sort_order" value="<?php echo escape((string) ($editingOrganization['sort_order'] ?? 0)); ?>">
+        </div>
+
+        <div class="form-group">
+            <label>
+                <input type="checkbox" name="active" value="1" <?php echo (!isset($editingOrganization['active']) || !empty($editingOrganization['active'])) ? 'checked' : ''; ?>>
+                Organisation ist auswählbar
+            </label>
+        </div>
+
+        <button type="submit" name="<?php echo $editingOrganization ? 'update_organization' : 'add_organization'; ?>" class="btn">
+            <?php echo $editingOrganization ? 'Speichern' : 'Organisation anlegen'; ?>
+        </button>
+        <?php if ($editingOrganization): ?>
+            <a href="organizations.php" class="btn btn-secondary">Abbrechen</a>
+        <?php endif; ?>
+    </form>
+</div>
+
+<div class="panel">
+    <h3>Organisationen</h3>
+    <div class="table-responsive">
+        <table class="responsive-table">
+            <thead>
+                <tr>
+                    <th>Name</th>
+                    <th>ID</th>
+                    <th>Sortierung</th>
+                    <th>Status</th>
+                    <th>Aktionen</th>
+                </tr>
+            </thead>
+            <tbody>
+                <?php foreach ($organizations as $organization): ?>
+                    <tr>
+                        <td data-label="Name"><?php echo escape($organization['label']); ?></td>
+                        <td data-label="ID"><code><?php echo escape($organization['id']); ?></code></td>
+                        <td data-label="Sortierung"><?php echo (int) $organization['sort_order']; ?></td>
+                        <td data-label="Status">
+                            <span class="status <?php echo !empty($organization['active']) ? 'status-open' : 'status-cancelled'; ?>">
+                                <?php echo !empty($organization['active']) ? 'Aktiv' : 'Inaktiv'; ?>
+                            </span>
+                        </td>
+                        <td data-label="Aktionen">
+                            <a href="organizations.php?edit=<?php echo urlencode($organization['id']); ?>" class="btn btn-small">Bearbeiten</a>
+                            <form method="POST" style="display: inline;" onsubmit="return confirm('Organisation wirklich löschen?');">
+                                <input type="hidden" name="organization_id" value="<?php echo escape($organization['id']); ?>">
+                                <button type="submit" name="delete_organization" class="btn btn-secondary btn-small">Löschen</button>
+                            </form>
+                        </td>
+                    </tr>
+                <?php endforeach; ?>
+            </tbody>
+        </table>
+    </div>
+</div>
+
+<?php include __DIR__ . '/../includes/footer.php'; ?>

+ 203 - 284
admin/products.php

@@ -2,8 +2,7 @@
 require_once __DIR__ . '/../config.php';
 require_once __DIR__ . '/../includes/functions.php';
 
-// Check admin login
-if (!isset($_SESSION['admin_logged_in']) || !$_SESSION['admin_logged_in']) {
+if (empty($_SESSION['admin_logged_in'])) {
     header('Location: login.php');
     exit;
 }
@@ -20,21 +19,21 @@ function handleImageUpload($fileInputName = 'image_file') {
 
     $file = $_FILES[$fileInputName];
     if ($file['error'] !== UPLOAD_ERR_OK) {
-        return ['success' => false, 'message' => 'Upload fehlgeschlagen. Bitte erneut versuchen.'];
+        return ['success' => false, 'message' => 'Upload fehlgeschlagen.'];
     }
 
     $allowedExtensions = ['jpg', 'jpeg', 'png', 'webp', 'gif'];
     $originalName = basename($file['name']);
     $extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
     if (!in_array($extension, $allowedExtensions, true)) {
-        return ['success' => false, 'message' => 'Ungültiger Dateityp. Erlaubt: JPG, PNG, WEBP, GIF.'];
+        return ['success' => false, 'message' => 'Ungültiger Dateityp.'];
     }
 
     $finfo = new finfo(FILEINFO_MIME_TYPE);
     $mimeType = $finfo->file($file['tmp_name']);
     $allowedMimes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
     if (!in_array($mimeType, $allowedMimes, true)) {
-        return ['success' => false, 'message' => 'Die hochgeladene Datei ist kein gültiges Bild.'];
+        return ['success' => false, 'message' => 'Die Datei ist kein gültiges Bild.'];
     }
 
     $imagesDir = __DIR__ . '/../assets/images';
@@ -64,8 +63,26 @@ function handleImageUpload($fileInputName = 'image_file') {
     return ['success' => true, 'filename' => $targetFilename];
 }
 
-function isValidProductCategoryInput($categoryId) {
-    return getCategoryById($categoryId) !== null;
+function buildProductAvailabilityFields($sizesInput, $submittedValues = [], $existingValues = []) {
+    $sizes = getProductSizes(['sizes' => (string) $sizesInput]);
+    if (empty($sizes)) {
+        $sizes = ['Standard'];
+    }
+
+    $availabilityLabels = [];
+    foreach ($sizes as $size) {
+        $fieldName = 'availability_' . str_replace([' ', ','], '_', $size);
+        if (isset($submittedValues[$fieldName])) {
+            $availabilityLabels[$size] = trim((string) $submittedValues[$fieldName]);
+        } else {
+            $availabilityLabels[$size] = trim((string) ($existingValues[$size] ?? ''));
+        }
+    }
+
+    return [
+        'sizes' => implode(',', $sizes),
+        'availability_labels' => $availabilityLabels,
+    ];
 }
 
 function getSubmittedProductCategoryIds($submittedValues) {
@@ -73,7 +90,7 @@ function getSubmittedProductCategoryIds($submittedValues) {
     $validCategoryIds = [];
 
     foreach ($selectedCategoryIds as $categoryId) {
-        if (isValidProductCategoryInput($categoryId)) {
+        if (getCategoryById($categoryId) !== null) {
             $validCategoryIds[] = $categoryId;
         }
     }
@@ -81,146 +98,93 @@ function getSubmittedProductCategoryIds($submittedValues) {
     return $validCategoryIds;
 }
 
-function buildProductSizeStock($sizesInput, $submittedValues = [], $existingValues = []) {
-    $sizes = getProductSizes(['sizes' => (string) $sizesInput]);
-    $stockBySize = [];
-
-    foreach ($sizes as $size) {
-        $stockKey = 'stock_' . str_replace([' ', ','], '_', $size);
-        if (isset($submittedValues[$stockKey])) {
-            $stockBySize[$size] = max(0, (int) $submittedValues[$stockKey]);
-        } elseif (isset($existingValues[$size])) {
-            $stockBySize[$size] = max(0, (int) $existingValues[$size]);
-        } else {
-            $stockBySize[$size] = 0;
-        }
-    }
-
-    return [
-        'sizes' => implode(',', $sizes),
-        'stock_by_size' => $stockBySize
-    ];
-}
-
-// Handle product operations
 if ($_SERVER['REQUEST_METHOD'] === 'POST') {
     $products = getProducts();
 
     if (empty($categories)) {
-        $message = 'Es ist keine Kategorie vorhanden. Bitte zuerst Kategorien anlegen.';
+        $message = 'Bitte zuerst mindestens eine Kategorie anlegen.';
         $messageType = 'error';
-    } elseif (isset($_POST['add_product'])) {
-        $uploadResult = handleImageUpload();
-        if (!$uploadResult['success']) {
-            $message = $uploadResult['message'];
-            $messageType = 'error';
-        } else {
-            $categoryIds = getSubmittedProductCategoryIds($_POST);
-            $sizeData = buildProductSizeStock($_POST['sizes'] ?? '', $_POST);
-
-            if (empty($categoryIds)) {
-                $message = 'Bitte wählen Sie mindestens eine gültige Kategorie aus.';
-                $messageType = 'error';
-            } elseif ($sizeData['sizes'] === '') {
-                $message = 'Bitte geben Sie mindestens eine Größe ein.';
-                $messageType = 'error';
-            } else {
-                $newId = 1;
-                if (!empty($products)) {
-                    $ids = array_column($products, 'id');
-                    $newId = max($ids) + 1;
-                }
-
-                $products[] = [
-                    'id' => $newId,
-                    'name' => sanitize($_POST['name']),
-                    'description' => sanitize($_POST['description']),
-                    'price' => (float) ($_POST['price'] ?? 0),
-                    'categories' => $categoryIds,
-                    'image' => $uploadResult['filename'] !== null ? $uploadResult['filename'] : sanitize($_POST['image']),
-                    'sizes' => $sizeData['sizes'],
-                    'stock_by_size' => $sizeData['stock_by_size']
-                ];
-
-                saveProducts($products);
-                $message = 'Produkt erfolgreich hinzugefügt.';
-                $messageType = 'success';
-            }
-        }
-    }
-
-    if (isset($_POST['update_product'])) {
+    } elseif (isset($_POST['add_product']) || isset($_POST['update_product'])) {
         $uploadResult = handleImageUpload();
         if (!$uploadResult['success']) {
             $message = $uploadResult['message'];
             $messageType = 'error';
         } else {
-            $productId = (int) ($_POST['product_id'] ?? 0);
             $categoryIds = getSubmittedProductCategoryIds($_POST);
-
-            $existingProduct = null;
+            $existingLabels = [];
+            $productId = isset($_POST['product_id']) ? (int) $_POST['product_id'] : 0;
             foreach ($products as $product) {
-                if ($product['id'] === $productId) {
-                    $existingProduct = $product;
+                if ((int) $product['id'] === $productId) {
+                    $existingLabels = $product['availability_labels'] ?? [];
                     break;
                 }
             }
 
-            $existingStockBySize = isset($existingProduct['stock_by_size']) && is_array($existingProduct['stock_by_size'])
-                ? $existingProduct['stock_by_size']
-                : [];
-            $sizeData = buildProductSizeStock($_POST['sizes'] ?? '', $_POST, $existingStockBySize);
+            $sizeData = buildProductAvailabilityFields($_POST['sizes'] ?? '', $_POST, $existingLabels);
 
             if (empty($categoryIds)) {
-                $message = 'Bitte wählen Sie mindestens eine gültige Kategorie aus.';
-                $messageType = 'error';
-            } elseif ($existingProduct === null) {
-                $message = 'Produkt nicht gefunden.';
-                $messageType = 'error';
-            } elseif ($sizeData['sizes'] === '') {
-                $message = 'Bitte geben Sie mindestens eine Größe ein.';
+                $message = 'Bitte mindestens eine gültige Kategorie auswählen.';
                 $messageType = 'error';
             } else {
-                foreach ($products as &$product) {
-                    if ($product['id'] === $productId) {
-                        $product['name'] = sanitize($_POST['name']);
-                        $product['description'] = sanitize($_POST['description']);
-                        $product['price'] = (float) ($_POST['price'] ?? 0);
-                        $product['categories'] = $categoryIds;
-                        $product['image'] = $uploadResult['filename'] !== null ? $uploadResult['filename'] : sanitize($_POST['image']);
-                        $product['sizes'] = $sizeData['sizes'];
-                        $product['stock_by_size'] = $sizeData['stock_by_size'];
-                        unset($product['stock']);
-                        break;
+                $record = [
+                    'name' => sanitize($_POST['name'] ?? ''),
+                    'description' => trim((string) ($_POST['description'] ?? '')),
+                    'categories' => $categoryIds,
+                    'image' => $uploadResult['filename'] !== null ? $uploadResult['filename'] : trim((string) ($_POST['image'] ?? '')),
+                    'sizes' => $sizeData['sizes'],
+                    'availability_labels' => $sizeData['availability_labels'],
+                ];
+
+                if ($record['name'] === '') {
+                    $message = 'Bitte einen Produktnamen eingeben.';
+                    $messageType = 'error';
+                } elseif (isset($_POST['add_product'])) {
+                    $newId = empty($products) ? 1 : (max(array_map(function ($product) {
+                        return (int) $product['id'];
+                    }, $products)) + 1);
+                    $record['id'] = $newId;
+                    $products[] = $record;
+                    saveProducts($products);
+                    $message = 'Produkt wurde angelegt.';
+                    $messageType = 'success';
+                } else {
+                    $updated = false;
+                    foreach ($products as &$product) {
+                        if ((int) $product['id'] === $productId) {
+                            $record['id'] = $productId;
+                            $product = $record;
+                            $updated = true;
+                            break;
+                        }
+                    }
+                    unset($product);
+
+                    if ($updated) {
+                        saveProducts($products);
+                        $message = 'Produkt wurde aktualisiert.';
+                        $messageType = 'success';
+                    } else {
+                        $message = 'Produkt nicht gefunden.';
+                        $messageType = 'error';
                     }
                 }
-                unset($product);
-
-                saveProducts($products);
-                $message = 'Produkt erfolgreich aktualisiert.';
-                $messageType = 'success';
             }
         }
     }
 
     if (isset($_POST['delete_product'])) {
         $productId = (int) ($_POST['product_id'] ?? 0);
-        $products = array_filter($products, function ($product) use ($productId) {
-            return $product['id'] !== $productId;
-        });
-        $products = array_values($products);
+        $products = array_values(array_filter($products, function ($product) use ($productId) {
+            return (int) $product['id'] !== $productId;
+        }));
         saveProducts($products);
-        $message = 'Produkt erfolgreich gelöscht.';
+        $message = 'Produkt wurde gelöscht.';
         $messageType = 'success';
     }
 }
 
 $products = getProducts();
-$editingProduct = null;
-
-if (isset($_GET['edit'])) {
-    $editingProduct = getProductById((int) $_GET['edit']);
-}
+$editingProduct = isset($_GET['edit']) ? getProductById((int) $_GET['edit']) : null;
 
 $bodyClass = 'admin-page';
 include __DIR__ . '/../includes/header.php';
@@ -234,167 +198,120 @@ include __DIR__ . '/../includes/header.php';
 </div>
 
 <?php if ($message !== ''): ?>
-    <div class="alert alert-<?php echo $messageType; ?>">
-        <?php echo htmlspecialchars($message); ?>
+    <div class="alert alert-<?php echo escape($messageType); ?>">
+        <?php echo escape($message); ?>
     </div>
 <?php endif; ?>
 
-<?php if ($editingProduct): ?>
-    <div class="panel" style="padding: 2rem;">
-        <h3>Produkt bearbeiten</h3>
-        <form method="POST" enctype="multipart/form-data">
-            <input type="hidden" name="product_id" value="<?php echo $editingProduct['id']; ?>">
-            <div class="form-group">
-                <label for="name">Name *</label>
-                <input type="text" id="name" name="name" required value="<?php echo htmlspecialchars($editingProduct['name']); ?>">
-            </div>
-            <div class="form-group">
-                <label for="description">Beschreibung</label>
-                <textarea id="description" name="description" rows="4"><?php echo htmlspecialchars($editingProduct['description']); ?></textarea>
-            </div>
-            <div class="form-group">
-                <label for="price">Preis (€) *</label>
-                <input type="number" id="price" name="price" step="0.01" min="0" required value="<?php echo $editingProduct['price']; ?>">
-            </div>
-            <div class="form-group">
-                <label for="categories">Kategorien *</label>
-                <select id="categories" name="categories[]" multiple size="<?php echo max(3, min(8, count($categories))); ?>" required>
-                    <?php foreach ($categories as $category): ?>
-                        <option value="<?php echo htmlspecialchars($category['id']); ?>" <?php echo in_array($category['id'], getProductCategoryIds($editingProduct), true) ? 'selected' : ''; ?>>
-                            <?php echo htmlspecialchars($category['label']); ?>
-                        </option>
-                    <?php endforeach; ?>
-                </select>
-                <small>Mehrfachauswahl mit Strg/Cmd oder Umschalt. Auf Touch-Geräten können mehrere Einträge nacheinander markiert werden.</small>
-            </div>
-            <div class="form-group">
-                <label for="sizes">Größen (kommagetrennt, z.B. Standard oder S,M,L,XL) *</label>
-                <input type="text" id="sizes" name="sizes" required value="<?php echo htmlspecialchars($editingProduct['sizes'] ?? ''); ?>" placeholder="Standard" oninput="updateSizeStockFields()">
-                <small>Alle Produkte nutzen größenbasierten Bestand. Für Artikel ohne Varianten z.B. <code>Standard</code> oder <code>Einheitsgröße</code> verwenden.</small>
-            </div>
-            <div id="size-stock-group">
-                <?php
-                $sizes = getProductSizes($editingProduct);
-                $stockBySize = isset($editingProduct['stock_by_size']) && is_array($editingProduct['stock_by_size']) ? $editingProduct['stock_by_size'] : [];
-                foreach ($sizes as $size):
-                ?>
-                    <div class="form-group">
-                        <label>Lagerbestand für Größe "<?php echo htmlspecialchars($size); ?>" *</label>
-                        <input type="number" name="stock_<?php echo htmlspecialchars(str_replace([' ', ','], '_', $size)); ?>" min="0" value="<?php echo isset($stockBySize[$size]) ? (int) $stockBySize[$size] : 0; ?>" required>
-                    </div>
+<?php
+$currentProduct = $editingProduct ?: [
+    'id' => '',
+    'name' => '',
+    'description' => '',
+    'categories' => [],
+    'sizes' => 'Standard',
+    'availability_labels' => ['Standard' => ''],
+    'image' => '',
+];
+$currentSizes = getProductSizes($currentProduct);
+if (empty($currentSizes)) {
+    $currentSizes = ['Standard'];
+}
+?>
+
+<div class="panel" style="padding: 2rem;">
+    <h3><?php echo $editingProduct ? 'Produkt bearbeiten' : 'Neues Produkt anlegen'; ?></h3>
+    <form method="POST" enctype="multipart/form-data">
+        <?php if ($editingProduct): ?>
+            <input type="hidden" name="product_id" value="<?php echo (int) $editingProduct['id']; ?>">
+        <?php endif; ?>
+
+        <div class="form-group">
+            <label for="name">Name *</label>
+            <input type="text" id="name" name="name" required value="<?php echo escape($currentProduct['name']); ?>">
+        </div>
+
+        <div class="form-group">
+            <label for="description">Beschreibung</label>
+            <textarea id="description" name="description" rows="4"><?php echo escape($currentProduct['description']); ?></textarea>
+        </div>
+
+        <div class="form-group">
+            <label for="categories">Kategorien *</label>
+            <select id="categories" name="categories[]" multiple size="<?php echo max(3, min(8, count($categories))); ?>" required>
+                <?php foreach ($categories as $category): ?>
+                    <option value="<?php echo escape($category['id']); ?>" <?php echo in_array($category['id'], getProductCategoryIds($currentProduct), true) ? 'selected' : ''; ?>>
+                        <?php echo escape($category['label']); ?>
+                    </option>
                 <?php endforeach; ?>
-            </div>
-            <div class="form-group">
-                <label for="image">Bilddateiname (z.B. tshirt.jpg)</label>
-                <input type="text" id="image" name="image" value="<?php echo htmlspecialchars($editingProduct['image']); ?>">
-            </div>
-            <div class="form-group">
-                <label for="image_file">Oder neues Bild hochladen</label>
-                <input type="file" id="image_file" name="image_file" accept=".jpg,.jpeg,.png,.webp,.gif,image/*">
-                <small>Upload nach <code>assets/images</code>; ersetzt den Dateinamen oben automatisch.</small>
-            </div>
-            <script>
-                function updateSizeStockFields() {
-                    const sizesInput = document.getElementById('sizes');
-                    const sizeStockGroup = document.getElementById('size-stock-group');
-                    const currentValues = {};
-
-                    sizeStockGroup.querySelectorAll('input[type="number"]').forEach((input) => {
-                        currentValues[input.name] = input.value;
-                    });
-
-                    const sizes = sizesInput.value.split(',').map((size) => size.trim()).filter((size, index, all) => size && all.indexOf(size) === index);
-                    let html = '';
-
-                    sizes.forEach((size) => {
-                        const safeName = size.replace(/[ ,]/g, '_');
-                        const fieldName = 'stock_' + safeName;
-                        const currentValue = Object.prototype.hasOwnProperty.call(currentValues, fieldName) ? currentValues[fieldName] : '0';
-                        html += '<div class="form-group">';
-                        html += '<label>Lagerbestand für Größe "' + size.replace(/"/g, '&quot;') + '" *</label>';
-                        html += '<input type="number" name="' + fieldName + '" min="0" value="' + currentValue + '" required>';
-                        html += '</div>';
-                    });
-
-                    sizeStockGroup.innerHTML = html;
-                }
-            </script>
-            <button type="submit" name="update_product" class="btn">Produkt aktualisieren</button>
+            </select>
+        </div>
+
+        <div class="form-group">
+            <label for="sizes">Größen (kommagetrennt) *</label>
+            <input type="text" id="sizes" name="sizes" required value="<?php echo escape($currentProduct['sizes']); ?>" placeholder="Standard" oninput="updateAvailabilityFields()">
+            <small>Für Artikel ohne Varianten z. B. <code>Standard</code> oder <code>Einheitsgröße</code>.</small>
+        </div>
+
+        <div id="availability-group">
+            <?php foreach ($currentSizes as $size): ?>
+                <div class="form-group">
+                    <label>Lieferhinweis für Größe "<?php echo escape($size); ?>"</label>
+                    <textarea name="availability_<?php echo escape(str_replace([' ', ','], '_', $size)); ?>" rows="2" placeholder="Optionaler Hinweis"><?php echo escape($currentProduct['availability_labels'][$size] ?? ''); ?></textarea>
+                </div>
+            <?php endforeach; ?>
+        </div>
+
+        <div class="form-group">
+            <label for="image">Bilddateiname</label>
+            <input type="text" id="image" name="image" value="<?php echo escape($currentProduct['image']); ?>">
+        </div>
+
+        <div class="form-group">
+            <label for="image_file">Oder Bild hochladen</label>
+            <input type="file" id="image_file" name="image_file" accept=".jpg,.jpeg,.png,.webp,.gif,image/*">
+        </div>
+
+        <button type="submit" name="<?php echo $editingProduct ? 'update_product' : 'add_product'; ?>" class="btn">
+            <?php echo $editingProduct ? 'Produkt aktualisieren' : 'Produkt anlegen'; ?>
+        </button>
+        <?php if ($editingProduct): ?>
             <a href="products.php" class="btn btn-secondary">Abbrechen</a>
-        </form>
-    </div>
-<?php else: ?>
-    <div class="panel" style="padding: 2rem;">
-        <h3>Neues Produkt hinzufügen</h3>
-        <form method="POST" enctype="multipart/form-data">
-            <div class="form-group">
-                <label for="name">Name *</label>
-                <input type="text" id="name" name="name" required>
-            </div>
-            <div class="form-group">
-                <label for="description">Beschreibung</label>
-                <textarea id="description" name="description" rows="4"></textarea>
-            </div>
-            <div class="form-group">
-                <label for="price">Preis (€) *</label>
-                <input type="number" id="price" name="price" step="0.01" min="0" required>
-            </div>
-            <div class="form-group">
-                <label for="categories">Kategorien *</label>
-                <select id="categories" name="categories[]" multiple size="<?php echo max(3, min(8, count($categories))); ?>" required>
-                    <?php foreach ($categories as $category): ?>
-                        <option value="<?php echo htmlspecialchars($category['id']); ?>">
-                            <?php echo htmlspecialchars($category['label']); ?>
-                        </option>
-                    <?php endforeach; ?>
-                </select>
-                <small>Mehrfachauswahl mit Strg/Cmd oder Umschalt. Auf Touch-Geräten können mehrere Einträge nacheinander markiert werden.</small>
-            </div>
-            <div class="form-group">
-                <label for="sizes">Größen (kommagetrennt, z.B. Standard oder S,M,L,XL) *</label>
-                <input type="text" id="sizes" name="sizes" required placeholder="Standard" oninput="updateSizeStockFields()">
-                <small>Alle Produkte nutzen größenbasierten Bestand. Für Artikel ohne Varianten z.B. <code>Standard</code> oder <code>Einheitsgröße</code> verwenden.</small>
-            </div>
-            <div id="size-stock-group"></div>
-            <div class="form-group">
-                <label for="image">Bilddateiname (z.B. tshirt.jpg)</label>
-                <input type="text" id="image" name="image">
-            </div>
-            <div class="form-group">
-                <label for="image_file">Oder Bild hochladen</label>
-                <input type="file" id="image_file" name="image_file" accept=".jpg,.jpeg,.png,.webp,.gif,image/*">
-                <small>Upload nach <code>assets/images</code>; Dateiname wird automatisch übernommen.</small>
-            </div>
-            <script>
-                function updateSizeStockFields() {
-                    const sizesInput = document.getElementById('sizes');
-                    const sizeStockGroup = document.getElementById('size-stock-group');
-                    const currentValues = {};
-
-                    sizeStockGroup.querySelectorAll('input[type="number"]').forEach((input) => {
-                        currentValues[input.name] = input.value;
-                    });
-
-                    const sizes = sizesInput.value.split(',').map((size) => size.trim()).filter((size, index, all) => size && all.indexOf(size) === index);
-                    let html = '';
-
-                    sizes.forEach((size) => {
-                        const safeName = size.replace(/[ ,]/g, '_');
-                        const fieldName = 'stock_' + safeName;
-                        const currentValue = Object.prototype.hasOwnProperty.call(currentValues, fieldName) ? currentValues[fieldName] : '0';
-                        html += '<div class="form-group">';
-                        html += '<label>Lagerbestand für Größe "' + size.replace(/"/g, '&quot;') + '" *</label>';
-                        html += '<input type="number" name="' + fieldName + '" min="0" value="' + currentValue + '" required>';
-                        html += '</div>';
-                    });
-
-                    sizeStockGroup.innerHTML = html;
-                }
-            </script>
-            <button type="submit" name="add_product" class="btn">Produkt hinzufügen</button>
-        </form>
-    </div>
-<?php endif; ?>
+        <?php endif; ?>
+    </form>
+</div>
+
+<script>
+function updateAvailabilityFields() {
+    const sizesInput = document.getElementById('sizes');
+    const group = document.getElementById('availability-group');
+    const currentValues = {};
+
+    group.querySelectorAll('textarea').forEach((textarea) => {
+        currentValues[textarea.name] = textarea.value;
+    });
+
+    const sizes = sizesInput.value
+        .split(',')
+        .map((size) => size.trim())
+        .filter((size, index, all) => size && all.indexOf(size) === index);
+
+    const finalSizes = sizes.length > 0 ? sizes : ['Standard'];
+    let html = '';
+
+    finalSizes.forEach((size) => {
+        const safeName = size.replace(/[ ,]/g, '_');
+        const fieldName = 'availability_' + safeName;
+        const currentValue = Object.prototype.hasOwnProperty.call(currentValues, fieldName) ? currentValues[fieldName] : '';
+        html += '<div class="form-group">';
+        html += '<label>Lieferhinweis für Größe "' + size.replace(/"/g, '&quot;') + '"</label>';
+        html += '<textarea name="' + fieldName + '" rows="2" placeholder="Optionaler Hinweis">' + currentValue.replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</textarea>';
+        html += '</div>';
+    });
+
+    group.innerHTML = html;
+}
+</script>
 
 <h3>Alle Produkte</h3>
 <?php if (empty($products)): ?>
@@ -407,31 +324,33 @@ include __DIR__ . '/../includes/header.php';
                     <th>ID</th>
                     <th>Name</th>
                     <th>Kategorien</th>
-                    <th>Preis</th>
-                    <th>Lagerbestand</th>
+                    <th>Größen</th>
+                    <th>Lieferhinweise</th>
                     <th>Aktionen</th>
                 </tr>
             </thead>
             <tbody>
                 <?php foreach ($products as $product): ?>
                     <tr>
-                        <td data-label="ID"><?php echo $product['id']; ?></td>
-                        <td data-label="Name"><?php echo htmlspecialchars($product['name']); ?></td>
-                        <td data-label="Kategorien"><?php echo htmlspecialchars(implode(', ', getCategoryLabels(getProductCategoryIds($product)))); ?></td>
-                        <td data-label="Preis"><?php echo formatPrice($product['price']); ?></td>
-                        <td data-label="Lagerbestand">
+                        <td data-label="ID"><?php echo (int) $product['id']; ?></td>
+                        <td data-label="Name"><?php echo escape($product['name']); ?></td>
+                        <td data-label="Kategorien"><?php echo escape(implode(', ', getCategoryLabels(getProductCategoryIds($product)))); ?></td>
+                        <td data-label="Größen"><?php echo escape(implode(', ', getProductSizes($product))); ?></td>
+                        <td data-label="Lieferhinweise">
                             <?php
-                            $stockInfo = [];
-                            foreach (($product['stock_by_size'] ?? []) as $size => $stock) {
-                                $stockInfo[] = $size . ': ' . (int) $stock;
+                            $labels = [];
+                            foreach (($product['availability_labels'] ?? []) as $size => $label) {
+                                if (trim((string) $label) !== '') {
+                                    $labels[] = $size . ': ' . $label;
+                                }
                             }
-                            echo !empty($stockInfo) ? htmlspecialchars(implode(', ', $stockInfo)) : '0';
+                            echo empty($labels) ? 'Keine' : escape(implode(' | ', $labels));
                             ?>
                         </td>
                         <td data-label="Aktionen">
-                            <a href="?edit=<?php echo $product['id']; ?>" class="btn btn-small">Bearbeiten</a>
-                            <form method="POST" style="display: inline;" onsubmit="return confirm('Möchten Sie dieses Produkt wirklich löschen?');">
-                                <input type="hidden" name="product_id" value="<?php echo $product['id']; ?>">
+                            <a href="?edit=<?php echo (int) $product['id']; ?>" class="btn btn-small">Bearbeiten</a>
+                            <form method="POST" style="display: inline;" onsubmit="return confirm('Produkt wirklich löschen?');">
+                                <input type="hidden" name="product_id" value="<?php echo (int) $product['id']; ?>">
                                 <button type="submit" name="delete_product" class="btn btn-secondary btn-small">Löschen</button>
                             </form>
                         </td>

+ 2 - 233
admin/reservations.php

@@ -1,236 +1,5 @@
 <?php
 require_once __DIR__ . '/../config.php';
-require_once __DIR__ . '/../includes/functions.php';
 
-// Check admin login
-if (!isset($_SESSION['admin_logged_in']) || !$_SESSION['admin_logged_in']) {
-    header('Location: login.php');
-    exit;
-}
-
-$pageTitle = 'Reservierungen verwalten';
-
-// Expire old reservations
-expireOldReservations();
-
-// Handle mark as picked up
-if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['mark_picked_up'])) {
-    $reservationId = sanitize($_POST['reservation_id']);
-    markReservationPickedUp($reservationId);
-    $message = 'Reservierung als abgeholt markiert.';
-    $messageType = 'success';
-}
-if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['mark_hidden'])) {
-    $reservationId = sanitize($_POST['reservation_id']);
-    $result = markReservationHidden($reservationId);
-    $message = $result['success'] ? 'Reservierung als Spam/Gelöscht markiert und ausgeblendet.' : $result['message'];
-    $messageType = $result['success'] ? 'success' : 'error';
-}
-
-$reservations = getReservations();
-$filter = isset($_GET['filter']) ? sanitize($_GET['filter']) : 'open';
-$searchOrderNumber = isset($_GET['order_number']) ? sanitize($_GET['order_number']) : '';
-$showHidden = in_array($filter, ['all', 'hidden'], true);
-
-// Exclude backorders
-$reservations = array_filter($reservations, function($r) {
-    return !isset($r['type']) || $r['type'] !== 'backorder';
-});
-
-if (!$showHidden) {
-    $reservations = array_filter($reservations, function($r) {
-        return !isReservationHidden($r);
-    });
-}
-
-// Filter reservations
-if ($searchOrderNumber) {
-    $reservations = array_filter($reservations, function($r) use ($searchOrderNumber) {
-        return stripos($r['id'], $searchOrderNumber) !== false;
-    });
-} else {
-    switch ($filter) {
-        case 'open':
-            $reservations = array_filter($reservations, function($r) {
-                return $r['status'] === 'open' && !$r['picked_up'];
-            });
-            break;
-        case 'picked_up':
-            $reservations = array_filter($reservations, function($r) {
-                return $r['picked_up'];
-            });
-            break;
-        case 'expired':
-            $reservations = array_filter($reservations, function($r) {
-                return $r['status'] === 'expired';
-            });
-            break;
-        case 'hidden':
-            $reservations = array_filter($reservations, function($r) {
-                return isReservationHidden($r);
-            });
-            break;
-    }
-}
-
-$reservations = array_reverse($reservations); // Newest first
-
-$bodyClass = 'admin-page';
-include __DIR__ . '/../includes/header.php';
-?>
-
-<div class="admin-header">
-    <h2>Reservierungen verwalten</h2>
-    <div>
-        <a href="index.php" class="btn btn-secondary">Zurück zum Dashboard</a>
-    </div>
-</div>
-
-<?php if (isset($message)): ?>
-    <div class="alert alert-<?php echo $messageType; ?>">
-        <?php echo htmlspecialchars($message); ?>
-    </div>
-<?php endif; ?>
-
-<div class="panel">
-    <form method="GET" style="display: flex; gap: 1rem; align-items: end; flex-wrap: wrap;">
-        <div style="flex: 1; min-width: 200px;">
-            <label for="order_number">Bestellnummer suchen:</label>
-            <input type="text" id="order_number" name="order_number" value="<?php echo htmlspecialchars($searchOrderNumber); ?>" placeholder="Bestellnummer">
-        </div>
-        <div>
-            <label for="filter">Filter:</label>
-            <select id="filter" name="filter">
-                <option value="all" <?php echo $filter === 'all' ? 'selected' : ''; ?>>Alle</option>
-                <option value="open" <?php echo $filter === 'open' ? 'selected' : ''; ?>>Offen</option>
-                <option value="picked_up" <?php echo $filter === 'picked_up' ? 'selected' : ''; ?>>Abgeholt</option>
-                <option value="expired" <?php echo $filter === 'expired' ? 'selected' : ''; ?>>Abgelaufen</option>
-                <option value="hidden" <?php echo $filter === 'hidden' ? 'selected' : ''; ?>>Spam/Gelöscht</option>
-            </select>
-        </div>
-        <div>
-            <button type="submit" class="btn">Filtern</button>
-            <a href="reservations.php" class="btn btn-secondary">Zurücksetzen</a>
-        </div>
-    </form>
-</div>
-
-<?php if (empty($reservations)): ?>
-    <div class="alert alert-info">
-        <p>Keine Reservierungen gefunden.</p>
-    </div>
-<?php else: ?>
-    <div class="table-responsive">
-        <table class="responsive-table">
-            <thead>
-                <tr>
-                    <th>Bestellnummer</th>
-                    <th>Kunde</th>
-                    <th>Artikel</th>
-                    <th>Erstellt</th>
-                    <th>Status</th>
-                    <th>Aktionen</th>
-                </tr>
-            </thead>
-            <tbody>
-            <?php foreach ($reservations as $reservation): ?>
-                <tr>
-                    <td data-label="Bestellnummer"><strong><?php echo htmlspecialchars($reservation['id']); ?></strong></td>
-                    <td data-label="Kunde"><?php echo htmlspecialchars($reservation['customer_name']); ?></td>
-                    <td data-label="Artikel">
-                        <?php
-                        $itemCount = 0;
-                        foreach ($reservation['items'] as $item) {
-                            $itemCount += $item['quantity'];
-                        }
-                        echo $itemCount . ' Artikel';
-                        ?>
-                    </td>
-                    <td data-label="Erstellt"><?php echo formatDate($reservation['created']); ?></td>
-                    <td data-label="Status">
-                        <?php
-                        if (isReservationHidden($reservation)) {
-                            echo '<span class="status status-hidden">Spam/Gelöscht</span>';
-                        } elseif ($reservation['picked_up']) {
-                            echo '<span class="status status-picked">Abgeholt</span>';
-                        } elseif ($reservation['status'] === 'expired') {
-                            echo '<span class="status status-expired">Abgelaufen</span>';
-                        } else {
-                            echo '<span class="status status-open">Offen</span>';
-                        }
-                        ?>
-                    </td>
-                    <td data-label="Aktionen">
-                        <?php if (!isReservationHidden($reservation) && !$reservation['picked_up'] && $reservation['status'] === 'open'): ?>
-                            <form method="POST" style="display: inline;" onsubmit="return confirm('Reservierung als abgeholt markieren?');">
-                                <input type="hidden" name="reservation_id" value="<?php echo htmlspecialchars($reservation['id']); ?>">
-                                <button type="submit" name="mark_picked_up" class="btn btn-small">Als abgeholt markieren</button>
-                            </form>
-                        <?php endif; ?>
-                        <?php if (!isReservationHidden($reservation)): ?>
-                            <form method="POST" style="display: inline;" onsubmit="return confirm('Reservierung als Spam/Gelöscht markieren? Die Reservierung wird überall ausgeblendet.');">
-                                <input type="hidden" name="reservation_id" value="<?php echo htmlspecialchars($reservation['id']); ?>">
-                                <button type="submit" name="mark_hidden" class="btn btn-secondary btn-small">Spam/Gelöscht</button>
-                            </form>
-                        <?php endif; ?>
-                        <button onclick="showDetails('<?php echo htmlspecialchars($reservation['id']); ?>')" class="btn btn-secondary btn-small">Details</button>
-                    </td>
-                </tr>
-            <?php endforeach; ?>
-            </tbody>
-        </table>
-    </div>
-<?php endif; ?>
-
-<!-- Details Modal -->
-<div id="detailsModal" class="modal">
-    <div class="modal-content">
-        <button onclick="closeDetails()" class="btn btn-small modal-close">Schließen</button>
-        <div id="detailsContent"></div>
-    </div>
-</div>
-
-<script>
-function showDetails(reservationId) {
-    const reservations = <?php echo json_encode(getReservations()); ?>;
-    const reservation = reservations.find(r => r.id === reservationId);
-    
-    if (!reservation) return;
-    
-    let itemsHtml = '<h3>Artikel:</h3><ul>';
-    reservation.items.forEach(item => {
-        const product = <?php echo json_encode(getProducts()); ?>.find(p => p.id == item.product_id);
-        if (product) {
-            let sizeInfo = '';
-            if (item.size && item.size !== '') {
-                sizeInfo = ` - Größe: ${item.size}`;
-            }
-            itemsHtml += `<li>${product.name}${sizeInfo} - Menge: ${item.quantity}</li>`;
-        }
-    });
-    itemsHtml += '</ul>';
-    
-    const isHidden = reservation.is_hidden === true;
-    const statusText = isHidden ? 'Spam/Gelöscht' : (reservation.picked_up ? 'Abgeholt' : (reservation.status === 'expired' ? 'Abgelaufen' : 'Offen'));
-    const statusClass = isHidden ? 'status-hidden' : (reservation.picked_up ? 'status-picked' : (reservation.status === 'expired' ? 'status-expired' : 'status-open'));
-    const html = `
-        <h2>Reservierungsdetails</h2>
-        <p><strong>Bestellnummer:</strong> <strong class="order-highlight">${reservation.id}</strong></p>
-        <p><strong>Kunde:</strong> ${reservation.customer_name}</p>
-        <p><strong>E-Mail:</strong> ${reservation.customer_email}</p>
-        <p><strong>Erstellt:</strong> ${reservation.created}</p>
-        <p><strong>Gültig bis:</strong> ${reservation.expires}</p>
-        <p><strong>Status:</strong> <span class="status ${statusClass}">${statusText}</span></p>
-        ${itemsHtml}
-    `;
-    
-    document.getElementById('detailsContent').innerHTML = html;
-    document.getElementById('detailsModal').style.display = 'flex';
-}
-
-function closeDetails() {
-    document.getElementById('detailsModal').style.display = 'none';
-}
-</script>
-
-<?php include __DIR__ . '/../includes/footer.php'; ?>
+header('Location: orders.php');
+exit;

+ 76 - 0
admin/settings.php

@@ -0,0 +1,76 @@
+<?php
+require_once __DIR__ . '/../config.php';
+require_once __DIR__ . '/../includes/functions.php';
+
+if (empty($_SESSION['admin_logged_in'])) {
+    header('Location: login.php');
+    exit;
+}
+
+$pageTitle = 'Einstellungen';
+$message = '';
+$messageType = '';
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_settings'])) {
+    $settings = [
+        'order_recipient_email' => $_POST['order_recipient_email'] ?? '',
+        'order_confirmation_required' => isset($_POST['order_confirmation_required']),
+        'order_confirmation_expiry_days' => (int) ($_POST['order_confirmation_expiry_days'] ?? 7),
+        'attach_order_pdf_to_admin_email' => isset($_POST['attach_order_pdf_to_admin_email']),
+    ];
+
+    saveSystemSettings($settings);
+    $message = 'Einstellungen wurden gespeichert.';
+    $messageType = 'success';
+}
+
+$settings = getSystemSettings();
+
+$bodyClass = 'admin-page';
+include __DIR__ . '/../includes/header.php';
+?>
+
+<div class="admin-header">
+    <h2>Einstellungen</h2>
+    <div>
+        <a href="index.php" class="btn btn-secondary">Zurück zum Dashboard</a>
+    </div>
+</div>
+
+<?php if ($message !== ''): ?>
+    <div class="alert alert-<?php echo escape($messageType); ?>">
+        <?php echo escape($message); ?>
+    </div>
+<?php endif; ?>
+
+<div class="panel" style="padding: 2rem;">
+    <form method="POST">
+        <div class="form-group">
+            <label for="order_recipient_email">Empfängeradresse für interne Bestellungen *</label>
+            <input type="email" id="order_recipient_email" name="order_recipient_email" required value="<?php echo escape($settings['order_recipient_email']); ?>">
+        </div>
+
+        <div class="form-group">
+            <label>
+                <input type="checkbox" name="order_confirmation_required" value="1" <?php echo !empty($settings['order_confirmation_required']) ? 'checked' : ''; ?>>
+                Bestellungen müssen vor interner Weiterleitung per E-Mail bestätigt werden
+            </label>
+        </div>
+
+        <div class="form-group">
+            <label for="order_confirmation_expiry_days">Bestätigungsfrist in Tagen *</label>
+            <input type="number" id="order_confirmation_expiry_days" name="order_confirmation_expiry_days" min="1" required value="<?php echo (int) $settings['order_confirmation_expiry_days']; ?>">
+        </div>
+
+        <div class="form-group">
+            <label>
+                <input type="checkbox" name="attach_order_pdf_to_admin_email" value="1" <?php echo !empty($settings['attach_order_pdf_to_admin_email']) ? 'checked' : ''; ?>>
+                PDF an interne Bestell-E-Mails anhängen
+            </label>
+        </div>
+
+        <button type="submit" name="save_settings" class="btn">Speichern</button>
+    </form>
+</div>
+
+<?php include __DIR__ . '/../includes/footer.php'; ?>

+ 25 - 0
assets/css/style.css

@@ -205,6 +205,11 @@ main {
     color: var(--brand-danger);
 }
 
+.product-summary {
+    font-size: 0.95rem;
+    color: var(--brand-muted);
+}
+
 /* Forms */
 .form-group {
     margin-bottom: 1.5rem;
@@ -573,6 +578,26 @@ footer {
     border-color: #6b7280;
 }
 
+.status-unconfirmed {
+    color: #facc15;
+    border-color: #facc15;
+}
+
+.status-partial {
+    color: #93c5fd;
+    border-color: #93c5fd;
+}
+
+.status-processed {
+    color: #86efac;
+    border-color: #86efac;
+}
+
+.status-cancelled {
+    color: #fca5a5;
+    border-color: #fca5a5;
+}
+
 .order-highlight {
     font-size: 1.5rem;
     color: var(--brand-accent);

+ 28 - 93
cart.php

@@ -4,58 +4,11 @@ require_once __DIR__ . '/includes/functions.php';
 
 $pageTitle = 'Warenkorb';
 
-// Handle cart updates
-$currentCart = $_SESSION['cart'] ?? [];
-
-if ($_SERVER['REQUEST_METHOD'] === 'POST') {
-    if (isset($_POST['update_cart'])) {
-        $quantities = $_POST['quantities'] ?? [];
-        $newCart = [];
-
-        foreach ($currentCart as $index => $item) {
-            $newQty = isset($quantities[$index]) ? (int)$quantities[$index] : $item['quantity'];
-
-            if ($newQty > 0) {
-                $newItem = [
-                    'product_id' => $item['product_id'],
-                    'quantity' => $newQty
-                ];
-                if (isset($item['size']) && $item['size'] !== '') {
-                    $newItem['size'] = $item['size'];
-                }
-                $newCart[] = $newItem;
-            }
-            // If newQty <= 0, item is removed from cart
-        }
-
-        $_SESSION['cart'] = $newCart;
-    } elseif (isset($_POST['remove_item_index'])) {
-        $removeIndex = (int)$_POST['remove_item_index'];
-        if (isset($currentCart[$removeIndex])) {
-            unset($currentCart[$removeIndex]);
-            $_SESSION['cart'] = array_values($currentCart); // Re-index
-        }
-    }
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['remove_item_index'])) {
+    removeCartItemByIndex((int) $_POST['remove_item_index']);
 }
 
-$cart = $_SESSION['cart'] ?? [];
-$cartItems = [];
-$total = 0;
-
-foreach ($cart as $index => $item) {
-    $product = getProductById($item['product_id']);
-    if ($product) {
-        $itemTotal = $product['price'] * $item['quantity'];
-        $total += $itemTotal;
-        $cartItems[] = [
-            'product' => $product,
-            'quantity' => $item['quantity'],
-            'total' => $itemTotal,
-            'size' => isset($item['size']) ? $item['size'] : null,
-            'cart_index' => $index
-        ];
-    }
-}
+$cartItems = getCartItemsDetailed();
 
 include __DIR__ . '/includes/header.php';
 ?>
@@ -65,54 +18,36 @@ include __DIR__ . '/includes/header.php';
 <?php if (empty($cartItems)): ?>
     <div class="alert alert-info">
         <p>Ihr Warenkorb ist leer.</p>
-        <a href="index.php" class="btn">Weiter einkaufen</a>
+        <a href="index.php" class="btn">Weiter zur Produktübersicht</a>
     </div>
 <?php else: ?>
-    <form method="POST">
-        <?php foreach ($cartItems as $cartItem): ?>
-            <div class="cart-item">
-                <div class="cart-item-info">
-                    <h3><?php echo htmlspecialchars($cartItem['product']['name']); ?></h3>
-                    <?php if (isset($cartItem['size']) && !empty($cartItem['size'])): ?>
-                        <p><strong>Größe:</strong> <?php echo htmlspecialchars($cartItem['size']); ?></p>
-                    <?php endif; ?>
-                    <p>Einzelpreis: <?php echo formatPrice($cartItem['product']['price']); ?></p>
-                    <p>Gesamt: <?php echo formatPrice($cartItem['total']); ?></p>
-                    <?php 
-                    $itemStock = getStock($cartItem['product'], isset($cartItem['size']) ? $cartItem['size'] : null);
-                    $hasEnoughStock = $itemStock >= $cartItem['quantity'];
-                    ?>
-                    <p class="stock <?php echo $hasEnoughStock ? 'in-stock' : 'out-of-stock'; ?>">
-                        Lagerbestand: <?php echo $itemStock; ?> Stück
-                        <?php if (!$hasEnoughStock): ?>
-                            <br><strong class="status status-open">Vorbestellung möglich</strong>
-                        <?php endif; ?>
-                    </p>
-                </div>
-                <div class="cart-item-actions">
-                    <label>
-                        Menge:
-                        <input type="number" name="quantities[<?php echo $cartItem['cart_index']; ?>]" 
-                               value="<?php echo $cartItem['quantity']; ?>" 
-                               min="0" 
-                               max="999"
-                               class="quantity-input">
-                    </label>
-                    <button type="submit" name="remove_item_index" value="<?php echo $cartItem['cart_index']; ?>" class="btn btn-secondary btn-small">Entfernen</button>
-                </div>
-            </div>
-        <?php endforeach; ?>
-        
-        <div class="cart-actions">
-            <div class="cart-total">
-                Gesamtsumme: <?php echo formatPrice($total); ?>
+    <?php foreach ($cartItems as $cartItem): ?>
+        <div class="cart-item">
+            <div class="cart-item-info">
+                <h3><?php echo escape($cartItem['product']['name']); ?></h3>
+                <?php if ($cartItem['size'] !== ''): ?>
+                    <p><strong>Größe:</strong> <?php echo escape($cartItem['size']); ?></p>
+                <?php endif; ?>
+                <?php if ($cartItem['availability_label'] !== ''): ?>
+                    <p><strong>Lieferhinweis:</strong> <?php echo escape($cartItem['availability_label']); ?></p>
+                <?php endif; ?>
+                <p><strong>Menge:</strong> 1</p>
             </div>
-            <div class="cart-buttons">
-                <button type="submit" name="update_cart" class="btn btn-secondary">Warenkorb aktualisieren</button>
-                <a href="checkout.php" class="btn">Zur Reservierung</a>
+            <div class="cart-item-actions">
+                <form method="POST">
+                    <button type="submit" name="remove_item_index" value="<?php echo (int) $cartItem['cart_index']; ?>" class="btn btn-secondary btn-small">Entfernen</button>
+                </form>
             </div>
         </div>
-    </form>
+    <?php endforeach; ?>
+
+    <div class="cart-actions">
+        <div class="cart-total">Artikel im Warenkorb: <?php echo count($cartItems); ?></div>
+        <div class="cart-buttons">
+            <a href="index.php" class="btn btn-secondary">Weiter auswählen</a>
+            <a href="checkout.php" class="btn">Zur Bestellung</a>
+        </div>
+    </div>
 <?php endif; ?>
 
 <?php include __DIR__ . '/includes/footer.php'; ?>

+ 62 - 156
checkout.php

@@ -2,123 +2,42 @@
 require_once __DIR__ . '/config.php';
 require_once __DIR__ . '/includes/functions.php';
 
-$pageTitle = 'Reservierung';
+$pageTitle = 'Bestellung abschließen';
+$cartItems = getCartItemsDetailed();
+$organizations = getOrganizations(true);
+$errors = [];
 
-$cart = $_SESSION['cart'] ?? [];
-if (empty($cart)) {
+if (empty($cartItems)) {
     header('Location: cart.php');
     exit;
 }
 
-// Validate cart items and stock
-$cartItems = [];
-$regularItems = [];
-$backorderItems = [];
-$errors = [];
-$total = 0;
-$regularTotal = 0;
-$backorderTotal = 0;
-
-foreach ($cart as $item) {
-    $product = getProductById($item['product_id']);
-    if (!$product) {
-        $errors[] = 'Ein Produkt wurde nicht gefunden.';
-        continue;
-    }
-    
-    $size = isset($item['size']) ? $item['size'] : null;
-    $itemTotal = $product['price'] * $item['quantity'];
-    $total += $itemTotal;
-    $isInStock = checkStock($item['product_id'], $item['quantity'], $size);
-    if ($isInStock) {
-        $regularTotal += $itemTotal;
-    } else {
-        $backorderTotal += $itemTotal;
-    }
-    $cartItems[] = [
-        'product' => $product,
-        'quantity' => $item['quantity'],
-        'total' => $itemTotal,
-        'size' => isset($item['size']) ? $item['size'] : null,
-        'in_stock' => $isInStock
-    ];
-}
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_order'])) {
+    $customerName = $_POST['customer_name'] ?? '';
+    $customerEmail = $_POST['customer_email'] ?? '';
+    $organizationId = $_POST['organization_id'] ?? '';
+    $comment = $_POST['comment'] ?? '';
 
-// Handle form submission
-if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_reservation'])) {
-    $customerName = sanitize($_POST['customer_name'] ?? '');
-    $customerEmail = sanitize($_POST['customer_email'] ?? '');
-    
-    if (empty($customerName)) {
-        $errors[] = 'Bitte geben Sie Ihren Namen ein.';
-    }
-    
-    if (empty($customerEmail) || !filter_var($customerEmail, FILTER_VALIDATE_EMAIL)) {
-        $errors[] = 'Bitte geben Sie eine gültige E-Mail-Adresse ein.';
-    }
-    
-    if (empty($errors)) {
-        // Create reservation
-        $regularItems = [];
-        $backorderItems = [];
-        foreach ($cart as $cartItem) {
-            $item = [
-                'product_id' => $cartItem['product_id'],
-                'quantity' => $cartItem['quantity']
-            ];
-            if (isset($cartItem['size']) && !empty($cartItem['size'])) {
-                $item['size'] = $cartItem['size'];
-            }
-            
-            $size = isset($cartItem['size']) ? $cartItem['size'] : null;
-            if (checkStock($cartItem['product_id'], $cartItem['quantity'], $size)) {
-                $regularItems[] = $item;
-            } else {
-                $backorderItems[] = $item;
-            }
-        }
-        
-        $regularResult = null;
-        $backorderResult = null;
-        
-        if (!empty($regularItems)) {
-            $regularResult = createReservation($customerName, $customerEmail, $regularItems);
-            if (!$regularResult['success']) {
-                $errors[] = $regularResult['message'];
-            }
-        }
-        
-        if (empty($errors) && !empty($backorderItems)) {
-            $backorderResult = createBackorderReservation($customerName, $customerEmail, $backorderItems);
-        }
-        
-        if (empty($errors)) {
-            $_SESSION['cart'] = [];
-            $query = [];
-            if ($regularResult && $regularResult['success']) {
-                rememberOrderId($regularResult['reservation']['id']);
-                $query[] = 'order_number=' . urlencode($regularResult['reservation']['id']);
-            }
-            if ($backorderResult && $backorderResult['success']) {
-                rememberOrderId($backorderResult['reservation']['id']);
-                $query[] = 'backorder_number=' . urlencode($backorderResult['reservation']['id']);
-            }
-            header('Location: reservation.php?' . implode('&', $query));
-            exit;
-        }
+    $result = createOrder($customerName, $customerEmail, $organizationId, $comment, buildOrderItemsFromCart());
+    if (!$result['success']) {
+        $errors[] = $result['message'];
+    } else {
+        clearCart();
+        header('Location: order-success.php?id=' . urlencode($result['order']['id']));
+        exit;
     }
 }
 
 include __DIR__ . '/includes/header.php';
 ?>
 
-<h2>Reservierung abschließen</h2>
+<h2>Bestellung abschließen</h2>
 
 <?php if (!empty($errors)): ?>
     <div class="alert alert-error">
         <ul style="margin-left: 1.5rem;">
             <?php foreach ($errors as $error): ?>
-                <li><?php echo htmlspecialchars($error); ?></li>
+                <li><?php echo escape($error); ?></li>
             <?php endforeach; ?>
         </ul>
     </div>
@@ -126,76 +45,63 @@ include __DIR__ . '/includes/header.php';
 
 <div class="checkout-grid">
     <div>
-        <h3>Ihre Bestellung</h3>
-        
-        <?php if ($regularTotal > 0): ?>
-            <h4 style="margin-top: 1rem;">Sofort verfügbar</h4>
-            <?php foreach ($cartItems as $cartItem): ?>
-                <?php if ($cartItem['in_stock']): ?>
-                <div class="panel" style="padding: 1rem; margin-bottom: 1rem;">
-                    <strong><?php echo htmlspecialchars($cartItem['product']['name']); ?></strong><br>
-                    <?php if (isset($cartItem['size']) && !empty($cartItem['size'])): ?>
-                        Größe: <?php echo htmlspecialchars($cartItem['size']); ?><br>
-                    <?php endif; ?>
-                    Menge: <?php echo $cartItem['quantity']; ?><br>
-                    Preis: <?php echo formatPrice($cartItem['total']); ?>
-                </div>
+        <h3>Ihre Auswahl</h3>
+
+        <?php foreach ($cartItems as $cartItem): ?>
+            <div class="panel" style="padding: 1rem; margin-bottom: 1rem;">
+                <strong><?php echo escape($cartItem['product']['name']); ?></strong><br>
+                <?php if ($cartItem['size'] !== ''): ?>
+                    Größe: <?php echo escape($cartItem['size']); ?><br>
                 <?php endif; ?>
-            <?php endforeach; ?>
-        <?php endif; ?>
-        
-        <?php if ($backorderTotal > 0): ?>
-            <h4 style="margin-top: 1.5rem;">Vorbestellung</h4>
-            <div class="alert alert-warning" style="margin-bottom: 1rem;">
-                <strong>Hinweis:</strong> Lieferzeiten sind nicht bekannt, da die Bestellung in Chargen erfolgt.
-            </div>
-            <?php foreach ($cartItems as $cartItem): ?>
-                <?php if (!$cartItem['in_stock']): ?>
-                <div class="panel" style="padding: 1rem; margin-bottom: 1rem;">
-                    <strong><?php echo htmlspecialchars($cartItem['product']['name']); ?></strong><br>
-                    <?php if (isset($cartItem['size']) && !empty($cartItem['size'])): ?>
-                        Größe: <?php echo htmlspecialchars($cartItem['size']); ?><br>
-                    <?php endif; ?>
-                    Menge: <?php echo $cartItem['quantity']; ?><br>
-                    Preis: <?php echo formatPrice($cartItem['total']); ?>
-                </div>
+                Menge: 1<br>
+                <?php if ($cartItem['availability_label'] !== ''): ?>
+                    Lieferhinweis: <?php echo escape($cartItem['availability_label']); ?>
                 <?php endif; ?>
-            <?php endforeach; ?>
-        <?php endif; ?>
-        
-        <div class="panel" style="padding: 1rem; margin-top: 1rem;">
-            <?php if ($regularTotal > 0): ?>
-                <div>Summe sofort verfügbar: <strong><?php echo formatPrice($regularTotal); ?></strong></div>
-            <?php endif; ?>
-            <?php if ($backorderTotal > 0): ?>
-                <div>Summe Vorbestellung: <strong><?php echo formatPrice($backorderTotal); ?></strong></div>
-            <?php endif; ?>
-            <strong style="font-size: 1.2rem;">Gesamtsumme: <?php echo formatPrice($total); ?></strong>
-        </div>
+            </div>
+        <?php endforeach; ?>
     </div>
-    
+
     <div>
-        <h3>Ihre Daten</h3>
+        <h3>Bestelldaten</h3>
         <form method="POST">
             <div class="form-group">
                 <label for="customer_name">Name *</label>
-                <input type="text" id="customer_name" name="customer_name" required 
-                       value="<?php echo isset($_POST['customer_name']) ? htmlspecialchars($_POST['customer_name']) : ''; ?>">
+                <input type="text" id="customer_name" name="customer_name" required value="<?php echo isset($_POST['customer_name']) ? escape($_POST['customer_name']) : ''; ?>">
             </div>
-            
+
             <div class="form-group">
                 <label for="customer_email">E-Mail-Adresse *</label>
-                <input type="email" id="customer_email" name="customer_email" required 
-                       value="<?php echo isset($_POST['customer_email']) ? htmlspecialchars($_POST['customer_email']) : ''; ?>">
+                <input type="email" id="customer_email" name="customer_email" required value="<?php echo isset($_POST['customer_email']) ? escape($_POST['customer_email']) : ''; ?>">
             </div>
-            
+
+            <div class="form-group">
+                <label for="organization_id">Organisation *</label>
+                <select id="organization_id" name="organization_id" required>
+                    <option value="">Bitte wählen</option>
+                    <?php foreach ($organizations as $organization): ?>
+                        <option value="<?php echo escape($organization['id']); ?>" <?php echo (isset($_POST['organization_id']) && $_POST['organization_id'] === $organization['id']) ? 'selected' : ''; ?>>
+                            <?php echo escape($organization['label']); ?>
+                        </option>
+                    <?php endforeach; ?>
+                </select>
+            </div>
+
+            <div class="form-group">
+                <label for="comment">Kommentar</label>
+                <textarea id="comment" name="comment" rows="5"><?php echo isset($_POST['comment']) ? escape($_POST['comment']) : ''; ?></textarea>
+            </div>
+
             <div class="alert alert-info">
-                <strong>Hinweis:</strong> Nach der Reservierung erhalten Sie eine Bestellnummer. Für Vorbestellungen informieren wir Sie, sobald die komplette Bestellung zur Abholung bereit ist.
+                <?php if (isOrderConfirmationRequired()): ?>
+                    Nach dem Absenden erhalten Sie eine E-Mail mit einem Bestätigungslink. Erst danach wird die Bestellung intern weitergeleitet.
+                <?php else: ?>
+                    Nach dem Absenden wird die Bestellung direkt intern weitergeleitet.
+                <?php endif; ?>
             </div>
-            
-            <button type="submit" name="create_reservation" class="btn" style="width: 100%;">Reservierung abschließen</button>
+
+            <button type="submit" name="create_order" class="btn" style="width: 100%;">Bestellung absenden</button>
         </form>
-        
+
         <div style="margin-top: 1rem;">
             <a href="cart.php" class="btn btn-secondary">Zurück zum Warenkorb</a>
         </div>

+ 17 - 42
config.sample.php

@@ -1,61 +1,36 @@
 <?php
-// Configuration file for the webshop
+// Configuration for the PSA order system.
 
 // Site settings
-define('SITE_NAME', 'Feuerwehr Freising Test Shop');
-define('SITE_URL', '/shop'); // Leave empty for relative URLs
+define('SITE_NAME', 'PSA-Bestellung Feuerwehr Freising');
+define('SITE_URL', '/shop'); // Leave empty for root, or use absolute URL
 
-// Disclaimer (placeholder text shown on start page)
 define('DISCLAIMER_LINES', [
-    'Dieser Shop ist ein internes System für Mitglieder der Freiwilligen Feuerwehr Freising.',
-    'Diese Produkte können nicht von externen Personen bestellt werden.',
+    'Dieses System dient der internen Bestellung von persönlicher Schutzausrüstung für die Feuerwehr Freising.',
+    'Bestellungen werden ausschließlich intern bearbeitet.',
 ]);
 
 // Admin settings
-// Default password: admin123
-// Change these hashes after first login!
-//
-// To generate a new password hash in bash (using Python bcrypt):
-// python3 -c "import bcrypt; print(bcrypt.hashpw(b'your_new_password', bcrypt.gensalt(rounds=10, prefix=b'2y')).decode())"
-// 
-// Alternative using htpasswd (if Apache tools are installed):
-// htpasswd -bnBC 10 "" your_new_password | sed 's/^://' | sed 's/\$2y\$/\$2y\$/'
-//
-// To add a new admin user:
-// 1) Create a new hash for the password (see commands above).
-// 2) Add a new entry to ADMIN_USERS: 'username' => 'hash'
-//
-// Note:
-// Runtime login source of truth is data/admins.json.
-// ADMIN_USERS is kept only as optional legacy reference.
-//
-// Example:
-// 'max' => '$2y$10$your_hash_here'
-//
-define('ADMIN_USERS', [
-    'admin' => '$2y$10$gArNDW.HhPmDcwYJ/xWRiOPkNop3695UIYzkV.G8WHQRUtLJVPLhy',
-    'manager' => '$2y$10$gArNDW.HhPmDcwYJ/xWRiOPkNop3695UIYzkV.G8WHQRUtLJVPLhy'
-]);
-
-// Reservation settings
-define('RESERVATION_EXPIRY_DAYS', 60);
-define('ORDER_PREFIX', 'FWFS'); // Prefix for order number pattern: PREFIX-YEAR-SEQ
+// Runtime source of truth for admin logins is data/admins.json.
 
-// Browser-linked order history settings (no login required)
-define('ORDER_HISTORY_COOKIE_NAME', 'fw_shop_order_history');
-define('ORDER_HISTORY_COOKIE_TTL_DAYS', 365);
-define('ORDER_HISTORY_MAX_IDS', 10);
-define('ORDER_HISTORY_COOKIE_SECRET', 'change-this-order-history-secret'); // Change this to a long random secret
+// Order settings
+define('ORDER_PREFIX', 'FWFS');
+define('ORDER_RECIPIENT_EMAIL', 'psa@feuerwehr-freising.de');
+define('ORDER_CONFIRMATION_REQUIRED', true);
+define('ORDER_CONFIRMATION_EXPIRY_DAYS', 7);
+define('ATTACH_ORDER_PDF_TO_ADMIN_EMAIL', true);
 
 // Email settings
-define('ADMIN_EMAIL', 'inbox@medowar.de'); // Fallback recipient if no admin account emails are configured
-define('FROM_EMAIL', 'shop@med0.de'); // Change to your sender email
+define('ADMIN_EMAIL', 'psa@feuerwehr-freising.de'); // Fallback for admin profile email defaults
+define('FROM_EMAIL', 'shop@example.org');
 define('FROM_NAME', SITE_NAME);
 
 // Data file paths
 define('DATA_DIR', __DIR__ . '/data/');
 define('PRODUCTS_FILE', DATA_DIR . 'products.json');
-define('RESERVATIONS_FILE', DATA_DIR . 'reservations.json');
+define('ORDERS_FILE', DATA_DIR . 'orders.json');
+define('ORGANIZATIONS_FILE', DATA_DIR . 'organizations.json');
+define('SETTINGS_FILE', DATA_DIR . 'settings.json');
 define('ADMINS_FILE', DATA_DIR . 'admins.json');
 define('CATEGORIES_FILE', DATA_DIR . 'categories.json');
 define('FAQ_FILE', DATA_DIR . 'faq.json');

+ 3 - 0
data/orders.json

@@ -0,0 +1,3 @@
+{
+    "orders": []
+}

+ 16 - 0
data/organizations.json

@@ -0,0 +1,16 @@
+{
+    "organizations": [
+        {
+            "id": "feuerwehr-freising",
+            "label": "Feuerwehr Freising",
+            "sort_order": 10,
+            "active": true
+        },
+        {
+            "id": "landkreis-freising",
+            "label": "Landkreis Freising",
+            "sort_order": 20,
+            "active": true
+        }
+    ]
+}

+ 42 - 51
data/products.json

@@ -3,78 +3,69 @@
         {
             "id": 1,
             "name": "Feuerwehr T-Shirt",
-            "description": "T-Shirt aus 100% Baumwolle mit Feuerwehr-Logo. \r\n\r\n Farbe: schwarz",
-            "price": 18,
-            "category": "apparel",
+            "description": "T-Shirt aus 100% Baumwolle mit Feuerwehr-Logo.\n\nFarbe: schwarz",
             "image": "tshirt.jpg",
+            "categories": [
+                "apparel",
+                "unisex"
+            ],
             "sizes": "S,M,L,XL,XXL",
-            "stock_by_size": {
-                "S": 0,
-                "M": 0,
-                "L": 0,
-                "XL": 0,
-                "XXL": 0
+            "availability_labels": {
+                "S": "",
+                "M": "",
+                "L": "",
+                "XL": "",
+                "XXL": "Erhöhte Lieferzeit, normalerweise nicht auf Lager"
             }
         },
         {
             "id": 2,
             "name": "Feuerwehr Polo-Shirt",
-            "description": "Polo-Shirt mit gesticktem Feuerwehr-Emblem. Für offizielle Anlässe.\n\nFarbe: Dunkelblau",
-            "price": 25,
-            "category": "apparel",
+            "description": "Polo-Shirt mit gesticktem Feuerwehr-Emblem. Für offizielle Anlässe.\n\nFarbe: dunkelblau",
             "image": "poloshirt.jpg",
+            "categories": [
+                "apparel",
+                "mannlich"
+            ],
             "sizes": "S,M,L,XL,XXL",
-            "stock_by_size": {
-                "S": 0,
-                "M": 0,
-                "L": 0,
-                "XL": 0,
-                "XXL": 0
+            "availability_labels": {
+                "S": "",
+                "M": "",
+                "L": "",
+                "XL": "",
+                "XXL": "Erhöhte Lieferzeit, normalerweise nicht auf Lager"
             }
         },
         {
             "id": 3,
-            "name": "Feuerwehr Kapuzenpullover",
-            "description": "Kapuzenpullover mit Feuerwehr-Aufdruck.\n\nFarbe: dunkelblau",
-            "price": 45,
-            "category": "apparel",
+            "name": "Feuerwehr Softshelljacke",
+            "description": "Farbe: Schwarz",
             "image": "",
-            "sizes": "M,L,XL,XXL",
-            "stock_by_size": {
-                "S": 0,
-                "M": 0,
-                "L": 0,
-                "XL": 0,
-                "XXL": 0
+            "categories": [
+                "apparel",
+                "unisex"
+            ],
+            "sizes": "S,M,L,XL,XXL",
+            "availability_labels": {
+                "S": "",
+                "M": "",
+                "L": "",
+                "XL": "",
+                "XXL": "Erhöhte Lieferzeit, normalerweise nicht auf Lager"
             }
         },
         {
             "id": 4,
             "name": "Feuerwehr Cap",
-            "description": "Klassische Basecap mit Feuerwehr-Logo.\n\n Wird nicht gelagert, nur auf Bestellung\n\n Einheitsgröße, verstellbar",
-            "price": 15,
-            "category": "apparel",
+            "description": "Klassische Basecap mit Feuerwehr-Logo.\n\nEinheitsgröße, verstellbar",
             "image": "",
+            "categories": [
+                "apparel"
+            ],
             "sizes": "Einheitsgröße",
-            "stock_by_size": {
-                "Einheitsgröße": 0
-            }
-        },
-        {
-            "id": 3,
-            "name": "Feuerwehr Softshelljacke",
-            "description": "Farbe: Schwarz",
-            "price": 45,
-            "category": "apparel",
-            "image": "",
-            "sizes": "M,L,XL,XXL",
-            "stock_by_size": {
-                "S": 0,
-                "M": 0,
-                "L": 0,
-                "XL": 0,
-                "XXL": 0
+            "availability_labels": {
+                "Einheitsgröße": "Nur auf Bestellung verfügbar"
             }
         }
     ]
-}
+}

+ 0 - 131
data/reservations.json

@@ -1,131 +0,0 @@
-{
-    "reservations": [
-        {
-            "id": "FWFS-2026-001",
-            "customer_name": "Josef Test",
-            "customer_email": "inbox@medowar.de",
-            "items": [
-                {
-                    "product_id": 1,
-                    "quantity": 3,
-                    "size": "L"
-                }
-            ],
-            "created": "2026-02-08 23:50:25",
-            "expires": "2026-04-09 23:50:25",
-            "status": "expired",
-            "picked_up": false,
-            "type": "regular"
-        },
-        {
-            "id": "FWFS-2026-002",
-            "customer_name": "Testing 123",
-            "customer_email": "inbox@medowar.de",
-            "items": [
-                {
-                    "product_id": 1,
-                    "quantity": 5,
-                    "size": "L"
-                }
-            ],
-            "created": "2026-02-08 23:50:58",
-            "expires": "2026-04-09 23:56:38",
-            "status": "expired",
-            "picked_up": false,
-            "type": "regular"
-        },
-        {
-            "id": "FWFS-2026-003",
-            "customer_name": "Josef Test",
-            "customer_email": "inbox@medowar.de",
-            "items": [
-                {
-                    "product_id": 1,
-                    "quantity": 15,
-                    "size": "M"
-                }
-            ],
-            "created": "2026-02-08 23:52:29",
-            "expires": "2026-04-09 23:52:29",
-            "status": "expired",
-            "picked_up": false,
-            "type": "regular"
-        },
-        {
-            "id": "FWFS-2026-004",
-            "customer_name": "Josef test3",
-            "customer_email": "inbox@medowar.de",
-            "items": [
-                {
-                    "product_id": 7,
-                    "quantity": 80
-                }
-            ],
-            "created": "2026-02-08 23:53:56",
-            "expires": "2026-04-09 23:53:56",
-            "status": "picked_up",
-            "picked_up": true,
-            "type": "regular"
-        },
-        {
-            "id": "FWFS-2026-005",
-            "customer_name": "Josef test3",
-            "customer_email": "inbox@medowar.de",
-            "items": [
-                {
-                    "product_id": 1,
-                    "quantity": 3,
-                    "size": "M"
-                },
-                {
-                    "product_id": 1,
-                    "quantity": 2,
-                    "size": "L"
-                }
-            ],
-            "created": "2026-02-08 23:53:58",
-            "expires": "",
-            "status": "open",
-            "picked_up": false,
-            "type": "backorder",
-            "backorder_status": "pending",
-            "is_hidden": true,
-            "hidden_at": "2026-04-12 12:27:01",
-            "hidden_reason": "spam_deleted"
-        },
-        {
-            "id": "FWFS-2026-006",
-            "customer_name": "Inbox Testing mail",
-            "customer_email": "medowar@AllFreeMail.net",
-            "items": [
-                {
-                    "product_id": 1,
-                    "quantity": 2,
-                    "size": "L"
-                }
-            ],
-            "created": "2026-02-09 00:23:51",
-            "expires": "2026-04-10 00:25:03",
-            "status": "expired",
-            "picked_up": false,
-            "type": "regular"
-        },
-        {
-            "id": "FWFS-2026-007",
-            "customer_name": "Josef Test Responsive",
-            "customer_email": "medowar@AllFreeMail.net",
-            "items": [
-                {
-                    "product_id": 1,
-                    "quantity": 1,
-                    "size": "S"
-                }
-            ],
-            "created": "2026-02-09 12:05:43",
-            "expires": "2026-04-10 12:05:43",
-            "status": "expired",
-            "picked_up": false,
-            "type": "regular"
-        }
-    ]
-}

+ 8 - 0
data/settings.json

@@ -0,0 +1,8 @@
+{
+    "settings": {
+        "order_recipient_email": "inbox@medowar.de",
+        "order_confirmation_required": true,
+        "order_confirmation_expiry_days": 7,
+        "attach_order_pdf_to_admin_email": true
+    }
+}

+ 26 - 44
docs/CONFIG_REFERENCE.md

@@ -1,46 +1,28 @@
 # Config Reference (`config.php`)
 
-This document explains what each config value is used for at runtime.
-
-## `ADMIN_EMAIL` (your specific question)
-
-`ADMIN_EMAIL` is the **fallback** recipient address for internal admin notifications.
-
-Primary recipients come from each admin account in `data/admins.json` (`email` field).
-The fallback is used only when no valid admin account email is available.
-
-Notification scope:
-
-- New reservation created (`sendReservationEmails()` in `includes/functions.php`)
-- New backorder created (`sendBackorderEmails()` in `includes/functions.php`)
-
-It is **not** used for customer login, admin login, password reset, or contact form logic.
-
-## Constant-by-constant reference
-
-| Constant | What it controls | Where it is used |
-|---|---|---|
-| `SITE_NAME` | Shop name shown in UI and email subjects/content | `includes/header.php`, `admin/login.php`, mail templates in `includes/functions.php` |
-| `SITE_URL` | Base path for links/assets (e.g. `/shop`) and order-history cookie path | `includes/header.php`, `includes/footer.php`, `admin/login.php`, `orders.php`, cookie path helper in `includes/functions.php` |
-| `DISCLAIMER_LINES` | Text lines shown on homepage disclaimer box | `index.php` |
-| `RESERVATION_EXPIRY_DAYS` | Number of days until a regular reservation expires | Reservation creation/conversion in `includes/functions.php` |
-| `ORDER_PREFIX` | Prefix for generated order IDs, format `PREFIX-YEAR-SEQ` | ID generation + validation in `includes/functions.php` |
-| `ORDER_HISTORY_COOKIE_NAME` | Cookie key name for browser-linked order history | `includes/functions.php` |
-| `ORDER_HISTORY_COOKIE_TTL_DAYS` | Retention time (days) for order-history cookie | `includes/functions.php` |
-| `ORDER_HISTORY_MAX_IDS` | Max remembered order IDs in history cookie | `includes/functions.php` |
-| `ORDER_HISTORY_COOKIE_SECRET` | HMAC signing secret for order-history cookie integrity | `includes/functions.php` |
-| `ADMIN_EMAIL` | Fallback recipient for admin notification emails when no valid per-admin email exists | `includes/functions.php` |
-| `FROM_EMAIL` | Sender + reply-to email in outgoing mails | `sendEmail()` in `includes/functions.php` |
-| `FROM_NAME` | Sender display name in outgoing mails | `sendEmail()` in `includes/functions.php` |
-| `DATA_DIR` | Base directory for JSON data files | Used to compose file constants in `config.php` |
-| `PRODUCTS_FILE` | Product data JSON path | Product read/write helpers in `includes/functions.php` |
-| `RESERVATIONS_FILE` | Reservation/backorder JSON path | Reservation read/write helpers in `includes/functions.php` |
-| `ADMINS_FILE` | Admin account JSON path | Admin account read/write helpers in `includes/functions.php` |
-| `FAQ_FILE` | FAQ content JSON path (`content` markdown text) | FAQ read/write and markdown rendering helpers in `includes/functions.php`, pages `faq.php` + `admin/faq.php` |
-
-## Important notes
-
-- `ORDER_HISTORY_COOKIE_SECRET` should be a long random value; changing it invalidates old browser history cookies.
-- `SITE_URL` should match the real subpath where the app is served. If wrong, links/assets/cookies may break.
-- In this codebase, admin login data is read from `data/admins.json` (`ADMINS_FILE`).  
-  The `ADMIN_USERS` block is currently a legacy reference in comments/sample config, not active runtime auth.
+## Relevante Konstanten
+
+| Konstante | Zweck |
+|---|---|
+| `SITE_NAME` | Name des Systems in Oberfläche und Mails |
+| `SITE_URL` | Basispfad oder Basis-URL für Links, Assets und Bestätigungslinks |
+| `DISCLAIMER_LINES` | Hinweistext auf der Startseite |
+| `ORDER_PREFIX` | Präfix für Bestellnummern |
+| `ORDER_RECIPIENT_EMAIL` | Standard-Empfänger für interne Bestellmails |
+| `ORDER_CONFIRMATION_REQUIRED` | Standard, ob Bestellungen vor interner Weiterleitung bestätigt werden müssen |
+| `ORDER_CONFIRMATION_EXPIRY_DAYS` | Standardfrist für Bestätigungslinks |
+| `ATTACH_ORDER_PDF_TO_ADMIN_EMAIL` | Standard, ob interne Bestellmails ein PDF erhalten |
+| `ADMIN_EMAIL` | Fallback für Admin-Profile ohne gültige Mailadresse |
+| `FROM_EMAIL` | Absenderadresse ausgehender Mails |
+| `FROM_NAME` | Anzeigename ausgehender Mails |
+| `PRODUCTS_FILE` | JSON-Datei für Produkte |
+| `ORDERS_FILE` | JSON-Datei für Bestellungen |
+| `ORGANIZATIONS_FILE` | JSON-Datei für Organisationen |
+| `SETTINGS_FILE` | JSON-Datei für im Admin veränderbare Systemeinstellungen |
+| `ADMINS_FILE` | JSON-Datei für Adminkonten |
+| `CATEGORIES_FILE` | JSON-Datei für Kategorien |
+| `FAQ_FILE` | JSON-Datei für FAQ-Inhalte |
+
+## Hinweis
+
+Die Konstanten definieren die Startwerte. Änderbare Betriebsparameter wie Bestätigungspflicht oder interne Empfängeradresse können zusätzlich im Adminbereich unter `Einstellungen` angepasst werden.

+ 0 - 56
docs/ORDER_HISTORY.md

@@ -1,56 +0,0 @@
-# Browser-Linked Bestellhistorie
-
-## Zweck
-
-Dieses Feature zeigt Kunden ihre zuletzt erstellten Bestellungen ohne Login-System.  
-Die Ansicht ist unter `orders.php` erreichbar und listet Bestellungen, die in diesem Browser gespeichert wurden.
-
-## Nutzerverhalten
-
-- Nach erfolgreicher Reservierung wird die Bestellnummer automatisch im Browser gemerkt.
-- Die Seite `Meine Bestellungen` zeigt diese Bestellungen mit Typ, Datum, Status und Detail-Link.
-- Es gibt keine manuelle Verknüpfung per Login oder Order-Import.
-
-## Technisches Modell (Best Effort)
-
-- Speicherung erfolgt in einem signierten Cookie.
-- Cookie-Inhalt: `base64url(payload).hmac`
-- Payload-Version `v=1`:
-  - `ids`: Liste der gemerkten Bestellnummern
-  - `iat`: Zeitstempel der Erstellung
-- Signatur: `hash_hmac('sha256', base64url(payload), ORDER_HISTORY_COOKIE_SECRET)`
-- Bei ungültiger/tamperter Signatur wird der Verlauf ignoriert (fail-safe, ohne Fehlerseite).
-
-## Limits und Aufbewahrung
-
-- Maximale Anzahl gemerkter Bestellungen: `ORDER_HISTORY_MAX_IDS` (Standard `10`)
-- Aufbewahrung: `ORDER_HISTORY_COOKIE_TTL_DAYS` (Standard `365` Tage)
-- Reihenfolge: neueste zuerst
-
-## Sicherheit und Datenschutz
-
-- Browser-gebunden, nicht account-gebunden.
-- Tamper-evident durch HMAC-Signatur.
-- Cookie-Flags:
-  - `HttpOnly`
-  - `SameSite=Lax`
-  - `Secure` bei HTTPS
-- Wenn Browserdaten/Cookies gelöscht werden, ist der Verlauf weg.
-
-## Konfiguration
-
-In `config.php` müssen folgende Werte gesetzt sein:
-
-- `ORDER_HISTORY_COOKIE_NAME`
-- `ORDER_HISTORY_COOKIE_TTL_DAYS`
-- `ORDER_HISTORY_MAX_IDS`
-- `ORDER_HISTORY_COOKIE_SECRET`
-
-`ORDER_HISTORY_COOKIE_SECRET` muss ein eigener, ausreichend zufälliger Wert sein.
-
-## Betrieb und Fehlerverhalten
-
-- Das Feature ist bewusst best effort:
-  - Kann der Cookie nicht geschrieben werden, bleibt der Checkout trotzdem erfolgreich.
-  - Nicht mehr vorhandene oder ausgeblendete Bestellungen erscheinen nicht in `orders.php`.
-  - Ungültige Cookie-Daten werden still verworfen.

+ 1074 - 1110
includes/functions.php

@@ -1,64 +1,54 @@
 <?php
 require_once __DIR__ . '/../config.php';
 
-/**
- * Read JSON file and return decoded data
- */
 function readJsonFile($file) {
     if (!file_exists($file)) {
         return [];
     }
+
     $content = file_get_contents($file);
-    if (empty($content)) {
+    if ($content === false || trim($content) === '') {
         return [];
     }
+
     $data = json_decode($content, true);
-    return $data ? $data : [];
+    return is_array($data) ? $data : [];
 }
 
-/**
- * Write data to JSON file
- */
 function writeJsonFile($file, $data) {
     $dir = dirname($file);
     if (!is_dir($dir)) {
         mkdir($dir, 0755, true);
     }
-    file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
+
+    file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
+}
+
+function sanitize($input) {
+    return trim(strip_tags((string) $input));
+}
+
+function escape($value) {
+    return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
 }
 
-/**
- * Normalize admin username input.
- */
 function normalizeAdminUsername($username) {
-    return trim((string)$username);
+    return trim((string) $username);
 }
 
-/**
- * Normalize admin description input.
- */
 function normalizeAdminDescription($description) {
-    return trim((string)$description);
+    return trim((string) $description);
 }
 
-/**
- * Normalize admin email input.
- */
 function normalizeAdminEmail($email) {
-    return strtolower(trim((string)$email));
+    return strtolower(trim((string) $email));
 }
 
-/**
- * Validate admin username format.
- */
 function isValidAdminUsername($username) {
     $username = normalizeAdminUsername($username);
     return preg_match('/^[A-Za-z0-9][A-Za-z0-9._-]{2,49}$/', $username) === 1;
 }
 
-/**
- * Validate admin description.
- */
 function isValidAdminDescription($description) {
     $description = normalizeAdminDescription($description);
     if ($description === '') {
@@ -69,189 +59,118 @@ function isValidAdminDescription($description) {
     return $length <= 120;
 }
 
-/**
- * Validate admin email.
- */
 function isValidAdminEmail($email) {
     $email = normalizeAdminEmail($email);
     return $email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
 }
 
-/**
- * Get default description for an admin account.
- */
 function getDefaultAdminDescription($username) {
     return 'Admin';
 }
 
-/**
- * Get fallback admin email address from config.
- */
 function getDefaultAdminEmail() {
-    if (!defined('ADMIN_EMAIL') || !is_string(ADMIN_EMAIL)) {
-        return '';
-    }
-
-    $fallbackEmail = normalizeAdminEmail(ADMIN_EMAIL);
-    if (!isValidAdminEmail($fallbackEmail)) {
-        return '';
-    }
-
-    return $fallbackEmail;
+    $email = defined('ADMIN_EMAIL') ? normalizeAdminEmail(ADMIN_EMAIL) : '';
+    return isValidAdminEmail($email) ? $email : '';
 }
 
-/**
- * Get full admin account records from JSON store.
- */
 function getAdminAccounts() {
     $data = readJsonFile(ADMINS_FILE);
+    $records = isset($data['admins']) && is_array($data['admins']) ? $data['admins'] : [];
     $accounts = [];
 
-    if (isset($data['admins']) && is_array($data['admins'])) {
-        foreach ($data['admins'] as $username => $record) {
-            $normalizedUsername = normalizeAdminUsername($username);
-            if ($normalizedUsername === '') {
-                continue;
-            }
-
-            $hash = '';
-            $description = '';
-            $email = getDefaultAdminEmail();
-            if (is_string($record)) {
-                $hash = $record;
-                $description = getDefaultAdminDescription($normalizedUsername);
-            } elseif (is_array($record)) {
-                $hash = isset($record['password_hash']) && is_string($record['password_hash']) ? $record['password_hash'] : '';
-                $description = isset($record['description']) ? normalizeAdminDescription($record['description']) : getDefaultAdminDescription($normalizedUsername);
-                $email = isset($record['email']) ? normalizeAdminEmail($record['email']) : getDefaultAdminEmail();
-            }
-
-            if ($hash === '') {
-                continue;
-            }
-            if (!isValidAdminDescription($description)) {
-                $description = getDefaultAdminDescription($normalizedUsername);
-            }
-            if (!isValidAdminEmail($email)) {
-                $email = getDefaultAdminEmail();
-            }
-
-            $accounts[$normalizedUsername] = [
-                'password_hash' => $hash,
-                'description' => $description,
-                'email' => $email
-            ];
-        }
-    }
-
-    return $accounts;
-}
-
-/**
- * Get admin users for authentication (username => password hash).
- */
-function getAdminUsers() {
-    $accounts = getAdminAccounts();
-    $admins = [];
-
-    foreach ($accounts as $username => $account) {
-        if (!isset($account['password_hash']) || !is_string($account['password_hash']) || $account['password_hash'] === '') {
+    foreach ($records as $username => $record) {
+        $username = normalizeAdminUsername($username);
+        if ($username === '') {
             continue;
         }
-        $admins[$username] = $account['password_hash'];
-    }
-
-    return $admins;
-}
 
-/**
- * Save full admin accounts to JSON store.
- */
-function saveAdminAccounts($accounts) {
-    $sanitizedAccounts = [];
+        if (is_string($record)) {
+            $record = [
+                'password_hash' => $record,
+                'description' => getDefaultAdminDescription($username),
+                'email' => getDefaultAdminEmail(),
+            ];
+        }
 
-    foreach ($accounts as $username => $account) {
-        $normalizedUsername = normalizeAdminUsername($username);
-        if ($normalizedUsername === '' || !is_array($account)) {
+        if (!is_array($record)) {
             continue;
         }
 
-        $hash = isset($account['password_hash']) && is_string($account['password_hash']) ? $account['password_hash'] : '';
+        $hash = isset($record['password_hash']) ? (string) $record['password_hash'] : '';
         if ($hash === '') {
             continue;
         }
 
-        $description = isset($account['description']) ? normalizeAdminDescription($account['description']) : getDefaultAdminDescription($normalizedUsername);
+        $description = normalizeAdminDescription($record['description'] ?? getDefaultAdminDescription($username));
         if (!isValidAdminDescription($description)) {
-            $description = getDefaultAdminDescription($normalizedUsername);
+            $description = getDefaultAdminDescription($username);
         }
 
-        $email = isset($account['email']) ? normalizeAdminEmail($account['email']) : getDefaultAdminEmail();
+        $email = normalizeAdminEmail($record['email'] ?? getDefaultAdminEmail());
         if (!isValidAdminEmail($email)) {
             $email = getDefaultAdminEmail();
         }
 
-        $sanitizedAccounts[$normalizedUsername] = [
+        $accounts[$username] = [
             'password_hash' => $hash,
             'description' => $description,
-            'email' => $email
+            'email' => $email,
         ];
     }
 
-    ksort($sanitizedAccounts);
-    writeJsonFile(ADMINS_FILE, ['admins' => $sanitizedAccounts]);
+    ksort($accounts);
+    return $accounts;
+}
+
+function getAdminUsers() {
+    $users = [];
+    foreach (getAdminAccounts() as $username => $record) {
+        $users[$username] = $record['password_hash'];
+    }
+    return $users;
 }
 
-/**
- * Save admin users to JSON store (username => password hash).
- */
-function saveAdminUsers($admins) {
-    $existingAccounts = getAdminAccounts();
-    $normalizedAccounts = [];
+function saveAdminAccounts($accounts) {
+    $result = [];
+
+    foreach ($accounts as $username => $record) {
+        $username = normalizeAdminUsername($username);
+        if ($username === '' || !is_array($record)) {
+            continue;
+        }
 
-    foreach ($admins as $username => $hash) {
-        $normalizedUsername = normalizeAdminUsername($username);
-        if ($normalizedUsername === '' || !is_string($hash) || $hash === '') {
+        $hash = isset($record['password_hash']) ? (string) $record['password_hash'] : '';
+        if ($hash === '') {
             continue;
         }
 
-        $description = isset($existingAccounts[$normalizedUsername]['description'])
-            ? normalizeAdminDescription($existingAccounts[$normalizedUsername]['description'])
-            : getDefaultAdminDescription($normalizedUsername);
+        $description = normalizeAdminDescription($record['description'] ?? getDefaultAdminDescription($username));
         if (!isValidAdminDescription($description)) {
-            $description = getDefaultAdminDescription($normalizedUsername);
+            $description = getDefaultAdminDescription($username);
         }
 
-        $email = isset($existingAccounts[$normalizedUsername]['email'])
-            ? normalizeAdminEmail($existingAccounts[$normalizedUsername]['email'])
-            : getDefaultAdminEmail();
+        $email = normalizeAdminEmail($record['email'] ?? getDefaultAdminEmail());
         if (!isValidAdminEmail($email)) {
             $email = getDefaultAdminEmail();
         }
 
-        $normalizedAccounts[$normalizedUsername] = [
+        $result[$username] = [
             'password_hash' => $hash,
             'description' => $description,
-            'email' => $email
+            'email' => $email,
         ];
     }
 
-    saveAdminAccounts($normalizedAccounts);
+    ksort($result);
+    writeJsonFile(ADMINS_FILE, ['admins' => $result]);
 }
 
-/**
- * Get default category records.
- */
 function getDefaultCategories() {
     return [
         ['id' => 'apparel', 'label' => 'Bekleidung'],
-        ['id' => 'merch', 'label' => 'Merchandise']
     ];
 }
 
-/**
- * Normalize category id input to a stable slug.
- */
 function normalizeCategoryId($id) {
     $id = trim((string) $id);
     if ($id === '') {
@@ -267,21 +186,13 @@ function normalizeCategoryId($id) {
 
     $id = strtolower($id);
     $id = preg_replace('/[^a-z0-9]+/', '-', $id);
-    $id = trim((string) $id, '-');
-
-    return $id;
+    return trim((string) $id, '-');
 }
 
-/**
- * Normalize category label input.
- */
 function normalizeCategoryLabel($label) {
     return trim((string) $label);
 }
 
-/**
- * Validate category label.
- */
 function isValidCategoryLabel($label) {
     $label = normalizeCategoryLabel($label);
     if ($label === '') {
@@ -292,12 +203,8 @@ function isValidCategoryLabel($label) {
     return $length <= 80;
 }
 
-/**
- * Normalize category records from storage.
- */
 function normalizeCategories($categories) {
     $normalized = [];
-
     if (!is_array($categories)) {
         $categories = [];
     }
@@ -315,7 +222,7 @@ function normalizeCategories($categories) {
 
         $normalized[$id] = [
             'id' => $id,
-            'label' => $label
+            'label' => $label,
         ];
     }
 
@@ -332,18 +239,15 @@ function normalizeCategories($categories) {
     return array_values($normalized);
 }
 
-/**
- * Get all categories.
- */
 function getCategories() {
     $data = readJsonFile(CATEGORIES_FILE);
-    $categories = isset($data['categories']) ? $data['categories'] : [];
-    return normalizeCategories($categories);
+    return normalizeCategories($data['categories'] ?? []);
+}
+
+function saveCategories($categories) {
+    writeJsonFile(CATEGORIES_FILE, ['categories' => normalizeCategories($categories)]);
 }
 
-/**
- * Get category by id.
- */
 function getCategoryById($categoryId) {
     $categoryId = normalizeCategoryId($categoryId);
     foreach (getCategories() as $category) {
@@ -351,59 +255,39 @@ function getCategoryById($categoryId) {
             return $category;
         }
     }
-
     return null;
 }
 
-/**
- * Get category label by id with fallback to raw id.
- */
 function getCategoryLabel($categoryId) {
     $category = getCategoryById($categoryId);
     if ($category !== null) {
         return $category['label'];
     }
-
-    $categoryId = trim((string) $categoryId);
-    return $categoryId !== '' ? $categoryId : 'Unbekannt';
+    return trim((string) $categoryId);
 }
 
-/**
- * Get category labels by ids.
- */
 function getCategoryLabels($categoryIds) {
     $labels = [];
     foreach (normalizeProductCategoryIds($categoryIds) as $categoryId) {
         $labels[] = getCategoryLabel($categoryId);
     }
-
     return $labels;
 }
 
-/**
- * Save categories.
- */
-function saveCategories($categories) {
-    writeJsonFile(CATEGORIES_FILE, ['categories' => normalizeCategories($categories)]);
-}
-
-/**
- * Generate a unique category id from a label.
- */
 function generateCategoryIdFromLabel($label, $existingCategories = []) {
     $baseId = normalizeCategoryId($label);
     if ($baseId === '') {
         $baseId = 'category';
     }
 
-    $usedIds = [];
+    $used = [];
     foreach (normalizeCategories($existingCategories) as $category) {
-        $usedIds[$category['id']] = true;
+        $used[$category['id']] = true;
     }
 
     $candidate = $baseId;
     $counter = 2;
-    while (isset($usedIds[$candidate])) {
+    while (isset($used[$candidate])) {
         $candidate = $baseId . '-' . $counter;
         $counter++;
     }
@@ -411,70 +295,50 @@ function generateCategoryIdFromLabel($label, $existingCategories = []) {
     return $candidate;
 }
 
-/**
- * Check whether any product uses a category id.
- */
 function isCategoryInUse($categoryId) {
-    $categoryId = normalizeCategoryId($categoryId);
     foreach (getProducts() as $product) {
         if (productHasCategory($product, $categoryId)) {
             return true;
         }
     }
-
     return false;
 }
 
-/**
- * Normalize product category ids from legacy or current storage.
- */
 function normalizeProductCategoryIds($categoryValue) {
     if (is_array($categoryValue)) {
-        $rawCategoryIds = $categoryValue;
+        $rawIds = $categoryValue;
     } elseif ($categoryValue === null || $categoryValue === '') {
-        $rawCategoryIds = [];
+        $rawIds = [];
     } else {
-        $rawCategoryIds = [$categoryValue];
+        $rawIds = [$categoryValue];
     }
 
     $normalized = [];
-    foreach ($rawCategoryIds as $categoryId) {
+    foreach ($rawIds as $categoryId) {
         $categoryId = normalizeCategoryId($categoryId);
-        if ($categoryId === '') {
-            continue;
+        if ($categoryId !== '') {
+            $normalized[$categoryId] = $categoryId;
         }
-        $normalized[$categoryId] = $categoryId;
     }
 
     return array_values($normalized);
 }
 
-/**
- * Get normalized category ids for a product.
- */
 function getProductCategoryIds($product) {
     if (isset($product['categories'])) {
         return normalizeProductCategoryIds($product['categories']);
     }
-
     return normalizeProductCategoryIds($product['category'] ?? []);
 }
 
-/**
- * Determine whether a product is assigned to a category.
- */
 function productHasCategory($product, $categoryId) {
     $categoryId = normalizeCategoryId($categoryId);
     if ($categoryId === '') {
         return false;
     }
-
     return in_array($categoryId, getProductCategoryIds($product), true);
 }
 
-/**
- * Parse product sizes into a normalized array.
- */
 function getProductSizes($product) {
     if (isset($product['sizes']) && is_array($product['sizes'])) {
         $sizes = $product['sizes'];
@@ -487,72 +351,78 @@ function getProductSizes($product) {
     $normalized = [];
     foreach ($sizes as $size) {
         $size = trim((string) $size);
-        if ($size === '') {
-            continue;
+        if ($size !== '') {
+            $normalized[$size] = $size;
         }
-        $normalized[$size] = $size;
     }
 
     return array_values($normalized);
 }
 
-/**
- * Determine whether a product uses size-based stock.
- */
 function productUsesSizeStock($product) {
     return !empty(getProductSizes($product));
 }
 
-/**
- * Normalize a single product record for backwards compatibility.
- */
+function normalizeAvailabilityLabels($sizes, $labels) {
+    $result = [];
+    if (!is_array($labels)) {
+        $labels = [];
+    }
+
+    foreach ($sizes as $size) {
+        $text = trim((string) ($labels[$size] ?? ''));
+        $result[$size] = $text;
+    }
+
+    return $result;
+}
+
+function getAvailabilityLabel($product, $size) {
+    $labels = isset($product['availability_labels']) && is_array($product['availability_labels'])
+        ? $product['availability_labels']
+        : [];
+    return trim((string) ($labels[$size] ?? ''));
+}
+
 function normalizeProductRecord($product, $defaultCategoryId = '') {
     if (!is_array($product)) {
         return null;
     }
 
-    $product['id'] = isset($product['id']) ? (int) $product['id'] : 0;
-    $product['name'] = isset($product['name']) ? trim((string) $product['name']) : '';
-    $product['description'] = isset($product['description']) ? trim((string) $product['description']) : '';
-    $product['price'] = isset($product['price']) ? (float) $product['price'] : 0.0;
-    $product['image'] = isset($product['image']) ? trim((string) $product['image']) : '';
-
-    $categoryIds = getProductCategoryIds($product);
-    if (empty($categoryIds) && $defaultCategoryId !== '') {
-        $categoryIds = [$defaultCategoryId];
+    $productId = isset($product['id']) ? (int) $product['id'] : 0;
+    $name = trim((string) ($product['name'] ?? ''));
+    if ($productId <= 0 || $name === '') {
+        return null;
     }
-    $product['categories'] = $categoryIds;
-    unset($product['category']);
 
     $sizes = getProductSizes($product);
-    $stockBySize = isset($product['stock_by_size']) && is_array($product['stock_by_size']) ? $product['stock_by_size'] : [];
-
-    if (empty($sizes) && array_key_exists('stock', $product)) {
+    if (empty($sizes)) {
         $sizes = ['Standard'];
-        $stockBySize = ['Standard' => (int) $product['stock']];
     }
 
-    if (!empty($sizes)) {
-        $normalizedStockBySize = [];
-        foreach ($sizes as $size) {
-            $normalizedStockBySize[$size] = isset($stockBySize[$size]) ? max(0, (int) $stockBySize[$size]) : 0;
-        }
-
-        $product['sizes'] = implode(',', $sizes);
-        $product['stock_by_size'] = $normalizedStockBySize;
-        unset($product['stock']);
-    } else {
-        $product['stock'] = isset($product['stock']) ? max(0, (int) $product['stock']) : 0;
-        unset($product['stock_by_size']);
-        unset($product['sizes']);
+    $categories = getProductCategoryIds($product);
+    if (empty($categories) && $defaultCategoryId !== '') {
+        $categories = [$defaultCategoryId];
     }
 
-    return $product;
+    $availabilityLabels = normalizeAvailabilityLabels(
+        $sizes,
+        isset($product['availability_labels']) && is_array($product['availability_labels'])
+            ? $product['availability_labels']
+            : []
+    );
+
+    return [
+        'id' => $productId,
+        'name' => $name,
+        'description' => trim((string) ($product['description'] ?? '')),
+        'image' => trim((string) ($product['image'] ?? '')),
+        'categories' => $categories,
+        'sizes' => implode(',', $sizes),
+        'availability_labels' => $availabilityLabels,
+    ];
 }
 
-/**
- * Get all products
- */
 function getProducts() {
     $data = readJsonFile(PRODUCTS_FILE);
     $rawProducts = isset($data['products']) && is_array($data['products']) ? $data['products'] : [];
@@ -561,54 +431,46 @@ function getProducts() {
     $products = [];
 
     foreach ($rawProducts as $product) {
-        $normalizedProduct = normalizeProductRecord($product, $defaultCategoryId);
-        if ($normalizedProduct === null) {
-            continue;
+        $normalized = normalizeProductRecord($product, $defaultCategoryId);
+        if ($normalized !== null) {
+            $products[] = $normalized;
         }
-        $products[] = $normalizedProduct;
     }
 
+    usort($products, function ($left, $right) {
+        return strcasecmp($left['name'], $right['name']);
+    });
+
     return $products;
 }
 
-/**
- * Get product by ID
- */
 function getProductById($id) {
-    $products = getProducts();
-    foreach ($products as $product) {
-        if ($product['id'] == $id) {
+    $id = (int) $id;
+    foreach (getProducts() as $product) {
+        if ((int) $product['id'] === $id) {
             return $product;
         }
     }
     return null;
 }
 
-/**
- * Save products
- */
 function saveProducts($products) {
     $categories = getCategories();
     $defaultCategoryId = !empty($categories) ? $categories[0]['id'] : 'apparel';
-    $normalizedProducts = [];
+    $normalized = [];
 
     foreach ($products as $product) {
-        $normalizedProduct = normalizeProductRecord($product, $defaultCategoryId);
-        if ($normalizedProduct === null) {
-            continue;
+        $record = normalizeProductRecord($product, $defaultCategoryId);
+        if ($record !== null) {
+            $normalized[] = $record;
         }
-        $normalizedProducts[] = $normalizedProduct;
     }
 
-    $data = ['products' => $normalizedProducts];
-    writeJsonFile(PRODUCTS_FILE, $data);
+    writeJsonFile(PRODUCTS_FILE, ['products' => array_values($normalized)]);
 }
 
-/**
- * Resolve FAQ file path and force storage inside DATA_DIR.
- */
 function getFaqFilePath(): string {
-    $dataDir = defined('DATA_DIR') && is_string(DATA_DIR) ? DATA_DIR : (dirname(__DIR__) . '/data/');
+    $dataDir = defined('DATA_DIR') ? DATA_DIR : dirname(__DIR__) . '/data/';
     $defaultPath = rtrim($dataDir, '/\\') . '/faq.json';
 
     if (!defined('FAQ_FILE') || !is_string(FAQ_FILE) || FAQ_FILE === '') {
@@ -626,15 +488,9 @@ function getFaqFilePath(): string {
     return $configuredPath;
 }
 
-/**
- * Get FAQ markdown content from JSON store.
- */
 function getFaqContent(): string {
     $defaultContent = "# FAQ\n\nHier kann der FAQ-Inhalt im Admin-Bereich bearbeitet werden.";
     $data = readJsonFile(getFaqFilePath());
-    if (!is_array($data)) {
-        return $defaultContent;
-    }
 
     if (!isset($data['content']) || !is_string($data['content'])) {
         return $defaultContent;
@@ -643,29 +499,17 @@ function getFaqContent(): string {
     return $data['content'];
 }
 
-/**
- * Save FAQ markdown content to JSON store.
- */
 function saveFaqContent(string $markdown): void {
     writeJsonFile(getFaqFilePath(), ['content' => (string) $markdown]);
 }
 
-/**
- * Render inline markdown safely (bold + italic).
- */
 function renderFaqInlineMarkdown(string $text): string {
-    $escaped = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
-
-    // Apply markdown after escaping so raw HTML is never executed.
+    $escaped = escape($text);
     $escaped = preg_replace('/\*\*(.+?)\*\*/s', '<strong>$1</strong>', $escaped);
     $escaped = preg_replace('/(?<!\*)\*(?!\s)(.+?)(?<!\s)\*(?!\*)/s', '<em>$1</em>', $escaped);
-
     return $escaped;
 }
 
-/**
- * Render a minimal safe markdown subset for FAQ content.
- */
 function renderFaqMarkdown(string $markdown): string {
     $normalized = str_replace(["\r\n", "\r"], "\n", $markdown);
     $lines = explode("\n", $normalized);
@@ -678,22 +522,20 @@ function renderFaqMarkdown(string $markdown): string {
             return;
         }
 
-        $renderedLines = [];
+        $rendered = [];
         foreach ($paragraphLines as $line) {
-            $renderedLines[] = renderFaqInlineMarkdown($line);
+            $rendered[] = renderFaqInlineMarkdown($line);
         }
 
-        $htmlParts[] = '<p>' . implode("<br>\n", $renderedLines) . '</p>';
+        $htmlParts[] = '<p>' . implode("<br>\n", $rendered) . '</p>';
         $paragraphLines = [];
     };
 
     $closeList = function () use (&$listType, &$htmlParts): void {
-        if ($listType === '') {
-            return;
+        if ($listType !== '') {
+            $htmlParts[] = '</' . $listType . '>';
+            $listType = '';
         }
-
-        $htmlParts[] = '</' . $listType . '>';
-        $listType = '';
     };
 
     foreach ($lines as $line) {
@@ -743,975 +585,1097 @@ function renderFaqMarkdown(string $markdown): string {
     $flushParagraph();
     $closeList();
 
-    if (empty($htmlParts)) {
-        return '<p>Keine FAQ-Inhalte vorhanden.</p>';
-    }
+    return empty($htmlParts) ? '<p>Keine FAQ-Inhalte vorhanden.</p>' : implode("\n", $htmlParts);
+}
 
-    return implode("\n", $htmlParts);
+function getDefaultOrganizations() {
+    return [
+        [
+            'id' => 'feuerwehr-freising',
+            'label' => 'Feuerwehr Freising',
+            'sort_order' => 10,
+            'active' => true,
+        ],
+    ];
 }
 
-/**
- * Get all reservations
- */
-function getReservations() {
-    $data = readJsonFile(RESERVATIONS_FILE);
-    return isset($data['reservations']) ? $data['reservations'] : [];
+function normalizeOrganizationId($id) {
+    return normalizeCategoryId($id);
 }
 
-/**
- * Get reservation by order number
- */
-function getReservationByOrderNumber($orderNumber) {
-    $reservations = getReservations();
-    foreach ($reservations as $reservation) {
-        if (isset($reservation['id']) && $reservation['id'] === $orderNumber && !isReservationHidden($reservation)) {
-            return $reservation;
-        }
-    }
-    return null;
+function normalizeOrganizationLabel($label) {
+    return trim((string) $label);
 }
 
-/**
- * Get remembered order IDs for the current browser profile.
- */
-function getRememberedOrderIds(): array {
-    return readSignedOrderHistoryCookie();
+function isValidOrganizationLabel($label) {
+    $label = normalizeOrganizationLabel($label);
+    if ($label === '') {
+        return false;
+    }
+
+    $length = function_exists('mb_strlen') ? mb_strlen($label) : strlen($label);
+    return $length <= 120;
 }
 
-/**
- * Remember a newly created order ID in browser history cookie.
- */
-function rememberOrderId(string $orderId): void {
-    if (!isValidOrderHistoryOrderId($orderId)) {
-        return;
+function normalizeOrganizations($organizations) {
+    $normalized = [];
+    if (!is_array($organizations)) {
+        $organizations = [];
     }
 
-    $existingIds = getRememberedOrderIds();
-    $updatedIds = [$orderId];
+    foreach ($organizations as $organization) {
+        if (!is_array($organization)) {
+            continue;
+        }
 
-    foreach ($existingIds as $existingId) {
-        if ($existingId !== $orderId) {
-            $updatedIds[] = $existingId;
+        $id = normalizeOrganizationId($organization['id'] ?? '');
+        $label = normalizeOrganizationLabel($organization['label'] ?? '');
+        if ($id === '' || !isValidOrganizationLabel($label)) {
+            continue;
         }
+
+        $sortOrder = isset($organization['sort_order']) ? (int) $organization['sort_order'] : 0;
+        $active = !isset($organization['active']) || (bool) $organization['active'];
+
+        $normalized[$id] = [
+            'id' => $id,
+            'label' => $label,
+            'sort_order' => $sortOrder,
+            'active' => $active,
+        ];
     }
 
-    $maxIds = getOrderHistoryMaxIds();
-    if (count($updatedIds) > $maxIds) {
-        $updatedIds = array_slice($updatedIds, 0, $maxIds);
+    if (empty($normalized)) {
+        foreach (getDefaultOrganizations() as $organization) {
+            $normalized[$organization['id']] = $organization;
+        }
     }
 
-    writeSignedOrderHistoryCookie($updatedIds);
+    uasort($normalized, function ($left, $right) {
+        if ($left['sort_order'] === $right['sort_order']) {
+            return strcasecmp($left['label'], $right['label']);
+        }
+        return $left['sort_order'] <=> $right['sort_order'];
+    });
+
+    return array_values($normalized);
 }
 
-/**
- * Read and validate signed browser order history cookie.
- */
-function readSignedOrderHistoryCookie(): array {
-    $cookieName = getOrderHistoryCookieName();
-    if (!isset($_COOKIE[$cookieName]) || !is_string($_COOKIE[$cookieName])) {
-        return [];
-    }
+function getOrganizations($onlyActive = false) {
+    $data = readJsonFile(ORGANIZATIONS_FILE);
+    $organizations = normalizeOrganizations($data['organizations'] ?? []);
 
-    $secret = getOrderHistorySecret();
-    if ($secret === '') {
-        return [];
+    if ($onlyActive) {
+        $organizations = array_values(array_filter($organizations, function ($organization) {
+            return !empty($organization['active']);
+        }));
     }
 
-    $cookieValue = $_COOKIE[$cookieName];
-    $parts = explode('.', $cookieValue, 2);
-    if (count($parts) !== 2) {
-        return [];
-    }
+    return $organizations;
+}
 
-    $encodedPayload = $parts[0];
-    $signature = $parts[1];
-    if ($encodedPayload === '' || $signature === '') {
-        return [];
-    }
+function saveOrganizations($organizations) {
+    writeJsonFile(ORGANIZATIONS_FILE, ['organizations' => normalizeOrganizations($organizations)]);
+}
 
-    $expectedSignature = hash_hmac('sha256', $encodedPayload, $secret);
-    if (!hash_equals($expectedSignature, $signature)) {
-        return [];
+function getOrganizationById($organizationId) {
+    $organizationId = normalizeOrganizationId($organizationId);
+    foreach (getOrganizations(false) as $organization) {
+        if ($organization['id'] === $organizationId) {
+            return $organization;
+        }
     }
+    return null;
+}
 
-    $payloadJson = base64UrlDecode($encodedPayload);
-    if ($payloadJson === null) {
-        return [];
+function generateOrganizationIdFromLabel($label, $existingOrganizations = []) {
+    $baseId = normalizeOrganizationId($label);
+    if ($baseId === '') {
+        $baseId = 'organization';
     }
 
-    $payload = json_decode($payloadJson, true);
-    if (!is_array($payload)) {
-        return [];
+    $used = [];
+    foreach (normalizeOrganizations($existingOrganizations) as $organization) {
+        $used[$organization['id']] = true;
     }
 
-    $version = isset($payload['v']) ? (int)$payload['v'] : 0;
-    if ($version !== 1) {
-        return [];
+    $candidate = $baseId;
+    $counter = 2;
+    while (isset($used[$candidate])) {
+        $candidate = $baseId . '-' . $counter;
+        $counter++;
     }
 
-    $ids = isset($payload['ids']) && is_array($payload['ids']) ? $payload['ids'] : [];
-    return sanitizeOrderHistoryIds($ids);
+    return $candidate;
 }
 
-/**
- * Write signed browser order history cookie.
- */
-function writeSignedOrderHistoryCookie(array $ids): void {
-    if (headers_sent()) {
-        return;
-    }
-
-    $secret = getOrderHistorySecret();
-    if ($secret === '') {
-        return;
-    }
-
-    $sanitizedIds = sanitizeOrderHistoryIds($ids);
-    $payload = [
-        'v' => 1,
-        'ids' => $sanitizedIds,
-        'iat' => time()
+function getDefaultSystemSettings() {
+    return [
+        'order_recipient_email' => defined('ORDER_RECIPIENT_EMAIL') ? ORDER_RECIPIENT_EMAIL : getDefaultAdminEmail(),
+        'order_confirmation_required' => defined('ORDER_CONFIRMATION_REQUIRED') ? (bool) ORDER_CONFIRMATION_REQUIRED : false,
+        'order_confirmation_expiry_days' => defined('ORDER_CONFIRMATION_EXPIRY_DAYS') ? (int) ORDER_CONFIRMATION_EXPIRY_DAYS : 7,
+        'attach_order_pdf_to_admin_email' => defined('ATTACH_ORDER_PDF_TO_ADMIN_EMAIL') ? (bool) ATTACH_ORDER_PDF_TO_ADMIN_EMAIL : true,
     ];
+}
 
-    $payloadJson = json_encode($payload, JSON_UNESCAPED_UNICODE);
-    if ($payloadJson === false) {
-        return;
+function normalizeSystemSettings($settings) {
+    $defaults = getDefaultSystemSettings();
+    if (!is_array($settings)) {
+        $settings = [];
     }
 
-    $encodedPayload = base64UrlEncode($payloadJson);
-    $signature = hash_hmac('sha256', $encodedPayload, $secret);
-    $cookieValue = $encodedPayload . '.' . $signature;
-
-    $expires = time() + (getOrderHistoryTtlDays() * 86400);
-    $success = setcookie(getOrderHistoryCookieName(), $cookieValue, [
-        'expires' => $expires,
-        'path' => getOrderHistoryCookiePath(),
-        'secure' => isHttpsRequest(),
-        'httponly' => true,
-        'samesite' => 'Lax'
-    ]);
+    $recipientEmail = normalizeAdminEmail($settings['order_recipient_email'] ?? $defaults['order_recipient_email']);
+    if (!isValidAdminEmail($recipientEmail)) {
+        $recipientEmail = $defaults['order_recipient_email'];
+    }
 
-    if ($success) {
-        $_COOKIE[getOrderHistoryCookieName()] = $cookieValue;
+    $expiryDays = isset($settings['order_confirmation_expiry_days']) ? (int) $settings['order_confirmation_expiry_days'] : $defaults['order_confirmation_expiry_days'];
+    if ($expiryDays < 1) {
+        $expiryDays = 7;
     }
-}
 
-/**
- * Build a safe, deduplicated order history ID list.
- */
-function sanitizeOrderHistoryIds(array $ids): array {
-    $result = [];
-    $seen = [];
-    $maxIds = getOrderHistoryMaxIds();
+    return [
+        'order_recipient_email' => $recipientEmail,
+        'order_confirmation_required' => !empty($settings['order_confirmation_required']),
+        'order_confirmation_expiry_days' => $expiryDays,
+        'attach_order_pdf_to_admin_email' => !empty($settings['attach_order_pdf_to_admin_email']),
+    ];
+}
 
-    foreach ($ids as $id) {
-        if (!is_string($id) || !isValidOrderHistoryOrderId($id)) {
-            continue;
-        }
-        if (isset($seen[$id])) {
-            continue;
-        }
-        $seen[$id] = true;
-        $result[] = $id;
+function getSystemSettings() {
+    $data = readJsonFile(SETTINGS_FILE);
+    return normalizeSystemSettings($data['settings'] ?? []);
+}
 
-        if (count($result) >= $maxIds) {
-            break;
-        }
-    }
+function saveSystemSettings($settings) {
+    writeJsonFile(SETTINGS_FILE, ['settings' => normalizeSystemSettings($settings)]);
+}
 
-    return $result;
+function getOrderRecipientEmail() {
+    $settings = getSystemSettings();
+    return $settings['order_recipient_email'];
 }
 
-/**
- * Check if order ID matches configured pattern.
- */
-function isValidOrderHistoryOrderId($orderId): bool {
-    if (!is_string($orderId) || $orderId === '') {
-        return false;
-    }
+function isOrderConfirmationRequired() {
+    $settings = getSystemSettings();
+    return !empty($settings['order_confirmation_required']);
+}
 
-    $prefix = defined('ORDER_PREFIX') ? ORDER_PREFIX : 'ORD';
-    $pattern = '/^' . preg_quote($prefix, '/') . '-\d{4}-\d+$/';
-    return preg_match($pattern, $orderId) === 1;
+function getOrderConfirmationExpiryDays() {
+    $settings = getSystemSettings();
+    return max(1, (int) $settings['order_confirmation_expiry_days']);
 }
 
-/**
- * Base64url encode helper.
- */
-function base64UrlEncode(string $data): string {
-    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
+function shouldAttachOrderPdfToAdminEmail() {
+    $settings = getSystemSettings();
+    return !empty($settings['attach_order_pdf_to_admin_email']);
 }
 
-/**
- * Base64url decode helper.
- */
-function base64UrlDecode(string $data): ?string {
-    $remainder = strlen($data) % 4;
-    if ($remainder > 0) {
-        $data .= str_repeat('=', 4 - $remainder);
+function normalizeOrderItem($item) {
+    if (!is_array($item)) {
+        return null;
     }
 
-    $decoded = base64_decode(strtr($data, '-_', '+/'), true);
-    if ($decoded === false) {
+    $productId = isset($item['product_id']) ? (int) $item['product_id'] : 0;
+    if ($productId <= 0) {
         return null;
     }
-    return $decoded;
-}
 
-/**
- * Get cookie name for order history.
- */
-function getOrderHistoryCookieName(): string {
-    return defined('ORDER_HISTORY_COOKIE_NAME') ? ORDER_HISTORY_COOKIE_NAME : 'order_history';
-}
+    $product = getProductById($productId);
+    if ($product === null) {
+        return null;
+    }
 
-/**
- * Get secret for order history cookie signing.
- */
-function getOrderHistorySecret(): string {
-    return defined('ORDER_HISTORY_COOKIE_SECRET') ? (string) ORDER_HISTORY_COOKIE_SECRET : '';
-}
+    $size = trim((string) ($item['size'] ?? ''));
+    $sizes = getProductSizes($product);
+    if (!empty($sizes)) {
+        if ($size === '' || !in_array($size, $sizes, true)) {
+            return null;
+        }
+    } else {
+        $size = '';
+    }
 
-/**
- * Get maximum number of remembered order IDs.
- */
-function getOrderHistoryMaxIds(): int {
-    $maxIds = defined('ORDER_HISTORY_MAX_IDS') ? (int) ORDER_HISTORY_MAX_IDS : 10;
-    return $maxIds > 0 ? $maxIds : 10;
+    return [
+        'product_id' => $productId,
+        'product_name' => $product['name'],
+        'size' => $size,
+        'availability_label' => $size !== '' ? getAvailabilityLabel($product, $size) : '',
+        'is_processed' => !empty($item['is_processed']),
+    ];
 }
 
-/**
- * Get order history cookie retention days.
- */
-function getOrderHistoryTtlDays(): int {
-    $ttlDays = defined('ORDER_HISTORY_COOKIE_TTL_DAYS') ? (int) ORDER_HISTORY_COOKIE_TTL_DAYS : 365;
-    return $ttlDays > 0 ? $ttlDays : 365;
-}
+function normalizeOrderItems($items) {
+    $normalized = [];
+    $seen = [];
 
-/**
- * Get cookie path from SITE_URL.
- */
-function getOrderHistoryCookiePath(): string {
-    $siteUrl = defined('SITE_URL') ? trim((string) SITE_URL) : '';
-    if (strpos($siteUrl, '://') !== false) {
-        $parsedPath = parse_url($siteUrl, PHP_URL_PATH);
-        $siteUrl = is_string($parsedPath) ? $parsedPath : '';
+    if (!is_array($items)) {
+        return [];
     }
 
-    if ($siteUrl === '' || $siteUrl === '/') {
-        return '/';
-    }
+    foreach ($items as $item) {
+        $record = normalizeOrderItem($item);
+        if ($record === null) {
+            continue;
+        }
+
+        $key = $record['product_id'] . '|' . $record['size'];
+        if (isset($seen[$key])) {
+            continue;
+        }
 
-    if ($siteUrl[0] !== '/') {
-        $siteUrl = '/' . $siteUrl;
+        $seen[$key] = true;
+        $normalized[] = $record;
     }
 
-    return rtrim($siteUrl, '/');
+    return array_values($normalized);
 }
 
-/**
- * Detect whether current request uses HTTPS.
- */
-function isHttpsRequest(): bool {
-    if (!empty($_SERVER['HTTPS']) && strtolower((string) $_SERVER['HTTPS']) !== 'off') {
-        return true;
-    }
-    if (isset($_SERVER['SERVER_PORT']) && (int) $_SERVER['SERVER_PORT'] === 443) {
-        return true;
-    }
-    if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower((string) $_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https') {
-        return true;
+function getOrders() {
+    $data = readJsonFile(ORDERS_FILE);
+    $orders = isset($data['orders']) && is_array($data['orders']) ? $data['orders'] : [];
+    $normalized = [];
+
+    foreach ($orders as $order) {
+        $record = normalizeOrderRecord($order);
+        if ($record !== null) {
+            $normalized[] = $record;
+        }
     }
-    return false;
-}
 
-/**
- * Check if reservation is hidden (spam/deleted)
- */
-function isReservationHidden($reservation) {
-    return isset($reservation['is_hidden']) && $reservation['is_hidden'] === true;
+    return $normalized;
 }
 
-/**
- * Save reservations
- */
-function saveReservations($reservations) {
-    $data = ['reservations' => $reservations];
-    writeJsonFile(RESERVATIONS_FILE, $data);
+function saveOrders($orders) {
+    $normalized = [];
+    foreach ($orders as $order) {
+        $record = normalizeOrderRecord($order);
+        if ($record !== null) {
+            $normalized[] = $record;
+        }
+    }
+
+    writeJsonFile(ORDERS_FILE, ['orders' => array_values($normalized)]);
 }
 
-/**
- * Generate order number
- * Pattern: PREFIX-YEAR-SEQ
- */
-function generateReservationId() {
-    $reservations = getReservations();
+function generateOrderId() {
+    $orders = getOrders();
     $year = date('Y');
     $prefix = defined('ORDER_PREFIX') ? ORDER_PREFIX : 'ORD';
     $max = 0;
-    $pattern = '/^' . preg_quote($prefix, '/') . '-\\d{4}-(\\d+)$/';
+    $pattern = '/^' . preg_quote($prefix, '/') . '-\d{4}-(\d+)$/';
 
-    foreach ($reservations as $reservation) {
-        if (!isset($reservation['id'])) {
-            continue;
-        }
-        if (preg_match($pattern, $reservation['id'], $matches)) {
-            $num = (int)$matches[1];
-            if ($num > $max) {
-                $max = $num;
+    foreach ($orders as $order) {
+        if (preg_match($pattern, (string) $order['id'], $matches) === 1) {
+            $number = (int) $matches[1];
+            if ($number > $max) {
+                $max = $number;
             }
         }
     }
 
-    $next = $max + 1;
-    return sprintf('%s-%s-%03d', $prefix, $year, $next);
+    return sprintf('%s-%s-%03d', $prefix, $year, $max + 1);
 }
 
-/**
- * Check if product has enough stock.
- */
-function checkStock($productId, $quantity, $size = null) {
-    $product = getProductById($productId);
-    if (!$product) {
-        return false;
+function normalizeOrderRecord($order) {
+    if (!is_array($order)) {
+        return null;
     }
 
-    if (productUsesSizeStock($product) && $size !== null) {
-        if (!isset($product['stock_by_size']) || !is_array($product['stock_by_size'])) {
-            return false;
-        }
-        $sizeStock = isset($product['stock_by_size'][$size]) ? (int) $product['stock_by_size'][$size] : 0;
-        return $sizeStock >= $quantity;
-    }
-
-    $stock = isset($product['stock']) ? (int) $product['stock'] : 0;
-    return $stock >= $quantity;
-}
-
-/**
- * Get stock for a product.
- */
-function getStock($product, $size = null) {
-    if (productUsesSizeStock($product) && $size !== null) {
-        if (isset($product['stock_by_size']) && is_array($product['stock_by_size'])) {
-            return isset($product['stock_by_size'][$size]) ? (int) $product['stock_by_size'][$size] : 0;
-        }
-    }
-    return isset($product['stock']) ? (int) $product['stock'] : 0;
-}
-
-/**
- * Get total stock for a product.
- */
-function getTotalStock($product) {
-    if (productUsesSizeStock($product) && isset($product['stock_by_size']) && is_array($product['stock_by_size'])) {
-        return array_sum($product['stock_by_size']);
-    }
-    return isset($product['stock']) ? (int) $product['stock'] : 0;
-}
-
-/**
- * Allocate stock for reservation
- */
-function allocateStock($productId, $quantity, $size = null) {
-    $products = getProducts();
-    foreach ($products as &$product) {
-        if ($product['id'] == $productId) {
-            if (productUsesSizeStock($product) && $size !== null) {
-                if (!isset($product['stock_by_size']) || !is_array($product['stock_by_size'])) {
-                    $product['stock_by_size'] = [];
-                }
-                if (!isset($product['stock_by_size'][$size])) {
-                    $product['stock_by_size'][$size] = 0;
-                }
-                $product['stock_by_size'][$size] -= $quantity;
-                if ($product['stock_by_size'][$size] < 0) {
-                    $product['stock_by_size'][$size] = 0;
-                }
-            } else {
-                if (!isset($product['stock'])) {
-                    $product['stock'] = 0;
-                }
-                $product['stock'] -= $quantity;
-                if ($product['stock'] < 0) {
-                    $product['stock'] = 0;
-                }
-            }
-            break;
-        }
-    }
-    saveProducts($products);
-}
-
-/**
- * Release stock from reservation
- */
-function releaseStock($productId, $quantity, $size = null) {
-    $products = getProducts();
-    foreach ($products as &$product) {
-        if ($product['id'] == $productId) {
-            if (productUsesSizeStock($product) && $size !== null) {
-                if (!isset($product['stock_by_size']) || !is_array($product['stock_by_size'])) {
-                    $product['stock_by_size'] = [];
-                }
-                if (!isset($product['stock_by_size'][$size])) {
-                    $product['stock_by_size'][$size] = 0;
-                }
-                $product['stock_by_size'][$size] += $quantity;
-            } else {
-                if (!isset($product['stock'])) {
-                    $product['stock'] = 0;
-                }
-                $product['stock'] += $quantity;
-            }
-            break;
-        }
+    $id = trim((string) ($order['id'] ?? ''));
+    $customerName = trim((string) ($order['customer_name'] ?? ''));
+    $customerEmail = normalizeAdminEmail($order['customer_email'] ?? '');
+    $organizationId = normalizeOrganizationId($order['organization_id'] ?? '');
+    $organizationLabel = trim((string) ($order['organization_label'] ?? ''));
+    $items = normalizeOrderItems($order['items'] ?? []);
+
+    if ($id === '' || $customerName === '' || !isValidAdminEmail($customerEmail) || $organizationId === '' || $organizationLabel === '' || empty($items)) {
+        return null;
     }
-    saveProducts($products);
-}
 
-/**
- * Create new reservation
- */
-function createReservation($customerName, $customerEmail, $items) {
-    $reservations = getReservations();
-    
-    // Validate stock for all items
-    foreach ($items as $item) {
-        $size = isset($item['size']) ? $item['size'] : null;
-        if (!checkStock($item['product_id'], $item['quantity'], $size)) {
-            $product = getProductById($item['product_id']);
-            $productName = $product ? $product['name'] : 'Produkt';
-            $sizeInfo = $size ? " (Größe: $size)" : '';
-            return ['success' => false, 'message' => "Nicht genügend Lagerbestand für: $productName$sizeInfo"];
-        }
+    $createdAt = trim((string) ($order['created_at'] ?? ''));
+    if ($createdAt === '') {
+        $createdAt = date('Y-m-d H:i:s');
     }
-    
-    // Allocate stock
-    foreach ($items as $item) {
-        $size = isset($item['size']) ? $item['size'] : null;
-        allocateStock($item['product_id'], $item['quantity'], $size);
-    }
-    
-    // Create reservation
-    $now = new DateTime();
-    $expires = clone $now;
-    $expires->modify('+' . RESERVATION_EXPIRY_DAYS . ' days');
-    
-    $reservation = [
-        'id' => generateReservationId(),
-        'customer_name' => $customerName,
-        'customer_email' => $customerEmail,
-        'items' => $items,
-        'created' => $now->format('Y-m-d H:i:s'),
-        'expires' => $expires->format('Y-m-d H:i:s'),
-        'status' => 'open',
-        'picked_up' => false,
-        'type' => 'regular',
-        'is_hidden' => false
-    ];
-    
-    $reservations[] = $reservation;
-    saveReservations($reservations);
-    
-    // Send confirmation emails
-    sendReservationEmails($reservation);
-    
-    return ['success' => true, 'reservation' => $reservation];
-}
-
-/**
- * Create new backorder reservation
- */
-function createBackorderReservation($customerName, $customerEmail, $items) {
-    $reservations = getReservations();
-    
-    $now = new DateTime();
-    
-    $reservation = [
-        'id' => generateReservationId(),
+
+    $confirmationStatus = trim((string) ($order['confirmation_status'] ?? 'confirmed'));
+    $allowedConfirmationStatuses = ['not_required', 'pending', 'confirmed', 'expired'];
+    if (!in_array($confirmationStatus, $allowedConfirmationStatuses, true)) {
+        $confirmationStatus = 'confirmed';
+    }
+
+    $status = trim((string) ($order['status'] ?? 'open'));
+    $allowedStatuses = ['open', 'partial', 'processed', 'cancelled'];
+    if (!in_array($status, $allowedStatuses, true)) {
+        $status = 'open';
+    }
+
+    $normalized = [
+        'id' => $id,
         'customer_name' => $customerName,
         'customer_email' => $customerEmail,
+        'organization_id' => $organizationId,
+        'organization_label' => $organizationLabel,
+        'comment' => trim((string) ($order['comment'] ?? '')),
         'items' => $items,
-        'created' => $now->format('Y-m-d H:i:s'),
-        'expires' => '',
-        'status' => 'open',
-        'picked_up' => false,
-        'type' => 'backorder',
-        'backorder_status' => 'pending',
-        'is_hidden' => false
+        'status' => $status,
+        'confirmation_status' => $confirmationStatus,
+        'confirmation_token' => trim((string) ($order['confirmation_token'] ?? '')),
+        'confirmation_expires_at' => trim((string) ($order['confirmation_expires_at'] ?? '')),
+        'confirmed_at' => trim((string) ($order['confirmed_at'] ?? '')),
+        'created_at' => $createdAt,
+        'updated_at' => trim((string) ($order['updated_at'] ?? $createdAt)),
+        'cancelled_at' => trim((string) ($order['cancelled_at'] ?? '')),
+        'cancelled_by' => trim((string) ($order['cancelled_by'] ?? '')),
+        'cancellation_reason' => trim((string) ($order['cancellation_reason'] ?? '')),
+        'admin_notified_at' => trim((string) ($order['admin_notified_at'] ?? '')),
     ];
-    
-    $reservations[] = $reservation;
-    saveReservations($reservations);
-    
-    // Send confirmation emails
-    sendBackorderEmails($reservation);
-    
-    return ['success' => true, 'reservation' => $reservation];
-}
-
-/**
- * Mark reservation as picked up
- */
-function markReservationPickedUp($reservationId) {
-    $reservations = getReservations();
-    foreach ($reservations as &$reservation) {
-        if ($reservation['id'] === $reservationId) {
-            if (isReservationHidden($reservation)) {
-                break;
-            }
-            $reservation['picked_up'] = true;
-            $reservation['status'] = 'picked_up';
-            break;
+
+    return refreshOrderState($normalized);
+}
+
+function refreshOrderState($order) {
+    if (!is_array($order)) {
+        return null;
+    }
+
+    if (($order['status'] ?? '') === 'cancelled') {
+        return $order;
+    }
+
+    if (($order['confirmation_status'] ?? '') === 'pending' && !empty($order['confirmation_expires_at'])) {
+        $expiresAt = strtotime((string) $order['confirmation_expires_at']);
+        if ($expiresAt !== false && time() > $expiresAt) {
+            $order['confirmation_status'] = 'expired';
+        }
+    }
+
+    $processedCount = 0;
+    foreach ($order['items'] as $item) {
+        if (!empty($item['is_processed'])) {
+            $processedCount++;
         }
     }
-    saveReservations($reservations);
+
+    if ($processedCount <= 0) {
+        $order['status'] = 'open';
+    } elseif ($processedCount >= count($order['items'])) {
+        $order['status'] = 'processed';
+    } else {
+        $order['status'] = 'partial';
+    }
+
+    return $order;
 }
 
-/**
- * Mark reservation/backorder as spam/deleted and hide it from non-admin views.
- * For open regular reservations we release stock, because the order is discarded.
- */
-function markReservationHidden($reservationId) {
-    $reservations = getReservations();
-    foreach ($reservations as &$reservation) {
-        if ($reservation['id'] !== $reservationId) {
-            continue;
+function expirePendingOrders() {
+    $orders = getOrders();
+    $changed = false;
+
+    foreach ($orders as &$order) {
+        $updated = refreshOrderState($order);
+        if ($updated !== $order) {
+            $order = $updated;
+            $changed = true;
         }
-        if (isReservationHidden($reservation)) {
-            return ['success' => false, 'message' => 'Bestellung ist bereits als Spam/Gelöscht markiert.'];
+    }
+    unset($order);
+
+    if ($changed) {
+        saveOrders($orders);
+    }
+}
+
+function getOrderById($orderId) {
+    $orderId = trim((string) $orderId);
+    if ($orderId === '') {
+        return null;
+    }
+
+    foreach (getOrders() as $order) {
+        if ($order['id'] === $orderId) {
+            return $order;
         }
+    }
 
-        $isBackorder = isset($reservation['type']) && $reservation['type'] === 'backorder';
-        if (!$isBackorder && isset($reservation['status']) && $reservation['status'] === 'open' && empty($reservation['picked_up'])) {
-            foreach ($reservation['items'] as $item) {
-                $size = isset($item['size']) ? $item['size'] : null;
-                releaseStock($item['product_id'], $item['quantity'], $size);
-            }
-            $reservation['status'] = 'deleted';
+    return null;
+}
+
+function findOrderByConfirmationToken($token) {
+    $token = trim((string) $token);
+    if ($token === '') {
+        return null;
+    }
+
+    foreach (getOrders() as $order) {
+        if (($order['confirmation_token'] ?? '') === $token) {
+            return $order;
         }
+    }
 
-        $reservation['is_hidden'] = true;
-        $reservation['hidden_at'] = date('Y-m-d H:i:s');
-        $reservation['hidden_reason'] = 'spam_deleted';
-        saveReservations($reservations);
+    return null;
+}
+
+function buildOrderConfirmationUrl($token) {
+    $path = '/order-confirm.php?token=' . urlencode($token);
+    return buildAbsoluteUrl($path);
+}
 
-        return ['success' => true];
+function buildAbsoluteUrl($path) {
+    $path = '/' . ltrim((string) $path, '/');
+    $siteUrl = defined('SITE_URL') ? trim((string) SITE_URL) : '';
+
+    if (strpos($siteUrl, '://') !== false) {
+        return rtrim($siteUrl, '/') . $path;
     }
 
-    return ['success' => false, 'message' => 'Bestellung nicht gefunden.'];
+    $basePath = trim($siteUrl);
+    if ($basePath !== '' && $basePath !== '/') {
+        $path = '/' . trim($basePath, '/') . $path;
+    }
+
+    $scheme = isHttpsRequest() ? 'https' : 'http';
+    $host = $_SERVER['HTTP_HOST'] ?? '';
+    if ($host === '') {
+        return $path;
+    }
+
+    return $scheme . '://' . $host . $path;
 }
 
-/**
- * Check and expire old reservations
- */
-function expireOldReservations() {
-    $reservations = getReservations();
-    $now = new DateTime();
-    $changed = false;
-    
-    foreach ($reservations as &$reservation) {
-        if (isReservationHidden($reservation)) {
-            continue;
+function isHttpsRequest(): bool {
+    if (!empty($_SERVER['HTTPS']) && strtolower((string) $_SERVER['HTTPS']) !== 'off') {
+        return true;
+    }
+    if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower((string) $_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https') {
+        return true;
+    }
+    if (!empty($_SERVER['SERVER_PORT']) && (int) $_SERVER['SERVER_PORT'] === 443) {
+        return true;
+    }
+    return false;
+}
+
+function createOrder($customerName, $customerEmail, $organizationId, $comment, $items) {
+    $customerName = sanitize($customerName);
+    $customerEmail = normalizeAdminEmail($customerEmail);
+    $organizationId = normalizeOrganizationId($organizationId);
+    $comment = trim((string) $comment);
+    $items = normalizeOrderItems($items);
+
+    if ($customerName === '') {
+        return ['success' => false, 'message' => 'Bitte geben Sie einen Namen ein.'];
+    }
+    if (!isValidAdminEmail($customerEmail)) {
+        return ['success' => false, 'message' => 'Bitte geben Sie eine gültige E-Mail-Adresse ein.'];
+    }
+    if (empty($items)) {
+        return ['success' => false, 'message' => 'Der Warenkorb ist leer oder enthält ungültige Positionen.'];
+    }
+
+    $organization = getOrganizationById($organizationId);
+    if ($organization === null || empty($organization['active'])) {
+        return ['success' => false, 'message' => 'Bitte wählen Sie eine gültige Organisation aus.'];
+    }
+
+    $now = date('Y-m-d H:i:s');
+    $requiresConfirmation = isOrderConfirmationRequired();
+    $confirmationToken = $requiresConfirmation ? bin2hex(random_bytes(24)) : '';
+    $confirmationExpiresAt = '';
+    if ($requiresConfirmation) {
+        $expires = new DateTimeImmutable();
+        $expires = $expires->modify('+' . getOrderConfirmationExpiryDays() . ' days');
+        $confirmationExpiresAt = $expires->format('Y-m-d H:i:s');
+    }
+
+    $order = [
+        'id' => generateOrderId(),
+        'customer_name' => $customerName,
+        'customer_email' => $customerEmail,
+        'organization_id' => $organization['id'],
+        'organization_label' => $organization['label'],
+        'comment' => $comment,
+        'items' => $items,
+        'status' => 'open',
+        'confirmation_status' => $requiresConfirmation ? 'pending' : 'not_required',
+        'confirmation_token' => $confirmationToken,
+        'confirmation_expires_at' => $confirmationExpiresAt,
+        'confirmed_at' => $requiresConfirmation ? '' : $now,
+        'created_at' => $now,
+        'updated_at' => $now,
+        'cancelled_at' => '',
+        'cancelled_by' => '',
+        'cancellation_reason' => '',
+        'admin_notified_at' => '',
+    ];
+
+    $orders = getOrders();
+    $orders[] = $order;
+    saveOrders($orders);
+
+    if ($requiresConfirmation) {
+        sendOrderConfirmationRequestEmail($order);
+    } else {
+        $result = sendConfirmedOrderAdminNotification($order);
+        if ($result) {
+            markOrderAdminNotified($order['id']);
+            $order = getOrderById($order['id']);
         }
-        if ($reservation['status'] === 'open' && !$reservation['picked_up']) {
-            if (isset($reservation['type']) && $reservation['type'] === 'backorder') {
-                continue;
-            }
-            if (empty($reservation['expires'])) {
-                continue;
-            }
-            $expires = new DateTime($reservation['expires']);
-            if ($now > $expires) {
-                $reservation['status'] = 'expired';
-                // Release stock
-                foreach ($reservation['items'] as $item) {
-                    $size = isset($item['size']) ? $item['size'] : null;
-                    releaseStock($item['product_id'], $item['quantity'], $size);
-                }
-                $changed = true;
-            }
+        sendOrderCreatedCustomerEmail($order);
+    }
+
+    return ['success' => true, 'order' => $order];
+}
+
+function markOrderAdminNotified($orderId) {
+    $orders = getOrders();
+    foreach ($orders as &$order) {
+        if ($order['id'] !== $orderId) {
+            continue;
         }
+
+        $order['admin_notified_at'] = date('Y-m-d H:i:s');
+        $order['updated_at'] = date('Y-m-d H:i:s');
+        break;
     }
-    
-    if ($changed) {
-        saveReservations($reservations);
+    unset($order);
+
+    saveOrders($orders);
+}
+
+function confirmOrderByToken($token) {
+    $orders = getOrders();
+    $now = date('Y-m-d H:i:s');
+
+    foreach ($orders as &$order) {
+        if (($order['confirmation_token'] ?? '') !== $token) {
+            continue;
+        }
+
+        $order = refreshOrderState($order);
+
+        if ($order['status'] === 'cancelled') {
+            return ['success' => false, 'message' => 'Diese Bestellung wurde storniert und kann nicht mehr bestätigt werden.'];
+        }
+        if ($order['confirmation_status'] === 'confirmed') {
+            return ['success' => false, 'message' => 'Diese Bestellung wurde bereits bestätigt.'];
+        }
+        if ($order['confirmation_status'] === 'expired') {
+            return ['success' => false, 'message' => 'Der Bestätigungslink ist abgelaufen.'];
+        }
+        if ($order['confirmation_status'] !== 'pending') {
+            return ['success' => false, 'message' => 'Für diese Bestellung ist keine Bestätigung erforderlich.'];
+        }
+
+        $expiresAt = strtotime((string) $order['confirmation_expires_at']);
+        if ($expiresAt !== false && time() > $expiresAt) {
+            $order['confirmation_status'] = 'expired';
+            $order['updated_at'] = $now;
+            saveOrders($orders);
+            return ['success' => false, 'message' => 'Der Bestätigungslink ist abgelaufen.'];
+        }
+
+        $order['confirmation_status'] = 'confirmed';
+        $order['confirmed_at'] = $now;
+        $order['updated_at'] = $now;
+        saveOrders($orders);
+
+        $sent = sendConfirmedOrderAdminNotification($order);
+        if ($sent) {
+            markOrderAdminNotified($order['id']);
+        }
+        sendOrderConfirmedCustomerEmail(getOrderById($order['id']));
+
+        return ['success' => true, 'order' => getOrderById($order['id'])];
     }
+    unset($order);
+
+    return ['success' => false, 'message' => 'Bestellung nicht gefunden.'];
 }
 
-/**
- * Check if all items are in stock
- */
-function canFulfillReservationItems($items) {
-    foreach ($items as $item) {
-        $size = isset($item['size']) ? $item['size'] : null;
-        if (!checkStock($item['product_id'], $item['quantity'], $size)) {
-            return false;
+function toggleOrderItemProcessed($orderId, $itemIndex) {
+    $orders = getOrders();
+    $now = date('Y-m-d H:i:s');
+
+    foreach ($orders as &$order) {
+        if ($order['id'] !== $orderId) {
+            continue;
+        }
+
+        if ($order['status'] === 'cancelled') {
+            return ['success' => false, 'message' => 'Stornierte Bestellungen können nicht mehr bearbeitet werden.'];
+        }
+        if (($order['confirmation_status'] ?? '') === 'pending') {
+            return ['success' => false, 'message' => 'Unbestätigte Bestellungen können noch nicht bearbeitet werden.'];
         }
+        if (($order['confirmation_status'] ?? '') === 'expired') {
+            return ['success' => false, 'message' => 'Abgelaufene unbestätigte Bestellungen können nicht bearbeitet werden.'];
+        }
+        if (!isset($order['items'][$itemIndex])) {
+            return ['success' => false, 'message' => 'Position nicht gefunden.'];
+        }
+
+        $order['items'][$itemIndex]['is_processed'] = empty($order['items'][$itemIndex]['is_processed']);
+        $order['updated_at'] = $now;
+        $order = refreshOrderState($order);
+        saveOrders($orders);
+
+        return ['success' => true, 'order' => $order];
     }
-    return true;
+    unset($order);
+
+    return ['success' => false, 'message' => 'Bestellung nicht gefunden.'];
 }
 
-/**
- * Mark backorder as available
- */
-function markBackorderAvailable($reservationId) {
-    $reservations = getReservations();
-    foreach ($reservations as &$reservation) {
-        if ($reservation['id'] === $reservationId) {
-            if (isReservationHidden($reservation)) {
-                return ['success' => false, 'message' => 'Diese Vorbestellung ist als Spam/Gelöscht markiert.'];
-            }
-            if (!isset($reservation['type']) || $reservation['type'] !== 'backorder') {
-                return ['success' => false, 'message' => 'Diese Vorbestellung wurde bereits in eine Bestellung umgewandelt.'];
-            }
-            if (isset($reservation['backorder_status']) && $reservation['backorder_status'] === 'notified') {
-                return ['success' => false, 'message' => 'Diese Vorbestellung wurde bereits informiert.'];
-            }
-            if (!canFulfillReservationItems($reservation['items'])) {
-                return ['success' => false, 'message' => 'Nicht alle Artikel sind verfügbar.'];
-            }
-            
-            foreach ($reservation['items'] as $item) {
-                $size = isset($item['size']) ? $item['size'] : null;
-                allocateStock($item['product_id'], $item['quantity'], $size);
-            }
+function cancelOrder($orderId, $adminUsername, $reason = '') {
+    $orders = getOrders();
+    $now = date('Y-m-d H:i:s');
+    $adminUsername = normalizeAdminUsername($adminUsername);
+    $reason = trim((string) $reason);
 
-            $now = new DateTime();
-            $expires = clone $now;
-            $expires->modify('+' . RESERVATION_EXPIRY_DAYS . ' days');
+    foreach ($orders as &$order) {
+        if ($order['id'] !== $orderId) {
+            continue;
+        }
 
-            $reservation['type'] = 'regular';
-            $reservation['status'] = 'open';
-            $reservation['picked_up'] = false;
-            $reservation['expires'] = $expires->format('Y-m-d H:i:s');
-            if (isset($reservation['backorder_status'])) {
-                unset($reservation['backorder_status']);
-            }
-            saveReservations($reservations);
-            
-            sendBackorderAvailableEmail($reservation);
-            
-            return ['success' => true, 'reservation' => $reservation];
+        if ($order['status'] === 'cancelled') {
+            return ['success' => false, 'message' => 'Die Bestellung ist bereits storniert.'];
         }
+
+        $order['status'] = 'cancelled';
+        $order['cancelled_at'] = $now;
+        $order['cancelled_by'] = $adminUsername;
+        $order['cancellation_reason'] = $reason;
+        $order['updated_at'] = $now;
+        saveOrders($orders);
+
+        return ['success' => true, 'order' => $order];
     }
-    return ['success' => false, 'message' => 'Vorbestellung nicht gefunden.'];
+    unset($order);
+
+    return ['success' => false, 'message' => 'Bestellung nicht gefunden.'];
 }
 
-/**
- * Sanitize input
- */
-function sanitize($input) {
-    return htmlspecialchars(strip_tags(trim($input)), ENT_QUOTES, 'UTF-8');
+function getOrderStatusLabel($order) {
+    if (($order['status'] ?? '') === 'cancelled') {
+        return 'Storniert';
+    }
+    if (($order['confirmation_status'] ?? '') === 'pending') {
+        return 'Unbestätigt';
+    }
+    if (($order['confirmation_status'] ?? '') === 'expired') {
+        return 'Bestätigung abgelaufen';
+    }
+    if (($order['status'] ?? '') === 'processed') {
+        return 'Bearbeitet';
+    }
+    if (($order['status'] ?? '') === 'partial') {
+        return 'Teilweise bearbeitet';
+    }
+    return 'Offen';
 }
 
-/**
- * Format price
- */
-function formatPrice($price) {
-    return number_format($price, 2, ',', '.') . ' €';
+function getOrderStatusClass($order) {
+    if (($order['status'] ?? '') === 'cancelled') {
+        return 'status-cancelled';
+    }
+    if (($order['confirmation_status'] ?? '') === 'pending') {
+        return 'status-unconfirmed';
+    }
+    if (($order['confirmation_status'] ?? '') === 'expired') {
+        return 'status-expired';
+    }
+    if (($order['status'] ?? '') === 'processed') {
+        return 'status-processed';
+    }
+    if (($order['status'] ?? '') === 'partial') {
+        return 'status-partial';
+    }
+    return 'status-open';
 }
 
-/**
- * Format date
- */
 function formatDate($dateString) {
-    $date = new DateTime($dateString);
-    return $date->format('d.m.Y H:i');
-}
+    $dateString = trim((string) $dateString);
+    if ($dateString === '') {
+        return '-';
+    }
 
-/**
- * Send email
- */
-function sendEmail($to, $subject, $message, $isHtml = true) {
-    $headers = [];
-    $headers[] = 'From: ' . FROM_NAME . ' <' . FROM_EMAIL . '>';
-    $headers[] = 'Reply-To: ' . FROM_EMAIL;
-    $headers[] = 'X-Mailer: PHP/' . phpversion();
-    
-    if ($isHtml) {
-        $headers[] = 'MIME-Version: 1.0';
-        $headers[] = 'Content-type: text/html; charset=UTF-8';
+    try {
+        $date = new DateTimeImmutable($dateString);
+        return $date->format('d.m.Y H:i');
+    } catch (Exception $exception) {
+        return $dateString;
     }
-    
-    return mail($to, $subject, $message, implode("\r\n", $headers));
 }
 
-/**
- * Get all admin notification recipients from admin accounts.
- * Falls back to ADMIN_EMAIL if no account email is configured.
- */
-function getAdminNotificationEmails() {
-    $accounts = getAdminAccounts();
-    $emails = [];
+function getCart() {
+    $cart = $_SESSION['cart'] ?? [];
+    if (!is_array($cart)) {
+        $cart = [];
+    }
 
-    foreach ($accounts as $account) {
-        if (!isset($account['email'])) {
+    $normalized = [];
+    $seen = [];
+
+    foreach ($cart as $item) {
+        $productId = isset($item['product_id']) ? (int) $item['product_id'] : 0;
+        $size = trim((string) ($item['size'] ?? ''));
+        $product = getProductById($productId);
+        if ($product === null) {
             continue;
         }
 
-        $email = normalizeAdminEmail($account['email']);
-        if (!isValidAdminEmail($email)) {
+        $sizes = getProductSizes($product);
+        if (!empty($sizes)) {
+            if ($size === '' || !in_array($size, $sizes, true)) {
+                continue;
+            }
+        } else {
+            $size = '';
+        }
+
+        $key = $productId . '|' . $size;
+        if (isset($seen[$key])) {
             continue;
         }
-        $emails[] = $email;
+
+        $seen[$key] = true;
+        $normalized[] = [
+            'product_id' => $productId,
+            'size' => $size,
+        ];
+    }
+
+    $_SESSION['cart'] = array_values($normalized);
+    return $_SESSION['cart'];
+}
+
+function addCartItem($productId, $size = '') {
+    $productId = (int) $productId;
+    $size = trim((string) $size);
+    $product = getProductById($productId);
+    if ($product === null) {
+        return false;
+    }
+
+    $sizes = getProductSizes($product);
+    if (!empty($sizes)) {
+        if ($size === '' || !in_array($size, $sizes, true)) {
+            return false;
+        }
+    } else {
+        $size = '';
     }
 
-    if (empty($emails)) {
-        $fallbackEmail = getDefaultAdminEmail();
-        if ($fallbackEmail !== '') {
-            $emails[] = $fallbackEmail;
+    $cart = getCart();
+    foreach ($cart as $item) {
+        if ((int) $item['product_id'] === $productId && (string) $item['size'] === $size) {
+            return true;
         }
     }
 
-    return array_values(array_unique($emails));
+    $cart[] = [
+        'product_id' => $productId,
+        'size' => $size,
+    ];
+
+    $_SESSION['cart'] = array_values($cart);
+    return true;
+}
+
+function removeCartItemByIndex($index) {
+    $cart = getCart();
+    if (isset($cart[$index])) {
+        unset($cart[$index]);
+        $_SESSION['cart'] = array_values($cart);
+    }
 }
 
-/**
- * Send admin notifications to all configured recipients.
- */
-function sendAdminNotificationEmails($subject, $message, $isHtml = true) {
-    $emails = getAdminNotificationEmails();
+function clearCart() {
+    $_SESSION['cart'] = [];
+}
 
-    if (empty($emails)) {
-        return false;
+function getCartItemsDetailed() {
+    $items = [];
+    foreach (getCart() as $index => $cartItem) {
+        $product = getProductById($cartItem['product_id']);
+        if ($product === null) {
+            continue;
+        }
+
+        $size = trim((string) ($cartItem['size'] ?? ''));
+        $items[] = [
+            'cart_index' => $index,
+            'product' => $product,
+            'size' => $size,
+            'availability_label' => $size !== '' ? getAvailabilityLabel($product, $size) : '',
+        ];
     }
 
-    $sent = false;
-    foreach ($emails as $email) {
-        $result = sendEmail($email, $subject, $message, $isHtml);
-        if ($result) {
-            $sent = true;
+    return $items;
+}
+
+function buildOrderItemsFromCart() {
+    $items = [];
+    foreach (getCart() as $cartItem) {
+        $product = getProductById($cartItem['product_id']);
+        if ($product === null) {
+            continue;
         }
+
+        $size = trim((string) ($cartItem['size'] ?? ''));
+        $items[] = [
+            'product_id' => $product['id'],
+            'size' => $size,
+            'is_processed' => false,
+        ];
     }
 
-    return $sent;
+    return normalizeOrderItems($items);
 }
 
-/**
- * Send reservation confirmation emails
- */
-function sendReservationEmails($reservation) {
-    $products = getProducts();
-    
-    // Build items list
-    $itemsHtml = '<ul style="list-style: none; margin: 0; padding: 0;">';
-    foreach ($reservation['items'] as $item) {
-        $product = getProductById($item['product_id']);
-        if ($product) {
-            $sizeInfo = '';
-            if (isset($item['size']) && !empty($item['size'])) {
-                $sizeInfo = ' - Größe: ' . htmlspecialchars($item['size']);
-            }
-            $itemsHtml .= '<li style="margin: 0 0 0.6rem 0; padding: 0.75rem 0.9rem; background: #28292a; border: 1px solid #4a5263; border-radius: 6px;"><strong style="color: #cac300;">' . htmlspecialchars($product['name']) . '</strong>' . $sizeInfo . ' - Menge: <strong>' . (int) $item['quantity'] . '</strong></li>';
+function buildOrderItemsHtml($order) {
+    $parts = [];
+    foreach ($order['items'] as $item) {
+        $label = '<strong>' . escape($item['product_name']) . '</strong>';
+        if ($item['size'] !== '') {
+            $label .= ' - Größe: ' . escape($item['size']);
+        }
+        if (!empty($item['availability_label'])) {
+            $label .= '<br><small>' . nl2br(escape($item['availability_label'])) . '</small>';
         }
+        $parts[] = '<li style="margin: 0 0 0.6rem 0; padding: 0.75rem 0.9rem; background: #28292a; border: 1px solid #4a5263; border-radius: 6px;">' . $label . '</li>';
     }
-    $itemsHtml .= '</ul>';
-    
-    // Customer email
-    $customerSubject = 'Ihre Reservierung bei ' . SITE_NAME;
-    $customerMessage = '
-    <html>
-    <head>
-        <meta charset="UTF-8">
-    </head>
-    <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #f5f7fb; background: #28292a; padding: 1.5rem;">
-        <div style="max-width: 640px; margin: 0 auto; background: #2f3541; padding: 1.5rem 2rem; border-radius: 10px; border: 1px solid #3b4252;">
-            <h2 style="color: #cac300; margin-top: 0;">Reservierung bestätigt</h2>
-            <p>Sehr geehrte/r ' . htmlspecialchars($reservation['customer_name']) . ',</p>
-            <p>vielen Dank für Ihre Reservierung bei ' . SITE_NAME . '.</p>
-            
-            <div style="background: #28292a; border: 2px solid #cac300; padding: 1.5rem; margin: 1.5rem 0; border-radius: 8px; text-align: center;">
-                <h3 style="margin-top: 0; color: #f5f7fb;">Ihre Bestellnummer:</h3>
-                <p style="margin: 0; color: #cac300; font-family: monospace;">' . htmlspecialchars($reservation['id']) . '</p>
-            </div>
-            
-            <h3>Reservierungsdetails:</h3>
-            <p><strong>Bestellnummer:</strong> ' . htmlspecialchars($reservation['id']) . '</p>
-            <p><strong>Erstellt am:</strong> ' . formatDate($reservation['created']) . '</p>
-            <p><strong>Gültig bis:</strong> ' . formatDate($reservation['expires']) . '</p>
-            
-            <h3>Reservierte Artikel:</h3>
-            <div style="background: #303745; border: 1px solid #4a5263; border-left: 4px solid #cac300; border-radius: 8px; padding: 1rem;">' . $itemsHtml . '</div>
-            
-            <p><strong>Wichtig:</strong> Bitte nennen Sie diese Bestellnummer bei der Abholung. Die Reservierung ist bis zum ' . formatDate($reservation['expires']) . ' gültig.</p>
-            
-            <p>Mit freundlichen Grüßen<br>' . SITE_NAME . '</p>
-        </div>
-    </body>
-    </html>';
-    
-    sendEmail($reservation['customer_email'], $customerSubject, $customerMessage);
-    
-    // Admin email
-    $adminSubject = 'Neue Reservierung: ' . $reservation['id'];
-    $adminMessage = '
+
+    return '<ul style="list-style: none; margin: 0; padding: 0;">' . implode('', $parts) . '</ul>';
+}
+
+function buildOrderSummaryHtml($order, $title, $introHtml, $extraHtml = '') {
+    $itemsHtml = buildOrderItemsHtml($order);
+
+    return '
     <html>
     <head>
         <meta charset="UTF-8">
     </head>
     <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #f5f7fb; background: #28292a; padding: 1.5rem;">
-        <div style="max-width: 640px; margin: 0 auto; background: #2f3541; padding: 1.5rem 2rem; border-radius: 10px; border: 1px solid #3b4252;">
-            <h2 style="color: #cac300; margin-top: 0;">Neue Reservierung</h2>
-            <p>Eine neue Reservierung wurde erstellt:</p>
-            
+        <div style="max-width: 720px; margin: 0 auto; background: #2f3541; padding: 1.5rem 2rem; border-radius: 10px; border: 1px solid #3b4252;">
+            <h2 style="color: #cac300; margin-top: 0;">' . escape($title) . '</h2>
+            ' . $introHtml . '
             <div style="background: #28292a; border: 2px solid #cac300; padding: 1.5rem; margin: 1.5rem 0; border-radius: 8px;">
-                <h3 style="margin-top: 0;">Bestellnummer:</h3>
-                <p style="margin: 0; color: #cac300; font-family: monospace;">' . htmlspecialchars($reservation['id']) . '</p>
+                <h3 style="margin-top: 0;">Bestellnummer</h3>
+                <p style="margin: 0; color: #cac300; font-family: monospace;">' . escape($order['id']) . '</p>
             </div>
-            
-            <h3>Kundendaten:</h3>
-            <p><strong>Name:</strong> ' . htmlspecialchars($reservation['customer_name']) . '</p>
-            <p><strong>E-Mail:</strong> ' . htmlspecialchars($reservation['customer_email']) . '</p>
-            
-            <h3>Reservierungsdetails:</h3>
-            <p><strong>Bestellnummer:</strong> ' . htmlspecialchars($reservation['id']) . '</p>
-            <p><strong>Erstellt am:</strong> ' . formatDate($reservation['created']) . '</p>
-            <p><strong>Gültig bis:</strong> ' . formatDate($reservation['expires']) . '</p>
-            
-            <h3>Reservierte Artikel:</h3>
+            <p><strong>Name:</strong> ' . escape($order['customer_name']) . '</p>
+            <p><strong>E-Mail:</strong> ' . escape($order['customer_email']) . '</p>
+            <p><strong>Organisation:</strong> ' . escape($order['organization_label']) . '</p>
+            <p><strong>Erstellt am:</strong> ' . escape(formatDate($order['created_at'])) . '</p>
+            <h3>Bestellte Artikel</h3>
             <div style="background: #303745; border: 1px solid #4a5263; border-left: 4px solid #cac300; border-radius: 8px; padding: 1rem;">' . $itemsHtml . '</div>
+            <p><strong>Kommentar:</strong><br>' . ($order['comment'] !== '' ? nl2br(escape($order['comment'])) : 'Kein Kommentar') . '</p>
+            ' . $extraHtml . '
         </div>
     </body>
     </html>';
-    
-    sendAdminNotificationEmails($adminSubject, $adminMessage);
-}
-
-/**
- * Send backorder confirmation emails
- */
-function sendBackorderEmails($reservation) {
-    // Build items list
-    $itemsHtml = '<ul style="list-style: none; margin: 0; padding: 0;">';
-    foreach ($reservation['items'] as $item) {
-        $product = getProductById($item['product_id']);
-        if ($product) {
-            $sizeInfo = '';
-            if (isset($item['size']) && !empty($item['size'])) {
-                $sizeInfo = ' - Größe: ' . htmlspecialchars($item['size']);
-            }
-            $itemsHtml .= '<li style="margin: 0 0 0.6rem 0; padding: 0.75rem 0.9rem; background: #28292a; border: 1px solid #4a5263; border-radius: 6px;"><strong style="color: #cac300;">' . htmlspecialchars($product['name']) . '</strong>' . $sizeInfo . ' - Menge: <strong>' . (int) $item['quantity'] . '</strong></li>';
+}
+
+function sendOrderConfirmationRequestEmail($order) {
+    $subject = 'Bitte Bestellung bestätigen: ' . $order['id'];
+    $link = buildOrderConfirmationUrl($order['confirmation_token']);
+    $expiryText = formatDate($order['confirmation_expires_at']);
+    $intro = '<p>Guten Tag ' . escape($order['customer_name']) . ',</p><p>bitte bestätigen Sie Ihre PSA-Bestellung über den folgenden Link.</p>';
+    $extra = '
+        <p><a href="' . escape($link) . '" style="display: inline-block; padding: 0.75rem 1.25rem; background: #cf2e2e; color: #ffffff; text-decoration: none; border-radius: 4px;">Bestellung bestätigen</a></p>
+        <p>Der Link ist gültig bis: <strong>' . escape($expiryText) . '</strong></p>
+        <p>Falls der Button nicht funktioniert, verwenden Sie bitte diesen Link:<br>' . escape($link) . '</p>';
+    $message = buildOrderSummaryHtml($order, 'Bestellung bestätigen', $intro, $extra);
+
+    return sendEmail($order['customer_email'], $subject, $message);
+}
+
+function sendOrderCreatedCustomerEmail($order) {
+    $subject = 'Ihre PSA-Bestellung: ' . $order['id'];
+    $intro = '<p>Guten Tag ' . escape($order['customer_name']) . ',</p><p>Ihre Bestellung wurde erfasst und an die zuständige Stelle weitergeleitet.</p>';
+    $message = buildOrderSummaryHtml($order, 'Bestellung eingegangen', $intro);
+
+    return sendEmail($order['customer_email'], $subject, $message);
+}
+
+function sendOrderConfirmedCustomerEmail($order) {
+    $subject = 'Ihre PSA-Bestellung wurde bestätigt: ' . $order['id'];
+    $intro = '<p>Guten Tag ' . escape($order['customer_name']) . ',</p><p>Ihre Bestellung wurde bestätigt und intern weitergeleitet.</p>';
+    $message = buildOrderSummaryHtml($order, 'Bestellung bestätigt', $intro);
+
+    return sendEmail($order['customer_email'], $subject, $message);
+}
+
+function sendConfirmedOrderAdminNotification($order) {
+    $recipient = getOrderRecipientEmail();
+    if (!isValidAdminEmail($recipient)) {
+        return false;
+    }
+
+    $subject = 'Neue PSA-Bestellung: ' . $order['id'];
+    $intro = '<p>Eine neue PSA-Bestellung wurde freigegeben und muss bearbeitet werden.</p>';
+    $message = buildOrderSummaryHtml($order, 'Neue PSA-Bestellung', $intro);
+
+    $attachments = [];
+    if (shouldAttachOrderPdfToAdminEmail()) {
+        $attachments[] = [
+            'filename' => 'bestellung-' . strtolower($order['id']) . '.pdf',
+            'content_type' => 'application/pdf',
+            'content' => generateOrderPdf($order),
+        ];
+    }
+
+    return sendEmail($recipient, $subject, $message, true, $attachments);
+}
+
+function sendEmail($to, $subject, $message, $isHtml = true, $attachments = []) {
+    $headers = [];
+    $headers[] = 'From: ' . FROM_NAME . ' <' . FROM_EMAIL . '>';
+    $headers[] = 'Reply-To: ' . FROM_EMAIL;
+    $headers[] = 'X-Mailer: PHP/' . phpversion();
+
+    if (empty($attachments)) {
+        if ($isHtml) {
+            $headers[] = 'MIME-Version: 1.0';
+            $headers[] = 'Content-Type: text/html; charset=UTF-8';
+        } else {
+            $headers[] = 'Content-Type: text/plain; charset=UTF-8';
         }
+
+        return mail($to, $subject, $message, implode("\r\n", $headers));
     }
-    $itemsHtml .= '</ul>';
-    
-    // Customer email
-    $customerSubject = 'Vorbestellung bei ' . SITE_NAME;
-    $customerMessage = '
-    <html>
-    <head>
-        <meta charset="UTF-8">
-    </head>
-    <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #f5f7fb; background: #28292a; padding: 1.5rem;">
-        <div style="max-width: 640px; margin: 0 auto; background: #2f3541; padding: 1.5rem 2rem; border-radius: 10px; border: 1px solid #3b4252;">
-            <h2 style="color: #cac300; margin-top: 0;">Vorbestellung bestätigt</h2>
-            <p>Sehr geehrte/r ' . htmlspecialchars($reservation['customer_name']) . ',</p>
-            <p>vielen Dank für Ihre Vorbestellung bei ' . SITE_NAME . '.</p>
-            
-            <div style="background: #28292a; border: 2px solid #cac300; padding: 1.5rem; margin: 1.5rem 0; border-radius: 8px; text-align: center;">
-                <h3 style="margin-top: 0; color: #f5f7fb;">Ihre Bestellnummer:</h3>
-                <p style="margin: 0; color: #cac300; font-family: monospace;">' . htmlspecialchars($reservation['id']) . '</p>
-            </div>
-            
-            <h3>Vorbestellungsdetails:</h3>
-            <p><strong>Bestellnummer:</strong> ' . htmlspecialchars($reservation['id']) . '</p>
-            <p><strong>Erstellt am:</strong> ' . formatDate($reservation['created']) . '</p>
-            
-            <h3>Vorbestellte Artikel:</h3>
-            <div style="background: #303745; border: 1px solid #4a5263; border-left: 4px solid #cac300; border-radius: 8px; padding: 1rem;">' . $itemsHtml . '</div>
-            
-            <div style="background: #28292a; border: 2px solid #cf2e2e; padding: 1.5rem; margin: 1.5rem 0; border-radius: 8px;">
-                <strong>Hinweis:</strong> Die Lieferzeiten sind nicht bekannt, da die Bestellung in Chargen erfolgt.
-            </div>
-            
-            <p>Wir informieren Sie, sobald die komplette Vorbestellung zur Abholung bereit ist.</p>
-            
-            <p>Mit freundlichen Grüßen<br>' . SITE_NAME . '</p>
-        </div>
-    </body>
-    </html>';
-    
-    sendEmail($reservation['customer_email'], $customerSubject, $customerMessage);
-    
-    // Admin email
-    $adminSubject = 'Neue Vorbestellung: ' . $reservation['id'];
-    $adminMessage = '
-    <html>
-    <head>
-        <meta charset="UTF-8">
-    </head>
-    <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #f5f7fb; background: #28292a; padding: 1.5rem;">
-        <div style="max-width: 640px; margin: 0 auto; background: #2f3541; padding: 1.5rem 2rem; border-radius: 10px; border: 1px solid #3b4252;">
-            <h2 style="color: #cac300; margin-top: 0;">Neue Vorbestellung</h2>
-            <p>Eine neue Vorbestellung wurde erstellt:</p>
-            
-            <div style="background: #28292a; border: 2px solid #cac300; padding: 1.5rem; margin: 1.5rem 0; border-radius: 8px;">
-                <h3 style="margin-top: 0;">Bestellnummer:</h3>
-                <p style="margin: 0; color: #cac300; font-family: monospace;">' . htmlspecialchars($reservation['id']) . '</p>
-            </div>
-            
-            <h3>Kundendaten:</h3>
-            <p><strong>Name:</strong> ' . htmlspecialchars($reservation['customer_name']) . '</p>
-            <p><strong>E-Mail:</strong> ' . htmlspecialchars($reservation['customer_email']) . '</p>
-            
-            <h3>Vorbestellungsdetails:</h3>
-            <p><strong>Bestellnummer:</strong> ' . htmlspecialchars($reservation['id']) . '</p>
-            <p><strong>Erstellt am:</strong> ' . formatDate($reservation['created']) . '</p>
-            
-            <h3>Vorbestellte Artikel:</h3>
-            <div style="background: #303745; border: 1px solid #4a5263; border-left: 4px solid #cac300; border-radius: 8px; padding: 1rem;">' . $itemsHtml . '</div>
-        </div>
-    </body>
-    </html>';
-    
-    sendAdminNotificationEmails($adminSubject, $adminMessage);
-}
-
-/**
- * Send backorder availability email
- */
-function sendBackorderAvailableEmail($reservation) {
-    $itemsHtml = '<ul>';
-    foreach ($reservation['items'] as $item) {
-        $product = getProductById($item['product_id']);
-        if ($product) {
-            $sizeInfo = '';
-            if (isset($item['size']) && !empty($item['size'])) {
-                $sizeInfo = ' - Größe: ' . htmlspecialchars($item['size']);
-            }
-            $itemsHtml .= '<li>' . htmlspecialchars($product['name']) . $sizeInfo . ' - Menge: ' . $item['quantity'] . '</li>';
+
+    $boundary = '=_Boundary_' . bin2hex(random_bytes(8));
+    $headers[] = 'MIME-Version: 1.0';
+    $headers[] = 'Content-Type: multipart/mixed; boundary="' . $boundary . '"';
+
+    $body = [];
+    $body[] = '--' . $boundary;
+    $body[] = 'Content-Type: ' . ($isHtml ? 'text/html' : 'text/plain') . '; charset=UTF-8';
+    $body[] = 'Content-Transfer-Encoding: 8bit';
+    $body[] = '';
+    $body[] = $message;
+
+    foreach ($attachments as $attachment) {
+        if (!is_array($attachment)) {
+            continue;
         }
+
+        $filename = trim((string) ($attachment['filename'] ?? 'attachment.bin'));
+        $contentType = trim((string) ($attachment['content_type'] ?? 'application/octet-stream'));
+        $content = isset($attachment['content']) ? (string) $attachment['content'] : '';
+
+        $body[] = '--' . $boundary;
+        $body[] = 'Content-Type: ' . $contentType . '; name="' . $filename . '"';
+        $body[] = 'Content-Transfer-Encoding: base64';
+        $body[] = 'Content-Disposition: attachment; filename="' . $filename . '"';
+        $body[] = '';
+        $body[] = chunk_split(base64_encode($content));
     }
-    $itemsHtml .= '</ul>';
-    
-    $subject = 'Ihre Vorbestellung ist zur Abholung bereit';
-    $message = '
-    <html>
-    <head>
-        <meta charset="UTF-8">
-    </head>
-    <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #f5f7fb; background: #28292a; padding: 1.5rem;">
-        <div style="max-width: 640px; margin: 0 auto; background: #2f3541; padding: 1.5rem 2rem; border-radius: 10px; border: 1px solid #3b4252;">
-            <h2 style="color: #cac300; margin-top: 0;">Vorbestellung zur Abholung bereit</h2>
-            <p>Sehr geehrte/r ' . htmlspecialchars($reservation['customer_name']) . ',</p>
-            <p>Ihre komplette Vorbestellung ist jetzt zur Abholung bereit.</p>
-            
-            <div style="background: #28292a; border: 2px solid #cac300; padding: 1.5rem; margin: 1.5rem 0; border-radius: 8px; text-align: center;">
-                <h3 style="margin-top: 0; color: #f5f7fb;">Ihre Bestellnummer:</h3>
-                <h2 style="font-size: 2rem; letter-spacing: 0.2rem; color: #cac300; font-family: monospace;">' . htmlspecialchars($reservation['id']) . '</h2>
-            </div>
-            
-            <h3>Bereitliegende Artikel:</h3>
-            ' . $itemsHtml . '
-            
-            <p>Bitte nennen Sie die Bestellnummer bei der Abholung.</p>
-            
-            <p>Mit freundlichen Grüßen<br>' . SITE_NAME . '</p>
-        </div>
-    </body>
-    </html>';
-    
-    sendEmail($reservation['customer_email'], $subject, $message);
+
+    $body[] = '--' . $boundary . '--';
+    $body[] = '';
+
+    return mail($to, $subject, implode("\r\n", $body), implode("\r\n", $headers));
+}
+
+function buildOrderPdfLines($order) {
+    $lines = [
+        SITE_NAME,
+        '',
+        'PSA-Bestellung',
+        'Bestellnummer: ' . $order['id'],
+        'Erstellt am: ' . formatDate($order['created_at']),
+        'Name: ' . $order['customer_name'],
+        'E-Mail: ' . $order['customer_email'],
+        'Organisation: ' . $order['organization_label'],
+        '',
+        'Artikel:',
+    ];
+
+    foreach ($order['items'] as $item) {
+        $line = '- ' . $item['product_name'];
+        if ($item['size'] !== '') {
+            $line .= ' | Größe: ' . $item['size'];
+        }
+        if ($item['availability_label'] !== '') {
+            $line .= ' | Hinweis: ' . preg_replace('/\s+/', ' ', $item['availability_label']);
+        }
+        $lines[] = $line;
+    }
+
+    $lines[] = '';
+    $lines[] = 'Kommentar:';
+    if ($order['comment'] !== '') {
+        foreach (preg_split('/\r\n|\r|\n/', $order['comment']) as $commentLine) {
+            $lines[] = $commentLine;
+        }
+    } else {
+        $lines[] = 'Kein Kommentar';
+    }
+
+    return $lines;
+}
+
+function pdfEscapeText($text) {
+    $text = str_replace('\\', '\\\\', $text);
+    $text = str_replace('(', '\(', $text);
+    $text = str_replace(')', '\)', $text);
+    $text = str_replace("\r", '', $text);
+
+    if (function_exists('iconv')) {
+        $converted = @iconv('UTF-8', 'Windows-1252//TRANSLIT//IGNORE', $text);
+        if (is_string($converted)) {
+            $text = $converted;
+        }
+    }
+
+    return $text;
+}
+
+function generateOrderPdf($order) {
+    $lines = buildOrderPdfLines($order);
+    $content = "BT\n/F1 12 Tf\n14 TL\n50 790 Td\n";
+    $first = true;
+
+    foreach ($lines as $line) {
+        if (!$first) {
+            $content .= "T*\n";
+        }
+        $first = false;
+        $content .= '(' . pdfEscapeText($line) . ") Tj\n";
+    }
+
+    $content .= "ET";
+    $length = strlen($content);
+
+    $objects = [];
+    $objects[] = "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n";
+    $objects[] = "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n";
+    $objects[] = "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>\nendobj\n";
+    $objects[] = "4 0 obj\n<< /Length " . $length . " >>\nstream\n" . $content . "\nendstream\nendobj\n";
+    $objects[] = "5 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n";
+
+    $pdf = "%PDF-1.4\n";
+    $offsets = [0];
+    foreach ($objects as $object) {
+        $offsets[] = strlen($pdf);
+        $pdf .= $object;
+    }
+
+    $xrefOffset = strlen($pdf);
+    $pdf .= "xref\n0 " . (count($objects) + 1) . "\n";
+    $pdf .= "0000000000 65535 f \n";
+
+    for ($i = 1; $i <= count($objects); $i++) {
+        $pdf .= sprintf("%010d 00000 n \n", $offsets[$i]);
+    }
+
+    $pdf .= "trailer\n<< /Size " . (count($objects) + 1) . " /Root 1 0 R >>\n";
+    $pdf .= "startxref\n" . $xrefOffset . "\n%%EOF";
+
+    return $pdf;
 }

+ 11 - 12
includes/header.php

@@ -3,25 +3,24 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title><?php echo isset($pageTitle) ? $pageTitle . ' - ' : ''; ?><?php echo SITE_NAME; ?></title>
-    <link rel="shortcut icon" href="favicon.png">
-    <link rel="stylesheet" href="<?php echo SITE_URL; ?>/assets/css/style.css">
+    <title><?php echo isset($pageTitle) ? escape($pageTitle) . ' - ' : ''; ?><?php echo escape(SITE_NAME); ?></title>
+    <link rel="shortcut icon" href="<?php echo escape(SITE_URL); ?>/favicon.png">
+    <link rel="stylesheet" href="<?php echo escape(SITE_URL); ?>/assets/css/style.css">
 </head>
-<body class="<?php echo isset($bodyClass) ? htmlspecialchars($bodyClass) : ''; ?>">
+<body class="<?php echo isset($bodyClass) ? escape($bodyClass) : ''; ?>">
     <header class="site-header">
         <div class="container header-inner">
-            <a class="brand" href="<?php echo SITE_URL; ?>/index.php">
-                <img class="brand-logo" src="<?php echo SITE_URL; ?>/assets/feuerwehr-Logo-invers.webp" alt="Freiwillige Feuerwehr Freising Logo">
+            <a class="brand" href="<?php echo escape(SITE_URL); ?>/index.php">
+                <img class="brand-logo" src="<?php echo escape(SITE_URL); ?>/assets/feuerwehr-Logo-invers.webp" alt="Freiwillige Feuerwehr Freising Logo">
                 <div class="brand-text">
-                    <span class="brand-title"><?php echo SITE_NAME; ?></span>
-                    <span class="brand-subtitle">Onlineshop</span>
+                    <span class="brand-title"><?php echo escape(SITE_NAME); ?></span>
+                    <span class="brand-subtitle">PSA-Bestellung</span>
                 </div>
             </a>
             <nav class="site-nav">
-                <a href="<?php echo SITE_URL; ?>/index.php">Startseite</a>
-                <a href="<?php echo SITE_URL; ?>/cart.php">Warenkorb</a>
-                <a href="<?php echo SITE_URL; ?>/faq.php">FAQ</a>
-                <a href="<?php echo SITE_URL; ?>/orders.php">Meine Bestellungen</a>
+                <a href="<?php echo escape(SITE_URL); ?>/index.php">Startseite</a>
+                <a href="<?php echo escape(SITE_URL); ?>/cart.php">Warenkorb</a>
+                <a href="<?php echo escape(SITE_URL); ?>/faq.php">FAQ</a>
             </nav>
         </div>
     </header>

+ 11 - 22
index.php

@@ -6,12 +6,11 @@ $pageTitle = 'Startseite';
 $products = getProducts();
 $categories = getCategories();
 
-// Filter by category if provided
 $category = isset($_GET['category']) ? normalizeCategoryId($_GET['category']) : '';
 if ($category !== '' && getCategoryById($category) !== null) {
-    $products = array_filter($products, function($product) use ($category) {
+    $products = array_values(array_filter($products, function ($product) use ($category) {
         return productHasCategory($product, $category);
-    });
+    }));
 } else {
     $category = '';
 }
@@ -19,11 +18,11 @@ if ($category !== '' && getCategoryById($category) !== null) {
 include __DIR__ . '/includes/header.php';
 ?>
 
-<h2>Unsere Produkte</h2>
+<h2>PSA bestellen</h2>
 
 <div class="disclaimer-box">
     <?php foreach (DISCLAIMER_LINES as $line): ?>
-    <p><?php echo htmlspecialchars($line); ?></p>
+        <p><?php echo escape($line); ?></p>
     <?php endforeach; ?>
 </div>
 
@@ -31,7 +30,7 @@ include __DIR__ . '/includes/header.php';
     <a href="?category=" class="btn btn-small <?php echo $category === '' ? '' : 'btn-secondary'; ?>">Alle</a>
     <?php foreach ($categories as $categoryOption): ?>
         <a href="?category=<?php echo urlencode($categoryOption['id']); ?>" class="btn btn-small <?php echo $category === $categoryOption['id'] ? '' : 'btn-secondary'; ?>">
-            <?php echo htmlspecialchars($categoryOption['label']); ?>
+            <?php echo escape($categoryOption['label']); ?>
         </a>
     <?php endforeach; ?>
 </div>
@@ -44,27 +43,17 @@ include __DIR__ . '/includes/header.php';
     <div class="products-grid">
         <?php foreach ($products as $product): ?>
             <div class="product-card">
-                <a href="product.php?id=<?php echo $product['id']; ?>">
+                <a href="product.php?id=<?php echo (int) $product['id']; ?>">
                     <?php if (!empty($product['image']) && file_exists(__DIR__ . '/assets/images/' . $product['image'])): ?>
-                        <img src="<?php echo SITE_URL; ?>/assets/images/<?php echo htmlspecialchars($product['image']); ?>" alt="<?php echo htmlspecialchars($product['name']); ?>">
+                        <img src="<?php echo escape(SITE_URL); ?>/assets/images/<?php echo escape($product['image']); ?>" alt="<?php echo escape($product['name']); ?>">
                     <?php else: ?>
-                        <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='250' height='200'%3E%3Crect fill='%23e9ecef' width='250' height='200'/%3E%3Ctext x='50%25' y='50%25' text-anchor='middle' dy='.3em' fill='%236c757d'%3EKein Bild%3C/text%3E%3C/svg%3E" alt="Kein Bild">
+                        <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='250' height='200'%3E%3Crect fill='%233a4150' width='250' height='200'/%3E%3Ctext x='50%25' y='50%25' text-anchor='middle' dy='.3em' fill='%23c7ccd6'%3EKein Bild%3C/text%3E%3C/svg%3E" alt="Kein Bild">
                     <?php endif; ?>
                 </a>
                 <div class="product-card-content">
-                    <h3><a href="product.php?id=<?php echo $product['id']; ?>" style="text-decoration: none; color: inherit;"><?php echo htmlspecialchars($product['name']); ?></a></h3>
-                    <div class="price"><?php echo formatPrice($product['price']); ?></div>
-                    <?php 
-                    $totalStock = getTotalStock($product);
-                    ?>
-                    <div class="stock <?php echo $totalStock > 0 ? 'in-stock' : 'out-of-stock'; ?>">
-                        <?php if ($totalStock > 0): ?>
-                            Verfügbar (<?php echo $totalStock; ?> Stück)
-                        <?php else: ?>
-                            Ausverkauft - Vorbestellung möglich
-                        <?php endif; ?>
-                    </div>
-                    <a href="product.php?id=<?php echo $product['id']; ?>" class="btn" style="width: 100%; text-align: center; margin-top: 1rem;">Details ansehen</a>
+                    <h3><a href="product.php?id=<?php echo (int) $product['id']; ?>" style="text-decoration: none; color: inherit;"><?php echo escape($product['name']); ?></a></h3>
+                    <p class="product-summary"><?php echo escape(implode(', ', getProductSizes($product))); ?></p>
+                    <a href="product.php?id=<?php echo (int) $product['id']; ?>" class="btn" style="width: 100%; text-align: center; margin-top: 1rem;">Details ansehen</a>
                 </div>
             </div>
         <?php endforeach; ?>

+ 29 - 0
order-confirm.php

@@ -0,0 +1,29 @@
+<?php
+require_once __DIR__ . '/config.php';
+require_once __DIR__ . '/includes/functions.php';
+
+expirePendingOrders();
+$result = confirmOrderByToken($_GET['token'] ?? '');
+$pageTitle = 'Bestellung bestätigen';
+
+include __DIR__ . '/includes/header.php';
+?>
+
+<h2>Bestellung bestätigen</h2>
+
+<?php if ($result['success']): ?>
+    <div class="alert alert-success">
+        <p>Die Bestellung wurde bestätigt und intern weitergeleitet.</p>
+    </div>
+    <div class="panel">
+        <p><strong>Bestellnummer:</strong> <span class="order-highlight"><?php echo escape($result['order']['id']); ?></span></p>
+    </div>
+<?php else: ?>
+    <div class="alert alert-error">
+        <p><?php echo escape($result['message']); ?></p>
+    </div>
+<?php endif; ?>
+
+<a href="index.php" class="btn">Zur Startseite</a>
+
+<?php include __DIR__ . '/includes/footer.php'; ?>

+ 34 - 0
order-success.php

@@ -0,0 +1,34 @@
+<?php
+require_once __DIR__ . '/config.php';
+require_once __DIR__ . '/includes/functions.php';
+
+$pageTitle = 'Bestellung eingegangen';
+$order = getOrderById($_GET['id'] ?? '');
+
+include __DIR__ . '/includes/header.php';
+?>
+
+<h2>Bestellung eingegangen</h2>
+
+<?php if ($order === null): ?>
+    <div class="alert alert-info">
+        <p>Die Bestellung wurde gespeichert.</p>
+    </div>
+<?php else: ?>
+    <div class="panel">
+        <p><strong>Bestellnummer:</strong> <span class="order-highlight"><?php echo escape($order['id']); ?></span></p>
+        <?php if ($order['confirmation_status'] === 'pending'): ?>
+            <p>Bitte bestätigen Sie die Bestellung über den Link in der E-Mail an <strong><?php echo escape($order['customer_email']); ?></strong>.</p>
+        <?php else: ?>
+            <p>Ihre Bestellung wurde erfasst und intern weitergeleitet.</p>
+        <?php endif; ?>
+    </div>
+<?php endif; ?>
+
+<div class="alert alert-info">
+    <p>Eine persönliche Bestellhistorie im Browser wird nicht gespeichert.</p>
+</div>
+
+<a href="index.php" class="btn">Zur Startseite</a>
+
+<?php include __DIR__ . '/includes/footer.php'; ?>

+ 2 - 91
orders.php

@@ -1,94 +1,5 @@
 <?php
 require_once __DIR__ . '/config.php';
-require_once __DIR__ . '/includes/functions.php';
 
-$pageTitle = 'Meine Bestellungen';
-$rememberedOrderIds = getRememberedOrderIds();
-$orders = [];
-
-foreach ($rememberedOrderIds as $orderId) {
-    $reservation = getReservationByOrderNumber($orderId);
-    if (!$reservation) {
-        continue;
-    }
-    if ((isset($reservation['status']) && $reservation['status'] === 'deleted') || isReservationHidden($reservation)) {
-        continue;
-    }
-
-    $isBackorder = isset($reservation['type']) && $reservation['type'] === 'backorder';
-    $typeLabel = $isBackorder ? 'Vorbestellung' : 'Reservierung';
-
-    if ($isBackorder) {
-        if (isset($reservation['backorder_status']) && $reservation['backorder_status'] === 'notified') {
-            $statusLabel = 'Informiert';
-            $statusClass = 'status-notified';
-        } else {
-            $statusLabel = 'Offen';
-            $statusClass = 'status-open';
-        }
-        $detailsUrl = SITE_URL . '/reservation.php?backorder_number=' . urlencode($reservation['id']);
-    } else {
-        if (!empty($reservation['picked_up'])) {
-            $statusLabel = 'Abgeholt';
-            $statusClass = 'status-picked';
-        } elseif (isset($reservation['status']) && $reservation['status'] === 'expired') {
-            $statusLabel = 'Abgelaufen';
-            $statusClass = 'status-expired';
-        } else {
-            $statusLabel = 'Offen';
-            $statusClass = 'status-open';
-        }
-        $detailsUrl = SITE_URL . '/reservation.php?order_number=' . urlencode($reservation['id']);
-    }
-
-    $orders[] = [
-        'id' => $reservation['id'],
-        'type_label' => $typeLabel,
-        'created_label' => !empty($reservation['created']) ? formatDate($reservation['created']) : '-',
-        'status_label' => $statusLabel,
-        'status_class' => $statusClass,
-        'details_url' => $detailsUrl
-    ];
-}
-
-include __DIR__ . '/includes/header.php';
-?>
-
-<h2>Meine Bestellungen</h2>
-
-<div class="alert alert-info">
-    <p>Hier sehen Sie Ihre zuletzt erstellten Bestellungen. Es werden nur Bestellungen in diesem Browser gezeigt.</p>
-</div>
-
-<?php if (empty($orders)): ?>
-    <div class="alert alert-info">
-        <p>Es wurden noch keine Bestellungen in diesem Browser gefunden.</p>
-    </div>
-<?php else: ?>
-    <div class="table-responsive">
-        <table class="responsive-table">
-            <thead>
-                <tr>
-                    <th>Bestellnummer</th>
-                    <th>Typ</th>
-                    <th>Erstellt</th>
-                    <th>Status</th>
-                    <th>Aktionen</th>
-                </tr>
-            </thead>
-            <tbody>
-                <?php foreach ($orders as $order): ?>
-                    <tr>
-                        <td data-label="Bestellnummer"><strong><?php echo htmlspecialchars($order['id']); ?></strong></td>
-                        <td data-label="Typ"><?php echo htmlspecialchars($order['type_label']); ?></td>
-                        <td data-label="Erstellt"><?php echo htmlspecialchars($order['created_label']); ?></td>
-                        <td data-label="Status"><span class="status <?php echo htmlspecialchars($order['status_class']); ?>"><?php echo htmlspecialchars($order['status_label']); ?></span></td>
-                        <td data-label="Aktionen"><a href="<?php echo htmlspecialchars($order['details_url']); ?>" class="btn btn-small">Details</a></td>
-                    </tr>
-                <?php endforeach; ?>
-            </tbody>
-        </table>
-    </div>
-<?php endif; ?>
-
-<?php include __DIR__ . '/includes/footer.php'; ?>
+header('Location: index.php');
+exit;

+ 45 - 120
product.php

@@ -2,62 +2,25 @@
 require_once __DIR__ . '/config.php';
 require_once __DIR__ . '/includes/functions.php';
 
-$productId = isset($_GET['id']) ? (int)$_GET['id'] : 0;
+$productId = isset($_GET['id']) ? (int) $_GET['id'] : 0;
 $product = getProductById($productId);
 
-if (!$product) {
+if ($product === null) {
     header('Location: index.php');
     exit;
 }
 
 $pageTitle = $product['name'];
-$usesSizeStock = productUsesSizeStock($product);
+$sizes = getProductSizes($product);
 
-// Handle add to cart
 if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_to_cart'])) {
-    $quantity = isset($_POST['quantity']) ? (int)$_POST['quantity'] : 1;
-    $size = isset($_POST['size']) ? sanitize($_POST['size']) : '';
-    
-    if ($usesSizeStock && empty($size)) {
+    $size = trim((string) ($_POST['size'] ?? ''));
+
+    if (!empty($sizes) && ($size === '' || !in_array($size, $sizes, true))) {
         $error = 'Bitte wählen Sie eine Größe aus.';
-    } elseif ($quantity < 1) {
-        $error = 'Menge muss mindestens 1 sein.';
+    } elseif (!addCartItem($product['id'], $size)) {
+        $error = 'Der Artikel konnte nicht in den Warenkorb gelegt werden.';
     } else {
-        // Add to session cart
-        if (!isset($_SESSION['cart'])) {
-            $_SESSION['cart'] = [];
-        }
-        
-        $found = false;
-        if ($usesSizeStock) {
-            foreach ($_SESSION['cart'] as &$item) {
-                if ($item['product_id'] == $productId && isset($item['size']) && $item['size'] === $size) {
-                    $item['quantity'] += $quantity;
-                    $found = true;
-                    break;
-                }
-            }
-        } else {
-            foreach ($_SESSION['cart'] as &$item) {
-                if ($item['product_id'] == $productId && !isset($item['size'])) {
-                    $item['quantity'] += $quantity;
-                    $found = true;
-                    break;
-                }
-            }
-        }
-        
-        if (!$found) {
-            $cartItem = [
-                'product_id' => $productId,
-                'quantity' => $quantity
-            ];
-            if ($usesSizeStock && !empty($size)) {
-                $cartItem['size'] = $size;
-            }
-            $_SESSION['cart'][] = $cartItem;
-        }
-        
         header('Location: cart.php');
         exit;
     }
@@ -68,108 +31,70 @@ include __DIR__ . '/includes/header.php';
 
 <?php if (isset($error)): ?>
     <div class="alert alert-error">
-        <?php echo htmlspecialchars($error); ?>
+        <?php echo escape($error); ?>
     </div>
 <?php endif; ?>
 
 <div class="product-detail-grid">
     <div>
         <?php if (!empty($product['image']) && file_exists(__DIR__ . '/assets/images/' . $product['image'])): ?>
-            <img src="<?php echo SITE_URL; ?>/assets/images/<?php echo htmlspecialchars($product['image']); ?>" alt="<?php echo htmlspecialchars($product['name']); ?>" style="width: 100%; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
+            <img src="<?php echo escape(SITE_URL); ?>/assets/images/<?php echo escape($product['image']); ?>" alt="<?php echo escape($product['name']); ?>" style="width: 100%; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
         <?php else: ?>
-            <div class="product-placeholder">
-                Kein Bild verfügbar
-            </div>
+            <div class="product-placeholder">Kein Bild verfügbar</div>
         <?php endif; ?>
     </div>
-    
+
     <div>
-        <h1><?php echo htmlspecialchars($product['name']); ?></h1>
-        <div class="price" style="font-size: 2rem; margin: 1rem 0;"><?php echo formatPrice($product['price']); ?></div>
-        
-        <?php 
-        $totalStock = getTotalStock($product);
-        $hasStock = $totalStock > 0;
-        ?>
-        <div class="stock <?php echo $hasStock ? 'in-stock' : 'out-of-stock'; ?>" style="font-size: 1.1rem; margin: 1rem 0;">
-            <?php if ($hasStock): ?>
-                <?php if ($usesSizeStock && isset($product['stock_by_size']) && is_array($product['stock_by_size'])): ?>
-                    Verfügbar:
-                    <?php 
-                    $sizes = getProductSizes($product);
-                    $stockInfo = [];
-                    foreach ($sizes as $sizeOption) {
-                        $sizeStock = isset($product['stock_by_size'][$sizeOption]) ? (int)$product['stock_by_size'][$sizeOption] : 0;
-                        if ($sizeStock > 0) {
-                            $stockInfo[] = "$sizeOption: $sizeStock";
-                        }
-                    }
-                    echo implode(', ', $stockInfo);
-                    if (empty($stockInfo)) {
-                        echo 'Ausverkauft';
-                    }
-                    ?>
-                <?php else: ?>
-                    Verfügbar (<?php echo $totalStock; ?> Stück)
-                <?php endif; ?>
-            <?php else: ?>
-                Ausverkauft - Vorbestellung möglich
-            <?php endif; ?>
-        </div>
-        
-        <div style="margin: 2rem 0;">
+        <h1><?php echo escape($product['name']); ?></h1>
+
+        <div style="margin: 1.5rem 0 2rem;">
             <h3>Beschreibung</h3>
-            <p style="margin-top: 0.5rem; line-height: 1.8;"><?php echo nl2br(htmlspecialchars($product['description'])); ?></p>
+            <p style="margin-top: 0.5rem; line-height: 1.8;"><?php echo nl2br(escape($product['description'])); ?></p>
         </div>
-        
+
         <form method="POST" style="margin-top: 2rem;">
-            <?php if ($usesSizeStock): ?>
+            <?php if (!empty($sizes)): ?>
                 <div class="form-group">
                     <label for="size">Größe *</label>
-                    <select id="size" name="size" required style="width: 100%;" onchange="updateMaxQuantity()">
+                    <select id="size" name="size" required style="width: 100%;" onchange="updateAvailabilityNotice()">
                         <option value="">Bitte wählen</option>
-                        <?php 
-                        $sizes = getProductSizes($product);
-                        $stockBySize = isset($product['stock_by_size']) && is_array($product['stock_by_size']) ? $product['stock_by_size'] : [];
-                        foreach ($sizes as $sizeOption): 
-                            $sizeStock = isset($stockBySize[$sizeOption]) ? (int)$stockBySize[$sizeOption] : 0;
-                        ?>
-                            <option value="<?php echo htmlspecialchars($sizeOption); ?>" data-stock="<?php echo $sizeStock; ?>">
-                                <?php echo htmlspecialchars($sizeOption); ?><?php echo $sizeStock <= 0 ? ' (Ausverkauft - Vorbestellung möglich)' : " ($sizeStock verfügbar)"; ?>
+                        <?php foreach ($sizes as $sizeOption): ?>
+                            <?php $label = getAvailabilityLabel($product, $sizeOption); ?>
+                            <option value="<?php echo escape($sizeOption); ?>" data-label="<?php echo escape($label); ?>">
+                                <?php echo escape($sizeOption); ?><?php echo $label !== '' ? ' - ' . escape($label) : ''; ?>
                             </option>
                         <?php endforeach; ?>
                     </select>
                 </div>
+                <div id="availabilityNotice" class="alert alert-warning" style="display: none;"></div>
             <?php endif; ?>
-            <div class="form-group">
-                <label for="quantity">Menge:</label>
-                <input type="number" id="quantity" name="quantity" value="1" min="1" max="<?php echo $hasStock ? $totalStock : 999; ?>" class="quantity-input" style="width: 100px;">
+
+            <div class="alert alert-info">
+                Jeder Artikel wird mit Menge 1 in den Warenkorb gelegt.
             </div>
-            <?php if (!$hasStock): ?>
-                <div class="alert alert-warning">
-                    <strong>Hinweis:</strong> Dieses Produkt wird vorbestellt. Lieferzeiten sind nicht bekannt.
-                </div>
-            <?php endif; ?>
+
             <button type="submit" name="add_to_cart" class="btn" style="width: 100%;">In den Warenkorb</button>
         </form>
-        <?php if ($usesSizeStock): ?>
-        <script>
-            function updateMaxQuantity() {
-                const sizeSelect = document.getElementById('size');
-                const quantityInput = document.getElementById('quantity');
-                const selectedOption = sizeSelect.options[sizeSelect.selectedIndex];
-                if (selectedOption && selectedOption.value) {
-                    const stock = parseInt(selectedOption.getAttribute('data-stock')) || 0;
-                    const max = stock > 0 ? stock : 999;
-                    quantityInput.max = max;
-                    if (parseInt(quantityInput.value) > max) {
-                        quantityInput.value = 1;
+
+        <?php if (!empty($sizes)): ?>
+            <script>
+                function updateAvailabilityNotice() {
+                    const sizeSelect = document.getElementById('size');
+                    const notice = document.getElementById('availabilityNotice');
+                    const selectedOption = sizeSelect.options[sizeSelect.selectedIndex];
+                    const text = selectedOption ? selectedOption.getAttribute('data-label') : '';
+
+                    if (text) {
+                        notice.textContent = text;
+                        notice.style.display = 'block';
+                    } else {
+                        notice.textContent = '';
+                        notice.style.display = 'none';
                     }
                 }
-            }
-        </script>
+            </script>
         <?php endif; ?>
-        
+
         <div style="margin-top: 2rem;">
             <a href="index.php" class="btn btn-secondary">Zurück zur Übersicht</a>
         </div>

+ 2 - 163
reservation.php

@@ -1,166 +1,5 @@
 <?php
 require_once __DIR__ . '/config.php';
-require_once __DIR__ . '/includes/functions.php';
 
-$pageTitle = 'Reservierung bestätigt';
-
-$orderNumber = isset($_GET['order_number']) ? sanitize($_GET['order_number']) : '';
-$backorderNumber = isset($_GET['backorder_number']) ? sanitize($_GET['backorder_number']) : '';
-$reservation = null;
-$backorderReservation = null;
-
-if ($orderNumber) {
-    $reservation = getReservationByOrderNumber($orderNumber);
-}
-if ($backorderNumber) {
-    $backorderReservation = getReservationByOrderNumber($backorderNumber);
-}
-
-if (!$reservation && !$backorderReservation) {
-    header('Location: index.php');
-    exit;
-}
-
-function buildReservationItems($reservation) {
-    $items = [];
-    foreach ($reservation['items'] as $item) {
-        $product = getProductById($item['product_id']);
-        if ($product) {
-            $items[] = [
-                'product' => $product,
-                'quantity' => $item['quantity'],
-                'size' => isset($item['size']) ? $item['size'] : null
-            ];
-        }
-    }
-    return $items;
-}
-
-include __DIR__ . '/includes/header.php';
-?>
-
-<style media="print">
-    header, footer, .btn, nav {
-        display: none !important;
-    }
-    body {
-        padding: 20px;
-    }
-    .order-number {
-        page-break-inside: avoid;
-    }
-    table {
-        page-break-inside: avoid;
-    }
-</style>
-
-
-<?php if ($reservation): ?>
-    <?php $items = buildReservationItems($reservation); ?>
-    <div class="alert alert-success">
-        <h2>Reservierung erfolgreich!</h2>
-        <p>Vielen Dank für Ihre Reservierung, <?php echo htmlspecialchars($reservation['customer_name']); ?>.</p>
-    </div>
-
-    <div class="order-number">
-        <h3>Ihre Bestellnummer:</h3>
-        <h2><?php echo htmlspecialchars($reservation['id']); ?></h2>
-        <p>Bitte notieren Sie sich diese Bestellnummer und nennen Sie sie bei der Abholung.</p>
-    </div>
-
-    <div class="panel" style="padding: 2rem; margin: 2rem 0;">
-        <h3>Reservierungsdetails</h3>
-        
-        <p><strong>Bestellnummer:</strong> <?php echo htmlspecialchars($reservation['id']); ?></p>
-        <p><strong>Name:</strong> <?php echo htmlspecialchars($reservation['customer_name']); ?></p>
-        <p><strong>E-Mail:</strong> <?php echo htmlspecialchars($reservation['customer_email']); ?></p>
-        <p><strong>Erstellt am:</strong> <?php echo formatDate($reservation['created']); ?></p>
-        <p><strong>Gültig bis:</strong> <?php echo formatDate($reservation['expires']); ?></p>
-        
-        <h4 style="margin-top: 1.5rem;">Reservierte Artikel:</h4>
-        <table style="margin-top: 1rem;">
-            <thead>
-                <tr>
-                    <th>Produkt</th>
-                    <?php if (array_filter($items, function($i) { return !empty($i['size']); })): ?>
-                        <th>Größe</th>
-                    <?php endif; ?>
-                    <th>Menge</th>
-                </tr>
-            </thead>
-            <tbody>
-                <?php foreach ($items as $item): ?>
-                    <tr>
-                        <td><?php echo htmlspecialchars($item['product']['name']); ?></td>
-                        <?php if (array_filter($items, function($i) { return !empty($i['size']); })): ?>
-                            <td><?php echo isset($item['size']) && !empty($item['size']) ? htmlspecialchars($item['size']) : '-'; ?></td>
-                        <?php endif; ?>
-                        <td><?php echo $item['quantity']; ?></td>
-                    </tr>
-                <?php endforeach; ?>
-            </tbody>
-        </table>
-    </div>
-
-    <div class="alert alert-info">
-        <p><strong>Wichtig:</strong> Bitte nennen Sie die Bestellnummer bei der Abholung. Die Reservierung ist bis zum <?php echo formatDate($reservation['expires']); ?> gültig.</p>
-    </div>
-<?php endif; ?>
-
-<?php if ($backorderReservation): ?>
-    <?php $backorderItems = buildReservationItems($backorderReservation); ?>
-    <div class="alert alert-warning" style="margin-top: 2rem;">
-        <h2>Vorbestellung bestätigt</h2>
-        <p>Vielen Dank für Ihre Vorbestellung, <?php echo htmlspecialchars($backorderReservation['customer_name']); ?>.</p>
-    </div>
-
-    <div class="order-number">
-        <h3>Ihre Bestellnummer:</h3>
-        <h2><?php echo htmlspecialchars($backorderReservation['id']); ?></h2>
-        <p>Bitte notieren Sie sich diese Bestellnummer. Wir informieren Sie, sobald die komplette Vorbestellung zur Abholung bereit ist.</p>
-    </div>
-
-    <div class="panel" style="padding: 2rem; margin: 2rem 0;">
-        <h3>Vorbestellungsdetails</h3>
-        
-        <p><strong>Bestellnummer:</strong> <?php echo htmlspecialchars($backorderReservation['id']); ?></p>
-        <p><strong>Name:</strong> <?php echo htmlspecialchars($backorderReservation['customer_name']); ?></p>
-        <p><strong>E-Mail:</strong> <?php echo htmlspecialchars($backorderReservation['customer_email']); ?></p>
-        <p><strong>Erstellt am:</strong> <?php echo formatDate($backorderReservation['created']); ?></p>
-        
-        <h4 style="margin-top: 1.5rem;">Vorbestellte Artikel:</h4>
-        <table style="margin-top: 1rem;">
-            <thead>
-                <tr>
-                    <th>Produkt</th>
-                    <?php if (array_filter($backorderItems, function($i) { return !empty($i['size']); })): ?>
-                        <th>Größe</th>
-                    <?php endif; ?>
-                    <th>Menge</th>
-                </tr>
-            </thead>
-            <tbody>
-                <?php foreach ($backorderItems as $item): ?>
-                    <tr>
-                        <td><?php echo htmlspecialchars($item['product']['name']); ?></td>
-                        <?php if (array_filter($backorderItems, function($i) { return !empty($i['size']); })): ?>
-                            <td><?php echo isset($item['size']) && !empty($item['size']) ? htmlspecialchars($item['size']) : '-'; ?></td>
-                        <?php endif; ?>
-                        <td><?php echo $item['quantity']; ?></td>
-                    </tr>
-                <?php endforeach; ?>
-            </tbody>
-        </table>
-    </div>
-
-    <div class="alert alert-warning">
-        <p><strong>Hinweis:</strong> Lieferzeiten sind nicht bekannt, da die Bestellung in Chargen erfolgt.</p>
-    </div>
-<?php endif; ?>
-
-<div style="text-align: center; margin-top: 2rem;">
-    <button onclick="window.print()" class="btn">Reservierung drucken</button>
-    <a href="index.php" class="btn btn-secondary" style="margin-left: 1rem;">Zurück zur Startseite</a>
-</div>
-
-<?php include __DIR__ . '/includes/footer.php'; ?>
+header('Location: index.php');
+exit;