1
0

13 Коммиты ec38e6151b ... 562bc60ee4

Автор SHA1 Сообщение Дата
  Medowar 562bc60ee4 Merge origin/main: integrate rate limits and security headers with backorder changes. 6 дней назад
  Medowar f9eb644e12 Fix backorder dashboard visibility, FIFO sorting, and cancel cleanup. 6 дней назад
  Medowar 53944c08d6 Remove optional email order confirmation flow. 6 дней назад
  Medowar 0775de7af8 updating testorders 6 дней назад
  Medowar f3c716a039 admin dashboard changes 6 дней назад
  Medowar ff531fa922 Add reusable order PDF rendering and admin print action. 6 дней назад
  Medowar 31d6a42043 updating docs 6 дней назад
  Medowar 4a6668a676 creating dedicated order view page 6 дней назад
  Medowar 00415e5d07 Checkpoint #1 Bugfixes and feature 6 дней назад
  Medowar 15163bbfdd cleanup admin dash 6 дней назад
  Medowar e57f2f48a0 feat: implem backorder logic 6 дней назад
  Medowar 6f0d0f268c more testorders 6 дней назад
  Medowar 3b405b0e2e adding open products to admin 6 дней назад

+ 2 - 2
README.md

@@ -10,7 +10,7 @@ Dieses Projekt ist ein internes Bestellsystem für persönliche Schutzausrüstun
 - Produktdetailseiten mit Größenwahl
 - Warenkorb ohne Mengensteuerung
 - Checkout mit Name, E-Mail, Organisation und Kommentar
-- Optionaler Bestätigungslink vor interner Weiterleitung
+- Direkte interne Weiterleitung nach Bestellabschluss
 - Adminbereich für Bestellungen, Produkte, Kategorien, Organisationen, Einstellungen, FAQ und Admins
 - Positionsbezogene Bearbeitung und Stornierung von Bestellungen
 
@@ -34,7 +34,7 @@ Dieses Projekt ist ein internes Bestellsystem für persönliche Schutzausrüstun
 
 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.
+3. Empfängeradresse und PDF-Anhang im Admin unter `Einstellungen` prüfen.
 4. Organisationen im Admin unter `Organisationen verwalten` pflegen.
 
 ## Hinweise

+ 329 - 3
admin/backorders.php

@@ -1,5 +1,331 @@
 <?php
-require_once __DIR__ . '/../config.php';
+require_once __DIR__ . "/../config.php";
+require_once __DIR__ . "/../includes/functions.php";
 
-header('Location: orders.php');
-exit;
+if (empty($_SESSION['admin_logged_in'])) {
+    header("Location: login.php");
+    exit();
+}
+
+$pageTitle = "Nachbestellungen";
+$message = "";
+$messageType = "";
+
+if ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['add_manual_backorder'])) {
+    if (!validateCsrfToken($_POST['csrf_token'] ?? "")) {
+        $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
+        $messageType = "error";
+    } else {
+        $result = addManualBackorderItems(
+            $_POST['product_id'] ?? 0,
+            $_POST['size'] ?? "",
+            $_POST['quantity'] ?? 0,
+        );
+        $message = $result["success"]
+            ? ($result["added"] ?? 0) . " Position(en) zur Nachbestellung hinzugefügt."
+            : $result["message"];
+        $messageType = $result["success"] ? "success" : "error";
+
+        if ($result["success"]) {
+            logAccess("Admin added manual backorder items", [
+                "admin" => $_SESSION['admin_username'] ?? "unknown",
+                "product_id" => $_POST['product_id'] ?? 0,
+                "size" => $_POST['size'] ?? "",
+                "quantity" => $_POST['quantity'] ?? 0,
+            ]);
+        }
+    }
+}
+
+if ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['mark_ordered'])) {
+    if (!validateCsrfToken($_POST['csrf_token'] ?? "")) {
+        $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
+        $messageType = "error";
+    } else {
+        $result = markBackorderItemsOrdered(
+            $_POST['product_id'] ?? 0,
+            $_POST['size'] ?? "",
+            $_POST['quantity'] ?? 0,
+        );
+        $message = $result["success"]
+            ? ($result["updated"] ?? 0) .
+                " Position(en) als bestellt markiert."
+            : $result["message"];
+        $messageType = $result["success"] ? "success" : "error";
+
+        if ($result["success"]) {
+            logAccess("Admin marked backorder items ordered", [
+                "admin" => $_SESSION['admin_username'] ?? "unknown",
+                "product_id" => $_POST['product_id'] ?? 0,
+                "size" => $_POST['size'] ?? "",
+                "quantity" => $_POST['quantity'] ?? 0,
+            ]);
+        }
+    }
+}
+
+if ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['mark_delivered'])) {
+    if (!validateCsrfToken($_POST['csrf_token'] ?? "")) {
+        $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
+        $messageType = "error";
+    } else {
+        $result = markBackorderItemsDelivered(
+            $_POST['product_id'] ?? 0,
+            $_POST['size'] ?? "",
+            $_POST['quantity'] ?? 0,
+        );
+        $message = $result["success"]
+            ? ($result["updated"] ?? 0) .
+                " Position(en) als geliefert markiert."
+            : $result["message"];
+        $messageType = $result["success"] ? "success" : "error";
+
+        if ($result["success"]) {
+            logAccess("Admin marked backorder items delivered", [
+                "admin" => $_SESSION['admin_username'] ?? "unknown",
+                "product_id" => $_POST['product_id'] ?? 0,
+                "size" => $_POST['size'] ?? "",
+                "quantity" => $_POST['quantity'] ?? 0,
+            ]);
+        }
+    }
+}
+
+$groups = getBackorderGroups();
+$products = getProducts();
+$productSizeMap = [];
+foreach ($products as $product) {
+    $sizes = getProductSizes($product);
+    if (empty($sizes)) {
+        $sizes = ["Standard"];
+    }
+    $productSizeMap[(int) $product["id"]] = $sizes;
+}
+
+$bodyClass = "admin-page";
+include __DIR__ . "/../includes/header.php";
+?>
+
+<div class="admin-header">
+    <h2>Nachbestellungen</h2>
+    <div>
+        <a href="index.php" class="btn btn-secondary">Zurück zum Dashboard</a>
+        <a href="orders.php" class="btn btn-secondary">Bestellungen</a>
+    </div>
+</div>
+
+<?php if ($message !== ""): ?>
+    <div class="alert alert-<?php echo escape($messageType); ?>">
+        <?php echo escape($message); ?>
+    </div>
+<?php endif; ?>
+
+<p class="text-muted">
+    Artikel werden nach Produkt und Größe zusammengefasst. Aktionen bearbeiten die ältesten Bestellungen zuerst (FIFO).
+</p>
+
+<div class="admin-panel backorder-add-panel">
+    <h3>Artikel manuell hinzufügen</h3>
+    <p class="text-muted">
+        Für Bedarf ohne zugehörige Kundenbestellung (z.&nbsp;B. Lagerauffüllung).
+    </p>
+    <?php if (empty($products)): ?>
+        <p>Keine Artikel im Katalog vorhanden.</p>
+    <?php else: ?>
+        <form method="POST" class="admin-filter-form backorder-add-form">
+            <?php echo csrfField(); ?>
+            <div class="admin-filter-field">
+                <label for="manual_backorder_product">Artikel</label>
+                <select id="manual_backorder_product" name="product_id" required>
+                    <option value="">Bitte wählen</option>
+                    <?php foreach ($products as $product): ?>
+                        <option value="<?php echo (int) $product["id"]; ?>">
+                            <?php echo escape($product["name"]); ?>
+                        </option>
+                    <?php endforeach; ?>
+                </select>
+            </div>
+            <div class="admin-filter-field">
+                <label for="manual_backorder_size">Größe</label>
+                <select id="manual_backorder_size" name="size" required disabled>
+                    <option value="">Zuerst Artikel wählen</option>
+                </select>
+            </div>
+            <div class="admin-filter-field">
+                <label for="manual_backorder_quantity">Anzahl</label>
+                <input
+                    type="number"
+                    id="manual_backorder_quantity"
+                    name="quantity"
+                    min="1"
+                    max="100"
+                    value="1"
+                    required
+                    class="backorder-qty-input"
+                >
+            </div>
+            <button type="submit" name="add_manual_backorder" class="btn">
+                Zur Nachbestellung hinzufügen
+            </button>
+        </form>
+    <?php endif; ?>
+</div>
+
+<?php if (empty($groups)): ?>
+    <div class="alert alert-info">
+        <p>Keine Nachbestellungen vorhanden.</p>
+    </div>
+<?php else: ?>
+    <div class="table-responsive">
+        <table class="responsive-table">
+            <thead>
+                <tr>
+                    <th>Artikel</th>
+                    <th>Größe</th>
+                    <th>Nachzubestellen</th>
+                    <th>Wartet auf Lieferung</th>
+                    <th>Als bestellt markieren</th>
+                    <th>Lieferung eingetroffen</th>
+                </tr>
+            </thead>
+            <tbody>
+                <?php foreach ($groups as $group): ?>
+                    <tr>
+                        <td data-label="Artikel"><?php echo escape(
+                            $group["product_name"],
+                        ); ?></td>
+                        <td data-label="Größe"><?php echo $group["size"] !== ""
+                            ? escape($group["size"])
+                            : "-"; ?></td>
+                        <td data-label="Nachzubestellen">
+                            <strong><?php echo (int) $group[
+                                "to_be_backordered"
+                            ]; ?></strong>
+                        </td>
+                        <td data-label="Wartet auf Lieferung">
+                            <strong><?php echo (int) $group["ordered"]; ?></strong>
+                        </td>
+                        <td data-label="Als bestellt markieren">
+                            <?php if ($group["to_be_backordered"] > 0): ?>
+                                <form method="POST" class="backorder-action-form">
+                                    <?php echo csrfField(); ?>
+                                    <input type="hidden" name="product_id" value="<?php echo (int) $group[
+                                        "product_id"
+                                    ]; ?>">
+                                    <input type="hidden" name="size" value="<?php echo escape(
+                                        $group["size"],
+                                    ); ?>">
+                                    <label class="sr-only" for="qty_ordered_<?php echo (int) $group[
+                                        "product_id"
+                                    ]; ?>_<?php echo escape(
+    preg_replace("/[^a-z0-9]/i", "_", $group["size"]),
+); ?>">Menge</label>
+                                    <input
+                                        type="number"
+                                        id="qty_ordered_<?php echo (int) $group[
+                                            "product_id"
+                                        ]; ?>_<?php echo escape(
+    preg_replace("/[^a-z0-9]/i", "_", $group["size"]),
+); ?>"
+                                        name="quantity"
+                                        min="1"
+                                        max="<?php echo (int) $group[
+                                            "to_be_backordered"
+                                        ]; ?>"
+                                        value="1"
+                                        class="backorder-qty-input"
+                                    >
+                                    <button type="submit" name="mark_ordered" class="btn btn-small">
+                                        Als bestellt markieren
+                                    </button>
+                                </form>
+                            <?php else: ?>
+                                -
+                            <?php endif; ?>
+                        </td>
+                        <td data-label="Lieferung eingetroffen">
+                            <?php if ($group["ordered"] > 0): ?>
+                                <form method="POST" class="backorder-action-form">
+                                    <?php echo csrfField(); ?>
+                                    <input type="hidden" name="product_id" value="<?php echo (int) $group[
+                                        "product_id"
+                                    ]; ?>">
+                                    <input type="hidden" name="size" value="<?php echo escape(
+                                        $group["size"],
+                                    ); ?>">
+                                    <label class="sr-only" for="qty_delivered_<?php echo (int) $group[
+                                        "product_id"
+                                    ]; ?>_<?php echo escape(
+    preg_replace("/[^a-z0-9]/i", "_", $group["size"]),
+); ?>">Menge</label>
+                                    <input
+                                        type="number"
+                                        id="qty_delivered_<?php echo (int) $group[
+                                            "product_id"
+                                        ]; ?>_<?php echo escape(
+    preg_replace("/[^a-z0-9]/i", "_", $group["size"]),
+); ?>"
+                                        name="quantity"
+                                        min="1"
+                                        max="<?php echo (int) $group["ordered"]; ?>"
+                                        value="1"
+                                        class="backorder-qty-input"
+                                    >
+                                    <button type="submit" name="mark_delivered" class="btn btn-small btn-secondary">
+                                        Lieferung eingetroffen
+                                    </button>
+                                </form>
+                            <?php else: ?>
+                                -
+                            <?php endif; ?>
+                        </td>
+                    </tr>
+                <?php endforeach; ?>
+            </tbody>
+        </table>
+    </div>
+<?php endif; ?>
+
+<?php if (!empty($productSizeMap)): ?>
+<script>
+(function () {
+    const sizesByProduct = <?php echo json_encode(
+        $productSizeMap,
+        JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT,
+    ); ?>;
+    const productSelect = document.getElementById("manual_backorder_product");
+    const sizeSelect = document.getElementById("manual_backorder_size");
+
+    if (!productSelect || !sizeSelect) {
+        return;
+    }
+
+    function updateSizeOptions() {
+        const productId = productSelect.value;
+        const sizes = sizesByProduct[productId] || [];
+        sizeSelect.innerHTML = "";
+
+        if (sizes.length === 0) {
+            sizeSelect.disabled = true;
+            const option = document.createElement("option");
+            option.value = "";
+            option.textContent = "Zuerst Artikel wählen";
+            sizeSelect.appendChild(option);
+            return;
+        }
+
+        sizes.forEach(function (size) {
+            const option = document.createElement("option");
+            option.value = size;
+            option.textContent = size;
+            sizeSelect.appendChild(option);
+        });
+        sizeSelect.disabled = false;
+    }
+
+    productSelect.addEventListener("change", updateSizeOptions);
+})();
+</script>
+<?php endif; ?>
+
+<?php include __DIR__ . "/../includes/footer.php"; ?>

+ 119 - 28
admin/index.php

@@ -7,27 +7,25 @@ if (empty($_SESSION['admin_logged_in'])) {
     exit;
 }
 
-expirePendingOrders();
-
 $pageTitle = 'Admin Dashboard';
 $orders = getOrders();
-$products = getProducts();
-$organizations = getOrganizations(false);
+
+$backorderGroups = getBackorderGroups();
+$backorderCount = 0;
+foreach ($backorderGroups as $group) {
+    $backorderCount += (int) $group['to_be_backordered'] + (int) $group['ordered'];
+}
 
 $stats = [
-    'products' => count($products),
-    'organizations' => count($organizations),
-    'unconfirmed' => 0,
     'open' => 0,
     'partial' => 0,
     'processed' => 0,
     'cancelled' => 0,
+    'backorder' => $backorderCount,
 ];
 
 foreach ($orders as $order) {
-    if ($order['confirmation_status'] === 'pending') {
-        $stats['unconfirmed']++;
-    } elseif ($order['status'] === 'cancelled') {
+    if ($order['status'] === 'cancelled') {
         $stats['cancelled']++;
     } elseif ($order['status'] === 'processed') {
         $stats['processed']++;
@@ -38,11 +36,56 @@ foreach ($orders as $order) {
     }
 }
 
-$recentOrders = $orders;
+$recentOrders = array_values(array_filter($orders, function ($order) {
+    $label = getOrderStatusLabel($order);
+    if ($label !== 'Offen' && $label !== 'Teilweise bearbeitet') {
+        return false;
+    }
+    foreach ($order['items'] as $item) {
+        if (empty($item['is_processed'])) {
+            return true;
+        }
+    }
+    return false;
+}));
 usort($recentOrders, function ($left, $right) {
     return strcmp($right['created_at'], $left['created_at']);
 });
-$recentOrders = array_slice($recentOrders, 0, 8);
+$recentOrders = array_slice($recentOrders, 0, 5);
+
+$outstandingItems = [];
+foreach ($orders as $order) {
+    $label = getOrderStatusLabel($order);
+    if ($label !== 'Offen' && $label !== 'Teilweise bearbeitet') {
+        continue;
+    }
+    foreach ($order['items'] as $item) {
+        if (!empty($item['is_processed'])) {
+            continue;
+        }
+        $outstandingItems[] = [
+            'order_id' => $order['id'],
+            'customer_name' => $order['customer_name'],
+            'organization_label' => $order['organization_label'],
+            'created_at' => $order['created_at'],
+            'product_name' => $item['product_name'],
+            'size' => $item['size'],
+            'availability_label' => $item['availability_label'],
+            'backorder_status' => $item['backorder_status'] ?? '',
+        ];
+    }
+}
+usort($outstandingItems, function ($left, $right) {
+    $cmp = strcmp($left['created_at'], $right['created_at']);
+    if ($cmp !== 0) {
+        return $cmp;
+    }
+    $cmp = strcmp($left['order_id'], $right['order_id']);
+    if ($cmp !== 0) {
+        return $cmp;
+    }
+    return strcmp($left['product_name'], $right['product_name']);
+});
 
 $bodyClass = 'admin-page';
 include __DIR__ . '/../includes/header.php';
@@ -60,6 +103,7 @@ include __DIR__ . '/../includes/header.php';
                 <a href="organizations.php">Organisationen verwalten</a>
                 <a href="settings.php">Einstellungen</a>
                 <a href="faq.php">FAQ bearbeiten</a>
+                <a href="backorders.php">Nachbestellungen verwalten</a>
                 <a href="admins.php">Admins verwalten</a>
                 <form method="POST" action="login.php" class="inline-form">
                     <?php echo csrfField(); ?>
@@ -71,18 +115,6 @@ include __DIR__ . '/../includes/header.php';
 </div>
 
 <div class="admin-stats">
-    <div class="stat-card">
-        <h3>Produkte</h3>
-        <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>Unbestätigt</h3>
-        <div class="stat-value"><?php echo $stats['unconfirmed']; ?></div>
-    </div>
     <div class="stat-card">
         <h3>Offen</h3>
         <div class="stat-value"><?php echo $stats['open']; ?></div>
@@ -99,12 +131,16 @@ include __DIR__ . '/../includes/header.php';
         <h3>Storniert</h3>
         <div class="stat-value"><?php echo $stats['cancelled']; ?></div>
     </div>
+    <div class="stat-card">
+        <h3>Nachbestellung</h3>
+        <div class="stat-value"><?php echo $stats['backorder']; ?></div>
+    </div>
 </div>
 
-<h3 class="section-title mt-4">Letzte Bestellungen</h3>
+<h3 class="section-title mt-4">Letzte offene Bestellungen</h3>
 
 <?php if (empty($recentOrders)): ?>
-    <p>Keine Bestellungen vorhanden.</p>
+    <p>Keine offenen Bestellungen vorhanden.</p>
 <?php else: ?>
     <div class="table-responsive">
         <table class="responsive-table">
@@ -115,18 +151,73 @@ include __DIR__ . '/../includes/header.php';
                     <th>Organisation</th>
                     <th>Erstellt</th>
                     <th>Status</th>
+                    <th>Nachbestellung</th>
                     <th>Aktionen</th>
                 </tr>
             </thead>
             <tbody>
-                <?php foreach ($recentOrders as $order): ?>
+                <?php foreach ($recentOrders as $order):
+                    $backorderSummary = getOrderOpenBackorderSummary($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="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>
+                        <td data-label="Nachbestellung">
+                            <?php if ($backorderSummary['label'] !== ''): ?>
+                                <span class="status <?php echo escape($backorderSummary['class']); ?>"><?php echo escape($backorderSummary['label']); ?></span>
+                            <?php else: ?>
+                                -
+                            <?php endif; ?>
+                        </td>
+                        <td data-label="Aktionen"><a href="order.php?id=<?php echo urlencode($order['id']); ?>" class="btn btn-small">Details</a></td>
+                    </tr>
+                <?php endforeach; ?>
+            </tbody>
+        </table>
+    </div>
+<?php endif; ?>
+
+<h3 class="section-title mt-4">Offene Positionen</h3>
+
+<?php if (empty($outstandingItems)): ?>
+    <p>Keine offenen Positionen vorhanden.</p>
+<?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>Größe</th>
+                    <th>Lieferhinweis</th>
+                    <th>Nachbestellung</th>
+                    <th>Erstellt</th>
+                    <th>Aktionen</th>
+                </tr>
+            </thead>
+            <tbody>
+                <?php foreach ($outstandingItems as $row): ?>
+                    <tr>
+                        <td data-label="Bestellnummer"><strong><?php echo escape($row['order_id']); ?></strong></td>
+                        <td data-label="Name"><?php echo escape($row['customer_name']); ?></td>
+                        <td data-label="Organisation"><?php echo escape($row['organization_label']); ?></td>
+                        <td data-label="Artikel"><?php echo escape($row['product_name']); ?></td>
+                        <td data-label="Größe"><?php echo $row['size'] !== '' ? escape($row['size']) : '-'; ?></td>
+                        <td data-label="Lieferhinweis"><?php echo $row['availability_label'] !== '' ? escape($row['availability_label']) : '-'; ?></td>
+                        <td data-label="Nachbestellung">
+                            <?php if (($row['backorder_status'] ?? '') !== ''): ?>
+                                <span class="status <?php echo escape(getBackorderStatusClass($row['backorder_status'])); ?>"><?php echo escape(getBackorderStatusLabel($row['backorder_status'])); ?></span>
+                            <?php else: ?>
+                                -
+                            <?php endif; ?>
+                        </td>
+                        <td data-label="Erstellt"><?php echo escape(formatDate($row['created_at'])); ?></td>
+                        <td data-label="Aktionen"><a href="order.php?id=<?php echo urlencode($row['order_id']); ?>" class="btn btn-small">Details</a></td>
                     </tr>
                 <?php endforeach; ?>
             </tbody>

+ 30 - 0
admin/order-pdf.php

@@ -0,0 +1,30 @@
+<?php
+require_once __DIR__ . "/../config.php";
+require_once __DIR__ . "/../includes/functions.php";
+
+if (empty($_SESSION['admin_logged_in'])) {
+    header("Location: login.php");
+    exit();
+}
+
+$orderId = trim((string) ($_GET['id'] ?? ""));
+if ($orderId === "") {
+    http_response_code(400);
+    header("Content-Type: text/plain; charset=UTF-8");
+    echo "Keine Bestellnummer angegeben.";
+    exit();
+}
+
+$order = getOrderById($orderId);
+if ($order === null) {
+    http_response_code(404);
+    header("Content-Type: text/plain; charset=UTF-8");
+    echo "Bestellung nicht gefunden.";
+    exit();
+}
+
+streamOrderPdf(
+    $order,
+    "bestellung-" . strtolower($order["id"]) . ".pdf",
+    true,
+);

+ 407 - 0
admin/order.php

@@ -0,0 +1,407 @@
+<?php
+require_once __DIR__ . "/../config.php";
+require_once __DIR__ . "/../includes/functions.php";
+
+if (empty($_SESSION['admin_logged_in'])) {
+    header("Location: login.php");
+    exit();
+}
+
+$message = "";
+$messageType = "";
+
+if (
+    $_SERVER['REQUEST_METHOD'] === "POST" &&
+    isset($_POST['toggle_item_backorder'])
+) {
+    if (!validateCsrfToken($_POST['csrf_token'] ?? "")) {
+        $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
+        $messageType = "error";
+    } else {
+        $result = toggleOrderItemBackorder(
+            $_POST['order_id'] ?? "",
+            (int) ($_POST['item_index'] ?? -1),
+        );
+        $message = $result["success"]
+            ? "Nachbestellstatus wurde aktualisiert."
+            : $result["message"];
+        $messageType = $result["success"] ? "success" : "error";
+
+        if ($result["success"]) {
+            logAccess("Admin toggled order item backorder", [
+                "admin" => $_SESSION['admin_username'] ?? "unknown",
+                "order_id" => $_POST['order_id'] ?? "",
+                "item_index" => $_POST['item_index'] ?? -1,
+            ]);
+        }
+    }
+}
+
+if (
+    $_SERVER['REQUEST_METHOD'] === "POST" &&
+    isset($_POST['toggle_item_processed'])
+) {
+    if (!validateCsrfToken($_POST['csrf_token'] ?? "")) {
+        $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
+        $messageType = "error";
+    } else {
+        $result = toggleOrderItemProcessed(
+            $_POST['order_id'] ?? "",
+            (int) ($_POST['item_index'] ?? -1),
+        );
+        $message = $result["success"]
+            ? "Position wurde aktualisiert."
+            : $result["message"];
+        $messageType = $result["success"] ? "success" : "error";
+
+        if ($result["success"]) {
+            logAccess("Admin toggled order item", [
+                "admin" => $_SESSION['admin_username'] ?? "unknown",
+                "order_id" => $_POST['order_id'] ?? "",
+                "item_index" => $_POST['item_index'] ?? -1,
+            ]);
+        }
+    }
+}
+
+if ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['cancel_order'])) {
+    if (!validateCsrfToken($_POST['csrf_token'] ?? "")) {
+        $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
+        $messageType = "error";
+    } else {
+        $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";
+
+        if ($result["success"]) {
+            logAccess("Admin cancelled order", [
+                "admin" => $adminUsername,
+                "order_id" => $_POST['order_id'] ?? "",
+            ]);
+        }
+    }
+}
+
+if ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['uncancel_order'])) {
+    if (!validateCsrfToken($_POST['csrf_token'] ?? "")) {
+        $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
+        $messageType = "error";
+    } else {
+        $adminUsername = $_SESSION['admin_username'] ?? "";
+        $result = uncancelOrder($_POST['order_id'] ?? "");
+        $message = $result["success"]
+            ? "Stornierung wurde aufgehoben."
+            : $result["message"];
+        $messageType = $result["success"] ? "success" : "error";
+
+        if ($result["success"]) {
+            logAccess("Admin uncancelled order", [
+                "admin" => $adminUsername,
+                "order_id" => $_POST['order_id'] ?? "",
+            ]);
+        }
+    }
+}
+
+$orderId = trim((string) ($_GET['id'] ?? $_POST['order_id'] ?? ""));
+$order = $orderId !== "" ? getOrderById($orderId) : null;
+
+$pageTitle =
+    $order !== null
+        ? "Bestellung " . $order["id"]
+        : ($orderId !== ""
+            ? "Bestellung nicht gefunden"
+            : "Bestellung");
+
+$bodyClass = "admin-page";
+include __DIR__ . "/../includes/header.php";
+?>
+
+<div class="admin-header">
+    <h2><?php echo $order !== null
+        ? "Bestellung " . escape($order["id"])
+        : "Bestellung"; ?></h2>
+    <div class="admin-dashboard-actions">
+        <?php if ($order !== null): ?>
+            <a
+                href="order-pdf.php?id=<?php echo urlencode($order["id"]); ?>"
+                class="btn btn-secondary"
+                target="_blank"
+                rel="noopener noreferrer"
+            >Bestellung drucken</a>
+        <?php endif; ?>
+        <a href="index.php" class="btn btn-secondary">Zurück zum Dashboard</a>
+        <a href="orders.php" class="btn">Zurück zur Bestellliste</a>
+    </div>
+</div>
+
+<?php if ($message !== ""): ?>
+    <div class="alert alert-<?php echo escape($messageType); ?>">
+        <?php echo escape($message); ?>
+    </div>
+<?php endif; ?>
+
+<?php if ($order === null): ?>
+    <div class="alert alert-info">
+        <p><?php echo $orderId !== ""
+            ? "Die Bestellung wurde nicht gefunden."
+            : "Keine Bestellnummer angegeben."; ?></p>
+    </div>
+<?php else: ?>
+    <div class="panel">
+        <p><strong>Status:</strong> <span class="status <?php echo escape(
+            getOrderStatusClass($order),
+        ); ?>"><?php echo escape(
+    getOrderStatusLabel($order),
+); ?></span>
+        <?php if (orderHasBackorder($order)): ?>
+            <span class="status status-backorder">Nachbestellung</span>
+        <?php endif; ?>
+        </p>
+        <p><strong>Name:</strong> <?php echo escape(
+            $order["customer_name"],
+        ); ?></p>
+        <p><strong>E-Mail:</strong> <?php echo escape(
+            $order["customer_email"],
+        ); ?></p>
+        <p><strong>Organisation:</strong> <?php echo escape(
+            $order["organization_label"],
+        ); ?></p>
+        <p><strong>Erstellt:</strong> <?php echo escape(
+            formatDate($order["created_at"]),
+        ); ?></p>
+        <?php if ($order["admin_notified_at"] !== ""): ?>
+            <p><strong>Intern weitergeleitet:</strong> <?php echo escape(
+                formatDate($order["admin_notified_at"]),
+            ); ?></p>
+        <?php endif; ?>
+        <p><strong>Kommentar:</strong><br><?php echo $order[
+            "comment"
+        ] !== ""
+            ? nl2br(escape($order["comment"]))
+            : "Kein Kommentar"; ?></p>
+
+        <?php if ($order["status"] === "cancelled"): ?>
+            <div class="alert alert-warning">
+                <p><strong>Storniert am:</strong> <?php echo escape(
+                    formatDate($order["cancelled_at"]),
+                ); ?></p>
+                <p><strong>Storniert durch:</strong> <?php echo escape(
+                    $order["cancelled_by"],
+                ); ?></p>
+                <p><strong>Stornogrund:</strong><br><?php echo $order[
+                    "cancellation_reason"
+                ] !== ""
+                    ? nl2br(escape($order["cancellation_reason"]))
+                    : "Kein Grund angegeben"; ?></p>
+            </div>
+            <form
+                method="POST"
+                class="inline-form"
+                onsubmit="return confirm('Stornierung wirklich aufheben? Die Bestellung kann danach wieder bearbeitet werden.');"
+            >
+                <?php echo csrfField(); ?>
+                <input type="hidden" name="order_id" value="<?php echo escape(
+                    $order["id"],
+                ); ?>">
+                <button type="submit" name="uncancel_order" class="btn btn-small">
+                    Stornierung aufheben
+                </button>
+            </form>
+        <?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>Nachbestellung</th>
+                        <th>Aktion</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    <?php foreach ($order["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="Nachbestellung">
+                                <?php
+                                $backorderStatus = (string) ($item["backorder_status"] ?? "");
+                                if ($backorderStatus !== ""): ?>
+                                    <span class="status <?php echo escape(
+                                        getBackorderStatusClass($backorderStatus),
+                                    ); ?>"><?php echo escape(
+    getBackorderStatusLabel($backorderStatus),
+); ?></span>
+                                <?php else: ?>
+                                    -
+                                <?php endif; ?>
+                            </td>
+                            <td data-label="Aktionen">
+                                <?php if ($order["status"] !== "cancelled"): ?>
+                                    <form method="POST" class="inline-form">
+                                        <?php echo csrfField(); ?>
+                                        <input type="hidden" name="order_id" value="<?php echo escape(
+                                            $order["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
+                                    $canToggleBackorder =
+                                        $backorderStatus === "to_be_backordered" ||
+                                        ($backorderStatus === "" &&
+                                            empty($item["is_processed"]));
+                                    if ($canToggleBackorder): ?>
+                                    <form method="POST" class="inline-form">
+                                        <?php echo csrfField(); ?>
+                                        <input type="hidden" name="order_id" value="<?php echo escape(
+                                            $order["id"],
+                                        ); ?>">
+                                        <input type="hidden" name="item_index" value="<?php echo (int) $index; ?>">
+                                        <button type="submit" name="toggle_item_backorder" class="btn btn-small btn-secondary">
+                                            <?php echo $backorderStatus === "to_be_backordered"
+                                                ? "Nachbestellung aufheben"
+                                                : "Als Nachbestellung markieren"; ?>
+                                        </button>
+                                    </form>
+                                    <?php endif; ?>
+                                <?php else: ?>
+                                    -
+                                <?php endif; ?>
+                            </td>
+                        </tr>
+                    <?php endforeach; ?>
+                </tbody>
+            </table>
+        </div>
+
+        <?php if (
+            $order["status"] !== "cancelled" &&
+            $order["status"] !== "processed"
+        ): ?>
+            <button
+                type="button"
+                class="btn btn-secondary btn-small"
+                id="cancel-order-open"
+            >
+                Bestellung stornieren
+            </button>
+
+            <div
+                id="cancel-order-modal"
+                class="modal"
+                role="dialog"
+                aria-labelledby="cancel-order-title"
+                aria-hidden="true"
+            >
+                <div class="modal-content modal-content-compact">
+                    <button
+                        type="button"
+                        class="modal-close btn btn-secondary btn-small"
+                        id="cancel-order-close"
+                        aria-label="Schließen"
+                    >
+                        &times;
+                    </button>
+                    <h4 id="cancel-order-title">Bestellung stornieren</h4>
+                    <form method="POST" id="cancel-order-form">
+                        <?php echo csrfField(); ?>
+                        <input type="hidden" name="order_id" value="<?php echo escape(
+                            $order["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">
+                            Stornierung bestätigen
+                        </button>
+                    </form>
+                </div>
+            </div>
+            <script>
+            (function () {
+                const modal = document.getElementById("cancel-order-modal");
+                const openBtn = document.getElementById("cancel-order-open");
+                const closeBtn = document.getElementById("cancel-order-close");
+                if (!modal || !openBtn || !closeBtn) {
+                    return;
+                }
+
+                function openModal() {
+                    modal.classList.add("is-open");
+                    modal.setAttribute("aria-hidden", "false");
+                    const reason = document.getElementById("cancellation_reason");
+                    if (reason) {
+                        reason.focus();
+                    }
+                }
+
+                function closeModal() {
+                    modal.classList.remove("is-open");
+                    modal.setAttribute("aria-hidden", "true");
+                }
+
+                openBtn.addEventListener("click", openModal);
+                closeBtn.addEventListener("click", closeModal);
+                modal.addEventListener("click", function (event) {
+                    if (event.target === modal) {
+                        closeModal();
+                    }
+                });
+                document.addEventListener("keydown", function (event) {
+                    if (event.key === "Escape" && modal.classList.contains("is-open")) {
+                        closeModal();
+                    }
+                });
+            })();
+            </script>
+        <?php endif; ?>
+    </div>
+<?php endif; ?>
+
+<?php include __DIR__ . "/../includes/footer.php"; ?>

+ 9 - 227
admin/orders.php

@@ -7,65 +7,15 @@ if (empty($_SESSION['admin_logged_in'])) {
     exit();
 }
 
-expirePendingOrders();
-
-$pageTitle = "Bestellungen";
-$message = "";
-$messageType = "";
-
-if (
-    $_SERVER['REQUEST_METHOD'] === "POST" &&
-    isset($_POST['toggle_item_processed'])
-) {
-    // Validate CSRF token
-    if (!validateCsrfToken($_POST['csrf_token'] ?? "")) {
-        $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
-        $messageType = "error";
-    } else {
-        $result = toggleOrderItemProcessed(
-            $_POST['order_id'] ?? "",
-            (int) ($_POST['item_index'] ?? -1),
-        );
-        $message = $result["success"]
-            ? "Position wurde aktualisiert."
-            : $result["message"];
-        $messageType = $result["success"] ? "success" : "error";
-
-        if ($result["success"]) {
-            logAccess("Admin toggled order item", [
-                "admin" => $_SESSION['admin_username'] ?? "unknown",
-                "order_id" => $_POST['order_id'] ?? "",
-                "item_index" => $_POST['item_index'] ?? -1,
-            ]);
-        }
-    }
+if (isset($_GET['details']) && trim((string) $_GET['details']) !== "") {
+    header(
+        "Location: order.php?id=" .
+            urlencode(trim((string) $_GET['details'])),
+    );
+    exit();
 }
 
-if ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['cancel_order'])) {
-    // Validate CSRF token
-    if (!validateCsrfToken($_POST['csrf_token'] ?? "")) {
-        $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
-        $messageType = "error";
-    } else {
-        $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";
-
-        if ($result["success"]) {
-            logAccess("Admin cancelled order", [
-                "admin" => $adminUsername,
-                "order_id" => $_POST['order_id'] ?? "",
-            ]);
-        }
-    }
-}
+$pageTitle = "Bestellungen";
 
 $orders = getOrders();
 usort($orders, function ($left, $right) {
@@ -74,7 +24,6 @@ usort($orders, function ($left, $right) {
 
 $filter = trim((string) ($_GET['filter'] ?? "all"));
 $searchOrderId = trim((string) ($_GET['order_id'] ?? ""));
-$selectedOrderId = trim((string) ($_GET['details'] ?? $searchOrderId));
 
 if ($searchOrderId !== "") {
     $orders = array_values(
@@ -88,13 +37,8 @@ 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";
+                    return $order["status"] === "open";
                 case "partial":
                     return $order["status"] === "partial";
                 case "processed":
@@ -107,9 +51,6 @@ if ($filter !== "all") {
     );
 }
 
-$selectedOrder =
-    $selectedOrderId !== "" ? getOrderById($selectedOrderId) : null;
-
 $bodyClass = "admin-page";
 include __DIR__ . "/../includes/header.php";
 ?>
@@ -121,12 +62,6 @@ include __DIR__ . "/../includes/header.php";
     </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" class="admin-filter-form">
         <div class="admin-filter-field admin-filter-field-wide">
@@ -141,12 +76,6 @@ include __DIR__ . "/../includes/header.php";
                 <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>
@@ -210,7 +139,7 @@ include __DIR__ . "/../includes/header.php";
     getOrderStatusLabel($order),
 ); ?></span></td>
                         <td data-label="Aktionen">
-                            <a href="orders.php?details=<?php echo urlencode(
+                            <a href="order.php?id=<?php echo urlencode(
                                 $order["id"],
                             ); ?>" class="btn btn-small">Details</a>
                         </td>
@@ -221,151 +150,4 @@ include __DIR__ . "/../includes/header.php";
     </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="Aktionen">
-                                <?php if (
-                                    $selectedOrder["status"] !== "cancelled" &&
-                                    $selectedOrder["confirmation_status"] !==
-                                        "pending" &&
-                                    $selectedOrder["confirmation_status"] !==
-                                        "expired"
-                                ): ?>
-                                     <form method="POST">
-                                        <?php echo csrfField(); ?>
-                                        <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?');">
-                <?php echo csrfField(); ?>
-                <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"; ?>

+ 0 - 23
admin/settings.php

@@ -19,11 +19,6 @@ if ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['save_settings'])) {
     } else {
         $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'],
             ),
@@ -69,24 +64,6 @@ include __DIR__ . "/../includes/header.php";
             ); ?>">
         </div>
 
-        <div class="form-group">
-            <label class="checkbox-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 class="checkbox-label">
                 <input type="checkbox" name="attach_order_pdf_to_admin_email" value="1" <?php echo !empty(

+ 60 - 12
assets/css/style.css

@@ -730,30 +730,30 @@ body.admin-page .container {
     border-color: rgba(25, 107, 59, 0.2);
 }
 
-.status-expired {
-    color: var(--brand-danger);
-    background: #fff3f1;
-    border-color: rgba(180, 35, 24, 0.2);
-}
-
 .status-hidden {
     color: #9ca3af;
     border-color: #6b7280;
     background: #f1f3f5;
 }
 
-.status-unconfirmed {
-    color: var(--brand-primary);
-    background: #fff0a3;
-    border-color: #e0c038;
-}
-
 .status-partial {
     color: #7c6517;
     background: #f9efcb;
     border-color: #e2ce7f;
 }
 
+.status-backorder {
+    color: #8b4513;
+    background: #fde8d8;
+    border-color: #e8b896;
+}
+
+.status-backorder-waiting {
+    color: #5c4d8a;
+    background: #ebe6f7;
+    border-color: #c4b8e8;
+}
+
 .status-cancelled {
     color: var(--brand-danger);
     background: #fff3f1;
@@ -784,6 +784,15 @@ body.admin-page .container {
     justify-content: center;
 }
 
+.modal.is-open {
+    display: flex;
+}
+
+.modal-content-compact {
+    max-width: 32rem;
+    width: calc(100% - 2rem);
+}
+
 .modal-content {
     background: var(--brand-surface);
     color: var(--brand-text);
@@ -893,6 +902,45 @@ body.admin-page .container {
     display: inline;
 }
 
+.backorder-action-form {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 0.5rem;
+    align-items: center;
+}
+
+.backorder-qty-input {
+    width: 4rem;
+}
+
+.backorder-add-panel {
+    margin-bottom: 1.5rem;
+    padding: 1rem 1.25rem;
+    border: 1px solid var(--border-color, #ddd);
+    border-radius: 4px;
+    background: var(--surface-muted, #f8f9fa);
+}
+
+.backorder-add-panel h3 {
+    margin-top: 0;
+}
+
+.backorder-add-form {
+    margin-top: 1rem;
+}
+
+.sr-only {
+    position: absolute;
+    width: 1px;
+    height: 1px;
+    padding: 0;
+    margin: -1px;
+    overflow: hidden;
+    clip: rect(0, 0, 0, 0);
+    white-space: nowrap;
+    border: 0;
+}
+
 .admin-filter-form {
     display: flex;
     gap: 1rem;

+ 1 - 5
checkout.php

@@ -158,11 +158,7 @@ $_POST['organization_id'] === $organization["id"]
             </div>
 
             <div class="alert alert-info">
-                <?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 an die Gerätewarte weitergeleitet.
-                <?php endif; ?>
+                Nach dem Absenden wird die Bestellung direkt an die Gerätewarte weitergeleitet.
             </div>
 
             <?php echo csrfField(); ?>

+ 1 - 2
config.sample.php

@@ -32,8 +32,6 @@ define('DISCLAIMER_LINES', [
 // 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
@@ -51,6 +49,7 @@ 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');
+define('MANUAL_BACKORDERS_FILE', DATA_DIR . 'manual_backorders.json');
 define('UPLOADS_URL', SITE_URL . '/data/uploads');
 
 // Session settings

+ 3 - 0
data/manual_backorders.json

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

+ 201 - 8
data/orders.json

@@ -13,14 +13,20 @@
                     "product_name": "Beil",
                     "size": "Universalgröße",
                     "availability_label": "",
-                    "is_processed": true
+                    "is_processed": true,
+                    "backorder_status": "",
+                    "backordered_at": "",
+                    "ordered_at": ""
                 },
                 {
                     "product_id": 17,
                     "product_name": "Allwetterjacke",
                     "size": "M",
                     "availability_label": "",
-                    "is_processed": false
+                    "is_processed": false,
+                    "backorder_status": "",
+                    "backordered_at": "",
+                    "ordered_at": ""
                 }
             ],
             "status": "cancelled",
@@ -48,14 +54,20 @@
                     "product_name": "Fleece",
                     "size": "M",
                     "availability_label": "",
-                    "is_processed": true
+                    "is_processed": true,
+                    "backorder_status": "",
+                    "backordered_at": "",
+                    "ordered_at": ""
                 },
                 {
                     "product_id": 5,
                     "product_name": "Handschuhe AT",
                     "size": "9",
                     "availability_label": "",
-                    "is_processed": true
+                    "is_processed": true,
+                    "backorder_status": "",
+                    "backordered_at": "",
+                    "ordered_at": ""
                 }
             ],
             "status": "processed",
@@ -83,27 +95,208 @@
                     "product_name": "Beil",
                     "size": "Universalgröße",
                     "availability_label": "",
-                    "is_processed": false
+                    "is_processed": true,
+                    "backorder_status": "",
+                    "backordered_at": "",
+                    "ordered_at": ""
                 },
                 {
                     "product_id": 4,
                     "product_name": "Fleece",
                     "size": "M",
                     "availability_label": "",
-                    "is_processed": false
+                    "is_processed": true,
+                    "backorder_status": "",
+                    "backordered_at": "",
+                    "ordered_at": ""
                 }
             ],
-            "status": "open",
+            "status": "processed",
             "confirmation_status": "not_required",
             "confirmation_token": "",
             "confirmation_expires_at": "",
             "confirmed_at": "2026-05-11 22:17:07",
             "created_at": "2026-05-11 22:17:07",
-            "updated_at": "2026-05-11 22:17:07",
+            "updated_at": "2026-05-30 10:53:57",
             "cancelled_at": "",
             "cancelled_by": "",
             "cancellation_reason": "",
             "admin_notified_at": ""
+        },
+        {
+            "id": "FWFS-2026-004",
+            "customer_name": "ihdfbvhdf",
+            "customer_email": "1234@mailpit.medowar.de",
+            "organization_id": "feuerwehr-freising",
+            "organization_label": "Feuerwehr Freising TEST",
+            "comment": "",
+            "items": [
+                {
+                    "product_id": 5,
+                    "product_name": "Handschuhe AT",
+                    "size": "9",
+                    "availability_label": "",
+                    "is_processed": true,
+                    "backorder_status": "",
+                    "backordered_at": "",
+                    "ordered_at": ""
+                },
+                {
+                    "product_id": 10,
+                    "product_name": "Beil",
+                    "size": "Universalgröße",
+                    "availability_label": "",
+                    "is_processed": true,
+                    "backorder_status": "",
+                    "backordered_at": "",
+                    "ordered_at": ""
+                },
+                {
+                    "product_id": 4,
+                    "product_name": "Fleece",
+                    "size": "M",
+                    "availability_label": "",
+                    "is_processed": false,
+                    "backorder_status": "",
+                    "backordered_at": "2026-05-30 11:08:23",
+                    "ordered_at": ""
+                }
+            ],
+            "status": "partial",
+            "confirmation_status": "not_required",
+            "confirmation_token": "",
+            "confirmation_expires_at": "",
+            "confirmed_at": "2026-05-30 09:58:30",
+            "created_at": "2026-05-30 09:58:30",
+            "updated_at": "2026-05-30 11:10:17",
+            "cancelled_at": "",
+            "cancelled_by": "",
+            "cancellation_reason": "",
+            "admin_notified_at": ""
+        },
+        {
+            "id": "FS-2026-001",
+            "customer_name": "Backorder test",
+            "customer_email": "1235@mailpit.medowar.de",
+            "organization_id": "feuerwehr-freising",
+            "organization_label": "Feuerwehr Freising TEST",
+            "comment": "",
+            "items": [
+                {
+                    "product_id": 3,
+                    "product_name": "Jacke THL",
+                    "size": "L",
+                    "availability_label": "",
+                    "is_processed": false,
+                    "backorder_status": "ordered",
+                    "backordered_at": "2026-05-30 10:59:59",
+                    "ordered_at": "2026-05-30 11:10:19"
+                }
+            ],
+            "status": "open",
+            "confirmation_status": "not_required",
+            "confirmation_token": "",
+            "confirmation_expires_at": "",
+            "confirmed_at": "2026-05-30 10:11:13",
+            "created_at": "2026-05-30 10:11:13",
+            "updated_at": "2026-05-30 11:10:19",
+            "cancelled_at": "",
+            "cancelled_by": "",
+            "cancellation_reason": "",
+            "admin_notified_at": ""
+        },
+        {
+            "id": "FS-2026-002",
+            "customer_name": "Backorder test2",
+            "customer_email": "1235@mailpit.medowar.de",
+            "organization_id": "feuerwehr-freising",
+            "organization_label": "Feuerwehr Freising TEST",
+            "comment": "",
+            "items": [
+                {
+                    "product_id": 27,
+                    "product_name": "Krawatte",
+                    "size": "Universalgröße",
+                    "availability_label": "",
+                    "is_processed": false,
+                    "backorder_status": "",
+                    "backordered_at": "2026-05-30 11:10:05",
+                    "ordered_at": ""
+                }
+            ],
+            "status": "open",
+            "confirmation_status": "not_required",
+            "confirmation_token": "",
+            "confirmation_expires_at": "",
+            "confirmed_at": "2026-05-30 10:28:37",
+            "created_at": "2026-05-30 10:28:37",
+            "updated_at": "2026-05-30 11:10:21",
+            "cancelled_at": "",
+            "cancelled_by": "",
+            "cancellation_reason": "",
+            "admin_notified_at": "2026-05-30 10:28:37"
+        },
+        {
+            "id": "FS-2026-003",
+            "customer_name": "bsudfzb",
+            "customer_email": "bisdfb@mailpit.medowar.de",
+            "organization_id": "feuerwehr-freising",
+            "organization_label": "Feuerwehr Freising TEST",
+            "comment": "",
+            "items": [
+                {
+                    "product_id": 27,
+                    "product_name": "Krawatte",
+                    "size": "Universalgröße",
+                    "availability_label": "",
+                    "is_processed": false,
+                    "backorder_status": "ordered",
+                    "backordered_at": "2026-05-30 11:00:04",
+                    "ordered_at": "2026-05-30 11:10:21"
+                }
+            ],
+            "status": "open",
+            "confirmation_status": "not_required",
+            "confirmation_token": "",
+            "confirmation_expires_at": "",
+            "confirmed_at": "2026-05-30 10:44:49",
+            "created_at": "2026-05-30 10:44:49",
+            "updated_at": "2026-05-30 11:10:21",
+            "cancelled_at": "",
+            "cancelled_by": "",
+            "cancellation_reason": "",
+            "admin_notified_at": "2026-05-30 10:44:49"
+        },
+        {
+            "id": "FS-2026-004",
+            "customer_name": "asdasd",
+            "customer_email": "asd@mailpit.medowar.de",
+            "organization_id": "feuerwehr-freising",
+            "organization_label": "Feuerwehr Freising TEST",
+            "comment": "",
+            "items": [
+                {
+                    "product_id": 7,
+                    "product_name": "Hose AT",
+                    "size": "M",
+                    "availability_label": "",
+                    "is_processed": false,
+                    "backorder_status": "",
+                    "backordered_at": "",
+                    "ordered_at": ""
+                }
+            ],
+            "status": "open",
+            "confirmation_status": "not_required",
+            "confirmation_token": "",
+            "confirmation_expires_at": "",
+            "confirmed_at": "2026-05-30 11:11:13",
+            "created_at": "2026-05-30 11:11:13",
+            "updated_at": "2026-05-30 11:11:13",
+            "cancelled_at": "",
+            "cancelled_by": "",
+            "cancellation_reason": "",
+            "admin_notified_at": "2026-05-30 11:11:13"
         }
     ]
 }

+ 0 - 2
data/settings.json

@@ -1,8 +1,6 @@
 {
     "settings": {
         "order_recipient_email": "bestellungen@mailpit.medowar.de",
-        "order_confirmation_required": false,
-        "order_confirmation_expiry_days": 7,
         "attach_order_pdf_to_admin_email": true,
         "startpage_intro_text": "Dieses System dient der internen Bestellung persönlicher Schutzausrüstung der Stadt Freising.\r\nDie Bearbeitung erfolgt durch die Gerätewarte der Feuerwehr Freising."
     }

+ 275 - 0
docs/ADMIN_BUSINESS_LOGIC.md

@@ -0,0 +1,275 @@
+# Admin Business Logic — Leitfaden für Operatoren
+
+Dieses Dokument beschreibt die **Geschäftslogik der Bestellverwaltung** im Admin-Bereich. Es richtet sich an Operatoren, die Bestellungen im Alltag bearbeiten: Welche Status es gibt, was sie bedeuten, wo Bestellungen und Positionen angezeigt werden, und welche Aktionen wo möglich sind.
+
+Technische Details zum Kundenprozess (Checkout) stehen in [ORDER_PROCESS.md](ORDER_PROCESS.md). Login und Admin-Konten: [ADMIN_SYSTEM.md](ADMIN_SYSTEM.md).
+
+---
+
+## Admin-Seiten im Überblick
+
+Vier Seiten sind für die Bestellbearbeitung relevant:
+
+| Seite | Datei | Rolle für Operatoren |
+| --- | --- | --- |
+| Dashboard | `admin/index.php` | Tagesübersicht, offene Arbeit, Statistik |
+| Bestellliste | `admin/orders.php` | Suchen, filtern, alle Bestellungen finden |
+| Bestelldetail | `admin/order.php` | Positionen bearbeiten, stornieren |
+| Nachbestellungen | `admin/backorders.php` | Externe Bestellung und Lieferung verwalten |
+
+Weitere Admin-Seiten (Produkte, Kategorien, Organisationen, FAQ, Einstellungen, Admins) haben **keinen Bezug** zu Bestellstatus oder -anzeige.
+
+---
+
+## Zwei unabhängige Status-Dimensionen
+
+Eine Bestellung hat **keinen einzelnen Status**, sondern mehrere Felder, die unabhängig voneinander wirken:
+
+```mermaid
+flowchart TB
+    subgraph orderLevel [Bestellung]
+        opStatus["status"]
+    end
+    subgraph lineLevel [Position]
+        backorder["backorder_status"]
+        processed["is_processed"]
+    end
+    backorder --> processed
+```
+
+- **Bestellung — Bearbeitung** (`status`): Wie weit ist die Abarbeitung insgesamt? Wird automatisch aus den Positionen berechnet (`open`, `partial`, `processed`) oder manuell gesetzt (`cancelled`).
+- **Position — Nachbestellung** (`backorder_status`): Liegt der Artikel auf Lager oder muss extern nachbestellt werden?
+- **Position — Bearbeitet** (`is_processed`): Wurde diese Position ausgegeben/abarbeitet?
+
+In Tabellen und Listen erscheint **ein Anzeige-Label** (z. B. „Offen" oder „Teilweise bearbeitet"). Die Nachbestellung wird auf Positionsebene geführt und erscheint zusätzlich als Badge „Nachbestellung" auf der Detailseite, wenn mindestens eine Position einen Nachbestell-Status hat.
+
+---
+
+## Bestellstatus — was Operatoren sehen
+
+### Anzeige-Label in Listen und Dashboard
+
+Das sichtbare Label folgt einer **festen Priorität**. Höhere Priorität überschreibt niedrigere:
+
+| Anzeige-Label | Bedingung | Bedeutung für Operatoren |
+| --- | --- | --- |
+| **Storniert** | Bestellung storniert | Bestellung ist ungültig; nur „Stornierung aufheben" möglich |
+| **Bearbeitet** | Alle Positionen bearbeitet | Bestellung abgeschlossen |
+| **Teilweise bearbeitet** | Mindestens eine, nicht alle Positionen bearbeitet | Bestellung in Arbeit |
+| **Offen** | Sonst | Noch keine Position bearbeitet |
+
+Auf der Detailseite ist ggf. sichtbar: Zeitpunkt der internen Weiterleitung (`admin_notified_at`).
+
+### Interner Bearbeitungsstatus der Bestellung
+
+| Wert | Bedeutung | Wie gesetzt |
+| --- | --- | --- |
+| `open` | Keine Position bearbeitet | Automatisch |
+| `partial` | Einige Positionen bearbeitet | Automatisch |
+| `processed` | Alle Positionen bearbeitet | Automatisch |
+| `cancelled` | Bestellung storniert | Manuell durch Operator |
+
+Stornierte Bestellungen werden **nicht** automatisch aus Positionen neu berechnet. Nach „Stornierung aufheben" wird der Status wieder aus den Positionen abgeleitet.
+
+---
+
+## Positionsstatus — Bearbeitung und Nachbestellung
+
+### Bearbeitet (`is_processed`)
+
+- Pro Position auf der **Bestelldetail**-Seite umschaltbar: „Als bearbeitet markieren" / „Als offen markieren".
+- Steuert den Bestellstatus automatisch: keine Position bearbeitet → Offen; alle → Bearbeitet; dazwischen → Teilweise bearbeitet.
+
+### Nachbestellung (`backorder_status`)
+
+| Wert | Anzeige-Label | Bedeutung |
+| --- | --- | --- |
+| *(leer)* | — | Normale offene Position (Lager/Abgabe) |
+| `to_be_backordered` | Nachzubestellen | Position muss extern nachbestellt werden |
+| `ordered` | Wartet auf Lieferung | Extern bestellt, Lieferung steht aus |
+
+**Manuelle Nachbestellungen** (`data/manual_backorders.json`): Gleicher Ablauf wie bei Bestellpositionen, aber **ohne Kundenbestellung** — nur auf der Seite Nachbestellungen sichtbar und verwaltbar.
+
+---
+
+## Wo erscheint was?
+
+### Dashboard (`admin/index.php`)
+
+#### Statistik-Karten
+
+| Karte | Was gezählt wird |
+| --- | --- |
+| **Offen** | Bestellungen mit operativem Status `open` |
+| **Teilweise bearbeitet** | `status = partial` |
+| **Bearbeitet** | `status = processed` |
+| **Storniert** | `status = cancelled` |
+| **Nachbestellung** | Summe aller Positionen in `to_be_backordered` oder `ordered` (inkl. manueller Nachbestellungen) |
+
+#### „Letzte offene Bestellungen" (max. 5)
+
+Eine Bestellung erscheint hier nur, wenn:
+
+1. Das Anzeige-Label **Offen** oder **Teilweise bearbeitet** ist (nicht Bearbeitet, nicht Storniert), **und**
+2. Mindestens **eine Position** existiert, die **nicht** bearbeitet ist (unabhängig vom Nachbestell-Status).
+
+Spalte **Nachbestellung:** Zusammenfassung der Nachbestell-Status aller **offenen** Positionen (`-`, ein Status-Badge, oder **Gemischt**).
+
+Sortierung: neueste zuerst.
+
+#### „Offene Positionen"
+
+Eine Position erscheint hier nur, wenn:
+
+1. Die zugehörige Bestellung das Label **Offen** oder **Teilweise bearbeitet** hat, **und**
+2. Die Position **nicht** bearbeitet ist (auch mit Nachbestell-Status).
+
+Spalte **Nachbestellung:** pro Zeile **Nachzubestellen**, **Wartet auf Lieferung** oder `-`.
+
+Sortierung: **älteste zuerst** (FIFO-Arbeitsliste).
+
+Bulk-Aktionen für Nachbestellungen (als bestellt markieren, Lieferung eingetroffen) bleiben auf **Nachbestellungen**; das Dashboard dient der Übersicht und dem Sprung zur Bestelldetailseite.
+
+---
+
+### Bestellliste (`admin/orders.php`)
+
+Filter und Suchfeld (Bestellnummer):
+
+| Filter | Was angezeigt wird |
+| --- | --- |
+| **Alle** | Alle Bestellungen |
+| **Offen** | `status = open` |
+| **Teilweise bearbeitet** | `status = partial` |
+| **Bearbeitet** | `status = processed` |
+| **Storniert** | `status = cancelled` |
+
+Die Status-Spalte zeigt das **Anzeige-Label** aus dem operativen Bestellstatus.
+
+---
+
+### Bestelldetail (`admin/order.php`)
+
+Jede Bestellung ist immer über die Bestellliste oder Dashboard-Links erreichbar — unabhängig vom Status.
+
+| Aktion | Wann sichtbar / erlaubt |
+| --- | --- |
+| **Als bearbeitet / offen markieren** | Bestellung nicht storniert |
+| **Als Nachbestellung markieren / aufheben** | Bestellung nicht storniert; nur Wechsel zwischen *(leer)* und **Nachzubestellen** — **nicht** von **Wartet auf Lieferung** |
+| **Bestellung stornieren** | Nicht storniert; nicht vollständig Bearbeitet; löscht auf allen Positionen Nachbestell-Status und zugehörige Zeitstempel |
+| **Stornierung aufheben** | Bestellung storniert |
+
+Zusätzliches Badge **„Nachbestellung"**, wenn mindestens eine Position einen Nachbestell-Status hat (nicht bei stornierten Bestellungen, sofern beim Stornieren zurückgesetzt).
+
+**Bearbeitet + Nachbestellung:** Eine Position kann als **bearbeitet** markiert sein und gleichzeitig einen Nachbestell-Status behalten (z. B. Retoure deckt die Bestellung ab, während die Nachbestellung auf Lieferantenware wartet). „Als bearbeitet markieren" löscht den Nachbestell-Status **nicht**. Umgekehrt: **Nachzubestellen** kann nicht auf bereits bearbeitete Positionen gesetzt werden.
+
+Positionen im Status **Wartet auf Lieferung** (`ordered`): Kein Nachbestell-Toggle auf der Detailseite — Weiterbearbeitung nur über **Nachbestellungen** („Lieferung eingetroffen").
+
+---
+
+### Nachbestellungen (`admin/backorders.php`)
+
+#### Was erscheint in der Liste?
+
+- Alle **manuellen** Nachbestell-Einträge mit Status Nachzubestellen oder Wartet auf Lieferung.
+- Bestellpositionen mit Nachbestell-Status, **ausgenommen** stornierte Bestellungen.
+
+Positionen werden nach **Produkt und Größe** gruppiert. Spalten **Nachzubestellen** und **Wartet auf Lieferung** werden getrennt gezählt.
+
+#### Bulk-Aktionen (FIFO — älteste zuerst)
+
+Kandidaten und Anzeige-Reihenfolge nutzen dieselbe Sortierung nach `sort_at` (u. a. `backordered_at` / `ordered_at`, sonst Bestelldatum).
+
+| Aktion | Wirkung |
+| --- | --- |
+| **Als bestellt markieren** | Verschiebt N Positionen von **Nachzubestellen** → **Wartet auf Lieferung** |
+| **Lieferung eingetroffen** | Setzt N Positionen von **Wartet auf Lieferung** → *(leer)*; Position bleibt offen zur normalen Abarbeitung auf der Detailseite |
+
+Manuelle Nachbestellungen können über ein Formular auf derselben Seite hinzugefügt werden.
+
+---
+
+## Sichtbarkeits-Matrix
+
+Übersicht: Wo erscheint ein Zustand?
+
+| Zustand / Bedingung | Dashboard-Statistik | Letzte offene Bestellungen | Offene Positionen | Bestellliste-Filter | Bestelldetail | Nachbestellungen |
+| --- | --- | --- | --- | --- | --- | --- |
+| **Offen** | Offen | Ja*, wenn offene Positionen ohne NB | Ja*, pro Position | Offen, Alle | Ja, bearbeitbar | Ja, wenn NB markiert |
+| **Teilweise bearbeitet** | Teilweise | Ja*, wenn offene Positionen ohne NB | Ja*, pro Position | Teilweise, Alle | Ja, bearbeitbar | Ja, wenn NB markiert |
+| **Bearbeitet** | Bearbeitet | — | — | Bearbeitet, Alle | Ja (nur Ansehen) | Ja, wenn NB markiert |
+| **Storniert** | Storniert | — | — | Storniert, Alle | Ja (Stornierung aufheben) | — |
+| Position **Nachzubestellen** | In NB-Statistik | Position ausgeblendet | Position ausgeblendet | Bestellung in Liste | Toggle auf Detail | Ja |
+| Position **Wartet auf Lieferung** | In NB-Statistik | Position ausgeblendet | Position ausgeblendet | Bestellung in Liste | Kein Toggle auf Detail | Ja |
+| Nur NB-Positionen, Bestellung operativ **Offen** | Offen | — (keine qualif. Position) | — | Alle, Offen | Ja | Ja |
+| Manuelle Nachbestellung | In NB-Statistik | — | — | — | — | Ja |
+
+*NB = Nachbestell-Status. „Ja*" = nur wenn mindestens eine unverarbeitete Position **ohne** Nachbestell-Status existiert.
+
+---
+
+## Typische Operator-Workflows
+
+### 1. Neue Bestellung abarbeiten
+
+1. **Dashboard** → „Offene Positionen" oder „Letzte offene Bestellungen" prüfen.
+2. **Details** öffnen.
+3. Pro Position **„Als bearbeitet markieren"**, sobald ausgegeben.
+4. Wenn alle Positionen bearbeitet: Status wechselt automatisch zu **Bearbeitet**.
+
+### 2. Artikel nicht auf Lager (Nachbestellung)
+
+1. **Bestelldetail** → Position **„Als Nachbestellung markieren"** (Status: Nachzubestellen).
+2. Position verschwindet aus Dashboard-Listen.
+3. **Nachbestellungen** → gruppiert nach Produkt/Größe → **„Als bestellt markieren"** (extern bestellt).
+4. Nach Lieferung: **„Lieferung eingetroffen"** → Position wieder normal offen.
+5. Zurück auf **Bestelldetail** → **„Als bearbeitet markieren"**.
+
+```mermaid
+stateDiagram-v2
+    direction LR
+    state backorder {
+        none --> toBeBackordered: Detail markieren
+        toBeBackordered --> ordered: Nachbestellungen bestellt
+        ordered --> none: Lieferung eingetroffen
+        toBeBackordered --> none: Detail aufheben
+    }
+```
+
+### 3. Manuelle Nachbestellung (ohne Kundenbestellung)
+
+1. **Nachbestellungen** → Formular „Manuelle Nachbestellung".
+2. Produkt, Größe, Anzahl wählen.
+3. Weiter wie Workflow 2 (bestellt → geliefert).
+
+### 4. Bestellung stornieren
+
+1. **Bestelldetail** → **„Bestellung stornieren"** (nicht möglich bei Status Bearbeitet).
+2. Optional Stornogrund angeben.
+3. Stornierte Bestellungen: Filter **Storniert** in der Bestellliste; **Stornierung aufheben** auf der Detailseite.
+
+---
+
+## Einstellungen mit Einfluss auf neue Bestellungen
+
+Unter **Einstellungen** (`admin/settings.php`):
+
+- **Empfängeradresse für interne Bestellmails**
+- **PDF an interne Bestell-E-Mails anhängen**
+
+---
+
+## Bekannte Unstimmigkeiten
+
+Die folgenden Punkte sind **keine Bedienanleitung**, sondern dokumentierte bewusste Einschränkungen.
+
+1. **Status „Wartet auf Lieferung" nicht auf Detailseite änderbar**  
+   Bewusste Trennung: Weiterführung nur über **Nachbestellungen**. Operatoren, die nur die Detailseite nutzen, finden keinen Button dafür.
+
+---
+
+## Querverweise
+
+- [ORDER_PROCESS.md](ORDER_PROCESS.md) — Gesamtprozess von Bestellung bis Abarbeitung, technische Abläufe
+- [ADMIN_SYSTEM.md](ADMIN_SYSTEM.md) — Admin-Login und Kontenverwaltung
+- [CONFIG_REFERENCE.md](CONFIG_REFERENCE.md) — Konfigurationsoptionen (sofern relevant für Betrieb)

+ 3 - 4
docs/CONFIG_REFERENCE.md

@@ -12,7 +12,7 @@
 | `SITE_IMPRINT_URL` | Ziel-URL für den Impressumslink |
 | `SITE_PRIVACY_URL` | Ziel-URL für den Datenschutzlink |
 | `SITE_FULL_NAME` | Kombinierter Anzeigename aus Marke und Service-Header |
-| `SITE_URL` | Basispfad oder Basis-URL für Links, Assets und Bestätigungslinks |
+| `SITE_URL` | Basispfad oder Basis-URL für Links und Assets |
 | `SITE_CANONICAL_ORIGIN` | Optional: `https://hostname` (ohne Pfad) für absolute E-Mail-Links, falls `HTTP_HOST` unzuverlässig ist |
 | `ADMIN_LOGIN_RATE_LIMIT_MAX` | Optional: max. fehlgeschlagene Admin-Logins pro IP und Zeitfenster (Standard: 10) |
 | `ADMIN_LOGIN_RATE_LIMIT_WINDOW` | Optional: Zeitfenster in Sekunden für Admin-Login-Limit (Standard: 900) |
@@ -21,8 +21,6 @@
 | `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 |
@@ -34,7 +32,8 @@
 | `ADMINS_FILE` | JSON-Datei für Adminkonten |
 | `CATEGORIES_FILE` | JSON-Datei für Kategorien |
 | `FAQ_FILE` | JSON-Datei für FAQ-Inhalte |
+| `MANUAL_BACKORDERS_FILE` | JSON-Datei für manuelle Nachbestell-Einträge (ohne Bestellbezug) |
 
 ## 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.
+Die Konstanten definieren die Startwerte. Änderbare Betriebsparameter wie interne Empfängeradresse können zusätzlich im Adminbereich unter `Einstellungen` angepasst werden.

+ 11 - 49
docs/MAIL_PROCESS.md

@@ -5,7 +5,7 @@
 Der Versand aller Bestellmails ist zentral in `includes/functions.php` implementiert und wird synchron innerhalb der jeweiligen HTTP-Requests ausgeführt.
 
 - Kernfunktion für Versand: `sendEmail(...)`
-- Fachliche Mail-Trigger: `createOrder(...)` und `confirmOrderByToken(...)`
+- Fachlicher Mail-Trigger: `createOrder(...)`
 - Laufzeit-Einstellungen: `getSystemSettings()` aus `data/settings.json`
 - Startwerte/Fallbacks: `config.php`
 
@@ -20,68 +20,33 @@ Die folgenden Parameter steuern den Mailfluss:
 | Schlüssel | Quelle | Wirkung |
 |---|---|---|
 | `order_recipient_email` | Admin-Einstellungen (`data/settings.json`) | Empfänger für interne Bestellmail |
-| `order_confirmation_required` | Admin-Einstellungen | Erzwingt Bestätigungsmail vor interner Weiterleitung |
-| `order_confirmation_expiry_days` | Admin-Einstellungen | Gültigkeit des Bestätigungslinks |
 | `attach_order_pdf_to_admin_email` | Admin-Einstellungen | Hängt PDF an interne Bestellmail an |
 | `FROM_EMAIL`, `FROM_NAME` | `config.php` | Absender/Anzeigename für alle ausgehenden Mails |
-| `SITE_URL` | `config.php` | Basis für Bestätigungslink in Mails |
 
 ## Mail-Typen und Auslöser
 
-### 1) Bestätigung an Besteller
-
-- Funktion: `sendOrderConfirmationRequestEmail($order)`
-- Trigger: direkt nach `createOrder(...)`, wenn `order_confirmation_required = true`
-- Empfänger: `order.customer_email`
-- Inhalt: Bestellzusammenfassung + Button/Link auf `order-confirm.php?token=...`
-
-### 2) Eingangsbestätigung an Besteller (ohne Pflichtbestätigung)
+### 1) Eingangsbestätigung an Besteller
 
 - Funktion: `sendOrderCreatedCustomerEmail($order)`
-- Trigger: direkt nach `createOrder(...)`, wenn `order_confirmation_required = false`
+- Trigger: direkt nach `createOrder(...)`
 - Empfänger: `order.customer_email`
 - Inhalt: Bestellung erfasst und intern weitergeleitet
 
-### 3) Bestätigungsinfo an Besteller (nach Klick auf Token-Link)
-
-- Funktion: `sendOrderConfirmedCustomerEmail($order)`
-- Trigger: nach erfolgreicher Token-Bestätigung in `confirmOrderByToken(...)`
-- Empfänger: `order.customer_email`
-- Inhalt: Bestellung bestätigt und intern weitergeleitet
-
-### 4) Interne Bestellmail
+### 2) Interne Bestellmail
 
 - Funktion: `sendConfirmedOrderAdminNotification($order)`
-- Trigger:
-  - direkt nach `createOrder(...)`, wenn `order_confirmation_required = false`
-  - nach erfolgreicher Token-Bestätigung in `confirmOrderByToken(...)`, wenn vorher `pending`
+- Trigger: direkt nach `createOrder(...)`
 - Empfänger: `getOrderRecipientEmail()` (normalisiert/validiert)
 - Inhalt: HTML-Bestellzusammenfassung
 - Optional: PDF-Anhang `bestellung-<order-id>.pdf` bei aktivem `attach_order_pdf_to_admin_email`
+- PDF-Erzeugung: `renderOrderPdf($order)` (intern `prepareOrderForDocument()` + `generateOrderPdf()`); enthält keine Bearbeitungs-/Lieferstatus-Felder aus der Admin-Oberfläche
+- Admin-Nachdruck: `admin/order-pdf.php?id=<order-id>` (Schaltfläche „Bestellung drucken“ auf `admin/order.php`)
 
-## Ablauf nach Konfiguration
-
-### Fall A: Bestätigung erforderlich
-
-1. `createOrder(...)` speichert Bestellung mit `confirmation_status = pending`.
-2. Besteller erhält Bestätigungsmail mit Token-Link.
-3. Klick auf Link ruft `order-confirm.php` auf und startet `confirmOrderByToken(...)`.
-4. Bei gültigem, nicht abgelaufenem Token:
-   - `confirmation_status` wird auf `confirmed` gesetzt,
-   - interne Bestellmail wird versendet,
-   - Besteller erhält Bestätigungsinfo.
-
-### Fall B: Keine Bestätigung erforderlich
-
-1. `createOrder(...)` speichert Bestellung mit `confirmation_status = not_required` und `confirmed_at`.
-2. Interne Bestellmail wird sofort versendet.
-3. Besteller erhält direkt Eingangsbestätigung.
-
-## Ablauf bei Fristablauf
+## Ablauf
 
-- Offene Bestätigungen (`pending`) wechseln nach Frist auf `expired`.
-- Der Status wird in `refreshOrderState(...)` berechnet und über `expirePendingOrders()` beim Aufruf von `admin/index.php`, `admin/orders.php` und `order-confirm.php` fortgeschrieben.
-- Für `expired` gibt es keine zusätzliche Mail.
+1. `createOrder(...)` speichert die Bestellung.
+2. Interne Bestellmail wird versendet.
+3. Besteller erhält Eingangsbestätigung.
 
 ## Technische Versanddetails
 
@@ -91,9 +56,6 @@ Die folgenden Parameter steuern den Mailfluss:
   - `From: <FROM_NAME> <FROM_EMAIL>`
   - `Reply-To: FROM_EMAIL`
   - `X-Mailer: PHP/<version>`
-- Bestätigungslink wird über `buildAbsoluteUrl(...)` erzeugt:
-  - absolute `SITE_URL` wird direkt verwendet,
-  - bei relativer `SITE_URL` wird aus Request-Kontext (`HTTP_HOST`, HTTPS) eine absolute URL gebaut.
 
 ## Fehlerverhalten und Nachvollziehbarkeit
 

+ 36 - 26
docs/ORDER_PROCESS.md

@@ -2,13 +2,14 @@
 
 ## Überblick
 
-Das System bildet keinen klassischen E-Commerce-Checkout ab, sondern einen internen PSA-Anforderungsprozess mit optionaler E-Mail-Bestätigung vor der internen Weiterleitung.
+Das System bildet keinen klassischen E-Commerce-Checkout ab, sondern einen internen PSA-Anforderungsprozess mit direkter interner Weiterleitung nach dem Absenden.
 
 - Einstieg: `index.php` -> `product.php` -> `cart.php` -> `checkout.php`
 - Persistenz: `data/orders.json`
 - Kernlogik: `includes/functions.php`
 - Interne Bearbeitung: `admin/orders.php`
-- Einstellungen für Bestätigung/Weiterleitung: `admin/settings.php`
+- Nachbestellungen: `admin/backorders.php`
+- Einstellungen für Weiterleitung: `admin/settings.php`
 
 Hinweis:
 
@@ -24,30 +25,13 @@ Hinweis:
    - Organisation (Pflicht, muss aktiv sein)
    - Kommentar (optional)
 4. `createOrder(...)` erzeugt eine Bestellnummer (`ORDER_PREFIX-JAHR-LFDNR`, z. B. `FWFS-2026-001`) und speichert die Bestellung.
-5. Nach dem Speichern wird der Warenkorb geleert und auf `order-success.php` weitergeleitet.
+5. Nach dem Speichern werden interne Admin-Benachrichtigung und Kunden-Eingangsbestätigung per E-Mail versendet, der Warenkorb geleert und auf `order-success.php` weitergeleitet.
 
-## Potenzielle Freigaben / Bestätigungen
-
-### 1) Organisation als Vorbedingung
+## Vorbedingungen
 
 Eine Bestellung ist nur möglich, wenn die gewählte Organisation existiert und als aktiv markiert ist. Inaktive/ungültige Organisationen blockieren den Abschluss.
 
-### 2) Optionale E-Mail-Bestätigung durch Besteller
-
-Die Einstellung `order_confirmation_required` steuert, ob eine Bestätigung nötig ist:
-
-- **Aktiviert**:
-  - Status beim Anlegen: `confirmation_status = pending`
-  - Es wird eine Bestätigungs-Mail mit Token-Link (`order-confirm.php?token=...`) versendet.
-  - Interne Weiterleitung an die Empfängeradresse erfolgt erst nach erfolgreicher Bestätigung.
-  - Frist über `order_confirmation_expiry_days`; danach `confirmation_status = expired`.
-- **Deaktiviert**:
-  - Status beim Anlegen: `confirmation_status = not_required`
-  - Bestellung wird direkt intern weitergeleitet.
-
-### 3) Keine separate Admin-Freigabe vor Eingang
-
-Es gibt keinen expliziten Admin-Approve-Schritt, der den Eingang einer Bestellung freischaltet. Die formale Freigabelogik ist die optionale E-Mail-Bestätigung durch den Besteller.
+Es gibt keinen separaten Admin-Freigabeschritt vor dem Eingang einer Bestellung.
 
 ## Interner Prozess nach Eingang
 
@@ -57,20 +41,46 @@ In `admin/orders.php` werden Bestellungen nach Status geführt und manuell bearb
 - `partial`: mindestens eine Position bearbeitet
 - `processed`: alle Positionen bearbeitet
 - `cancelled`: Bestellung storniert
-- zusätzlich `pending`/`expired` über `confirmation_status`
 
 Regeln:
 
-- Positionsbearbeitung ist gesperrt, solange Bestellung `pending` oder `expired` ist.
 - Stornierung ist jederzeit möglich (solange noch nicht storniert).
 - Zeitstempel für interne Weiterleitung (`admin_notified_at`) wird gesetzt, wenn die Admin-Benachrichtigungsmail erfolgreich versendet wurde.
 
+## Nachbestellungen (nur Admin)
+
+Wenn Artikel nicht auf Lager sind, können Admins Positionen als Nachbestellung markieren. Der Vorgang ist nur im Admin-Bereich sichtbar, nicht im Frontend.
+
+### Daten pro Position
+
+Jede Bestellposition kann optional folgende Felder haben:
+
+- `backorder_status`: `""` (keine), `to_be_backordered`, `ordered`
+- `backordered_at`, `ordered_at` (Zeitstempel)
+
+### Zustände
+
+1. **Nachzubestellen** (`to_be_backordered`): Position ist als Nachbestellung markiert, externe Bestellung steht noch aus.
+2. **Wartet auf Lieferung** (`ordered`): Extern bestellt, Lieferung ausstehend.
+
+### Aktionen
+
+| Aktion | Ort | Wirkung |
+| --- | --- | --- |
+| Als Nachbestellung markieren | `admin/orders.php` (pro Position) | Setzt `to_be_backordered` |
+| Nachbestellung aufheben | `admin/orders.php` | Setzt `backorder_status` zurück auf `""` (nur aus Zustand 1) |
+| Als bestellt markieren | `admin/backorders.php` | Verschiebt N Positionen von `to_be_backordered` nach `ordered` (FIFO) |
+| Lieferung eingetroffen | `admin/backorders.php` | Setzt N Positionen von `ordered` zurück auf `""` (FIFO); Position bleibt offen zur normalen Abarbeitung |
+
+In `admin/backorders.php` werden Positionen nach Produkt und Größe gruppiert. Die Spalten **Nachzubestellen** und **Wartet auf Lieferung** werden getrennt gezählt, damit neu markierte Artikel nicht mit bereits bestellten vermischt werden.
+
+Positionen mit Nachbestell-Status (**Nachzubestellen** oder **Wartet auf Lieferung**) erscheinen nicht in **Offene Positionen** auf dem Dashboard; sie werden über **Nachbestellungen** verwaltet.
+
 ## Abweichungen zum Standard-Webshop
 
 - Kein Payment-Schritt (kein Warenwert, keine Zahlungsarten, keine Zahlungsfreigabe).
 - Keine Mengenlogik im Warenkorb (pro Produkt nur ein Eintrag, keine Stückzahl).
 - Keine Lieferadresse / kein Versandprozess / kein Fulfillment-Tracking.
 - Keine Endnutzer-Bestellhistorie oder Kundenkonto-Workflow.
-- Optionaler Double-Opt-in-ähnlicher Schritt per E-Mail-Bestätigung vor interner Weiterleitung.
 - Bearbeitung ist positionsbasiert im Admin (operativer Abarbeitungsstatus statt klassischer Versandstatus).
-- Legacy-Routen `reservation.php` und `orders.php` (Frontend) sowie `admin/reservations.php` und `admin/backorders.php` leiten auf die Bestellverwaltung um und sind kein eigener Prozess mehr.
+- Legacy-Routen `reservation.php` und `orders.php` (Frontend) sowie `admin/reservations.php` leiten auf die Bestellverwaltung um.

+ 2 - 3
docs/STYLE_SYSTEM.md

@@ -187,7 +187,6 @@ This is the portable public style interface. Implement these selectors and seman
 - `.status-open`: open/pending/active text+border.
 - `.status-notified`: notified/informed text+border.
 - `.status-picked`: completed/processed text+border.
-- `.status-expired`: expired/invalid text+border.
 - `.status-hidden`: hidden/archived text+border.
 
 ### Structural / Operations / Modal
@@ -353,7 +352,7 @@ Email styles are inline HTML/CSS and must keep the same dark-theme palette.
 - [ ] RGBA shadow/focus values match exactly.
 
 ### 2) Service UI parity
-- [ ] Header, cards, detail views, action rows, forms, and confirmation/detail pages match hierarchy and style.
+- [ ] Header, cards, detail views, action rows, forms, and detail pages match hierarchy and style.
 
 ### 3) Operations parity
 - [ ] Dashboard stats, data tables, status pills, panels, modal overlays, and action buttons match behavior and appearance.
@@ -366,7 +365,7 @@ Email styles are inline HTML/CSS and must keep the same dark-theme palette.
 - [ ] Printing detail pages hides header/footer/nav/buttons and preserves key blocks from page breaks.
 
 ### 6) Email parity
-- [ ] Notification/confirmation emails render with dark card theme, accent highlights, and warning block usage where applicable.
+- [ ] Notification emails render with dark card theme, accent highlights, and warning block usage where applicable.
 
 ### 7) Accessibility sanity
 - [ ] Text contrast remains readable on dark surfaces.

Разница между файлами не показана из-за своего большого размера
+ 573 - 233
includes/functions.php


+ 0 - 29
order-confirm.php

@@ -1,29 +0,0 @@
-<?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 an <?php echo escape(SITE_DEPARTMENT_NAME); ?> 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'; ?>

+ 1 - 5
order-success.php

@@ -17,11 +17,7 @@ include __DIR__ . '/includes/header.php';
 <?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, die an die von Ihnen angegebene E-Mail-Adresse gesendet wurde.</p>
-        <?php else: ?>
-            <p>Ihre Bestellung wurde erfasst und an <?php echo escape(SITE_DEPARTMENT_NAME); ?> weitergeleitet.</p>
-        <?php endif; ?>
+        <p>Ihre Bestellung wurde erfasst und an <?php echo escape(SITE_DEPARTMENT_NAME); ?> weitergeleitet.</p>
     </div>
 <?php endif; ?>
 

Некоторые файлы не были показаны из-за большого количества измененных файлов