| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243 |
- <?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)) {
- return [];
- }
- $data = json_decode($content, true);
- return $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));
- }
- /**
- * Normalize admin username input.
- */
- function normalizeAdminUsername($username) {
- return trim((string)$username);
- }
- /**
- * Normalize admin description input.
- */
- function normalizeAdminDescription($description) {
- return trim((string)$description);
- }
- /**
- * Normalize admin email input.
- */
- function normalizeAdminEmail($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 === '') {
- return false;
- }
- $length = function_exists('mb_strlen') ? mb_strlen($description) : strlen($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;
- }
- /**
- * Get full admin account records from JSON store.
- */
- function getAdminAccounts() {
- $data = readJsonFile(ADMINS_FILE);
- $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'] === '') {
- continue;
- }
- $admins[$username] = $account['password_hash'];
- }
- return $admins;
- }
- /**
- * Save full admin accounts to JSON store.
- */
- function saveAdminAccounts($accounts) {
- $sanitizedAccounts = [];
- foreach ($accounts as $username => $account) {
- $normalizedUsername = normalizeAdminUsername($username);
- if ($normalizedUsername === '' || !is_array($account)) {
- continue;
- }
- $hash = isset($account['password_hash']) && is_string($account['password_hash']) ? $account['password_hash'] : '';
- if ($hash === '') {
- continue;
- }
- $description = isset($account['description']) ? normalizeAdminDescription($account['description']) : getDefaultAdminDescription($normalizedUsername);
- if (!isValidAdminDescription($description)) {
- $description = getDefaultAdminDescription($normalizedUsername);
- }
- $email = isset($account['email']) ? normalizeAdminEmail($account['email']) : getDefaultAdminEmail();
- if (!isValidAdminEmail($email)) {
- $email = getDefaultAdminEmail();
- }
- $sanitizedAccounts[$normalizedUsername] = [
- 'password_hash' => $hash,
- 'description' => $description,
- 'email' => $email
- ];
- }
- ksort($sanitizedAccounts);
- writeJsonFile(ADMINS_FILE, ['admins' => $sanitizedAccounts]);
- }
- /**
- * Save admin users to JSON store (username => password hash).
- */
- function saveAdminUsers($admins) {
- $existingAccounts = getAdminAccounts();
- $normalizedAccounts = [];
- foreach ($admins as $username => $hash) {
- $normalizedUsername = normalizeAdminUsername($username);
- if ($normalizedUsername === '' || !is_string($hash) || $hash === '') {
- continue;
- }
- $description = isset($existingAccounts[$normalizedUsername]['description'])
- ? normalizeAdminDescription($existingAccounts[$normalizedUsername]['description'])
- : getDefaultAdminDescription($normalizedUsername);
- if (!isValidAdminDescription($description)) {
- $description = getDefaultAdminDescription($normalizedUsername);
- }
- $email = isset($existingAccounts[$normalizedUsername]['email'])
- ? normalizeAdminEmail($existingAccounts[$normalizedUsername]['email'])
- : getDefaultAdminEmail();
- if (!isValidAdminEmail($email)) {
- $email = getDefaultAdminEmail();
- }
- $normalizedAccounts[$normalizedUsername] = [
- 'password_hash' => $hash,
- 'description' => $description,
- 'email' => $email
- ];
- }
- saveAdminAccounts($normalizedAccounts);
- }
- /**
- * Get all products
- */
- function getProducts() {
- $data = readJsonFile(PRODUCTS_FILE);
- return isset($data['products']) ? $data['products'] : [];
- }
- /**
- * Get product by ID
- */
- function getProductById($id) {
- $products = getProducts();
- foreach ($products as $product) {
- if ($product['id'] == $id) {
- return $product;
- }
- }
- return null;
- }
- /**
- * Save products
- */
- function saveProducts($products) {
- $data = ['products' => $products];
- writeJsonFile(PRODUCTS_FILE, $data);
- }
- /**
- * Get all reservations
- */
- function getReservations() {
- $data = readJsonFile(RESERVATIONS_FILE);
- return isset($data['reservations']) ? $data['reservations'] : [];
- }
- /**
- * 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;
- }
- /**
- * Get remembered order IDs for the current browser profile.
- */
- function getRememberedOrderIds(): array {
- return readSignedOrderHistoryCookie();
- }
- /**
- * Remember a newly created order ID in browser history cookie.
- */
- function rememberOrderId(string $orderId): void {
- if (!isValidOrderHistoryOrderId($orderId)) {
- return;
- }
- $existingIds = getRememberedOrderIds();
- $updatedIds = [$orderId];
- foreach ($existingIds as $existingId) {
- if ($existingId !== $orderId) {
- $updatedIds[] = $existingId;
- }
- }
- $maxIds = getOrderHistoryMaxIds();
- if (count($updatedIds) > $maxIds) {
- $updatedIds = array_slice($updatedIds, 0, $maxIds);
- }
- writeSignedOrderHistoryCookie($updatedIds);
- }
- /**
- * Read and validate signed browser order history cookie.
- */
- function readSignedOrderHistoryCookie(): array {
- $cookieName = getOrderHistoryCookieName();
- if (!isset($_COOKIE[$cookieName]) || !is_string($_COOKIE[$cookieName])) {
- return [];
- }
- $secret = getOrderHistorySecret();
- if ($secret === '') {
- return [];
- }
- $cookieValue = $_COOKIE[$cookieName];
- $parts = explode('.', $cookieValue, 2);
- if (count($parts) !== 2) {
- return [];
- }
- $encodedPayload = $parts[0];
- $signature = $parts[1];
- if ($encodedPayload === '' || $signature === '') {
- return [];
- }
- $expectedSignature = hash_hmac('sha256', $encodedPayload, $secret);
- if (!hash_equals($expectedSignature, $signature)) {
- return [];
- }
- $payloadJson = base64UrlDecode($encodedPayload);
- if ($payloadJson === null) {
- return [];
- }
- $payload = json_decode($payloadJson, true);
- if (!is_array($payload)) {
- return [];
- }
- $version = isset($payload['v']) ? (int)$payload['v'] : 0;
- if ($version !== 1) {
- return [];
- }
- $ids = isset($payload['ids']) && is_array($payload['ids']) ? $payload['ids'] : [];
- return sanitizeOrderHistoryIds($ids);
- }
- /**
- * 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()
- ];
- $payloadJson = json_encode($payload, JSON_UNESCAPED_UNICODE);
- if ($payloadJson === false) {
- return;
- }
- $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'
- ]);
- if ($success) {
- $_COOKIE[getOrderHistoryCookieName()] = $cookieValue;
- }
- }
- /**
- * Build a safe, deduplicated order history ID list.
- */
- function sanitizeOrderHistoryIds(array $ids): array {
- $result = [];
- $seen = [];
- $maxIds = getOrderHistoryMaxIds();
- foreach ($ids as $id) {
- if (!is_string($id) || !isValidOrderHistoryOrderId($id)) {
- continue;
- }
- if (isset($seen[$id])) {
- continue;
- }
- $seen[$id] = true;
- $result[] = $id;
- if (count($result) >= $maxIds) {
- break;
- }
- }
- return $result;
- }
- /**
- * Check if order ID matches configured pattern.
- */
- function isValidOrderHistoryOrderId($orderId): bool {
- if (!is_string($orderId) || $orderId === '') {
- return false;
- }
- $prefix = defined('ORDER_PREFIX') ? ORDER_PREFIX : 'ORD';
- $pattern = '/^' . preg_quote($prefix, '/') . '-\d{4}-\d+$/';
- return preg_match($pattern, $orderId) === 1;
- }
- /**
- * Base64url encode helper.
- */
- function base64UrlEncode(string $data): string {
- return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
- }
- /**
- * Base64url decode helper.
- */
- function base64UrlDecode(string $data): ?string {
- $remainder = strlen($data) % 4;
- if ($remainder > 0) {
- $data .= str_repeat('=', 4 - $remainder);
- }
- $decoded = base64_decode(strtr($data, '-_', '+/'), true);
- if ($decoded === false) {
- 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';
- }
- /**
- * Get secret for order history cookie signing.
- */
- function getOrderHistorySecret(): string {
- return defined('ORDER_HISTORY_COOKIE_SECRET') ? (string) ORDER_HISTORY_COOKIE_SECRET : '';
- }
- /**
- * 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;
- }
- /**
- * 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;
- }
- /**
- * 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 ($siteUrl === '' || $siteUrl === '/') {
- return '/';
- }
- if ($siteUrl[0] !== '/') {
- $siteUrl = '/' . $siteUrl;
- }
- return rtrim($siteUrl, '/');
- }
- /**
- * 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;
- }
- return false;
- }
- /**
- * Check if reservation is hidden (spam/deleted)
- */
- function isReservationHidden($reservation) {
- return isset($reservation['is_hidden']) && $reservation['is_hidden'] === true;
- }
- /**
- * Save reservations
- */
- function saveReservations($reservations) {
- $data = ['reservations' => $reservations];
- writeJsonFile(RESERVATIONS_FILE, $data);
- }
- /**
- * Generate order number
- * Pattern: PREFIX-YEAR-SEQ
- */
- function generateReservationId() {
- $reservations = getReservations();
- $year = date('Y');
- $prefix = defined('ORDER_PREFIX') ? ORDER_PREFIX : 'ORD';
- $max = 0;
- $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;
- }
- }
- }
- $next = $max + 1;
- return sprintf('%s-%s-%03d', $prefix, $year, $next);
- }
- /**
- * Check if product has enough stock
- * For apparel: checks stock for specific size
- * For merch: checks general stock
- */
- function checkStock($productId, $quantity, $size = null) {
- $product = getProductById($productId);
- if (!$product) {
- return false;
- }
-
- // For apparel with sizes, check stock per size
- if ($product['category'] === 'apparel' && !empty($product['sizes']) && $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;
- }
-
- // For merch or apparel without size-specific stock, use general stock
- $stock = isset($product['stock']) ? (int)$product['stock'] : 0;
- return $stock >= $quantity;
- }
- /**
- * Get stock for a product (per size for apparel, general for merch)
- */
- function getStock($product, $size = null) {
- if ($product['category'] === 'apparel' && !empty($product['sizes']) && $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 (sum of all sizes for apparel)
- */
- function getTotalStock($product) {
- if ($product['category'] === 'apparel' && 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) {
- // For apparel with sizes, allocate per size
- if ($product['category'] === 'apparel' && !empty($product['sizes']) && $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 {
- // For merch or general stock
- 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) {
- // For apparel with sizes, release per size
- if ($product['category'] === 'apparel' && !empty($product['sizes']) && $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 {
- // For merch or general stock
- if (!isset($product['stock'])) {
- $product['stock'] = 0;
- }
- $product['stock'] += $quantity;
- }
- break;
- }
- }
- 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"];
- }
- }
-
- // 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(),
- 'customer_name' => $customerName,
- 'customer_email' => $customerEmail,
- '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
- ];
-
- $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;
- }
- }
- saveReservations($reservations);
- }
- /**
- * 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;
- }
- if (isReservationHidden($reservation)) {
- return ['success' => false, 'message' => 'Bestellung ist bereits als Spam/Gelöscht markiert.'];
- }
- $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';
- }
- $reservation['is_hidden'] = true;
- $reservation['hidden_at'] = date('Y-m-d H:i:s');
- $reservation['hidden_reason'] = 'spam_deleted';
- saveReservations($reservations);
- return ['success' => true];
- }
- return ['success' => false, 'message' => 'Bestellung nicht gefunden.'];
- }
- /**
- * Check and expire old reservations
- */
- function expireOldReservations() {
- $reservations = getReservations();
- $now = new DateTime();
- $changed = false;
-
- foreach ($reservations as &$reservation) {
- if (isReservationHidden($reservation)) {
- continue;
- }
- 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;
- }
- }
- }
-
- if ($changed) {
- saveReservations($reservations);
- }
- }
- /**
- * 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;
- }
- }
- return true;
- }
- /**
- * 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);
- }
- $now = new DateTime();
- $expires = clone $now;
- $expires->modify('+' . RESERVATION_EXPIRY_DAYS . ' days');
- $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];
- }
- }
- return ['success' => false, 'message' => 'Vorbestellung nicht gefunden.'];
- }
- /**
- * Sanitize input
- */
- function sanitize($input) {
- return htmlspecialchars(strip_tags(trim($input)), ENT_QUOTES, 'UTF-8');
- }
- /**
- * Format price
- */
- function formatPrice($price) {
- return number_format($price, 2, ',', '.') . ' €';
- }
- /**
- * Format date
- */
- function formatDate($dateString) {
- $date = new DateTime($dateString);
- return $date->format('d.m.Y H:i');
- }
- /**
- * 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';
- }
-
- 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 = [];
- foreach ($accounts as $account) {
- if (!isset($account['email'])) {
- continue;
- }
- $email = normalizeAdminEmail($account['email']);
- if (!isValidAdminEmail($email)) {
- continue;
- }
- $emails[] = $email;
- }
- if (empty($emails)) {
- $fallbackEmail = getDefaultAdminEmail();
- if ($fallbackEmail !== '') {
- $emails[] = $fallbackEmail;
- }
- }
- return array_values(array_unique($emails));
- }
- /**
- * Send admin notifications to all configured recipients.
- */
- function sendAdminNotificationEmails($subject, $message, $isHtml = true) {
- $emails = getAdminNotificationEmails();
- if (empty($emails)) {
- return false;
- }
- $sent = false;
- foreach ($emails as $email) {
- $result = sendEmail($email, $subject, $message, $isHtml);
- if ($result) {
- $sent = true;
- }
- }
- return $sent;
- }
- /**
- * 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>';
- }
- }
- $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 = '
- <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="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>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>
- </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>';
- }
- }
- $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>';
- }
- }
- $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);
- }
|