'p.price ASC', 'price_desc' => 'p.price DESC', 'rating' => 'p.avg_rating DESC', default => 'p.created_at DESC' }; // Gesamtanzahl $countSql = " SELECT COUNT(*) as total FROM ws_product p LEFT JOIN ws_category c ON p.category_id = c.id WHERE $whereClause "; $stmt = $this->conn->prepare($countSql); $stmt->execute($params); $totalCount = $stmt->fetchAssociative()['total']; // Mobile-optimierte Produktdaten $sql = " SELECT p.id, p.name, p.price, p.avg_rating, p.review_count, p.stock, p.image_url, c.name as category_name, p.created_at, p.updated_at FROM ws_product p LEFT JOIN ws_category c ON p.category_id = c.id WHERE $whereClause ORDER BY $orderBy LIMIT $limit OFFSET $offset "; $stmt = $this->conn->prepare($sql); $stmt->execute($params); $products = $stmt->fetchAllAssociative(); // Mobile-optimierte Antwort $mobileProducts = []; foreach ($products as $product) { $mobileProducts[] = [ 'id' => $product['id'], 'name' => $product['name'], 'price' => floatval($product['price']), 'formatted_price' => '€' . number_format($product['price'], 2, ',', '.'), 'rating' => floatval($product['avg_rating'] ?? 0), 'review_count' => intval($product['review_count'] ?? 0), 'stock_status' => $this->getStockStatus($product['stock']), 'image_url' => $product['image_url'], 'category' => $product['category_name'], 'is_new' => $this->isNewProduct($product['created_at']), 'has_discount' => false, // TODO: Implement discount logic 'quick_add' => $product['stock'] > 0 ]; } $this->sendResponse([ 'success' => true, 'data' => [ 'products' => $mobileProducts, 'pagination' => [ 'page' => $page, 'limit' => $limit, 'total' => $totalCount, 'pages' => ceil($totalCount / $limit), 'has_next' => $page < ceil($totalCount / $limit), 'has_prev' => $page > 1 ], 'filters' => [ 'categories' => $this->getMobileCategories(), 'sort_options' => [ ['value' => 'newest', 'label' => 'Neueste'], ['value' => 'price_asc', 'label' => 'Preis: Niedrig zu Hoch'], ['value' => 'price_desc', 'label' => 'Preis: Hoch zu Niedrig'], ['value' => 'rating', 'label' => 'Beste Bewertung'] ] ] ] ]); } catch (Exception $e) { $this->sendError('Failed to fetch mobile products: ' . $e->getMessage(), 500); } } /** * Mobile-optimierte Produktdetails */ public function getMobileProduct($id) { try { $stmt = $this->conn->prepare(' SELECT p.*, c.name as category_name FROM ws_product p LEFT JOIN ws_category c ON p.category_id = c.id WHERE p.id = ? AND p.active = 1 '); $stmt->execute([$id]); $product = $stmt->fetchAssociative(); if (!$product) { $this->sendError('Product not found', 404); } // Mobile-optimierte Bewertungen $stmt = $this->conn->prepare(' SELECT r.rating, r.title, r.comment, r.created_at, c.first_name, c.last_name FROM ws_review r LEFT JOIN ws_customer c ON r.customer_id = c.id WHERE r.product_id = ? AND r.active = 1 ORDER BY r.created_at DESC LIMIT 5 '); $stmt->execute([$id]); $reviews = $stmt->fetchAllAssociative(); // Mobile-optimierte Antwort $mobileProduct = [ 'id' => $product['id'], 'name' => $product['name'], 'description' => $product['description'], 'price' => floatval($product['price']), 'formatted_price' => '€' . number_format($product['price'], 2, ',', '.'), 'rating' => floatval($product['avg_rating'] ?? 0), 'review_count' => intval($product['review_count'] ?? 0), 'stock' => intval($product['stock']), 'stock_status' => $this->getStockStatus($product['stock']), 'category' => [ 'id' => $product['category_id'], 'name' => $product['category_name'] ], 'images' => $this->getProductImages($id), 'variants' => $this->getProductVariants($id), 'reviews' => array_map(function($review) { return [ 'rating' => intval($review['rating']), 'title' => $review['title'], 'comment' => $review['comment'], 'author' => $review['first_name'] . ' ' . $review['last_name'], 'date' => $review['created_at'] ]; }, $reviews), 'related_products' => $this->getRelatedProducts($id, $product['category_id']), 'can_add_to_cart' => $product['stock'] > 0, 'can_add_to_wishlist' => true ]; $this->sendResponse([ 'success' => true, 'data' => $mobileProduct ]); } catch (Exception $e) { $this->sendError('Failed to fetch mobile product: ' . $e->getMessage(), 500); } } /** * Mobile Warenkorb */ public function getMobileCart() { $token = $this->getBearerToken(); $decodedToken = $this->validateJWT($token); if (!$decodedToken) { $this->sendError('Authentication required', 401); } try { $stmt = $this->conn->prepare(' SELECT ci.*, p.name, p.price, p.image_url, p.stock FROM ws_cart_item ci LEFT JOIN ws_product p ON ci.product_id = p.id WHERE ci.customer_id = ? ORDER BY ci.created_at DESC '); $stmt->execute([$decodedToken['customer_id']]); $cartItems = $stmt->fetchAllAssociative(); $total = 0; $itemCount = 0; $mobileCartItems = []; foreach ($cartItems as $item) { $subtotal = $item['price'] * $item['quantity']; $total += $subtotal; $itemCount += $item['quantity']; $mobileCartItems[] = [ 'id' => $item['id'], 'product_id' => $item['product_id'], 'name' => $item['name'], 'price' => floatval($item['price']), 'formatted_price' => '€' . number_format($item['price'], 2, ',', '.'), 'quantity' => intval($item['quantity']), 'subtotal' => floatval($subtotal), 'formatted_subtotal' => '€' . number_format($subtotal, 2, ',', '.'), 'image_url' => $item['image_url'], 'stock' => intval($item['stock']), 'can_update' => $item['stock'] >= $item['quantity'], 'max_quantity' => min($item['stock'], 10) ]; } $this->sendResponse([ 'success' => true, 'data' => [ 'items' => $mobileCartItems, 'summary' => [ 'item_count' => $itemCount, 'total' => floatval($total), 'formatted_total' => '€' . number_format($total, 2, ',', '.'), 'can_checkout' => $itemCount > 0 && $total > 0 ] ] ]); } catch (Exception $e) { $this->sendError('Failed to fetch mobile cart: ' . $e->getMessage(), 500); } } /** * Mobile Push-Benachrichtigungen */ public function subscribeToPush() { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->sendError('Method not allowed', 405); } $input = json_decode(file_get_contents('php://input'), true); if (!$input || empty($input['subscription'])) { $this->sendError('Subscription data required', 400); } $token = $this->getBearerToken(); $decodedToken = $this->validateJWT($token); if (!$decodedToken) { $this->sendError('Authentication required', 401); } try { // Prüfen ob bereits abonniert $stmt = $this->conn->prepare(' SELECT id FROM ws_push_subscription WHERE customer_id = ? AND endpoint = ? '); $stmt->execute([$decodedToken['customer_id'], $input['subscription']['endpoint']]); $existing = $stmt->fetchAssociative(); if ($existing) { // Update existing subscription $stmt = $this->conn->prepare(' UPDATE ws_push_subscription SET auth = ?, p256dh = ?, updated_at = NOW() WHERE id = ? '); $stmt->execute([ $input['subscription']['keys']['auth'], $input['subscription']['keys']['p256dh'], $existing['id'] ]); } else { // Create new subscription $stmt = $this->conn->prepare(' INSERT INTO ws_push_subscription (customer_id, endpoint, auth, p256dh, created_at) VALUES (?, ?, ?, ?, NOW()) '); $stmt->execute([ $decodedToken['customer_id'], $input['subscription']['endpoint'], $input['subscription']['keys']['auth'], $input['subscription']['keys']['p256dh'] ]); } $this->sendResponse([ 'success' => true, 'message' => 'Push subscription updated successfully' ]); } catch (Exception $e) { $this->sendError('Failed to subscribe to push: ' . $e->getMessage(), 500); } } /** * Offline-Synchronisation */ public function syncOfflineData() { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->sendError('Method not allowed', 405); } $input = json_decode(file_get_contents('php://input'), true); if (!$input || empty($input['data'])) { $this->sendError('Sync data required', 400); } $token = $this->getBearerToken(); $decodedToken = $this->validateJWT($token); if (!$decodedToken) { $this->sendError('Authentication required', 401); } try { $syncedItems = []; $errors = []; foreach ($input['data'] as $item) { try { switch ($item['type']) { case 'cart_add': $result = $this->syncCartAdd($decodedToken['customer_id'], $item['data']); $syncedItems[] = ['id' => $item['id'], 'type' => 'cart_add', 'result' => $result]; break; case 'review_create': $result = $this->syncReviewCreate($decodedToken['customer_id'], $item['data']); $syncedItems[] = ['id' => $item['id'], 'type' => 'review_create', 'result' => $result]; break; case 'wishlist_add': $result = $this->syncWishlistAdd($decodedToken['customer_id'], $item['data']); $syncedItems[] = ['id' => $item['id'], 'type' => 'wishlist_add', 'result' => $result]; break; default: $errors[] = ['id' => $item['id'], 'error' => 'Unknown sync type']; } } catch (Exception $e) { $errors[] = ['id' => $item['id'], 'error' => $e->getMessage()]; } } $this->sendResponse([ 'success' => true, 'data' => [ 'synced_items' => $syncedItems, 'errors' => $errors, 'total_synced' => count($syncedItems), 'total_errors' => count($errors) ] ]); } catch (Exception $e) { $this->sendError('Failed to sync offline data: ' . $e->getMessage(), 500); } } /** * Mobile-spezifische Kategorien */ private function getMobileCategories() { try { $stmt = $this->conn->prepare(' SELECT id, name, description, image_url FROM ws_category WHERE active = 1 ORDER BY sort_order ASC, name ASC '); $stmt->execute(); $categories = $stmt->fetchAllAssociative(); return array_map(function($category) { return [ 'id' => $category['id'], 'name' => $category['name'], 'description' => $category['description'], 'image_url' => $category['image_url'] ]; }, $categories); } catch (Exception $e) { return []; } } /** * Lagerbestand-Status */ private function getStockStatus($stock) { if ($stock <= 0) { return 'out_of_stock'; } elseif ($stock <= 5) { return 'low_stock'; } else { return 'in_stock'; } } /** * Neues Produkt prüfen */ private function isNewProduct($createdAt) { $created = strtotime($createdAt); $now = time(); $daysDiff = ($now - $created) / (60 * 60 * 24); return $daysDiff <= 30; // 30 Tage als "neu" } /** * Produktbilder abrufen */ private function getProductImages($productId) { try { $stmt = $this->conn->prepare(' SELECT image_url, alt_text, sort_order FROM ws_product_image WHERE product_id = ? AND active = 1 ORDER BY sort_order ASC '); $stmt->execute([$productId]); $images = $stmt->fetchAllAssociative(); return array_map(function($image) { return [ 'url' => $image['image_url'], 'alt' => $image['alt_text'], 'order' => intval($image['sort_order']) ]; }, $images); } catch (Exception $e) { return []; } } /** * Produktvarianten abrufen */ private function getProductVariants($productId) { try { $stmt = $this->conn->prepare(' SELECT id, name, price, stock, sku FROM ws_product_variant WHERE product_id = ? AND active = 1 ORDER BY sort_order ASC '); $stmt->execute([$productId]); $variants = $stmt->fetchAllAssociative(); return array_map(function($variant) { return [ 'id' => $variant['id'], 'name' => $variant['name'], 'price' => floatval($variant['price']), 'formatted_price' => '€' . number_format($variant['price'], 2, ',', '.'), 'stock' => intval($variant['stock']), 'sku' => $variant['sku'], 'available' => $variant['stock'] > 0 ]; }, $variants); } catch (Exception $e) { return []; } } /** * Verwandte Produkte */ private function getRelatedProducts($productId, $categoryId) { try { $stmt = $this->conn->prepare(' SELECT p.id, p.name, p.price, p.avg_rating, p.image_url FROM ws_product p WHERE p.category_id = ? AND p.id != ? AND p.active = 1 ORDER BY p.avg_rating DESC, p.created_at DESC LIMIT 4 '); $stmt->execute([$categoryId, $productId]); $related = $stmt->fetchAllAssociative(); return array_map(function($product) { return [ 'id' => $product['id'], 'name' => $product['name'], 'price' => floatval($product['price']), 'formatted_price' => '€' . number_format($product['price'], 2, ',', '.'), 'rating' => floatval($product['avg_rating'] ?? 0), 'image_url' => $product['image_url'] ]; }, $related); } catch (Exception $e) { return []; } } /** * Warenkorb-Synchronisation */ private function syncCartAdd($customerId, $data) { $stmt = $this->conn->prepare(' INSERT INTO ws_cart_item (customer_id, product_id, quantity, created_at) VALUES (?, ?, ?, NOW()) ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity) '); $stmt->execute([$customerId, $data['product_id'], $data['quantity']]); return ['success' => true, 'cart_item_id' => $this->conn->lastInsertId()]; } /** * Bewertungs-Synchronisation */ private function syncReviewCreate($customerId, $data) { $stmt = $this->conn->prepare(' INSERT INTO ws_review (product_id, customer_id, rating, title, comment, active, created_at) VALUES (?, ?, ?, ?, ?, 1, NOW()) '); $stmt->execute([ $data['product_id'], $customerId, $data['rating'], $data['title'], $data['comment'] ]); return ['success' => true, 'review_id' => $this->conn->lastInsertId()]; } /** * Wunschliste-Synchronisation */ private function syncWishlistAdd($customerId, $data) { $stmt = $this->conn->prepare(' INSERT INTO ws_wishlist_item (customer_id, product_id, created_at) VALUES (?, ?, NOW()) ON DUPLICATE KEY UPDATE updated_at = NOW() '); $stmt->execute([$customerId, $data['product_id']]); return ['success' => true, 'wishlist_item_id' => $this->conn->lastInsertId()]; } /** * JWT-Token validieren (aus CustomerApiController) */ private function validateJWT($token) { if (!$token) { return false; } $parts = explode('.', $token); if (count($parts) !== 3) { return false; } list($header, $payload, $signature) = $parts; $validSignature = hash_hmac('sha256', $header . "." . $payload, getenv('JWT_SECRET') ?: 'webshop_secret_key', true); $validSignature = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($validSignature)); if ($signature !== $validSignature) { return false; } $payload = json_decode(base64_decode(str_replace(['-', '_'], ['+', '/'], $payload)), true); if (!$payload || $payload['exp'] < time()) { return false; } return $payload; } /** * Bearer-Token aus Header extrahieren */ private function getBearerToken() { $headers = getallheaders(); $authHeader = $headers['Authorization'] ?? ''; if (preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) { return $matches[1]; } return null; } }