orderModel = new Order(); $this->productModel = new Product(); $this->userModel = new User(); $this->categoryModel = new Category(); } /** * Get dashboard overview statistics * GET /api/admin/dashboard */ public function dashboard() { try { // Get date range from query parameters $startDate = $_GET['start_date'] ?? date('Y-m-01'); // First day of current month $endDate = $_GET['end_date'] ?? date('Y-m-d'); // Today // Get real statistics from database $stats = [ 'sales' => $this->getSalesStats($startDate, $endDate), 'orders' => $this->getOrderStats($startDate, $endDate), 'products' => $this->getProductStats(), 'customers' => $this->getCustomerStats($startDate, $endDate) ]; $this->jsonResponse([ 'success' => true, 'data' => [ 'period' => [ 'start_date' => $startDate, 'end_date' => $endDate ], 'sales' => $stats['sales'], 'orders' => $stats['orders'], 'products' => $stats['products'], 'customers' => $stats['customers'] ] ]); } catch (Exception $e) { $this->jsonResponse([ 'success' => false, 'error' => 'Failed to fetch dashboard data', 'message' => $this->isDebugMode() ? $e->getMessage() : 'An error occurred' ], 500); } } /** * Get sales analytics * GET /api/admin/analytics/sales */ public function salesAnalytics() { try { $period = $_GET['period'] ?? 'month'; // day, week, month, year $startDate = $_GET['start_date'] ?? date('Y-m-01'); $endDate = $_GET['end_date'] ?? date('Y-m-d'); $analytics = [ 'revenue_trend' => $this->getRevenueTrend($period, $startDate, $endDate), 'top_products' => $this->getTopSellingProducts($startDate, $endDate), 'sales_by_category' => $this->getSalesByCategory($startDate, $endDate), 'payment_methods' => $this->getPaymentMethodStats($startDate, $endDate), 'order_status_distribution' => $this->getOrderStatusDistribution($startDate, $endDate) ]; $this->jsonResponse([ 'success' => true, 'data' => $analytics ]); } catch (Exception $e) { $this->jsonResponse([ 'success' => false, 'error' => 'Failed to fetch sales analytics', 'message' => $this->isDebugMode() ? $e->getMessage() : 'An error occurred' ], 500); } } /** * Get customer analytics * GET /api/admin/analytics/customers */ public function customerAnalytics() { try { $startDate = $_GET['start_date'] ?? date('Y-m-01'); $endDate = $_GET['end_date'] ?? date('Y-m-d'); $analytics = [ 'new_customers' => $this->getNewCustomersData($startDate, $endDate), 'customer_lifetime_value' => $this->getCustomerLifetimeValue(), 'repeat_customers' => $this->getRepeatCustomerStats($startDate, $endDate), 'customer_segments' => $this->getCustomerSegments(), 'geographic_distribution' => $this->getCustomerGeographicData() ]; $this->jsonResponse([ 'success' => true, 'data' => $analytics ]); } catch (Exception $e) { $this->jsonResponse([ 'success' => false, 'error' => 'Failed to fetch customer analytics', 'message' => $this->isDebugMode() ? $e->getMessage() : 'An error occurred' ], 500); } } /** * Get inventory analytics * GET /api/admin/analytics/inventory */ public function inventoryAnalytics() { try { $analytics = [ 'low_stock_products' => $this->getLowStockProducts(), 'out_of_stock_products' => $this->getOutOfStockProducts(), 'inventory_value' => $this->getInventoryValue(), 'stock_movement' => $this->getStockMovement(), 'product_performance' => $this->getProductPerformance() ]; $this->jsonResponse([ 'success' => true, 'data' => $analytics ]); } catch (Exception $e) { $this->jsonResponse([ 'success' => false, 'error' => 'Failed to fetch inventory analytics', 'message' => $this->isDebugMode() ? $e->getMessage() : 'An error occurred' ], 500); } } /** * Get inventory overview * GET /api/admin/inventory */ public function inventory() { try { $page = (int) ($_GET['page'] ?? 1); $limit = (int) ($_GET['limit'] ?? 20); $status = $_GET['status'] ?? null; // low_stock, out_of_stock, in_stock $search = $_GET['search'] ?? null; $inventory = $this->getInventoryData($page, $limit, $status, $search); $this->jsonResponse([ 'success' => true, 'data' => $inventory ]); } catch (Exception $e) { $this->jsonResponse([ 'success' => false, 'error' => 'Failed to fetch inventory data', 'message' => $this->isDebugMode() ? $e->getMessage() : 'An error occurred' ], 500); } } /** * Update product stock * POST /api/admin/inventory/{productId}/stock */ public function updateStock($productId) { try { if (!is_numeric($productId)) { $this->jsonResponse([ 'success' => false, 'error' => 'Invalid product ID' ], 400); return; } $data = $this->getJsonInput(); // Validate input $validator = new Validator(); $isValid = $validator->validate($data, [ 'variant_id' => ['numeric'], 'quantity' => ['required', 'numeric', 'min:0'], 'operation' => ['required', 'in:set,add,subtract'], 'reason' => ['max:500'] ]); if (!$isValid) { $this->jsonResponse([ 'success' => false, 'error' => 'Validation failed', 'errors' => $validator->getErrors() ], 400); return; } $variantId = $data['variant_id'] ?? null; $quantity = (int) $data['quantity']; $operation = $data['operation']; $reason = $data['reason'] ?? 'Manual adjustment'; // Check if product exists $product = $this->productModel->find($productId); if (!$product) { $this->jsonResponse([ 'success' => false, 'error' => 'Product not found' ], 404); return; } // Update stock based on operation $result = $this->updateProductStock($productId, $variantId, $quantity, $operation, $reason); if ($result['success']) { $this->jsonResponse([ 'success' => true, 'message' => 'Stock updated successfully', 'data' => [ 'product_id' => $productId, 'variant_id' => $variantId, 'new_quantity' => $result['new_quantity'], 'operation' => $operation, 'adjustment' => $quantity ] ]); } else { $this->jsonResponse([ 'success' => false, 'error' => $result['error'] ], 400); } } catch (Exception $e) { $this->jsonResponse([ 'success' => false, 'error' => 'Failed to update stock', 'message' => $this->isDebugMode() ? $e->getMessage() : 'An error occurred' ], 500); } } /** * Get inventory alerts * GET /api/admin/inventory/alerts */ public function inventoryAlerts() { try { $alerts = [ 'low_stock' => $this->getLowStockProducts(), 'out_of_stock' => $this->getOutOfStockProducts(), 'overstocked' => $this->getOverstockedProducts(), 'negative_stock' => $this->getNegativeStockProducts() ]; $this->jsonResponse([ 'success' => true, 'data' => $alerts ]); } catch (Exception $e) { $this->jsonResponse([ 'success' => false, 'error' => 'Failed to fetch inventory alerts', 'message' => $this->isDebugMode() ? $e->getMessage() : 'An error occurred' ], 500); } } /** * Get sales statistics */ private function getSalesStats(string $startDate, string $endDate): array { $db = \App\Core\Database::getInstance(); try { // Total revenue for current period $revenueResult = $db->queryOne( "SELECT SUM(total_amount) as total_revenue, COUNT(*) as total_orders, AVG(total_amount) as average_order_value FROM orders WHERE payment_status = 'paid' AND DATE(created_at) BETWEEN ? AND ?", [$startDate, $endDate] ); // Previous period comparison $daysDiff = (strtotime($endDate) - strtotime($startDate)) / (60 * 60 * 24); $prevStartDate = date('Y-m-d', strtotime($startDate.' -'.$daysDiff.' days')); $prevEndDate = date('Y-m-d', strtotime($startDate.' -1 day')); $prevRevenueResult = $db->queryOne( "SELECT SUM(total_amount) as total_revenue FROM orders WHERE payment_status = 'paid' AND DATE(created_at) BETWEEN ? AND ?", [$prevStartDate, $prevEndDate] ); $currentRevenue = (float) ($revenueResult['total_revenue'] ?? 0); $prevRevenue = (float) ($prevRevenueResult['total_revenue'] ?? 0); $revenueChange = $prevRevenue > 0 ? (($currentRevenue - $prevRevenue) / $prevRevenue) * 100 : 0; // Get daily sales trend for the period $trendData = $this->getDailySalesTrend($startDate, $endDate); return [ 'total_revenue' => $currentRevenue, 'total_orders' => (int) ($revenueResult['total_orders'] ?? 0), 'average_order_value' => (float) ($revenueResult['average_order_value'] ?? 0), 'revenue_change' => round($revenueChange, 1), 'currency' => 'THB', 'trend' => $trendData ]; } catch (Exception $e) { return [ 'total_revenue' => 0.0, 'total_orders' => 0, 'average_order_value' => 0.0, 'revenue_change' => 0.0, 'currency' => 'THB' ]; } } /** * Get order statistics */ private function getOrderStats(string $startDate, string $endDate): array { $db = \App\Core\Database::getInstance(); try { $result = $db->query( "SELECT status, COUNT(*) as count, SUM(total_amount) as total_amount FROM orders WHERE DATE(created_at) BETWEEN ? AND ? GROUP BY status", [$startDate, $endDate] ); $stats = [ 'total_orders' => 0, 'orders_change' => 0, 'by_status' => [] ]; foreach ($result as $row) { $stats['total_orders'] += $row['count']; $stats['by_status'][$row['status']] = [ 'count' => (int) $row['count'], 'total_amount' => (float) $row['total_amount'] ]; } return $stats; } catch (Exception $e) { // Return default stats if query fails return [ 'total_orders' => 0, 'orders_change' => 0, 'by_status' => [] ]; } } /** * Get product statistics */ private function getProductStats(): array { $db = \App\Core\Database::getInstance(); try { $result = $db->queryOne( "SELECT COUNT(*) as total_products, SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active_products, SUM(CASE WHEN status = 'inactive' THEN 1 ELSE 0 END) as inactive_products, SUM(CASE WHEN featured = 1 THEN 1 ELSE 0 END) as featured_products FROM products" ); // Get products created this month vs last month for change calculation $thisMonth = date('Y-m-01'); $lastMonth = date('Y-m-01', strtotime('-1 month')); $lastMonthEnd = date('Y-m-t', strtotime('-1 month')); $thisMonthResult = $db->queryOne( "SELECT COUNT(*) as this_month_products FROM products WHERE DATE(created_at) >= ?", [$thisMonth] ); $lastMonthResult = $db->queryOne( "SELECT COUNT(*) as last_month_products FROM products WHERE DATE(created_at) BETWEEN ? AND ?", [$lastMonth, $lastMonthEnd] ); $thisMonthCount = (int) ($thisMonthResult['this_month_products'] ?? 0); $lastMonthCount = (int) ($lastMonthResult['last_month_products'] ?? 0); $productsChange = $lastMonthCount > 0 ? (($thisMonthCount - $lastMonthCount) / $lastMonthCount) * 100 : 0; return [ 'total_products' => (int) ($result['total_products'] ?? 0), 'active_products' => (int) ($result['active_products'] ?? 0), 'inactive_products' => (int) ($result['inactive_products'] ?? 0), 'featured_products' => (int) ($result['featured_products'] ?? 0), 'products_change' => round($productsChange, 1) ]; } catch (Exception $e) { return [ 'total_products' => 0, 'active_products' => 0, 'inactive_products' => 0, 'featured_products' => 0, 'products_change' => 0 ]; } } /** * Get customer statistics */ private function getCustomerStats(string $startDate, string $endDate): array { $db = \App\Core\Database::getInstance(); try { $result = $db->queryOne( "SELECT COUNT(*) as total_customers, SUM(CASE WHEN DATE(created_at) BETWEEN ? AND ? THEN 1 ELSE 0 END) as new_customers, SUM(CASE WHEN is_active = 1 THEN 1 ELSE 0 END) as active_customers FROM users WHERE role = 'customer'", [$startDate, $endDate] ); // Get previous period for comparison $daysDiff = (strtotime($endDate) - strtotime($startDate)) / (60 * 60 * 24); $prevStartDate = date('Y-m-d', strtotime($startDate.' -'.$daysDiff.' days')); $prevEndDate = date('Y-m-d', strtotime($startDate.' -1 day')); $prevResult = $db->queryOne( "SELECT COUNT(*) as prev_new_customers FROM users WHERE role = 'customer' AND DATE(created_at) BETWEEN ? AND ?", [$prevStartDate, $prevEndDate] ); $currentNew = (int) ($result['new_customers'] ?? 0); $prevNew = (int) ($prevResult['prev_new_customers'] ?? 0); $customersChange = $prevNew > 0 ? (($currentNew - $prevNew) / $prevNew) * 100 : 0; return [ 'total_customers' => (int) ($result['total_customers'] ?? 0), 'new_customers' => $currentNew, 'active_customers' => (int) ($result['active_customers'] ?? 0), 'customers_change' => round($customersChange, 1) ]; } catch (Exception $e) { return [ 'total_customers' => 0, 'new_customers' => 0, 'active_customers' => 0, 'customers_change' => 0 ]; } } /** * Get inventory statistics */ private function getInventoryStats(): array { $db = \App\Core\Database::getInstance(); // Get low stock and out of stock counts $stockResult = $db->queryOne( "SELECT COUNT(CASE WHEN pv.inventory_quantity <= p.min_stock_level AND pv.inventory_quantity > 0 THEN 1 END) as low_stock_count, COUNT(CASE WHEN pv.inventory_quantity = 0 THEN 1 END) as out_of_stock_count, SUM(pv.inventory_quantity * COALESCE(pv.price, p.base_price)) as inventory_value FROM products p LEFT JOIN product_variants pv ON p.id = pv.product_id WHERE p.status = 'active' AND p.track_inventory = 1" ); return [ 'low_stock_count' => (int) ($stockResult['low_stock_count'] ?? 0), 'out_of_stock_count' => (int) ($stockResult['out_of_stock_count'] ?? 0), 'inventory_value' => (float) ($stockResult['inventory_value'] ?? 0) ]; } /** * Get inventory data with pagination and filtering */ private function getInventoryData(int $page, int $limit, ?string $status, ?string $search): array { $db = \App\Core\Database::getInstance(); $offset = ($page - 1) * $limit; // Build WHERE clause $whereConditions = ['p.status = ?']; $params = ['active']; if ($search) { $whereConditions[] = '(p.name LIKE ? OR p.sku LIKE ?)'; $params[] = "%{$search}%"; $params[] = "%{$search}%"; } $whereClause = implode(' AND ', $whereConditions); // Get total count $countSql = "SELECT COUNT(DISTINCT p.id) as total FROM products p WHERE {$whereClause}"; $totalResult = $db->queryOne($countSql, $params); $total = (int) ($totalResult['total'] ?? 0); // Get inventory data $sql = "SELECT p.id, p.name, p.sku, p.base_price, p.min_stock_level, p.max_stock_level, p.track_inventory, p.inventory_quantity, c.name as category_name FROM products p LEFT JOIN categories c ON p.category_id = c.id WHERE {$whereClause} ORDER BY p.name ASC LIMIT {$limit} OFFSET {$offset}"; $results = $db->query($sql, $params); // Format products $products = []; foreach ($results as $row) { $products[] = [ 'id' => (int) $row['id'], 'name' => $row['name'], 'sku' => $row['sku'], 'category' => $row['category_name'], 'base_price' => (float) $row['base_price'], 'min_stock_level' => (int) $row['min_stock_level'], 'max_stock_level' => (int) $row['max_stock_level'], 'track_inventory' => (bool) $row['track_inventory'], 'inventory_quantity' => (int) $row['inventory_quantity'], 'status' => $this->getStockStatus((int) $row['inventory_quantity'], (int) $row['min_stock_level']) ]; } return [ 'inventory' => $products, 'pagination' => [ 'current_page' => $page, 'per_page' => $limit, 'total' => $total, 'total_pages' => ceil($total / $limit) ], 'stats' => $this->getInventoryStats() ]; } /** * Update product stock */ private function updateProductStock(int $productId, ?int $variantId, int $quantity, string $operation, string $reason): array { $db = \App\Core\Database::getInstance(); try { $db->getConnection()->beginTransaction(); // Get current stock from products table $currentStock = $db->queryOne( "SELECT inventory_quantity FROM products WHERE id = ?", [$productId] ); if (!$currentStock) { $db->getConnection()->rollBack(); return ['success' => false, 'error' => 'Product not found']; } $currentQuantity = (int) $currentStock['inventory_quantity']; // Calculate new quantity based on operation switch ($operation) { case 'set': $newQuantity = $quantity; break; case 'add': $newQuantity = $currentQuantity + $quantity; break; case 'subtract': $newQuantity = $currentQuantity - $quantity; if ($newQuantity < 0) { $db->getConnection()->rollBack(); return ['success' => false, 'error' => 'Insufficient stock for subtraction']; } break; default: $db->getConnection()->rollBack(); return ['success' => false, 'error' => 'Invalid operation']; } // Update stock $db->execute( "UPDATE products SET inventory_quantity = ?, updated_at = NOW() WHERE id = ?", [$newQuantity, $productId] ); // Log the inventory adjustment $this->logInventoryAdjustment($productId, $variantId, $currentQuantity, $newQuantity, $operation, $reason); $db->getConnection()->commit(); return [ 'success' => true, 'new_quantity' => $newQuantity ]; } catch (Exception $e) { $db->getConnection()->rollBack(); return ['success' => false, 'error' => 'Database error: '.$e->getMessage()]; } } /** * Log inventory adjustment */ private function logInventoryAdjustment(int $productId, ?int $variantId, int $oldQuantity, int $newQuantity, string $operation, string $reason): void { $db = \App\Core\Database::getInstance(); // Create inventory_adjustments table if it doesn't exist $db->execute(" CREATE TABLE IF NOT EXISTS inventory_adjustments ( id INT AUTO_INCREMENT PRIMARY KEY, product_id INT NOT NULL, variant_id INT NULL, old_quantity INT NOT NULL, new_quantity INT NOT NULL, adjustment_quantity INT NOT NULL, operation ENUM('set', 'add', 'subtract') NOT NULL, reason TEXT, admin_id INT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE ) "); $adjustmentQuantity = $newQuantity - $oldQuantity; $adminId = $_SESSION['user_id'] ?? null; $db->execute( "INSERT INTO inventory_adjustments (product_id, variant_id, old_quantity, new_quantity, adjustment_quantity, operation, reason, admin_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [$productId, $variantId, $oldQuantity, $newQuantity, $adjustmentQuantity, $operation, $reason, $adminId] ); } /** * Get stock status based on quantity and min level */ private function getStockStatus(int $quantity, int $minLevel): string { if ($quantity <= 0) { return 'out_of_stock'; } elseif ($quantity <= $minLevel) { return 'low_stock'; } else { return 'in_stock'; } } /** * Get low stock products */ private function getLowStockProducts(): array { $db = \App\Core\Database::getInstance(); $result = $db->query( "SELECT p.id, p.name, p.sku, p.inventory_quantity, p.min_stock_level FROM products p WHERE p.track_inventory = 1 AND p.inventory_quantity <= p.min_stock_level AND p.inventory_quantity > 0 ORDER BY p.inventory_quantity ASC LIMIT 20" ); return array_map(function ($row) { return [ 'product_id' => (int) $row['id'], 'name' => $row['name'], 'sku' => $row['sku'], 'current_stock' => (int) $row['inventory_quantity'], 'min_stock_level' => (int) $row['min_stock_level'] ]; }, $result); } /** * Get out of stock products */ private function getOutOfStockProducts(): array { $db = \App\Core\Database::getInstance(); $result = $db->query( "SELECT p.id, p.name, p.sku FROM products p WHERE p.track_inventory = 1 AND p.inventory_quantity = 0 ORDER BY p.name ASC LIMIT 20" ); return array_map(function ($row) { return [ 'product_id' => (int) $row['id'], 'name' => $row['name'], 'sku' => $row['sku'] ]; }, $result); } /** * Get overstocked products */ private function getOverstockedProducts(): array { $db = \App\Core\Database::getInstance(); $result = $db->query( "SELECT p.id, p.name, p.sku, p.inventory_quantity, p.max_stock_level FROM products p WHERE p.track_inventory = 1 AND p.max_stock_level > 0 AND p.inventory_quantity > p.max_stock_level ORDER BY (p.inventory_quantity - p.max_stock_level) DESC LIMIT 20" ); return array_map(function ($row) { return [ 'product_id' => (int) $row['id'], 'name' => $row['name'], 'sku' => $row['sku'], 'current_stock' => (int) $row['inventory_quantity'], 'max_stock_level' => (int) $row['max_stock_level'], 'excess_quantity' => (int) $row['inventory_quantity'] - (int) $row['max_stock_level'] ]; }, $result); } /** * Get products with negative stock */ private function getNegativeStockProducts(): array { $db = \App\Core\Database::getInstance(); $result = $db->query( "SELECT p.id, p.name, p.sku, p.inventory_quantity FROM products p WHERE p.track_inventory = 1 AND p.inventory_quantity < 0 ORDER BY p.inventory_quantity ASC LIMIT 20" ); return array_map(function ($row) { return [ 'product_id' => (int) $row['id'], 'name' => $row['name'], 'sku' => $row['sku'], 'current_stock' => (int) $row['inventory_quantity'] ]; }, $result); } // Placeholder methods for additional analytics /** * @param string $period * @param string $startDate * @param string $endDate */ private function getRevenueTrend(string $period, string $startDate, string $endDate): array {return [];} /** * @param string $startDate * @param string $endDate */ private function getTopSellingProducts(string $startDate, string $endDate): array {return [];} /** * @param string $startDate * @param string $endDate */ private function getSalesByCategory(string $startDate, string $endDate): array {return [];} /** * @param string $startDate * @param string $endDate */ private function getPaymentMethodStats(string $startDate, string $endDate): array {return [];} /** * @param string $startDate * @param string $endDate */ private function getOrderStatusDistribution(string $startDate, string $endDate): array {return [];} /** * @param string $startDate * @param string $endDate */ private function getNewCustomersData(string $startDate, string $endDate): array {return [];} private function getCustomerLifetimeValue(): array {return [];} /** * @param string $startDate * @param string $endDate */ private function getRepeatCustomerStats(string $startDate, string $endDate): array {return [];} private function getCustomerSegments(): array {return [];} private function getCustomerGeographicData(): array {return [];} private function getInventoryValue(): array {return [];} private function getStockMovement(): array {return [];} private function getProductPerformance(): array {return [];} /** * Get customers for admin management * GET /api/admin/customers */ public function customers() { try { $page = (int) ($_GET['page'] ?? 1); $limit = min((int) ($_GET['limit'] ?? 20), 100); $search = $_GET['search'] ?? ''; $role = $_GET['role'] ?? ''; $status = $_GET['status'] ?? ''; $filters = []; if ($search) { $filters['search'] = $search; } if ($role) { $filters['role'] = $role; } if ($status !== '') { $filters['is_active'] = (bool) $status; } $offset = ($page - 1) * $limit; $result = $this->userModel->getUsers($filters, $limit, $offset); $this->jsonResponse([ 'success' => true, 'data' => [ 'users' => $result['users'], 'pagination' => [ 'current_page' => $page, 'per_page' => $limit, 'total' => $result['total'], 'total_pages' => ceil($result['total'] / $limit) ] ] ]); } catch (Exception $e) { $this->jsonResponse([ 'success' => false, 'error' => 'Failed to fetch customers', 'message' => $this->isDebugMode() ? $e->getMessage() : 'An error occurred' ], 500); } } /** * Get products for admin management * GET /api/admin/products */ public function products() { try { $page = (int) ($_GET['page'] ?? 1); $limit = min((int) ($_GET['limit'] ?? 20), 100); $category = $_GET['category'] ?? ''; $status = $_GET['status'] ?? ''; $search = $_GET['search'] ?? ''; $filters = [ 'status' => $status ?: null, 'category_id' => $category ?: null, 'search' => $search ?: null ]; $offset = ($page - 1) * $limit; $result = $this->productModel->getProducts($filters, $limit, $offset); $this->jsonResponse([ 'success' => true, 'data' => [ 'products' => $result['products'], 'pagination' => [ 'current_page' => $page, 'per_page' => $limit, 'total' => $result['total'], 'total_pages' => ceil($result['total'] / $limit) ] ] ]); } catch (Exception $e) { $this->jsonResponse([ 'success' => false, 'error' => 'Failed to fetch products', 'message' => $this->isDebugMode() ? $e->getMessage() : 'An error occurred' ], 500); } } /** * Get categories for admin management * GET /api/admin/categories */ public function categories() { try { $search = $_GET['search'] ?? ''; $status = $_GET['status'] ?? ''; $parent_id = $_GET['parent_id'] ?? null; $filters = [ 'search' => $search ?: null, 'status' => $status ?: null, 'parent_id' => $parent_id ]; $categories = $this->categoryModel->getCategories($filters); $this->jsonResponse([ 'success' => true, 'data' => [ 'categories' => $categories ] ]); } catch (Exception $e) { $this->jsonResponse([ 'success' => false, 'error' => 'Failed to fetch categories', 'message' => $this->isDebugMode() ? $e->getMessage() : 'An error occurred' ], 500); } } /** * Create a new category * POST /api/admin/categories */ public function createCategory() { try { $data = $this->getJsonInput(); // Validate input $validator = new Validator(); $rules = [ 'name' => ['required', 'string', 'min:2', 'max:100'], 'description' => ['optional', 'string'], 'parent_id' => ['optional', 'integer'], 'status' => ['optional', 'string'], 'sort_order' => ['optional', 'integer'] ]; if (!$validator->validate($data, $rules)) { $this->jsonResponse([ 'success' => false, 'error' => 'Validation failed', 'errors' => $validator->getErrors() ], 400); return; } $categoryData = [ 'name' => trim($data['name']), 'description' => isset($data['description']) ? trim($data['description']) : null, 'parent_id' => isset($data['parent_id']) ? (int)$data['parent_id'] : null, 'status' => $data['status'] ?? 'active', 'sort_order' => $data['sort_order'] ?? 0 ]; $categoryId = $this->categoryModel->create($categoryData); if ($categoryId) { $category = $this->categoryModel->findById($categoryId); $this->jsonResponse([ 'success' => true, 'message' => 'Category created successfully', 'data' => [ 'category' => $category ] ], 201); } else { throw new Exception('Failed to create category'); } } catch (Exception $e) { $this->jsonResponse([ 'success' => false, 'error' => 'Failed to create category', 'message' => $this->isDebugMode() ? $e->getMessage() : 'An error occurred' ], 500); } } /** * Update category * POST /api/admin/categories/{id} with _method=PUT */ public function updateCategory($categoryId) { try { $data = $this->getJsonInput(); // Validate input $validator = new Validator(); $rules = [ 'name' => ['required', 'string', 'min:2', 'max:100'], 'description' => ['optional', 'string'], 'parent_id' => ['optional', 'integer'], 'status' => ['optional', 'string'], 'sort_order' => ['optional', 'integer'] ]; if (!$validator->validate($data, $rules)) { $this->jsonResponse([ 'success' => false, 'error' => 'Validation failed', 'errors' => $validator->getErrors() ], 400); return; } // Check if category exists $category = $this->categoryModel->findById($categoryId); if (!$category) { $this->jsonResponse([ 'success' => false, 'error' => 'Category not found' ], 404); return; } $categoryData = [ 'name' => trim($data['name']), 'description' => isset($data['description']) ? trim($data['description']) : null, 'parent_id' => isset($data['parent_id']) ? (int)$data['parent_id'] : null, 'status' => $data['status'] ?? $category['status'], 'sort_order' => $data['sort_order'] ?? $category['sort_order'] ]; $updated = $this->categoryModel->update($categoryId, $categoryData); if ($updated) { $updatedCategory = $this->categoryModel->findById($categoryId); $this->jsonResponse([ 'success' => true, 'message' => 'Category updated successfully', 'data' => [ 'category' => $updatedCategory ] ]); } else { throw new Exception('Failed to update category'); } } catch (Exception $e) { $this->jsonResponse([ 'success' => false, 'error' => 'Failed to update category', 'message' => $this->isDebugMode() ? $e->getMessage() : 'An error occurred' ], 500); } } /** * Delete category * POST /api/admin/categories/{id}/delete with _method=DELETE */ public function deleteCategory($categoryId) { try { // Check if category exists $category = $this->categoryModel->findById($categoryId); if (!$category) { $this->jsonResponse([ 'success' => false, 'error' => 'Category not found' ], 404); return; } // Check if category has products $productCount = $this->productModel->countByCategory($categoryId); if ($productCount > 0) { $this->jsonResponse([ 'success' => false, 'error' => 'Cannot delete category with existing products', 'message' => "This category has {$productCount} products. Please move or delete them first." ], 400); return; } // Check if category has subcategories $subcategoryCount = $this->categoryModel->countSubcategories($categoryId); if ($subcategoryCount > 0) { $this->jsonResponse([ 'success' => false, 'error' => 'Cannot delete category with subcategories', 'message' => "This category has {$subcategoryCount} subcategories. Please move or delete them first." ], 400); return; } $deleted = $this->categoryModel->delete($categoryId); if ($deleted) { $this->jsonResponse([ 'success' => true, 'message' => 'Category deleted successfully' ]); } else { throw new Exception('Failed to delete category'); } } catch (Exception $e) { $this->jsonResponse([ 'success' => false, 'error' => 'Failed to delete category', 'message' => $this->isDebugMode() ? $e->getMessage() : 'An error occurred' ], 500); } } /** * Get orders for admin management * GET /api/admin/orders */ public function orders() { try { $page = (int) ($_GET['page'] ?? 1); $limit = min((int) ($_GET['limit'] ?? 20), 100); $status = $_GET['status'] ?? ''; $paymentStatus = $_GET['payment_status'] ?? ''; $dateFrom = $_GET['date_from'] ?? ''; $dateTo = $_GET['date_to'] ?? ''; $filters = []; if ($status) { $filters['status'] = $status; } if ($paymentStatus) { $filters['payment_status'] = $paymentStatus; } if ($dateFrom) { $filters['date_from'] = $dateFrom; } if ($dateTo) { $filters['date_to'] = $dateTo; } $offset = ($page - 1) * $limit; $result = $this->orderModel->getOrders($filters, $limit, $offset); $this->jsonResponse([ 'success' => true, 'data' => [ 'orders' => $result['orders'], 'pagination' => [ 'current_page' => $page, 'per_page' => $limit, 'total' => $result['total'], 'total_pages' => ceil($result['total'] / $limit) ] ] ]); } catch (Exception $e) { $this->jsonResponse([ 'success' => false, 'error' => 'Failed to fetch orders', 'message' => $this->isDebugMode() ? $e->getMessage() : 'An error occurred' ], 500); } } /** * Update order status * POST /api/admin/orders/{id}/status */ public function updateOrderStatus($orderId) { try { $data = $this->getJsonInput(); $validator = new Validator(); $isValid = $validator->validate($data, [ 'status' => ['required', 'in:pending,confirmed,processing,shipped,delivered,cancelled'], 'notes' => ['max:1000'] ]); if (!$isValid) { $this->jsonResponse([ 'success' => false, 'error' => 'Validation failed', 'errors' => $validator->getErrors() ], 400); return; } $success = $this->orderModel->updateStatus( $orderId, $data['status'], $data['notes'] ?? null ); if ($success) { // TODO: Send notification email to customer $this->jsonResponse([ 'success' => true, 'message' => 'Order status updated successfully' ]); } else { $this->jsonResponse([ 'success' => false, 'error' => 'Failed to update order status' ], 500); } } catch (Exception $e) { $this->jsonResponse([ 'success' => false, 'error' => 'Failed to update order status', 'message' => $this->isDebugMode() ? $e->getMessage() : 'An error occurred' ], 500); } } /** * Get daily sales trend data for chart */ private function getDailySalesTrend(string $startDate, string $endDate): array { $db = \App\Core\Database::getInstance(); try { // Get daily sales data $result = $db->query( "SELECT DATE(created_at) as date, SUM(total_amount) as daily_revenue, COUNT(*) as daily_orders FROM orders WHERE payment_status = 'paid' AND DATE(created_at) BETWEEN ? AND ? GROUP BY DATE(created_at) ORDER BY DATE(created_at)", [$startDate, $endDate] ); // Create labels and data arrays $labels = []; $data = []; // Fill in missing dates with 0 values $start = strtotime($startDate); $end = strtotime($endDate); $dailyData = []; // Index existing data by date foreach ($result as $row) { $dailyData[$row['date']] = (float) $row['daily_revenue']; } // Generate complete date range for ($current = $start; $current <= $end; $current += 86400) { $dateStr = date('Y-m-d', $current); $labels[] = date('M j', $current); // Format: "Aug 1" $data[] = $dailyData[$dateStr] ?? 0; } return [ 'labels' => $labels, 'data' => $data ]; } catch (Exception $e) { // Return empty trend data on error return [ 'labels' => [], 'data' => [] ]; } } }