Newwebshop/app/API/controllers/MobileApiController.php

649 lines
23 KiB
PHP

<?php
/**
* Copyright seit 2024 Webshop System
*
* Mobile API Controller für das Webshop-System
*
* @author Webshop System
* @license GPL v3
*/
namespace App\API\Controllers;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Exception;
class MobileApiController extends ApiController
{
/**
* Mobile-optimierte Produktliste
*/
public function getMobileProducts()
{
$page = max(1, intval($_GET['page'] ?? 1));
$limit = min(20, max(1, intval($_GET['limit'] ?? 10))); // Kleinere Limit für Mobile
$category = $_GET['category'] ?? null;
$search = $_GET['search'] ?? null;
$sort = $_GET['sort'] ?? 'newest'; // newest, price_asc, price_desc, rating
$offset = ($page - 1) * $limit;
try {
$whereConditions = ['p.active = 1'];
$params = [];
if ($category) {
$whereConditions[] = 'p.category_id = ?';
$params[] = $category;
}
if ($search) {
$whereConditions[] = '(p.name LIKE ? OR p.description LIKE ?)';
$params[] = '%' . $search . '%';
$params[] = '%' . $search . '%';
}
$whereClause = implode(' AND ', $whereConditions);
// Sortierung
$orderBy = match($sort) {
'price_asc' => '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;
}
}