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