'; } function getUploadFilename($filename) { $filename = trim((string) $filename); if ($filename === "") { return ""; } return basename($filename); } function getUploadPath($filename) { $filename = getUploadFilename($filename); if ($filename === "") { return null; } return rtrim(UPLOADS_DIR, "/\\") . "/" . $filename; } function getUploadUrl($filename) { $filename = getUploadFilename($filename); if ($filename === "") { return null; } return rtrim(UPLOADS_URL, "/\\") . "/" . rawurlencode($filename); } function setFlashMessage($key, $type, $message) { $key = trim((string) $key); $type = trim((string) $type); $message = trim((string) $message); if ($key === "" || $type === "" || $message === "") { return; } $_SESSION["flash_messages"][$key] = [ "type" => $type, "message" => $message, ]; } function consumeFlashMessage($key) { $key = trim((string) $key); if ($key === "") { return null; } $messages = $_SESSION["flash_messages"] ?? []; if ( !is_array($messages) || !isset($messages[$key]) || !is_array($messages[$key]) ) { return null; } $message = $messages[$key]; unset($_SESSION["flash_messages"][$key]); $type = trim((string) ($message["type"] ?? "")); $text = trim((string) ($message["message"] ?? "")); if ($type === "" || $text === "") { return null; } return [ "type" => $type, "message" => $text, ]; } function normalizeAdminUsername($username) { return trim((string) $username); } function normalizeAdminDescription($description) { return trim((string) $description); } function normalizeAdminEmail($email) { return strtolower(trim((string) $email)); } function isValidAdminUsername($username) { $username = normalizeAdminUsername($username); return preg_match('/^[A-Za-z0-9][A-Za-z0-9._-]{2,49}$/', $username) === 1; } function isValidAdminDescription($description) { $description = normalizeAdminDescription($description); if ($description === "") { return false; } $length = function_exists("mb_strlen") ? mb_strlen($description) : strlen($description); return $length <= 120; } function isValidAdminEmail($email) { $email = normalizeAdminEmail($email); return $email !== "" && filter_var($email, FILTER_VALIDATE_EMAIL) !== false; } function getDefaultAdminDescription($username) { return "Admin"; } function getDefaultAdminEmail() { $email = defined("ADMIN_EMAIL") ? normalizeAdminEmail(ADMIN_EMAIL) : ""; return isValidAdminEmail($email) ? $email : ""; } function getAdminAccounts() { $data = readJsonFile(ADMINS_FILE); $records = isset($data["admins"]) && is_array($data["admins"]) ? $data["admins"] : []; $accounts = []; foreach ($records as $username => $record) { $username = normalizeAdminUsername($username); if ($username === "") { continue; } if (is_string($record)) { $record = [ "password_hash" => $record, "description" => getDefaultAdminDescription($username), "email" => getDefaultAdminEmail(), ]; } if (!is_array($record)) { continue; } $hash = isset($record["password_hash"]) ? (string) $record["password_hash"] : ""; if ($hash === "") { continue; } $description = normalizeAdminDescription( $record["description"] ?? getDefaultAdminDescription($username), ); if (!isValidAdminDescription($description)) { $description = getDefaultAdminDescription($username); } $email = normalizeAdminEmail( $record["email"] ?? getDefaultAdminEmail(), ); if (!isValidAdminEmail($email)) { $email = getDefaultAdminEmail(); } $accounts[$username] = [ "password_hash" => $hash, "description" => $description, "email" => $email, ]; } ksort($accounts); return $accounts; } function getAdminUsers() { $users = []; foreach (getAdminAccounts() as $username => $record) { $users[$username] = $record["password_hash"]; } return $users; } function saveAdminAccounts($accounts) { $result = []; foreach ($accounts as $username => $record) { $username = normalizeAdminUsername($username); if ($username === "" || !is_array($record)) { continue; } $hash = isset($record["password_hash"]) ? (string) $record["password_hash"] : ""; if ($hash === "") { continue; } $description = normalizeAdminDescription( $record["description"] ?? getDefaultAdminDescription($username), ); if (!isValidAdminDescription($description)) { $description = getDefaultAdminDescription($username); } $email = normalizeAdminEmail( $record["email"] ?? getDefaultAdminEmail(), ); if (!isValidAdminEmail($email)) { $email = getDefaultAdminEmail(); } $result[$username] = [ "password_hash" => $hash, "description" => $description, "email" => $email, ]; } ksort($result); return writeJsonFile(ADMINS_FILE, ["admins" => $result]); } function getDefaultCategories() { return [["id" => "apparel", "label" => "Bekleidung"]]; } function normalizeCategoryId($id) { $id = trim((string) $id); if ($id === "") { return ""; } if (function_exists("iconv")) { $converted = @iconv("UTF-8", "ASCII//TRANSLIT//IGNORE", $id); if (is_string($converted) && $converted !== "") { $id = $converted; } } $id = strtolower($id); $id = preg_replace("/[^a-z0-9]+/", "-", $id); return trim((string) $id, "-"); } function normalizeCategoryLabel($label) { return trim((string) $label); } function isValidCategoryLabel($label) { $label = normalizeCategoryLabel($label); if ($label === "") { return false; } $length = function_exists("mb_strlen") ? mb_strlen($label) : strlen($label); return $length <= 80; } function normalizeCategories($categories) { $normalized = []; if (!is_array($categories)) { $categories = []; } foreach ($categories as $category) { if (!is_array($category)) { continue; } $id = normalizeCategoryId($category["id"] ?? ""); $label = normalizeCategoryLabel($category["label"] ?? ""); if ($id === "" || !isValidCategoryLabel($label)) { continue; } $normalized[$id] = [ "id" => $id, "label" => $label, ]; } if (empty($normalized)) { foreach (getDefaultCategories() as $category) { $normalized[$category["id"]] = $category; } } uasort($normalized, function ($left, $right) { return strcasecmp($left["label"], $right["label"]); }); return array_values($normalized); } function getCategories() { $data = readJsonFile(CATEGORIES_FILE); return normalizeCategories($data["categories"] ?? []); } function saveCategories($categories) { return writeJsonFile(CATEGORIES_FILE, [ "categories" => normalizeCategories($categories), ]); } function getCategoryById($categoryId) { $categoryId = normalizeCategoryId($categoryId); foreach (getCategories() as $category) { if ($category["id"] === $categoryId) { return $category; } } return null; } function getCategoryLabel($categoryId) { $category = getCategoryById($categoryId); if ($category !== null) { return $category["label"]; } return trim((string) $categoryId); } function getCategoryLabels($categoryIds) { $labels = []; foreach (normalizeProductCategoryIds($categoryIds) as $categoryId) { $labels[] = getCategoryLabel($categoryId); } return $labels; } function getCategoryChipPalette($categoryId) { $categoryId = normalizeCategoryId($categoryId); if ($categoryId === "") { return [ "background" => "#ebe8df", "border" => "#d0c8b5", "text" => "#4b4b4b", ]; } $hash = crc32($categoryId); if ($hash < 0) { $hash = $hash * -1; } // Spread hues across a distinct red -> blue range. $hueSteps = [0, 16, 32, 48, 66, 84, 104, 128, 152, 176, 200, 224]; $hue = $hueSteps[$hash % count($hueSteps)]; $saturation = 44 + (($hash >> 8) % 10); $lightness = 84 + (($hash >> 16) % 7); $background = "hsl(" . $hue . ", " . $saturation . "%, " . $lightness . "%)"; $border = "hsl(" . $hue . ", " . ($saturation + 8) . "%, " . ($lightness - 16) . "%)"; $text = "hsl(" . $hue . ", " . ($saturation + 18) . "%, 24%)"; return [ "background" => $background, "border" => $border, "text" => $text, ]; } function generateCategoryIdFromLabel($label, $existingCategories = []) { $baseId = normalizeCategoryId($label); if ($baseId === "") { $baseId = "category"; } $used = []; foreach (normalizeCategories($existingCategories) as $category) { $used[$category["id"]] = true; } $candidate = $baseId; $counter = 2; while (isset($used[$candidate])) { $candidate = $baseId . "-" . $counter; $counter++; } return $candidate; } function isCategoryInUse($categoryId) { foreach (getProducts() as $product) { if (productHasCategory($product, $categoryId)) { return true; } } return false; } function normalizeProductCategoryIds($categoryValue) { if (is_array($categoryValue)) { $rawIds = $categoryValue; } elseif ($categoryValue === null || $categoryValue === "") { $rawIds = []; } else { $rawIds = [$categoryValue]; } $normalized = []; foreach ($rawIds as $categoryId) { $categoryId = normalizeCategoryId($categoryId); if ($categoryId !== "") { $normalized[$categoryId] = $categoryId; } } return array_values($normalized); } function getProductCategoryIds($product) { if (isset($product["categories"])) { return normalizeProductCategoryIds($product["categories"]); } return normalizeProductCategoryIds($product["category"] ?? []); } function productHasCategory($product, $categoryId) { $categoryId = normalizeCategoryId($categoryId); if ($categoryId === "") { return false; } return in_array($categoryId, getProductCategoryIds($product), true); } function getProductSizes($product) { if (isset($product["sizes"]) && is_array($product["sizes"])) { $sizes = $product["sizes"]; } elseif (isset($product["sizes"]) && is_string($product["sizes"])) { $sizes = explode(",", $product["sizes"]); } else { $sizes = []; } $normalized = []; foreach ($sizes as $size) { $size = trim((string) $size); if ($size !== "") { $normalized[$size] = $size; } } return array_values($normalized); } function productUsesSizeStock($product) { return !empty(getProductSizes($product)); } function normalizeAvailabilityLabels($sizes, $labels) { $result = []; if (!is_array($labels)) { $labels = []; } foreach ($sizes as $size) { $text = trim((string) ($labels[$size] ?? "")); $result[$size] = $text; } return $result; } function getAvailabilityLabel($product, $size) { $labels = isset($product["availability_labels"]) && is_array($product["availability_labels"]) ? $product["availability_labels"] : []; return trim((string) ($labels[$size] ?? "")); } function normalizeProductRecord($product, $defaultCategoryId = "") { if (!is_array($product)) { return null; } $productId = isset($product["id"]) ? (int) $product["id"] : 0; $name = trim((string) ($product["name"] ?? "")); if ($productId <= 0 || $name === "") { return null; } $sizes = getProductSizes($product); if (empty($sizes)) { $sizes = ["Standard"]; } $categories = getProductCategoryIds($product); if (empty($categories) && $defaultCategoryId !== "") { $categories = [$defaultCategoryId]; } $availabilityLabels = normalizeAvailabilityLabels( $sizes, isset($product["availability_labels"]) && is_array($product["availability_labels"]) ? $product["availability_labels"] : [], ); return [ "id" => $productId, "name" => $name, "description" => trim((string) ($product["description"] ?? "")), "image" => trim((string) ($product["image"] ?? "")), "categories" => $categories, "sizes" => implode(",", $sizes), "availability_labels" => $availabilityLabels, ]; } function getProducts() { $data = readJsonFile(PRODUCTS_FILE); $rawProducts = isset($data["products"]) && is_array($data["products"]) ? $data["products"] : []; $categories = getCategories(); $defaultCategoryId = !empty($categories) ? $categories[0]["id"] : "apparel"; $products = []; foreach ($rawProducts as $product) { $normalized = normalizeProductRecord($product, $defaultCategoryId); if ($normalized !== null) { $products[] = $normalized; } } usort($products, function ($left, $right) { return strcasecmp($left["name"], $right["name"]); }); return $products; } function getProductById($id) { $id = (int) $id; foreach (getProducts() as $product) { if ((int) $product["id"] === $id) { return $product; } } return null; } function saveProducts($products) { $categories = getCategories(); $defaultCategoryId = !empty($categories) ? $categories[0]["id"] : "apparel"; $normalized = []; foreach ($products as $product) { $record = normalizeProductRecord($product, $defaultCategoryId); if ($record !== null) { $normalized[] = $record; } } return writeJsonFile(PRODUCTS_FILE, [ "products" => array_values($normalized), ]); } function getFaqFilePath(): string { $dataDir = defined("DATA_DIR") ? DATA_DIR : dirname(__DIR__) . "/data/"; $defaultPath = rtrim($dataDir, "/\\") . "/faq.json"; if (!defined("FAQ_FILE") || !is_string(FAQ_FILE) || FAQ_FILE === "") { return $defaultPath; } $configuredPath = FAQ_FILE; $normalizedDataDir = str_replace("\\", "/", rtrim($dataDir, "/\\")) . "/"; $normalizedConfigured = str_replace("\\", "/", $configuredPath); if (strpos($normalizedConfigured, $normalizedDataDir) !== 0) { return $defaultPath; } return $configuredPath; } function getFaqContent(): string { $defaultContent = "# FAQ\n\nHier kann der FAQ-Inhalt im Admin-Bereich bearbeitet werden."; $data = readJsonFile(getFaqFilePath()); if (!isset($data["content"]) || !is_string($data["content"])) { return $defaultContent; } return $data["content"]; } function saveFaqContent(string $markdown): bool { return writeJsonFile(getFaqFilePath(), ["content" => (string) $markdown]); } function renderFaqInlineMarkdown(string $text): string { $escaped = escape($text); $escaped = preg_replace_callback( '/\[([^\]]+)\]\(([^)\s]+)\)/', function ($matches) { $label = $matches[1]; $url = trim(html_entity_decode($matches[2], ENT_QUOTES, "UTF-8")); if ($url === "") { return $matches[0]; } $scheme = strtolower((string) parse_url($url, PHP_URL_SCHEME)); if (!in_array($scheme, ["http", "https", "mailto"], true)) { return $matches[0]; } return '' . $label . ""; }, $escaped, ); $escaped = preg_replace( "/\*\*(.+?)\*\*/s", '$1', $escaped, ); $escaped = preg_replace( "/(?$1', $escaped, ); return $escaped; } function renderFaqMarkdown(string $markdown): string { $normalized = str_replace(["\r\n", "\r"], "\n", $markdown); $lines = explode("\n", $normalized); $htmlParts = []; $paragraphLines = []; $listType = ""; $flushParagraph = function () use (&$paragraphLines, &$htmlParts): void { if (empty($paragraphLines)) { return; } $rendered = []; foreach ($paragraphLines as $line) { $rendered[] = renderFaqInlineMarkdown($line); } $htmlParts[] = "
" . implode("
\n", $rendered) . "
Keine FAQ-Inhalte vorhanden.
" : implode("\n", $htmlParts); } function getDefaultOrganizations() { return [ [ "id" => "feuerwehr-freising", "label" => "Amt 32 - Feuerwehr Freising", "sort_order" => 10, "active" => true, ], ]; } function normalizeOrganizationId($id) { return normalizeCategoryId($id); } function normalizeOrganizationLabel($label) { return trim((string) $label); } function isValidOrganizationLabel($label) { $label = normalizeOrganizationLabel($label); if ($label === "") { return false; } $length = function_exists("mb_strlen") ? mb_strlen($label) : strlen($label); return $length <= 120; } function normalizeOrganizations($organizations) { $normalized = []; if (!is_array($organizations)) { $organizations = []; } foreach ($organizations as $organization) { if (!is_array($organization)) { continue; } $id = normalizeOrganizationId($organization["id"] ?? ""); $label = normalizeOrganizationLabel($organization["label"] ?? ""); if ($id === "" || !isValidOrganizationLabel($label)) { continue; } $sortOrder = isset($organization["sort_order"]) ? (int) $organization["sort_order"] : 0; $active = !isset($organization["active"]) || (bool) $organization["active"]; $normalized[$id] = [ "id" => $id, "label" => $label, "sort_order" => $sortOrder, "active" => $active, ]; } if (empty($normalized)) { foreach (getDefaultOrganizations() as $organization) { $normalized[$organization["id"]] = $organization; } } uasort($normalized, function ($left, $right) { if ($left["sort_order"] === $right["sort_order"]) { return strcasecmp($left["label"], $right["label"]); } return $left["sort_order"] <=> $right["sort_order"]; }); return array_values($normalized); } function getOrganizations($onlyActive = false) { $data = readJsonFile(ORGANIZATIONS_FILE); $organizations = normalizeOrganizations($data["organizations"] ?? []); if ($onlyActive) { $organizations = array_values( array_filter($organizations, function ($organization) { return !empty($organization["active"]); }), ); } return $organizations; } function saveOrganizations($organizations) { return writeJsonFile(ORGANIZATIONS_FILE, [ "organizations" => normalizeOrganizations($organizations), ]); } function getOrganizationById($organizationId) { $organizationId = normalizeOrganizationId($organizationId); foreach (getOrganizations(false) as $organization) { if ($organization["id"] === $organizationId) { return $organization; } } return null; } function generateOrganizationIdFromLabel($label, $existingOrganizations = []) { $baseId = normalizeOrganizationId($label); if ($baseId === "") { $baseId = "organization"; } $used = []; foreach (normalizeOrganizations($existingOrganizations) as $organization) { $used[$organization["id"]] = true; } $candidate = $baseId; $counter = 2; while (isset($used[$candidate])) { $candidate = $baseId . "-" . $counter; $counter++; } return $candidate; } function getDefaultSystemSettings() { $startpageIntroText = ""; if (defined("DISCLAIMER_LINES") && is_array(DISCLAIMER_LINES)) { $lines = array_filter( array_map(function ($line) { return trim((string) $line); }, DISCLAIMER_LINES), function ($line) { return $line !== ""; }, ); $startpageIntroText = implode("\n", $lines); } return [ "order_recipient_email" => defined("ORDER_RECIPIENT_EMAIL") ? ORDER_RECIPIENT_EMAIL : getDefaultAdminEmail(), "attach_order_pdf_to_admin_email" => defined( "ATTACH_ORDER_PDF_TO_ADMIN_EMAIL", ) ? (bool) ATTACH_ORDER_PDF_TO_ADMIN_EMAIL : true, "startpage_intro_text" => $startpageIntroText, ]; } function normalizeSystemSettings($settings) { $defaults = getDefaultSystemSettings(); if (!is_array($settings)) { $settings = []; } $recipientEmail = normalizeAdminEmail( $settings["order_recipient_email"] ?? $defaults["order_recipient_email"], ); if (!isValidAdminEmail($recipientEmail)) { $recipientEmail = $defaults["order_recipient_email"]; } $startpageIntroText = trim( (string) ($settings["startpage_intro_text"] ?? $defaults["startpage_intro_text"]), ); return [ "order_recipient_email" => $recipientEmail, "attach_order_pdf_to_admin_email" => !empty( $settings["attach_order_pdf_to_admin_email"] ), "startpage_intro_text" => $startpageIntroText, ]; } function getSystemSettings() { $data = readJsonFile(SETTINGS_FILE); return normalizeSystemSettings($data["settings"] ?? []); } function saveSystemSettings($settings) { return writeJsonFile(SETTINGS_FILE, [ "settings" => normalizeSystemSettings($settings), ]); } function getOrderRecipientEmail() { $settings = getSystemSettings(); return $settings["order_recipient_email"]; } function shouldAttachOrderPdfToAdminEmail() { $settings = getSystemSettings(); return !empty($settings["attach_order_pdf_to_admin_email"]); } function getStartpageIntroLines() { $settings = getSystemSettings(); $text = trim((string) ($settings["startpage_intro_text"] ?? "")); if ($text === "") { return []; } $lines = preg_split('/\R+/', $text) ?: []; $lines = array_values( array_filter( array_map(function ($line) { return trim((string) $line); }, $lines), function ($line) { return $line !== ""; }, ), ); return $lines; } function normalizeOrderItem($item) { if (!is_array($item)) { return null; } $productId = isset($item["product_id"]) ? (int) $item["product_id"] : 0; if ($productId <= 0) { return null; } $product = getProductById($productId); if ($product === null) { return null; } $size = trim((string) ($item["size"] ?? "")); $sizes = getProductSizes($product); if (!empty($sizes)) { if ($size === "" || !in_array($size, $sizes, true)) { return null; } } else { $size = ""; } $backorderStatus = trim((string) ($item["backorder_status"] ?? "")); $allowedBackorderStatuses = ["", "to_be_backordered", "ordered"]; if (!in_array($backorderStatus, $allowedBackorderStatuses, true)) { $backorderStatus = ""; } return [ "product_id" => $productId, "product_name" => $product["name"], "size" => $size, "availability_label" => $size !== "" ? getAvailabilityLabel($product, $size) : "", "is_processed" => !empty($item["is_processed"]), "backorder_status" => $backorderStatus, "backordered_at" => trim((string) ($item["backordered_at"] ?? "")), "ordered_at" => trim((string) ($item["ordered_at"] ?? "")), ]; } function normalizeOrderItems($items) { $normalized = []; $seen = []; if (!is_array($items)) { return []; } foreach ($items as $item) { $record = normalizeOrderItem($item); if ($record === null) { continue; } $key = $record["product_id"] . "|" . $record["size"]; if (isset($seen[$key])) { continue; } $seen[$key] = true; $normalized[] = $record; } return array_values($normalized); } function getOrders() { $data = readJsonFile(ORDERS_FILE); $orders = isset($data["orders"]) && is_array($data["orders"]) ? $data["orders"] : []; $normalized = []; foreach ($orders as $order) { $record = normalizeOrderRecord($order); if ($record !== null) { $normalized[] = $record; } } return $normalized; } function saveOrders($orders) { $normalized = []; foreach ($orders as $order) { $record = normalizeOrderRecord($order); if ($record !== null) { $normalized[] = $record; } } $result = writeJsonFile(ORDERS_FILE, [ "orders" => array_values($normalized), ]); if (!$result) { logError("Failed to save orders", [ "file" => ORDERS_FILE, "order_count" => count($normalized), ]); } return (bool) $result; } function generateOrderId() { $orders = getOrders(); $year = date("Y"); $prefix = defined("ORDER_PREFIX") ? ORDER_PREFIX : "ORD"; $max = 0; $pattern = "/^" . preg_quote($prefix, "/") . '-\d{4}-(\d+)$/'; foreach ($orders as $order) { if (preg_match($pattern, (string) $order["id"], $matches) === 1) { $number = (int) $matches[1]; if ($number > $max) { $max = $number; } } } return sprintf("%s-%s-%03d", $prefix, $year, $max + 1); } function normalizeOrderRecord($order) { if (!is_array($order)) { return null; } $id = trim((string) ($order["id"] ?? "")); $customerName = trim((string) ($order["customer_name"] ?? "")); $customerEmail = normalizeAdminEmail($order["customer_email"] ?? ""); $organizationId = normalizeOrganizationId($order["organization_id"] ?? ""); $organizationLabel = trim((string) ($order["organization_label"] ?? "")); $items = normalizeOrderItems($order["items"] ?? []); if ( $id === "" || $customerName === "" || !isValidAdminEmail($customerEmail) || $organizationId === "" || $organizationLabel === "" || empty($items) ) { return null; } $createdAt = trim((string) ($order["created_at"] ?? "")); if ($createdAt === "") { $createdAt = date("Y-m-d H:i:s"); } $status = trim((string) ($order["status"] ?? "open")); $allowedStatuses = ["open", "partial", "processed", "cancelled"]; if (!in_array($status, $allowedStatuses, true)) { $status = "open"; } $normalized = [ "id" => $id, "customer_name" => $customerName, "customer_email" => $customerEmail, "organization_id" => $organizationId, "organization_label" => $organizationLabel, "comment" => trim((string) ($order["comment"] ?? "")), "items" => $items, "status" => $status, "created_at" => $createdAt, "updated_at" => trim((string) ($order["updated_at"] ?? $createdAt)), "cancelled_at" => trim((string) ($order["cancelled_at"] ?? "")), "cancelled_by" => trim((string) ($order["cancelled_by"] ?? "")), "cancellation_reason" => trim( (string) ($order["cancellation_reason"] ?? ""), ), "admin_notified_at" => trim( (string) ($order["admin_notified_at"] ?? ""), ), ]; return refreshOrderState($normalized); } function refreshOrderState($order) { if (!is_array($order)) { return null; } if (($order["status"] ?? "") === "cancelled") { return $order; } $processedCount = 0; foreach ($order["items"] as $item) { if (!empty($item["is_processed"])) { $processedCount++; } } if ($processedCount <= 0) { $order["status"] = "open"; } elseif ($processedCount >= count($order["items"])) { $order["status"] = "processed"; } else { $order["status"] = "partial"; } return $order; } function getOrderById($orderId) { $orderId = trim((string) $orderId); if ($orderId === "") { return null; } foreach (getOrders() as $order) { if ($order["id"] === $orderId) { return $order; } } return null; } function createOrder( $customerName, $customerEmail, $organizationId, $comment, $items, ) { $customerName = sanitize($customerName); $customerEmail = normalizeAdminEmail($customerEmail); $organizationId = normalizeOrganizationId($organizationId); $comment = trim((string) $comment); $items = normalizeOrderItems($items); if ($customerName === "") { return [ "success" => false, "message" => "Bitte geben Sie einen Namen ein.", ]; } if (!isValidAdminEmail($customerEmail)) { return [ "success" => false, "message" => "Bitte geben Sie eine gültige E-Mail-Adresse ein.", ]; } if (empty($items)) { return [ "success" => false, "message" => "Der Warenkorb ist leer oder enthält ungültige Positionen.", ]; } $organization = getOrganizationById($organizationId); if ($organization === null || empty($organization["active"])) { return [ "success" => false, "message" => "Bitte wählen Sie eine gültige Organisation aus.", ]; } $now = date("Y-m-d H:i:s"); $order = [ "id" => generateOrderId(), "customer_name" => $customerName, "customer_email" => $customerEmail, "organization_id" => $organization["id"], "organization_label" => $organization["label"], "comment" => $comment, "items" => $items, "status" => "open", "created_at" => $now, "updated_at" => $now, "cancelled_at" => "", "cancelled_by" => "", "cancellation_reason" => "", "admin_notified_at" => "", ]; $orders = getOrders(); $orders[] = $order; saveOrders($orders); logAccess("Order created in createOrder", [ "order_id" => $order["id"], "customer" => $customerEmail, "organization" => $organization["label"], ]); $result = sendConfirmedOrderAdminNotification($order); if ($result) { markOrderAdminNotified($order["id"]); $order = getOrderById($order["id"]); } sendOrderCreatedCustomerEmail($order); return ["success" => true, "order" => $order]; } function markOrderAdminNotified($orderId) { $orders = getOrders(); foreach ($orders as &$order) { if ($order["id"] !== $orderId) { continue; } $order["admin_notified_at"] = date("Y-m-d H:i:s"); $order["updated_at"] = date("Y-m-d H:i:s"); break; } unset($order); saveOrders($orders); } function toggleOrderItemProcessed($orderId, $itemIndex) { $orders = getOrders(); $now = date("Y-m-d H:i:s"); foreach ($orders as &$order) { if ($order["id"] !== $orderId) { continue; } if ($order["status"] === "cancelled") { return [ "success" => false, "message" => "Stornierte Bestellungen können nicht mehr bearbeitet werden.", ]; } if (!isset($order["items"][$itemIndex])) { return [ "success" => false, "message" => "Position nicht gefunden.", ]; } $order["items"][$itemIndex]["is_processed"] = empty( $order["items"][$itemIndex]["is_processed"] ); $order["updated_at"] = $now; $order = refreshOrderState($order); saveOrders($orders); return ["success" => true, "order" => $order]; } unset($order); return ["success" => false, "message" => "Bestellung nicht gefunden."]; } function cancelOrder($orderId, $adminUsername, $reason = "") { $orders = getOrders(); $now = date("Y-m-d H:i:s"); $adminUsername = normalizeAdminUsername($adminUsername); $reason = trim((string) $reason); foreach ($orders as &$order) { if ($order["id"] !== $orderId) { continue; } if ($order["status"] === "cancelled") { return [ "success" => false, "message" => "Die Bestellung ist bereits storniert.", ]; } $order["status"] = "cancelled"; $order["cancelled_at"] = $now; $order["cancelled_by"] = $adminUsername; $order["cancellation_reason"] = $reason; $order["updated_at"] = $now; saveOrders($orders); return ["success" => true, "order" => $order]; } unset($order); return ["success" => false, "message" => "Bestellung nicht gefunden."]; } function uncancelOrder($orderId) { $orders = getOrders(); $now = date("Y-m-d H:i:s"); foreach ($orders as &$order) { if ($order["id"] !== $orderId) { continue; } if ($order["status"] !== "cancelled") { return [ "success" => false, "message" => "Die Bestellung ist nicht storniert.", ]; } $order["cancelled_at"] = ""; $order["cancelled_by"] = ""; $order["cancellation_reason"] = ""; $order["status"] = "open"; $order["updated_at"] = $now; $order = refreshOrderState($order); saveOrders($orders); return ["success" => true, "order" => $order]; } unset($order); return ["success" => false, "message" => "Bestellung nicht gefunden."]; } function orderItemCanBeManaged($order) { if (($order["status"] ?? "") === "cancelled") { return [ "success" => false, "message" => "Stornierte Bestellungen können nicht mehr bearbeitet werden.", ]; } return ["success" => true]; } function setOrderItemBackorderStatus($orderId, $itemIndex, $status) { $allowedStatuses = ["", "to_be_backordered"]; if (!in_array($status, $allowedStatuses, true)) { return ["success" => false, "message" => "Ungültiger Nachbestellstatus."]; } $orders = getOrders(); $now = date("Y-m-d H:i:s"); foreach ($orders as &$order) { if ($order["id"] !== $orderId) { continue; } $guard = orderItemCanBeManaged($order); if (!$guard["success"]) { return $guard; } if (!isset($order["items"][$itemIndex])) { return [ "success" => false, "message" => "Position nicht gefunden.", ]; } if ($status === "to_be_backordered") { if (!empty($order["items"][$itemIndex]["is_processed"])) { return [ "success" => false, "message" => "Bearbeitete Positionen können nicht als Nachbestellung markiert werden.", ]; } $order["items"][$itemIndex]["backorder_status"] = "to_be_backordered"; $order["items"][$itemIndex]["backordered_at"] = $now; $order["items"][$itemIndex]["ordered_at"] = ""; } else { $order["items"][$itemIndex]["backorder_status"] = ""; $order["items"][$itemIndex]["backordered_at"] = ""; $order["items"][$itemIndex]["ordered_at"] = ""; } $order["updated_at"] = $now; saveOrders($orders); return ["success" => true, "order" => $order]; } unset($order); return ["success" => false, "message" => "Bestellung nicht gefunden."]; } function toggleOrderItemBackorder($orderId, $itemIndex) { $orders = getOrders(); foreach ($orders as $order) { if ($order["id"] !== $orderId) { continue; } if (!isset($order["items"][$itemIndex])) { return [ "success" => false, "message" => "Position nicht gefunden.", ]; } $current = (string) ($order["items"][$itemIndex]["backorder_status"] ?? ""); $newStatus = $current === "to_be_backordered" ? "" : "to_be_backordered"; return setOrderItemBackorderStatus($orderId, $itemIndex, $newStatus); } return ["success" => false, "message" => "Bestellung nicht gefunden."]; } function getManualBackorders() { $data = readJsonFile(MANUAL_BACKORDERS_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"] ?? "")); $productId = (int) ($entry["product_id"] ?? 0); $productName = trim((string) ($entry["product_name"] ?? "")); $size = trim((string) ($entry["size"] ?? "")); $status = trim((string) ($entry["backorder_status"] ?? "")); if ($id === "" || $productId <= 0 || $productName === "" || $status === "") { continue; } if (!in_array($status, ["to_be_backordered", "ordered"], true)) { continue; } $normalized[] = [ "id" => $id, "product_id" => $productId, "product_name" => $productName, "size" => $size, "backorder_status" => $status, "backordered_at" => trim((string) ($entry["backordered_at"] ?? "")), "ordered_at" => trim((string) ($entry["ordered_at"] ?? "")), "created_at" => trim((string) ($entry["created_at"] ?? "")), ]; } return $normalized; } function saveManualBackorders($entries) { return writeJsonFile(MANUAL_BACKORDERS_FILE, [ "entries" => array_values($entries), ]); } function generateManualBackorderId() { return "mb-" . date("YmdHis") . "-" . bin2hex(random_bytes(4)); } function addManualBackorderItems($productId, $size, $quantity) { $productId = (int) $productId; $size = trim((string) $size); $quantity = (int) $quantity; if ($productId <= 0 || $quantity <= 0) { return [ "success" => false, "message" => "Ungültige Menge oder Artikel.", ]; } if ($quantity > 100) { return [ "success" => false, "message" => "Maximal 100 Positionen auf einmal.", ]; } $product = getProductById($productId); if ($product === null) { return ["success" => false, "message" => "Artikel nicht gefunden."]; } $allowedSizes = getProductSizes($product); if (empty($allowedSizes)) { $allowedSizes = ["Standard"]; } if (!in_array($size, $allowedSizes, true)) { return ["success" => false, "message" => "Ungültige Größe für diesen Artikel."]; } $entries = getManualBackorders(); $now = date("Y-m-d H:i:s"); for ($i = 0; $i < $quantity; $i++) { $entries[] = [ "id" => generateManualBackorderId(), "product_id" => $productId, "product_name" => $product["name"], "size" => $size, "backorder_status" => "to_be_backordered", "backordered_at" => $now, "ordered_at" => "", "created_at" => $now, ]; } if (!saveManualBackorders($entries)) { return [ "success" => false, "message" => "Nachbestellungen konnten nicht gespeichert werden.", ]; } return ["success" => true, "added" => $quantity]; } function collectBackorderItemRefs() { $refs = []; foreach (getManualBackorders() as $entry) { $refs[] = [ "order_id" => "", "item_index" => 0, "manual_id" => $entry["id"], "is_manual" => true, "product_id" => $entry["product_id"], "product_name" => $entry["product_name"], "size" => $entry["size"], "backorder_status" => $entry["backorder_status"], "created_at" => $entry["created_at"] !== "" ? $entry["created_at"] : $entry["backordered_at"], "sort_at" => $entry["backorder_status"] === "ordered" ? ($entry["ordered_at"] !== "" ? $entry["ordered_at"] : $entry["created_at"]) : ($entry["backordered_at"] !== "" ? $entry["backordered_at"] : $entry["created_at"]), ]; } foreach (getOrders() as $order) { if (($order["status"] ?? "") === "cancelled") { continue; } foreach ($order["items"] as $itemIndex => $item) { $status = (string) ($item["backorder_status"] ?? ""); if ($status === "") { continue; } $refs[] = [ "order_id" => $order["id"], "item_index" => (int) $itemIndex, "product_id" => (int) $item["product_id"], "product_name" => $item["product_name"], "size" => $item["size"], "backorder_status" => $status, "created_at" => $order["created_at"], "sort_at" => $status === "ordered" ? ($item["ordered_at"] !== "" ? $item["ordered_at"] : $order["created_at"]) : ($item["backordered_at"] !== "" ? $item["backordered_at"] : $order["created_at"]), ]; } } return $refs; } function getBackorderGroups() { $refs = collectBackorderItemRefs(); $groups = []; foreach ($refs as $ref) { $key = $ref["product_id"] . "|" . $ref["size"]; if (!isset($groups[$key])) { $groups[$key] = [ "product_id" => $ref["product_id"], "product_name" => $ref["product_name"], "size" => $ref["size"], "to_be_backordered" => 0, "ordered" => 0, "items_to_be_backordered" => [], "items_ordered" => [], ]; } if ($ref["backorder_status"] === "to_be_backordered") { $groups[$key]["to_be_backordered"]++; $groups[$key]["items_to_be_backordered"][] = $ref; } elseif ($ref["backorder_status"] === "ordered") { $groups[$key]["ordered"]++; $groups[$key]["items_ordered"][] = $ref; } } foreach ($groups as &$group) { usort($group["items_to_be_backordered"], function ($left, $right) { $cmp = strcmp($left["created_at"], $right["created_at"]); if ($cmp !== 0) { return $cmp; } return strcmp($left["order_id"], $right["order_id"]); }); usort($group["items_ordered"], function ($left, $right) { $cmp = strcmp($left["sort_at"], $right["sort_at"]); if ($cmp !== 0) { return $cmp; } return strcmp($left["order_id"], $right["order_id"]); }); } unset($group); $result = array_values($groups); usort($result, function ($left, $right) { $cmp = strcmp($left["product_name"], $right["product_name"]); if ($cmp !== 0) { return $cmp; } return strcmp($left["size"], $right["size"]); }); return $result; } function applyBackorderBulkUpdate($productId, $size, $fromStatus, $toStatus, $quantity) { $productId = (int) $productId; $size = trim((string) $size); $quantity = (int) $quantity; if ($productId <= 0 || $quantity <= 0) { return [ "success" => false, "message" => "Ungültige Menge oder Artikel.", ]; } $refs = collectBackorderItemRefs(); $candidates = []; foreach ($refs as $ref) { if ( $ref["product_id"] !== $productId || $ref["size"] !== $size || $ref["backorder_status"] !== $fromStatus ) { continue; } $candidates[] = $ref; } usort($candidates, function ($left, $right) { $cmp = strcmp($left["created_at"], $right["created_at"]); if ($cmp !== 0) { return $cmp; } return strcmp($left["order_id"], $right["order_id"]); }); if ($quantity > count($candidates)) { return [ "success" => false, "message" => "Nur " . count($candidates) . " Position(en) verfügbar, " . $quantity . " angefordert.", ]; } $targets = array_slice($candidates, 0, $quantity); $orders = getOrders(); $manualEntries = getManualBackorders(); $manualChanged = false; $now = date("Y-m-d H:i:s"); $updated = 0; foreach ($targets as $target) { if (!empty($target["is_manual"])) { $manualId = (string) ($target["manual_id"] ?? ""); foreach ($manualEntries as &$entry) { if ($entry["id"] !== $manualId) { continue; } if ($toStatus === "ordered") { $entry["backorder_status"] = "ordered"; $entry["ordered_at"] = $now; } else { $entry["backorder_status"] = ""; $entry["ordered_at"] = ""; } $manualChanged = true; $updated++; break; } unset($entry); continue; } foreach ($orders as &$order) { if ($order["id"] !== $target["order_id"]) { continue; } $itemIndex = $target["item_index"]; if (!isset($order["items"][$itemIndex])) { continue 2; } if ($toStatus === "ordered") { $order["items"][$itemIndex]["backorder_status"] = "ordered"; $order["items"][$itemIndex]["ordered_at"] = $now; } else { $order["items"][$itemIndex]["backorder_status"] = ""; $order["items"][$itemIndex]["ordered_at"] = ""; } $order["updated_at"] = $now; $updated++; break; } unset($order); } if ($updated === 0) { return [ "success" => false, "message" => "Keine Positionen aktualisiert.", ]; } if ($manualChanged) { $manualEntries = array_values( array_filter($manualEntries, function ($entry) { return ($entry["backorder_status"] ?? "") !== ""; }), ); if (!saveManualBackorders($manualEntries)) { return [ "success" => false, "message" => "Manuelle Nachbestellungen konnten nicht gespeichert werden.", ]; } } saveOrders($orders); return ["success" => true, "updated" => $updated]; } function markBackorderItemsOrdered($productId, $size, $quantity) { return applyBackorderBulkUpdate( $productId, $size, "to_be_backordered", "ordered", $quantity, ); } function markBackorderItemsDelivered($productId, $size, $quantity) { return applyBackorderBulkUpdate($productId, $size, "ordered", "", $quantity); } function orderHasBackorder($order) { if (!is_array($order["items"] ?? null)) { return false; } foreach ($order["items"] as $item) { if (($item["backorder_status"] ?? "") !== "") { return true; } } return false; } function getBackorderStatusLabel($status) { switch ((string) $status) { case "to_be_backordered": return "Nachzubestellen"; case "ordered": return "Wartet auf Lieferung"; default: return "-"; } } function getBackorderStatusClass($status) { switch ((string) $status) { case "to_be_backordered": return "status-backorder"; case "ordered": return "status-backorder-waiting"; default: return ""; } } function getOrderStatusLabel($order) { if (($order["status"] ?? "") === "cancelled") { return "Storniert"; } if (($order["status"] ?? "") === "processed") { return "Bearbeitet"; } if (($order["status"] ?? "") === "partial") { return "Teilweise bearbeitet"; } return "Offen"; } function getOrderStatusClass($order) { if (($order["status"] ?? "") === "cancelled") { return "status-cancelled"; } if (($order["status"] ?? "") === "processed") { return "status-processed"; } if (($order["status"] ?? "") === "partial") { return "status-partial"; } return "status-open"; } function formatDate($dateString) { $dateString = trim((string) $dateString); if ($dateString === "") { return "-"; } try { $date = new DateTimeImmutable($dateString); return $date->format("d.m.Y H:i"); } catch (Exception $exception) { return $dateString; } } function getCart() { $cart = $_SESSION["cart"] ?? []; if (!is_array($cart)) { $cart = []; } $normalized = []; foreach ($cart as $item) { $productId = isset($item["product_id"]) ? (int) $item["product_id"] : 0; $size = trim((string) ($item["size"] ?? "")); $product = getProductById($productId); if ($product === null) { continue; } $sizes = getProductSizes($product); if (!empty($sizes)) { if ($size === "" || !in_array($size, $sizes, true)) { continue; } } else { $size = ""; } if (isset($normalized[$productId])) { unset($normalized[$productId]); } $normalized[$productId] = [ "product_id" => $productId, "size" => $size, ]; } $_SESSION["cart"] = array_values($normalized); return $_SESSION["cart"]; } function addCartItem($productId, $size = "") { $productId = (int) $productId; $size = trim((string) $size); $product = getProductById($productId); if ($product === null) { return [ "success" => false, "status" => "error", ]; } $sizes = getProductSizes($product); if (!empty($sizes)) { if ($size === "" || !in_array($size, $sizes, true)) { return [ "success" => false, "status" => "error", ]; } } else { $size = ""; } $cart = getCart(); foreach ($cart as $index => $item) { if ((int) $item["product_id"] !== $productId) { continue; } $existingSize = trim((string) ($item["size"] ?? "")); if ($existingSize === $size) { return [ "success" => true, "status" => "unchanged", "size" => $size, ]; } $cart[$index]["size"] = $size; $_SESSION["cart"] = array_values($cart); return [ "success" => true, "status" => "replaced", "size" => $size, "previous_size" => $existingSize, ]; } $cart[] = [ "product_id" => $productId, "size" => $size, ]; $_SESSION["cart"] = array_values($cart); return [ "success" => true, "status" => "added", "size" => $size, ]; } function removeCartItemByIndex($index) { $cart = getCart(); if (isset($cart[$index])) { unset($cart[$index]); $_SESSION["cart"] = array_values($cart); } } function clearCart() { $_SESSION["cart"] = []; } function getCartItemsDetailed() { $items = []; foreach (getCart() as $index => $cartItem) { $product = getProductById($cartItem["product_id"]); if ($product === null) { continue; } $size = trim((string) ($cartItem["size"] ?? "")); $items[] = [ "cart_index" => $index, "product" => $product, "size" => $size, "availability_label" => $size !== "" ? getAvailabilityLabel($product, $size) : "", ]; } return $items; } function buildOrderItemsFromCart() { $items = []; foreach (getCart() as $cartItem) { $product = getProductById($cartItem["product_id"]); if ($product === null) { continue; } $size = trim((string) ($cartItem["size"] ?? "")); $items[] = [ "product_id" => $product["id"], "size" => $size, "is_processed" => false, ]; } return normalizeOrderItems($items); } function buildOrderItemsHtml($order) { $parts = []; foreach ($order["items"] as $item) { $label = "" . escape($item["product_name"]) . ""; if ($item["size"] !== "") { $label .= " - Größe: " . escape($item["size"]); } if (!empty($item["availability_label"])) { $label .= "' . escape(SITE_DEPARTMENT_NAME) . '
' . escape(SITE_SERVICE_HEADER) . '
' . escape($order["id"]) . '
Name: ' . escape($order["customer_name"]) . '
E-Mail: ' . escape($order["customer_email"]) . '
Organisation: ' . escape($order["organization_label"]) . '
Erstellt am: ' . escape(formatDate($order["created_at"])) . '
Kommentar:
' .
($order["comment"] !== ""
? nl2br(escape($order["comment"]))
: "Kein Kommentar") .
'
' .
escape(SITE_NAME) .
" | " .
escape(SITE_DEPARTMENT_NAME) .
"
" .
escape(SITE_ADDRESS_LINE) .
'
Guten Tag " . escape($order["customer_name"]) . ",
Ihre Bestellung wurde erfasst und an " . escape(SITE_DEPARTMENT_NAME) . " weitergeleitet.
"; $message = buildOrderSummaryHtml($order, "Bestellung eingegangen", $intro); return sendEmail($order["customer_email"], $subject, $message); } function sendConfirmedOrderAdminNotification($order) { $recipient = getOrderRecipientEmail(); if (!isValidAdminEmail($recipient)) { return false; } $subject = SITE_SERVICE_NAME . ": Neue Bestellung - " . $order["id"]; $intro = "Eine neue PSA-Bestellung ist eingegangen und muss bearbeitet werden.
"; $message = buildOrderSummaryHtml($order, "Neue PSA-Bestellung", $intro); $attachments = []; if (shouldAttachOrderPdfToAdminEmail()) { $attachments[] = [ "filename" => "bestellung-" . strtolower($order["id"]) . ".pdf", "content_type" => "application/pdf", "content" => renderOrderPdf($order), ]; } return sendEmail($recipient, $subject, $message, true, $attachments); } function sendEmail($to, $subject, $message, $isHtml = true, $attachments = []) { $headers = []; $headers[] = "From: " . FROM_NAME . " <" . FROM_EMAIL . ">"; $headers[] = "Reply-To: " . FROM_EMAIL; $headers[] = "X-Mailer: PHP/" . phpversion(); if (empty($attachments)) { if ($isHtml) { $headers[] = "MIME-Version: 1.0"; $headers[] = "Content-Type: text/html; charset=UTF-8"; } else { $headers[] = "Content-Type: text/plain; charset=UTF-8"; } return mail($to, $subject, $message, implode("\r\n", $headers)); } $boundary = "=_Boundary_" . bin2hex(random_bytes(8)); $headers[] = "MIME-Version: 1.0"; $headers[] = 'Content-Type: multipart/mixed; boundary="' . $boundary . '"'; $body = []; $body[] = "--" . $boundary; $body[] = "Content-Type: " . ($isHtml ? "text/html" : "text/plain") . "; charset=UTF-8"; $body[] = "Content-Transfer-Encoding: 8bit"; $body[] = ""; $body[] = $message; foreach ($attachments as $attachment) { if (!is_array($attachment)) { continue; } $filename = trim( (string) ($attachment["filename"] ?? "attachment.bin"), ); $contentType = trim( (string) ($attachment["content_type"] ?? "application/octet-stream"), ); $content = isset($attachment["content"]) ? (string) $attachment["content"] : ""; $body[] = "--" . $boundary; $body[] = "Content-Type: " . $contentType . '; name="' . $filename . '"'; $body[] = "Content-Transfer-Encoding: base64"; $body[] = 'Content-Disposition: attachment; filename="' . $filename . '"'; $body[] = ""; $body[] = chunk_split(base64_encode($content)); } $body[] = "--" . $boundary . "--"; $body[] = ""; return mail( $to, $subject, implode("\r\n", $body), implode("\r\n", $headers), ); } function pdfEncodeWinAnsi($text) { $text = str_replace("\r", "", (string) $text); if (!function_exists("iconv")) { return preg_replace('/[^\x09\x0A\x20-\x7E]/', "?", $text); } $chars = preg_split("//u", $text, -1, PREG_SPLIT_NO_EMPTY); if (!is_array($chars)) { $fallback = @iconv("UTF-8", "Windows-1252//TRANSLIT//IGNORE", $text); return is_string($fallback) ? $fallback : $text; } $result = ""; foreach ($chars as $char) { $converted = @iconv("UTF-8", "Windows-1252", $char); if ($converted !== false) { $result .= $converted; continue; } $fallback = @iconv("UTF-8", "Windows-1252//TRANSLIT", $char); if ($fallback !== false && $fallback !== "") { $result .= $fallback; continue; } $result .= "?"; } return $result; } function pdfEscapeText($text) { $text = str_replace("\\", "\\\\", $text); $text = str_replace("(", "\(", $text); $text = str_replace(")", "\)", $text); return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', "", $text); } function pdfWrapAnsiText($text, $maxChars) { $lines = []; $paragraphs = explode("\n", str_replace("\r", "", (string) $text)); foreach ($paragraphs as $paragraph) { $normalized = trim(preg_replace("/\s+/", " ", $paragraph)); if ($normalized === "") { $lines[] = ""; continue; } $words = explode(" ", $normalized); $current = ""; foreach ($words as $word) { if ($word === "") { continue; } if (strlen($word) > $maxChars) { if ($current !== "") { $lines[] = $current; $current = ""; } $parts = str_split($word, $maxChars); $lastIndex = count($parts) - 1; for ($i = 0; $i < $lastIndex; $i++) { $lines[] = $parts[$i]; } $current = $parts[$lastIndex]; continue; } $candidate = $current === "" ? $word : $current . " " . $word; if (strlen($candidate) <= $maxChars) { $current = $candidate; } else { if ($current !== "") { $lines[] = $current; } $current = $word; } } if ($current !== "") { $lines[] = $current; } } if (empty($lines)) { $lines[] = ""; } return $lines; } function prepareOrderForDocument(array $order): array { $items = []; foreach ($order["items"] ?? [] as $item) { if (!is_array($item)) { continue; } $items[] = [ "product_name" => trim((string) ($item["product_name"] ?? "")), "size" => trim((string) ($item["size"] ?? "")), "availability_label" => trim( (string) ($item["availability_label"] ?? ""), ), ]; } return [ "id" => trim((string) ($order["id"] ?? "")), "created_at" => trim((string) ($order["created_at"] ?? "")), "customer_name" => trim((string) ($order["customer_name"] ?? "")), "customer_email" => trim((string) ($order["customer_email"] ?? "")), "organization_label" => trim( (string) ($order["organization_label"] ?? ""), ), "comment" => (string) ($order["comment"] ?? ""), "items" => $items, ]; } function renderOrderPdf(array $order): string { return generateOrderPdf(prepareOrderForDocument($order)); } function streamOrderPdf( array $order, string $filename, bool $inline = true, ): void { $pdf = renderOrderPdf($order); $safeFilename = preg_replace('/[^A-Za-z0-9._-]+/', "-", $filename) ?: "bestellung.pdf"; $disposition = $inline ? "inline" : "attachment"; header("Content-Type: application/pdf"); header( 'Content-Disposition: ' . $disposition . '; filename="' . $safeFilename . '"', ); header("Content-Length: " . strlen($pdf)); echo $pdf; exit(); } function generateOrderPdf($order) { $pageWidth = 595; $pageHeight = 842; $leftMargin = 45; $topY = 800; $bottomY = 60; $lineHeight = 14; $itemLineHeight = 16; $pages = []; $pageContent = ""; $y = $topY; $pageNumber = 0; $encodeText = function ($text) { return pdfEncodeWinAnsi((string) $text); }; $orderId = $encodeText($order["id"]); $createdAt = $encodeText(formatDate($order["created_at"])); $customerName = $encodeText($order["customer_name"]); $customerEmail = $encodeText($order["customer_email"]); $organization = $encodeText($order["organization_label"]); $commentRaw = (string) $order["comment"]; $writeText = function ($x, $y, $encodedText, $fontSize = 12) use (&$pageContent) { $pageContent .= "BT\n/F1 " . $fontSize . " Tf\n1 0 0 1 " . number_format($x, 2, ".", "") . " " . number_format($y, 2, ".", "") . " Tm\n(" . pdfEscapeText($encodedText) . ") Tj\nET\n"; }; $startPage = function () use ( &$pages, &$pageContent, &$y, &$pageNumber, $topY, $leftMargin, $lineHeight, $orderId, $createdAt, $writeText, $encodeText ) { if ($pageContent !== "") { $pages[] = $pageContent; } $pageNumber++; $pageContent = ""; $y = $topY; $headerLine = "Bestellung: " . $orderId; if ($pageNumber > 1) { $headerLine .= " | Seite " . $encodeText((string) $pageNumber); } $writeText($leftMargin, $y, $headerLine, 14); $y -= 19; $writeText($leftMargin, $y, "Erstellt am: " . $createdAt, 11); $y -= 20; }; $ensureSpace = function ($requiredHeight) use (&$y, $bottomY, $startPage) { if ($y - $requiredHeight < $bottomY) { $startPage(); } }; $writeWrapped = function ( $encodedText, $maxChars, $fontSize = 11, $x = null, $spacing = null ) use (&$y, $lineHeight, $leftMargin, $writeText, $ensureSpace) { $targetX = $x === null ? $leftMargin : $x; $step = $spacing === null ? $lineHeight : $spacing; $lines = pdfWrapAnsiText($encodedText, $maxChars); foreach ($lines as $line) { $ensureSpace($step); $writeText($targetX, $y, $line, $fontSize); $y -= $step; } }; $writeSectionTitle = function ($titleText, $fontSize = 13) use ( &$y, $leftMargin, $writeText, $ensureSpace, $encodeText ) { $ensureSpace(24); $writeText($leftMargin, $y, $encodeText($titleText), $fontSize); $y -= 18; }; $startPage(); $writeSectionTitle("Gehört zu"); $writeWrapped("Name: " . $customerName, 80); $writeWrapped("Organisation: " . $organization, 80); $writeWrapped("E-Mail: " . $customerEmail, 80); $y -= 6; $writeSectionTitle("Artikelliste", 15); $itemNumber = 1; if (empty($order["items"])) { $writeWrapped($encodeText("Keine Artikel"), 80, 12); } else { foreach ($order["items"] as $item) { $itemName = $encodeText($item["product_name"]); $sizeLabel = $encodeText($item["size"]); $hintLabel = $encodeText( preg_replace("/\s+/", " ", (string) $item["availability_label"]), ); $writeWrapped( $encodeText($itemNumber . ". ") . $itemName, 68, 14, null, $itemLineHeight, ); if ($sizeLabel !== "") { $writeWrapped( $encodeText(" Größe: ") . $sizeLabel, 66, 12, null, $itemLineHeight, ); } if ($hintLabel !== "") { $writeWrapped( $encodeText(" Hinweis: ") . $hintLabel, 66, 12, null, $itemLineHeight, ); } $y -= 8; $itemNumber++; } } $y -= 4; $writeSectionTitle("Kommentar"); if (trim($commentRaw) === "") { $writeWrapped($encodeText("Kein Kommentar"), 80); } else { foreach (preg_split('/\r\n|\r|\n/', $commentRaw) as $commentLine) { $writeWrapped($encodeText($commentLine), 82); } } $y -= 6; $writeSectionTitle("Lagerbearbeitung"); $warehouseLines = [ "Ausgegeben am / durch: __________________________________________________", "[ ] Vollständig ausgegeben", "[ ] Teilweise ausgegeben", ]; foreach ($warehouseLines as $line) { $writeWrapped($encodeText($line), 80); } if ($pageContent !== "") { $pages[] = $pageContent; } if (empty($pages)) { $pages[] = ""; } $fontObjectNumber = 3; $objects = []; $objects[1] = "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"; $objects[ $fontObjectNumber ] = "3 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>\nendobj\n"; $kids = []; $nextObjectNumber = 4; foreach ($pages as $pageStream) { $pageObjectNumber = $nextObjectNumber++; $contentObjectNumber = $nextObjectNumber++; $kids[] = $pageObjectNumber . " 0 R"; $objects[$pageObjectNumber] = $pageObjectNumber . " 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 " . $pageWidth . " " . $pageHeight . "] /Contents " . $contentObjectNumber . " 0 R /Resources << /Font << /F1 " . $fontObjectNumber . " 0 R >> >> >>\nendobj\n"; $objects[$contentObjectNumber] = $contentObjectNumber . " 0 obj\n<< /Length " . strlen($pageStream) . " >>\nstream\n" . $pageStream . "\nendstream\nendobj\n"; } $objects[2] = "2 0 obj\n<< /Type /Pages /Kids [" . implode(" ", $kids) . "] /Count " . count($kids) . " >>\nendobj\n"; ksort($objects); $pdf = "%PDF-1.4\n"; $offsets = [0]; foreach ($objects as $number => $objectContent) { $offsets[$number] = strlen($pdf); $pdf .= $objectContent; } $lastObjectNumber = (int) max(array_keys($objects)); $xrefOffset = strlen($pdf); $pdf .= "xref\n0 " . ($lastObjectNumber + 1) . "\n"; $pdf .= "0000000000 65535 f \n"; for ($i = 1; $i <= $lastObjectNumber; $i++) { if (isset($offsets[$i])) { $pdf .= sprintf("%010d 00000 n \n", $offsets[$i]); } else { $pdf .= "0000000000 00000 f \n"; } } $pdf .= "trailer\n<< /Size " . ($lastObjectNumber + 1) . " /Root 1 0 R >>\n"; $pdf .= "startxref\n" . $xrefOffset . "\n%%EOF"; return $pdf; } /** * Input Validation Class */ class Validator { private $errors = []; private $data = []; public function __construct($data) { $this->data = $data; } public function required($field, $label = null) { $value = $this->get($field); if (empty(trim($value ?? ""))) { $this->errors[] = ($label ?? $field) . " ist erforderlich."; } return $this; } public function email($field, $label = null) { $value = $this->get($field); if (!empty($value) && !filter_var($value, FILTER_VALIDATE_EMAIL)) { $this->errors[] = ($label ?? $field) . " muss eine gültige E-Mail-Adresse sein."; } return $this; } public function minLength($field, $min, $label = null) { $value = $this->get($field); if (!empty($value) && strlen($value) < $min) { $this->errors[] = ($label ?? $field) . " muss mindestens " . $min . " Zeichen lang sein."; } return $this; } public function maxLength($field, $max, $label = null) { $value = $this->get($field); if (!empty($value) && strlen($value) > $max) { $this->errors[] = ($label ?? $field) . " darf maximal " . $max . " Zeichen lang sein."; } return $this; } public function inArray($field, $allowed, $label = null) { $value = $this->get($field); if (!empty($value) && !in_array($value, $allowed, true)) { $this->errors[] = ($label ?? $field) . " ist ungültig."; } return $this; } public function get($field) { return $this->data[$field] ?? null; } public function getErrors() { return $this->errors; } public function isValid() { return empty($this->errors); } } /** * Error Logging Functions */ if (!defined("LOG_DIR")) { define("LOG_DIR", DATA_DIR . "logs/"); } if (!defined("ERROR_LOG_FILE")) { define("ERROR_LOG_FILE", LOG_DIR . "error.log"); } if (!defined("ACCESS_LOG_FILE")) { define("ACCESS_LOG_FILE", LOG_DIR . "access.log"); } function initLogging() { if (!is_dir(LOG_DIR)) { mkdir(LOG_DIR, 02775, true); @chmod(LOG_DIR, 02775); } } function logError($message, $context = [], $level = "ERROR") { initLogging(); $entry = [ "timestamp" => date("Y-m-d H:i:s.u"), "level" => $level, "message" => $message, "context" => $context, "ip" => $_SERVER["REMOTE_ADDR"] ?? "unknown", "user_agent" => $_SERVER["HTTP_USER_AGENT"] ?? "unknown", "request_uri" => $_SERVER["REQUEST_URI"] ?? "unknown", "session_id" => session_id() ? substr(session_id(), 0, 8) . "..." : "none", ]; $logLine = json_encode($entry, JSON_UNESCAPED_UNICODE) . PHP_EOL; file_put_contents(ERROR_LOG_FILE, $logLine, FILE_APPEND | LOCK_EX); if (defined("DEVELOPMENT_MODE") && DEVELOPMENT_MODE) { error_log( "PSA Order: " . $message . " | Context: " . json_encode($context), ); } } function logAccess($message, $context = []) { initLogging(); $entry = [ "timestamp" => date("Y-m-d H:i:s.u"), "message" => $message, "context" => $context, "ip" => $_SERVER["REMOTE_ADDR"] ?? "unknown", "request_method" => $_SERVER["REQUEST_METHOD"] ?? "unknown", "request_uri" => $_SERVER["REQUEST_URI"] ?? "unknown", ]; $logLine = json_encode($entry, JSON_UNESCAPED_UNICODE) . PHP_EOL; file_put_contents(ACCESS_LOG_FILE, $logLine, FILE_APPEND | LOCK_EX); } function handleException($exception) { logError( "Uncaught exception: " . $exception->getMessage(), [ "file" => $exception->getFile(), "line" => $exception->getLine(), "trace" => $exception->getTraceAsString(), ], "CRITICAL", ); if (defined("DEVELOPMENT_MODE") && DEVELOPMENT_MODE) { echo "" .
escape($exception->getMessage()) .
"";
} else {
echo "Bitte versuchen Sie es später erneut.
"; } exit(); } set_exception_handler("handleException"); function handleError($errno, $errstr, $errfile, $errline) { if (!(error_reporting() & $errno)) { return false; } logError( "PHP Error: " . $errstr, [ "type" => $errno, "file" => $errfile, "line" => $errline, ], "WARNING", ); return true; } set_error_handler("handleError");