2 Commits d915024af7 ... 9853804243

Autore SHA1 Messaggio Data
  Medowar 9853804243 bump version 1 settimana fa
  Medowar a68eb09477 adding nametag collector functionality 1 settimana fa
7 ha cambiato i file con 565 aggiunte e 1 eliminazioni
  1. 8 0
      admin/index.php
  2. 306 0
      admin/nametags.php
  3. 1 0
      config.feuerwehr.php
  4. 1 0
      config.sample.php
  5. 3 0
      data/nametags.json
  6. 245 0
      includes/functions.php
  7. 1 1
      includes/version.php

+ 8 - 0
admin/index.php

@@ -35,12 +35,15 @@ foreach ($backorderGroups as $group) {
     $backorderCount += (int) $group['to_be_backordered'] + (int) $group['ordered'];
     $backorderCount += (int) $group['to_be_backordered'] + (int) $group['ordered'];
 }
 }
 
 
+$openNametagCount = count(getOpenNametagOrders());
+
 $stats = [
 $stats = [
     'open' => 0,
     'open' => 0,
     'partial' => 0,
     'partial' => 0,
     'processed' => 0,
     'processed' => 0,
     'cancelled' => 0,
     'cancelled' => 0,
     'backorder' => $backorderCount,
     'backorder' => $backorderCount,
+    'nametags' => $openNametagCount,
 ];
 ];
 
 
 foreach ($orders as $order) {
 foreach ($orders as $order) {
@@ -128,6 +131,7 @@ include __DIR__ . '/../includes/header.php';
                 <a href="settings.php">Einstellungen</a>
                 <a href="settings.php">Einstellungen</a>
                 <a href="faq.php">FAQ bearbeiten</a>
                 <a href="faq.php">FAQ bearbeiten</a>
                 <a href="backorders.php">Nachbestellungen verwalten</a>
                 <a href="backorders.php">Nachbestellungen verwalten</a>
+                <a href="nametags.php">Namensschilder verwalten</a>
                 <a href="admins.php">Admins verwalten</a>
                 <a href="admins.php">Admins verwalten</a>
                 <form method="POST" action="login.php" class="inline-form">
                 <form method="POST" action="login.php" class="inline-form">
                     <?php echo csrfField(); ?>
                     <?php echo csrfField(); ?>
@@ -165,6 +169,10 @@ include __DIR__ . '/../includes/header.php';
         <h3>Nachbestellung</h3>
         <h3>Nachbestellung</h3>
         <div class="stat-value"><?php echo $stats['backorder']; ?></div>
         <div class="stat-value"><?php echo $stats['backorder']; ?></div>
     </div>
     </div>
+    <div class="stat-card">
+        <h3>Namensschilder offen</h3>
+        <div class="stat-value"><?php echo $stats['nametags']; ?></div>
+    </div>
 </div>
 </div>
 
 
 <h3 class="section-title mt-4">Bestellungen mit Aktion erforderlich</h3>
 <h3 class="section-title mt-4">Bestellungen mit Aktion erforderlich</h3>

+ 306 - 0
admin/nametags.php

@@ -0,0 +1,306 @@
+<?php
+require_once __DIR__ . "/../config.php";
+require_once __DIR__ . "/../includes/functions.php";
+
+if (empty($_SESSION['admin_logged_in'])) {
+    header("Location: login.php");
+    exit();
+}
+
+$pageTitle = "Namensschilder";
+$message = "";
+$messageType = "";
+
+if ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['add_nametag'])) {
+    if (!validateCsrfToken($_POST['csrf_token'] ?? "")) {
+        $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
+        $messageType = "error";
+    } else {
+        $result = addNametagOrder(
+            $_POST['person_name'] ?? "",
+            $_POST['organization_id'] ?? "",
+            $_POST['quantity'] ?? 1,
+            $_POST['note'] ?? "",
+        );
+        $message = $result["success"]
+            ? "Namensschild-Bestellung wurde hinzugefügt."
+            : $result["message"];
+        $messageType = $result["success"] ? "success" : "error";
+
+        if ($result["success"]) {
+            logAccess("Admin added nametag order", [
+                "admin" => $_SESSION['admin_username'] ?? "unknown",
+                "person_name" => $_POST['person_name'] ?? "",
+            ]);
+        }
+    }
+}
+
+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 = markNametagOrdered($_POST['id'] ?? "");
+        $message = $result["success"]
+            ? "Namensschild wurde als bestellt markiert."
+            : $result["message"];
+        $messageType = $result["success"] ? "success" : "error";
+
+        if ($result["success"]) {
+            logAccess("Admin marked nametag ordered", [
+                "admin" => $_SESSION['admin_username'] ?? "unknown",
+                "id" => $_POST['id'] ?? "",
+            ]);
+        }
+    }
+}
+
+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 = markNametagDelivered($_POST['id'] ?? "");
+        $message = $result["success"]
+            ? "Namensschild wurde als geliefert markiert."
+            : $result["message"];
+        $messageType = $result["success"] ? "success" : "error";
+
+        if ($result["success"]) {
+            logAccess("Admin marked nametag delivered", [
+                "admin" => $_SESSION['admin_username'] ?? "unknown",
+                "id" => $_POST['id'] ?? "",
+            ]);
+        }
+    }
+}
+
+if ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['delete_nametag'])) {
+    if (!validateCsrfToken($_POST['csrf_token'] ?? "")) {
+        $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
+        $messageType = "error";
+    } else {
+        $result = deleteNametagOrder($_POST['id'] ?? "");
+        $message = $result["success"]
+            ? "Position wurde gelöscht."
+            : $result["message"];
+        $messageType = $result["success"] ? "success" : "error";
+
+        if ($result["success"]) {
+            logAccess("Admin deleted nametag order", [
+                "admin" => $_SESSION['admin_username'] ?? "unknown",
+                "id" => $_POST['id'] ?? "",
+            ]);
+        }
+    }
+}
+
+$openNametags = getOpenNametagOrders();
+$orderedNametags = getOrderedNametagOrders();
+$organizations = getOrganizations(true);
+
+$bodyClass = "admin-page";
+include __DIR__ . "/../includes/header.php";
+?>
+
+<div class="admin-header">
+    <h2>Namensschilder</h2>
+    <div>
+        <a href="index.php" class="btn btn-secondary">Zurück zum Dashboard</a>
+        <a href="backorders.php" class="btn btn-secondary">Nachbestellungen</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">
+    Namensschilder werden personalisiert und separat vom Gerätewart bestellt.
+    Hier werden alle noch zu bestellenden Namensschilder mit dem Namen der Person aufgeführt.
+</p>
+
+<div class="admin-panel backorder-add-panel">
+    <h3>Namensschild-Bestellung anlegen</h3>
+    <form method="POST" class="admin-filter-form backorder-add-form">
+        <?php echo csrfField(); ?>
+        <div class="admin-filter-field">
+            <label for="nametag_person_name">Name der Person</label>
+            <input
+                type="text"
+                id="nametag_person_name"
+                name="person_name"
+                required
+                maxlength="120"
+            >
+        </div>
+        <div class="admin-filter-field">
+            <label for="nametag_organization">Organisation</label>
+            <select id="nametag_organization" name="organization_id">
+                <option value="">Keine</option>
+                <?php foreach ($organizations as $organization): ?>
+                    <option value="<?php echo escape(
+                        $organization["id"],
+                    ); ?>">
+                        <?php echo escape($organization["label"]); ?>
+                    </option>
+                <?php endforeach; ?>
+            </select>
+        </div>
+        <div class="admin-filter-field">
+            <label for="nametag_quantity">Anzahl</label>
+            <input
+                type="number"
+                id="nametag_quantity"
+                name="quantity"
+                min="1"
+                max="20"
+                value="1"
+                class="backorder-qty-input"
+            >
+        </div>
+        <div class="admin-filter-field admin-filter-field-wide">
+            <label for="nametag_note">Hinweis</label>
+            <input
+                type="text"
+                id="nametag_note"
+                name="note"
+                maxlength="200"
+                placeholder="Optional, z. B. Funktion oder Schreibweise"
+            >
+        </div>
+        <button type="submit" name="add_nametag" class="btn">
+            Zur Bestellliste hinzufügen
+        </button>
+    </form>
+</div>
+
+<h3 class="section-title mt-4">Ausstehende Bestellungen</h3>
+<?php if (empty($openNametags)): ?>
+    <div class="alert alert-info">
+        <p>Keine ausstehenden Namensschild-Bestellungen vorhanden.</p>
+    </div>
+<?php else: ?>
+    <div class="table-responsive">
+        <table class="responsive-table">
+            <thead>
+                <tr>
+                    <th>Name</th>
+                    <th>Organisation</th>
+                    <th>Anzahl</th>
+                    <th>Hinweis</th>
+                    <th>Angelegt</th>
+                    <th>Aktionen</th>
+                </tr>
+            </thead>
+            <tbody>
+                <?php foreach ($openNametags as $entry): ?>
+                    <tr>
+                        <td data-label="Name"><strong><?php echo escape(
+                            $entry["person_name"],
+                        ); ?></strong></td>
+                        <td data-label="Organisation"><?php echo $entry[
+                            "organization_label"
+                        ] !== ""
+                            ? escape($entry["organization_label"])
+                            : "-"; ?></td>
+                        <td data-label="Anzahl"><?php echo (int) $entry[
+                            "quantity"
+                        ]; ?></td>
+                        <td data-label="Hinweis"><?php echo $entry["note"] !== ""
+                            ? escape($entry["note"])
+                            : "-"; ?></td>
+                        <td data-label="Angelegt"><?php echo escape(
+                            formatDate($entry["created_at"]),
+                        ); ?></td>
+                        <td data-label="Aktionen">
+                            <form method="POST" class="backorder-action-form">
+                                <?php echo csrfField(); ?>
+                                <input type="hidden" name="id" value="<?php echo escape(
+                                    $entry["id"],
+                                ); ?>">
+                                <button type="submit" name="mark_ordered" class="btn btn-small">
+                                    Als bestellt markieren
+                                </button>
+                            </form>
+                            <form method="POST" class="backorder-action-form inline-form" onsubmit="return confirm('Position wirklich löschen?');">
+                                <?php echo csrfField(); ?>
+                                <input type="hidden" name="id" value="<?php echo escape(
+                                    $entry["id"],
+                                ); ?>">
+                                <button type="submit" name="delete_nametag" class="btn btn-small btn-secondary">
+                                    Löschen
+                                </button>
+                            </form>
+                        </td>
+                    </tr>
+                <?php endforeach; ?>
+            </tbody>
+        </table>
+    </div>
+<?php endif; ?>
+
+<h3 class="section-title mt-4">Bestellt, wartet auf Lieferung</h3>
+<?php if (empty($orderedNametags)): ?>
+    <div class="alert alert-info">
+        <p>Keine Namensschilder warten aktuell auf Lieferung.</p>
+    </div>
+<?php else: ?>
+    <div class="table-responsive">
+        <table class="responsive-table">
+            <thead>
+                <tr>
+                    <th>Name</th>
+                    <th>Organisation</th>
+                    <th>Anzahl</th>
+                    <th>Hinweis</th>
+                    <th>Status</th>
+                    <th>Bestellt am</th>
+                    <th>Aktionen</th>
+                </tr>
+            </thead>
+            <tbody>
+                <?php foreach ($orderedNametags as $entry): ?>
+                    <tr>
+                        <td data-label="Name"><strong><?php echo escape(
+                            $entry["person_name"],
+                        ); ?></strong></td>
+                        <td data-label="Organisation"><?php echo $entry[
+                            "organization_label"
+                        ] !== ""
+                            ? escape($entry["organization_label"])
+                            : "-"; ?></td>
+                        <td data-label="Anzahl"><?php echo (int) $entry[
+                            "quantity"
+                        ]; ?></td>
+                        <td data-label="Hinweis"><?php echo $entry["note"] !== ""
+                            ? escape($entry["note"])
+                            : "-"; ?></td>
+                        <td data-label="Status">
+                            <span class="status status-backorder-waiting">Wartet auf Lieferung</span>
+                        </td>
+                        <td data-label="Bestellt am"><?php echo escape(
+                            formatDate($entry["ordered_at"]),
+                        ); ?></td>
+                        <td data-label="Aktionen">
+                            <form method="POST" class="backorder-action-form">
+                                <?php echo csrfField(); ?>
+                                <input type="hidden" name="id" value="<?php echo escape(
+                                    $entry["id"],
+                                ); ?>">
+                                <button type="submit" name="mark_delivered" class="btn btn-small btn-secondary">
+                                    Lieferung eingetroffen
+                                </button>
+                            </form>
+                        </td>
+                    </tr>
+                <?php endforeach; ?>
+            </tbody>
+        </table>
+    </div>
+<?php endif; ?>
+
+<?php include __DIR__ . "/../includes/footer.php"; ?>

+ 1 - 0
config.feuerwehr.php

@@ -48,6 +48,7 @@ define('UPLOADS_DIR', DATA_DIR . 'uploads/');
 define('PRODUCTS_FILE', DATA_DIR . 'products.json');
 define('PRODUCTS_FILE', DATA_DIR . 'products.json');
 define('ORDERS_FILE', DATA_DIR . 'orders.json');
 define('ORDERS_FILE', DATA_DIR . 'orders.json');
 define('MANUAL_BACKORDERS_FILE', DATA_DIR . 'manual_backorders.json');
 define('MANUAL_BACKORDERS_FILE', DATA_DIR . 'manual_backorders.json');
+define('NAMETAGS_FILE', DATA_DIR . 'nametags.json');
 define('ORGANIZATIONS_FILE', DATA_DIR . 'organizations.json');
 define('ORGANIZATIONS_FILE', DATA_DIR . 'organizations.json');
 define('SETTINGS_FILE', DATA_DIR . 'settings.json');
 define('SETTINGS_FILE', DATA_DIR . 'settings.json');
 define('ADMINS_FILE', DATA_DIR . 'admins.json');
 define('ADMINS_FILE', DATA_DIR . 'admins.json');

+ 1 - 0
config.sample.php

@@ -48,6 +48,7 @@ define('UPLOADS_DIR', DATA_DIR . 'uploads/');
 define('PRODUCTS_FILE', DATA_DIR . 'products.json');
 define('PRODUCTS_FILE', DATA_DIR . 'products.json');
 define('ORDERS_FILE', DATA_DIR . 'orders.json');
 define('ORDERS_FILE', DATA_DIR . 'orders.json');
 define('MANUAL_BACKORDERS_FILE', DATA_DIR . 'manual_backorders.json');
 define('MANUAL_BACKORDERS_FILE', DATA_DIR . 'manual_backorders.json');
+define('NAMETAGS_FILE', DATA_DIR . 'nametags.json');
 define('ORGANIZATIONS_FILE', DATA_DIR . 'organizations.json');
 define('ORGANIZATIONS_FILE', DATA_DIR . 'organizations.json');
 define('SETTINGS_FILE', DATA_DIR . 'settings.json');
 define('SETTINGS_FILE', DATA_DIR . 'settings.json');
 define('ADMINS_FILE', DATA_DIR . 'admins.json');
 define('ADMINS_FILE', DATA_DIR . 'admins.json');

+ 3 - 0
data/nametags.json

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

+ 245 - 0
includes/functions.php

@@ -2389,6 +2389,251 @@ function markBackorderItemsDelivered($productId, $size, $quantity)
     return applyBackorderBulkUpdate($productId, $size, "ordered", "", $quantity);
     return applyBackorderBulkUpdate($productId, $size, "ordered", "", $quantity);
 }
 }
 
 
+function nametagStatusOpen(): string
+{
+    return "open";
+}
+
+function nametagStatusOrdered(): string
+{
+    return "ordered";
+}
+
+function getNametagOrders()
+{
+    $data = readJsonFile(NAMETAGS_FILE);
+    $entries =
+        isset($data["entries"]) && is_array($data["entries"])
+            ? $data["entries"]
+            : [];
+    $normalized = [];
+
+    foreach ($entries as $entry) {
+        if (!is_array($entry)) {
+            continue;
+        }
+
+        $id = trim((string) ($entry["id"] ?? ""));
+        $personName = trim((string) ($entry["person_name"] ?? ""));
+        $status = trim((string) ($entry["status"] ?? ""));
+        if (
+            $id === "" ||
+            $personName === "" ||
+            !in_array($status, [nametagStatusOpen(), nametagStatusOrdered()], true)
+        ) {
+            continue;
+        }
+
+        $normalized[] = [
+            "id" => $id,
+            "person_name" => $personName,
+            "organization_id" => trim((string) ($entry["organization_id"] ?? "")),
+            "organization_label" => trim(
+                (string) ($entry["organization_label"] ?? ""),
+            ),
+            "quantity" => max(1, (int) ($entry["quantity"] ?? 1)),
+            "note" => trim((string) ($entry["note"] ?? "")),
+            "status" => $status,
+            "created_at" => trim((string) ($entry["created_at"] ?? "")),
+            "ordered_at" => trim((string) ($entry["ordered_at"] ?? "")),
+        ];
+    }
+
+    return $normalized;
+}
+
+function saveNametagOrders($entries)
+{
+    return writeJsonFile(NAMETAGS_FILE, [
+        "entries" => array_values($entries),
+    ]);
+}
+
+function generateNametagOrderId()
+{
+    return "nt-" . date("YmdHis") . "-" . bin2hex(random_bytes(4));
+}
+
+function addNametagOrder($personName, $organizationId, $quantity, $note)
+{
+    $personName = trim((string) $personName);
+    $organizationId = trim((string) $organizationId);
+    $quantity = (int) $quantity;
+    $note = trim((string) $note);
+
+    if ($personName === "") {
+        return ["success" => false, "message" => "Bitte einen Namen eingeben."];
+    }
+
+    if ($quantity <= 0) {
+        $quantity = 1;
+    }
+    if ($quantity > 20) {
+        return [
+            "success" => false,
+            "message" => "Maximal 20 Namensschilder pro Position.",
+        ];
+    }
+
+    $organizationLabel = "";
+    if ($organizationId !== "") {
+        $organization = getOrganizationById($organizationId);
+        if ($organization === null) {
+            return ["success" => false, "message" => "Ungültige Organisation."];
+        }
+        $organizationLabel = $organization["label"];
+    }
+
+    $entries = getNametagOrders();
+    $entries[] = [
+        "id" => generateNametagOrderId(),
+        "person_name" => $personName,
+        "organization_id" => $organizationId,
+        "organization_label" => $organizationLabel,
+        "quantity" => $quantity,
+        "note" => $note,
+        "status" => nametagStatusOpen(),
+        "created_at" => date("Y-m-d H:i:s"),
+        "ordered_at" => "",
+    ];
+
+    if (!saveNametagOrders($entries)) {
+        return [
+            "success" => false,
+            "message" => "Namensschild-Bestellung konnte nicht gespeichert werden.",
+        ];
+    }
+
+    return ["success" => true];
+}
+
+function markNametagOrdered($id)
+{
+    $id = trim((string) $id);
+    $entries = getNametagOrders();
+    $now = date("Y-m-d H:i:s");
+    $found = false;
+
+    foreach ($entries as &$entry) {
+        if ($entry["id"] !== $id) {
+            continue;
+        }
+        if ($entry["status"] !== nametagStatusOpen()) {
+            return [
+                "success" => false,
+                "message" => "Position wurde bereits bestellt.",
+            ];
+        }
+        $entry["status"] = nametagStatusOrdered();
+        $entry["ordered_at"] = $now;
+        $found = true;
+        break;
+    }
+    unset($entry);
+
+    if (!$found) {
+        return ["success" => false, "message" => "Position nicht gefunden."];
+    }
+
+    if (!saveNametagOrders($entries)) {
+        return [
+            "success" => false,
+            "message" => "Namensschild-Bestellung konnte nicht gespeichert werden.",
+        ];
+    }
+
+    return ["success" => true];
+}
+
+function markNametagDelivered($id)
+{
+    $id = trim((string) $id);
+    $entries = getNametagOrders();
+    $target = null;
+
+    foreach ($entries as $entry) {
+        if ($entry["id"] === $id) {
+            $target = $entry;
+            break;
+        }
+    }
+
+    if ($target === null) {
+        return ["success" => false, "message" => "Position nicht gefunden."];
+    }
+    if ($target["status"] !== nametagStatusOrdered()) {
+        return [
+            "success" => false,
+            "message" => "Position wartet nicht auf Lieferung.",
+        ];
+    }
+
+    $remaining = array_values(
+        array_filter($entries, function ($entry) use ($id) {
+            return $entry["id"] !== $id;
+        }),
+    );
+
+    if (!saveNametagOrders($remaining)) {
+        return [
+            "success" => false,
+            "message" => "Namensschild-Bestellung konnte nicht gespeichert werden.",
+        ];
+    }
+
+    return ["success" => true];
+}
+
+function deleteNametagOrder($id)
+{
+    $id = trim((string) $id);
+    $entries = getNametagOrders();
+    $filtered = array_values(
+        array_filter($entries, function ($entry) use ($id) {
+            return $entry["id"] !== $id;
+        }),
+    );
+
+    if (count($filtered) === count($entries)) {
+        return ["success" => false, "message" => "Position nicht gefunden."];
+    }
+
+    if (!saveNametagOrders($filtered)) {
+        return [
+            "success" => false,
+            "message" => "Namensschild-Bestellung konnte nicht gelöscht werden.",
+        ];
+    }
+
+    return ["success" => true];
+}
+
+function getOpenNametagOrders()
+{
+    $entries = array_values(
+        array_filter(getNametagOrders(), function ($entry) {
+            return $entry["status"] === nametagStatusOpen();
+        }),
+    );
+    usort($entries, function ($left, $right) {
+        return strcmp($left["created_at"], $right["created_at"]);
+    });
+    return $entries;
+}
+
+function getOrderedNametagOrders()
+{
+    $entries = array_values(
+        array_filter(getNametagOrders(), function ($entry) {
+            return $entry["status"] === nametagStatusOrdered();
+        }),
+    );
+    usort($entries, function ($left, $right) {
+        return strcmp($left["ordered_at"], $right["ordered_at"]);
+    });
+    return $entries;
+}
+
 function orderHasBackorder($order)
 function orderHasBackorder($order)
 {
 {
     if (!is_array($order["items"] ?? null)) {
     if (!is_array($order["items"] ?? null)) {

+ 1 - 1
includes/version.php

@@ -1,3 +1,3 @@
 <?php
 <?php
 
 
-define("APP_VERSION", "v1.3.10");
+define("APP_VERSION", "v1.3.11");