= $isEditing ? 'Редактировать отзыв' : 'Оставить отзыв' ?>
+ + +Пока нет отзывов об этом товаре. Будьте первым!
+diff --git a/app/Controllers/AdminController.php b/app/Controllers/AdminController.php index f876cd6..1c0400f 100644 --- a/app/Controllers/AdminController.php +++ b/app/Controllers/AdminController.php @@ -153,6 +153,28 @@ class AdminController extends Controller $this->redirect('/admin/products?message=' . urlencode('Товар скрыт')); } + public function removeProduct(int $id): void + { + $this->requireAdmin(); + + $product = $this->productModel->find($id); + + if (!$product) { + $this->redirect('/admin/products?error=' . urlencode('Товар не найден')); + return; + } + + // Удаляем только если товара нет на складе + if ($product['stock_quantity'] > 0) { + $this->redirect('/admin/products?error=' . urlencode('Нельзя удалить товар, который есть на складе')); + return; + } + + // Полное удаление из БД + $this->productModel->delete($id); + $this->redirect('/admin/products?message=' . urlencode('Товар удален из базы данных')); + } + public function categories(): void { $this->requireAdmin(); diff --git a/app/Controllers/CartController.php b/app/Controllers/CartController.php index 6176046..33db5c5 100644 --- a/app/Controllers/CartController.php +++ b/app/Controllers/CartController.php @@ -21,6 +21,11 @@ class CartController extends Controller { $this->requireAuth(); + if ($this->isAdmin()) { + $this->redirect('/catalog'); + return; + } + $user = $this->getCurrentUser(); $cartItems = $this->cartModel->getUserCart($user['id']); $totals = $this->cartModel->getCartTotal($user['id']); @@ -43,6 +48,14 @@ class CartController extends Controller return; } + if ($this->isAdmin()) { + $this->json([ + 'success' => false, + 'message' => 'Доступ запрещен' + ]); + return; + } + $productId = (int) $this->getPost('product_id', 0); $quantity = (int) $this->getPost('quantity', 1); $userId = $this->getCurrentUser()['id']; @@ -104,6 +117,14 @@ class CartController extends Controller return; } + if ($this->isAdmin()) { + $this->json([ + 'success' => false, + 'message' => 'Доступ запрещен' + ]); + return; + } + $productId = (int) $this->getPost('product_id', 0); $quantity = (int) $this->getPost('quantity', 1); $userId = $this->getCurrentUser()['id']; @@ -154,6 +175,14 @@ class CartController extends Controller return; } + if ($this->isAdmin()) { + $this->json([ + 'success' => false, + 'message' => 'Доступ запрещен' + ]); + return; + } + $productId = (int) $this->getPost('product_id', 0); $userId = $this->getCurrentUser()['id']; diff --git a/app/Controllers/OrderController.php b/app/Controllers/OrderController.php index bf38d9c..79cd6e6 100644 --- a/app/Controllers/OrderController.php +++ b/app/Controllers/OrderController.php @@ -21,6 +21,11 @@ class OrderController extends Controller { $this->requireAuth(); + if ($this->isAdmin()) { + $this->redirect('/catalog'); + return; + } + $user = $this->getCurrentUser(); $cartItems = $this->cartModel->getUserCart($user['id']); $totals = $this->cartModel->getCartTotal($user['id']); @@ -43,6 +48,14 @@ class OrderController extends Controller return; } + if ($this->isAdmin()) { + $this->json([ + 'success' => false, + 'message' => 'Доступ запрещен' + ]); + return; + } + $user = $this->getCurrentUser(); $cartItems = $this->cartModel->getUserCart($user['id']); diff --git a/app/Controllers/ProductController.php b/app/Controllers/ProductController.php index e33acf1..c0c59b3 100644 --- a/app/Controllers/ProductController.php +++ b/app/Controllers/ProductController.php @@ -5,16 +5,19 @@ namespace App\Controllers; use App\Core\Controller; use App\Models\Product; use App\Models\Category; +use App\Models\Review; class ProductController extends Controller { private Product $productModel; private Category $categoryModel; + private Review $reviewModel; public function __construct() { $this->productModel = new Product(); $this->categoryModel = new Category(); + $this->reviewModel = new Review(); } public function catalog(): void @@ -33,10 +36,12 @@ class ProductController extends Controller 'materials' => $this->getQuery('materials', []) ]; - $showAll = $isAdmin && $this->getQuery('show_all') === '1'; + // Для админа по умолчанию показываем все товары (включая недоступные) + // Для обычных пользователей - только доступные + $showAll = $isAdmin && $this->getQuery('show_all') !== '0'; $categories = $this->categoryModel->getActive(); - $products = $showAll + $products = ($isAdmin && $showAll) ? $this->productModel->getAllForAdmin(true) : $this->productModel->getAvailable($filters); @@ -80,10 +85,22 @@ class ProductController extends Controller $product['category_id'] ); + // Load reviews + $reviews = $this->reviewModel->getByProduct($id, true); + + // Check if current user has already reviewed + $user = $this->getCurrentUser(); + $userReview = null; + if ($user && !$this->isAdmin()) { + $userReview = $this->reviewModel->getByUser($user['id'], $id); + } + $this->view('products/show', [ 'product' => $product, 'similarProducts' => $similarProducts, - 'user' => $this->getCurrentUser(), + 'reviews' => $reviews, + 'userReview' => $userReview, + 'user' => $user, 'isLoggedIn' => true, 'isAdmin' => $this->isAdmin() ]); diff --git a/app/Controllers/ReviewController.php b/app/Controllers/ReviewController.php new file mode 100644 index 0000000..2c4b3e6 --- /dev/null +++ b/app/Controllers/ReviewController.php @@ -0,0 +1,298 @@ +reviewModel = new Review(); + $this->productModel = new Product(); + } + + /** + * Create a new review (POST /reviews) + */ + public function create(): void + { + // Require authentication + if (!$this->isAuthenticated()) { + $this->json([ + 'success' => false, + 'message' => 'Требуется авторизация' + ], 401); + return; + } + + // Admins cannot leave reviews + if ($this->isAdmin()) { + $this->json([ + 'success' => false, + 'message' => 'Администраторы не могут оставлять отзывы' + ], 403); + return; + } + + $productId = (int) $this->getPost('product_id', 0); + $rating = (int) $this->getPost('rating', 0); + $comment = trim($this->getPost('comment', '')); + $user = $this->getCurrentUser(); + + // Validate product ID + if ($productId <= 0) { + $this->json([ + 'success' => false, + 'message' => 'Неверный ID товара' + ], 400); + return; + } + + // Check if product exists + $product = $this->productModel->find($productId); + if (!$product) { + $this->json([ + 'success' => false, + 'message' => 'Товар не найден' + ], 404); + return; + } + + // Validate rating + if ($rating < 1 || $rating > 5) { + $this->json([ + 'success' => false, + 'message' => 'Рейтинг должен быть от 1 до 5' + ], 400); + return; + } + + // Check if user can review + if (!$this->reviewModel->userCanReview($user['id'], $productId)) { + $this->json([ + 'success' => false, + 'message' => 'Вы уже оставили отзыв на этот товар' + ], 400); + return; + } + + // Create review + $reviewId = $this->reviewModel->createReview([ + 'product_id' => $productId, + 'user_id' => $user['id'], + 'rating' => $rating, + 'comment' => $comment, + 'is_approved' => true + ]); + + if ($reviewId) { + // Get updated product info + $updatedProduct = $this->productModel->find($productId); + + $this->json([ + 'success' => true, + 'message' => 'Спасибо за ваш отзыв!', + 'review_id' => $reviewId, + 'product_rating' => $updatedProduct['rating'] ?? 0, + 'product_review_count' => $updatedProduct['review_count'] ?? 0 + ]); + } else { + $this->json([ + 'success' => false, + 'message' => 'Ошибка при создании отзыва' + ], 500); + } + } + + /** + * Update a review (POST /reviews/{id}) + */ + public function update(int $id): void + { + // Require authentication + if (!$this->isAuthenticated()) { + $this->json([ + 'success' => false, + 'message' => 'Требуется авторизация' + ], 401); + return; + } + + $user = $this->getCurrentUser(); + $review = $this->reviewModel->find($id); + + if (!$review) { + $this->json([ + 'success' => false, + 'message' => 'Отзыв не найден' + ], 404); + return; + } + + // Check ownership (only owner or admin can update) + if ($review['user_id'] !== $user['id'] && !$this->isAdmin()) { + $this->json([ + 'success' => false, + 'message' => 'У вас нет прав для редактирования этого отзыва' + ], 403); + return; + } + + $rating = (int) $this->getPost('rating', 0); + $comment = trim($this->getPost('comment', '')); + + // Validate rating + if ($rating < 1 || $rating > 5) { + $this->json([ + 'success' => false, + 'message' => 'Рейтинг должен быть от 1 до 5' + ], 400); + return; + } + + // Update review + $success = $this->reviewModel->updateReview($id, [ + 'rating' => $rating, + 'comment' => $comment + ]); + + if ($success) { + // Get updated product info + $updatedProduct = $this->productModel->find($review['product_id']); + + $this->json([ + 'success' => true, + 'message' => 'Отзыв обновлен', + 'product_rating' => $updatedProduct['rating'] ?? 0, + 'product_review_count' => $updatedProduct['review_count'] ?? 0 + ]); + } else { + $this->json([ + 'success' => false, + 'message' => 'Ошибка при обновлении отзыва' + ], 500); + } + } + + /** + * Delete a review (POST /reviews/{id}/delete) + */ + public function delete(int $id): void + { + // Require authentication + if (!$this->isAuthenticated()) { + $this->json([ + 'success' => false, + 'message' => 'Требуется авторизация' + ], 401); + return; + } + + $user = $this->getCurrentUser(); + $review = $this->reviewModel->find($id); + + if (!$review) { + $this->json([ + 'success' => false, + 'message' => 'Отзыв не найден' + ], 404); + return; + } + + // Check ownership (only owner or admin can delete) + if ($review['user_id'] !== $user['id'] && !$this->isAdmin()) { + $this->json([ + 'success' => false, + 'message' => 'У вас нет прав для удаления этого отзыва' + ], 403); + return; + } + + $productId = $review['product_id']; + $success = $this->reviewModel->deleteReview($id); + + if ($success) { + // Get updated product info + $updatedProduct = $this->productModel->find($productId); + + $this->json([ + 'success' => true, + 'message' => 'Отзыв удален', + 'product_rating' => $updatedProduct['rating'] ?? 0, + 'product_review_count' => $updatedProduct['review_count'] ?? 0 + ]); + } else { + $this->json([ + 'success' => false, + 'message' => 'Ошибка при удалении отзыва' + ], 500); + } + } + + /** + * Get reviews for a product (GET /reviews/product/{id}) + */ + public function getByProduct(int $productId): void + { + $product = $this->productModel->find($productId); + + if (!$product) { + $this->json([ + 'success' => false, + 'message' => 'Товар не найден' + ], 404); + return; + } + + $reviews = $this->reviewModel->getByProduct($productId, true); + $distribution = $this->reviewModel->getRatingDistribution($productId); + + $this->json([ + 'success' => true, + 'reviews' => $reviews, + 'rating_distribution' => $distribution, + 'average_rating' => $product['rating'] ?? 0, + 'total_reviews' => $product['review_count'] ?? 0 + ]); + } + + /** + * Toggle review approval (admin only) (POST /reviews/{id}/toggle-approval) + */ + public function toggleApproval(int $id): void + { + $this->requireAdmin(); + + $review = $this->reviewModel->find($id); + if (!$review) { + $this->json([ + 'success' => false, + 'message' => 'Отзыв не найден' + ], 404); + return; + } + + $success = $this->reviewModel->toggleApproval($id); + + if ($success) { + $updatedReview = $this->reviewModel->find($id); + $this->json([ + 'success' => true, + 'message' => 'Статус отзыва изменен', + 'is_approved' => $updatedReview['is_approved'] + ]); + } else { + $this->json([ + 'success' => false, + 'message' => 'Ошибка при изменении статуса' + ], 500); + } + } +} + diff --git a/app/Models/Product.php b/app/Models/Product.php index 32ffacf..9af4bde 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -11,7 +11,9 @@ class Product extends Model public function findWithCategory(int $id): ?array { - $sql = "SELECT p.*, c.name as category_name, c.slug as category_slug + $sql = "SELECT p.*, c.name as category_name, c.slug as category_slug, + COALESCE(p.rating, 0) as rating, + COALESCE(p.review_count, 0) as review_count FROM {$this->table} p LEFT JOIN categories c ON p.category_id = c.category_id WHERE p.product_id = ?"; @@ -20,7 +22,9 @@ class Product extends Model public function getAvailable(array $filters = [], int $limit = 50): array { - $sql = "SELECT p.*, c.name as category_name + $sql = "SELECT p.*, c.name as category_name, + COALESCE(p.rating, 0) as rating, + COALESCE(p.review_count, 0) as review_count FROM {$this->table} p LEFT JOIN categories c ON p.category_id = c.category_id WHERE p.is_available = TRUE"; @@ -67,7 +71,9 @@ class Product extends Model public function getAllForAdmin(bool $showAll = true): array { - $sql = "SELECT p.*, c.name as category_name + $sql = "SELECT p.*, c.name as category_name, + COALESCE(p.rating, 0) as rating, + COALESCE(p.review_count, 0) as review_count FROM {$this->table} p LEFT JOIN categories c ON p.category_id = c.category_id"; @@ -82,7 +88,10 @@ class Product extends Model public function getSimilar(int $productId, int $categoryId, int $limit = 3): array { - $sql = "SELECT * FROM {$this->table} + $sql = "SELECT *, + COALESCE(rating, 0) as rating, + COALESCE(review_count, 0) as review_count + FROM {$this->table} WHERE category_id = ? AND product_id != ? AND is_available = TRUE diff --git a/app/Models/Review.php b/app/Models/Review.php new file mode 100644 index 0000000..c707337 --- /dev/null +++ b/app/Models/Review.php @@ -0,0 +1,246 @@ +getByUser($data['user_id'], $data['product_id']); + if ($existing) { + return null; // User already reviewed this product + } + + return $this->create([ + 'product_id' => $data['product_id'], + 'user_id' => $data['user_id'], + 'rating' => $data['rating'], + 'comment' => $data['comment'] ?? null, + 'is_approved' => $data['is_approved'] ?? true + ]); + } + + /** + * Get all reviews for a product + */ + public function getByProduct(int $productId, bool $approvedOnly = true): array + { + $sql = "SELECT r.*, u.full_name, u.email + FROM {$this->table} r + INNER JOIN users u ON r.user_id = u.user_id"; + + if ($approvedOnly) { + $sql .= " WHERE r.product_id = ? AND r.is_approved = TRUE"; + } else { + $sql .= " WHERE r.product_id = ?"; + } + + $sql .= " ORDER BY r.created_at DESC"; + + return $this->query($sql, [$productId]); + } + + /** + * Get review by specific user for a product + */ + public function getByUser(int $userId, int $productId): ?array + { + $sql = "SELECT * FROM {$this->table} + WHERE user_id = ? AND product_id = ?"; + return $this->queryOne($sql, [$userId, $productId]); + } + + /** + * Get review with user details + */ + public function findWithUser(int $reviewId): ?array + { + $sql = "SELECT r.*, u.full_name, u.email + FROM {$this->table} r + INNER JOIN users u ON r.user_id = u.user_id + WHERE r.review_id = ?"; + return $this->queryOne($sql, [$reviewId]); + } + + /** + * Update a review + */ + public function updateReview(int $reviewId, array $data): bool + { + $updateData = []; + + if (isset($data['rating'])) { + $updateData['rating'] = $data['rating']; + } + if (isset($data['comment'])) { + $updateData['comment'] = $data['comment']; + } + if (isset($data['is_approved'])) { + $updateData['is_approved'] = $data['is_approved']; + } + + if (empty($updateData)) { + return false; + } + + return $this->update($reviewId, $updateData); + } + + /** + * Delete a review + */ + public function deleteReview(int $reviewId): bool + { + return $this->delete($reviewId); + } + + /** + * Get average rating for a product + */ + public function getAverageRating(int $productId): float + { + $sql = "SELECT COALESCE(AVG(rating), 0.00) as avg_rating + FROM {$this->table} + WHERE product_id = ? AND is_approved = TRUE"; + $result = $this->queryOne($sql, [$productId]); + return round((float)$result['avg_rating'], 2); + } + + /** + * Get review count for a product + */ + public function getReviewCount(int $productId): int + { + return $this->count([ + 'product_id' => $productId, + 'is_approved' => true + ]); + } + + /** + * Update product's rating and review count + * This is called automatically by database triggers, + * but can also be called manually if needed + */ + public function updateProductRating(int $productId): void + { + // Call the PostgreSQL function + $sql = "SELECT update_product_rating(?)"; + $this->execute($sql, [$productId]); + } + + /** + * Get rating distribution for a product (useful for charts) + */ + public function getRatingDistribution(int $productId): array + { + $sql = "SELECT rating, COUNT(*) as count + FROM {$this->table} + WHERE product_id = ? AND is_approved = TRUE + GROUP BY rating + ORDER BY rating DESC"; + + $results = $this->query($sql, [$productId]); + + // Initialize all ratings with 0 + $distribution = [ + 5 => 0, + 4 => 0, + 3 => 0, + 2 => 0, + 1 => 0 + ]; + + // Fill in actual counts + foreach ($results as $row) { + $distribution[$row['rating']] = (int)$row['count']; + } + + return $distribution; + } + + /** + * Get recent reviews across all products (for admin dashboard) + */ + public function getRecent(int $limit = 10, bool $approvedOnly = false): array + { + $sql = "SELECT r.*, u.full_name, u.email, p.name as product_name + FROM {$this->table} r + INNER JOIN users u ON r.user_id = u.user_id + INNER JOIN products p ON r.product_id = p.product_id"; + + if ($approvedOnly) { + $sql .= " WHERE r.is_approved = TRUE"; + } + + $sql .= " ORDER BY r.created_at DESC LIMIT ?"; + + return $this->query($sql, [$limit]); + } + + /** + * Check if user can review (has purchased the product) + * This is optional - you might want to allow reviews only from buyers + */ + public function userCanReview(int $userId, int $productId): bool + { + // Check if user already reviewed + $existing = $this->getByUser($userId, $productId); + if ($existing) { + return false; + } + + // Optional: Check if user has purchased this product + // For now, we'll allow any authenticated user to review + return true; + + /* Uncomment this to require purchase before review: + $sql = "SELECT COUNT(*) as count + FROM order_items oi + INNER JOIN orders o ON oi.order_id = o.order_id + WHERE o.user_id = ? AND oi.product_id = ? AND o.status = 'completed'"; + $result = $this->queryOne($sql, [$userId, $productId]); + return $result && $result['count'] > 0; + */ + } + + /** + * Toggle review approval status (admin function) + */ + public function toggleApproval(int $reviewId): bool + { + $review = $this->find($reviewId); + if (!$review) { + return false; + } + + return $this->update($reviewId, [ + 'is_approved' => !$review['is_approved'] + ]); + } + + /** + * Get all reviews by a specific user + */ + public function getUserReviews(int $userId, int $limit = 50): array + { + $sql = "SELECT r.*, p.name as product_name, p.image_url as product_image + FROM {$this->table} r + INNER JOIN products p ON r.product_id = p.product_id + WHERE r.user_id = ? + ORDER BY r.created_at DESC + LIMIT ?"; + + return $this->query($sql, [$userId, $limit]); + } +} + diff --git a/app/Views/admin/products/index.php b/app/Views/admin/products/index.php index f9c783b..9ca523f 100644 --- a/app/Views/admin/products/index.php +++ b/app/Views/admin/products/index.php @@ -66,6 +66,14 @@ + +
+ diff --git a/app/Views/home/index.php b/app/Views/home/index.php index 7f7a613..f7299d4 100644 --- a/app/Views/home/index.php +++ b/app/Views/home/index.php @@ -155,6 +155,6 @@ - + Задать вопрос diff --git a/app/Views/layouts/main.php b/app/Views/layouts/main.php index 615e8e2..cfc6b28 100644 --- a/app/Views/layouts/main.php +++ b/app/Views/layouts/main.php @@ -38,7 +38,17 @@ - = \App\Core\View::partial('header', ['user' => $user ?? null, 'isLoggedIn' => $isLoggedIn ?? \App\Core\View::isAuthenticated(), 'isAdmin' => $isAdmin ?? \App\Core\View::isAdmin()]) ?> + getActive(); + ?> + = \App\Core\View::partial('header', [ + 'user' => $user ?? null, + 'isLoggedIn' => $isLoggedIn ?? \App\Core\View::isAuthenticated(), + 'isAdmin' => $isAdmin ?? \App\Core\View::isAdmin(), + 'categories' => $headerCategories ?? [] + ]) ?>Пока нет отзывов об этом товаре. Будьте первым!
+