Files
web_work/app/Controllers/AdminController.php
kirill.khorkov a4092adf2e feat: Add complete reviews system with star ratings
 New Features:
- Reviews system with 1-5 star ratings
- User can add, edit, and delete their own reviews
- One review per product per user (DB constraint)
- Automatic average rating calculation
- Review count tracking
- Interactive star selection UI
- AJAX-powered review submission
- Responsive design for all devices

🗄️ Database:
- New 'reviews' table with full structure
- Added 'rating' and 'review_count' fields to products
- PostgreSQL triggers for automatic rating updates
- Database functions for rating calculations
- Indexes for performance optimization

📦 Backend (PHP):
- Review model with 15+ methods
- ReviewController with 5 actions
- Updated Product model to include ratings
- Updated ProductController to load reviews
- 5 new API endpoints

🎨 Frontend:
- Reviews list component (_reviews_list.php)
- Review form component (_review_form.php)
- Reviews sechow page
- Star ratings in catalog view
- Interactive JavaScript (200+ lines)
- Adaptive styles (400+ lines)

🔒 Security:
- Server-side authorization checks
- XSS protection (htmlspecialchars)
- SQL injection protection (PDO prepared)
- Input validation (client + server)
- Access control for review editing

📝 Modified Files:
- app/Models/Product.php - added rating fields to queries
- app/Controllers/ProductController.php - loads reviews
- app/Views/products/show.php - reviews section
- app/Views/products/catalog.php - star ratings
- config/routes.php - review endpoints
- public/style_for_cite.less - rating styles

🆕 New Files:
- app/Models/Review.php
- app/Controllers/ReviewController.php
- app/Views/products/_reviews_list.php
- app/Views/products/_review_form.php
2026-01-06 17:04:09 +03:00

334 lines
11 KiB
PHP

<?php
namespace App\Controllers;
use App\Core\Controller;
use App\Models\Product;
use App\Models\Category;
use App\Models\Order;
use App\Models\User;
class AdminController extends Controller
{
private Product $productModel;
private Category $categoryModel;
private Order $orderModel;
private User $userModel;
public function __construct()
{
$this->productModel = new Product();
$this->categoryModel = new Category();
$this->orderModel = new Order();
$this->userModel = new User();
}
public function dashboard(): void
{
$this->requireAdmin();
$stats = [
'total_products' => $this->productModel->count(),
'active_products' => $this->productModel->count(['is_available' => true]),
'total_orders' => $this->orderModel->count(),
'total_users' => $this->userModel->count(),
'revenue' => $this->orderModel->getStats()['revenue']
];
$this->view('admin/dashboard', [
'stats' => $stats,
'user' => $this->getCurrentUser()
], 'admin');
}
public function products(): void
{
$this->requireAdmin();
$showAll = $this->getQuery('show_all') === '1';
$products = $this->productModel->getAllForAdmin($showAll);
$this->view('admin/products/index', [
'products' => $products,
'showAll' => $showAll,
'message' => $this->getQuery('message'),
'error' => $this->getQuery('error'),
'user' => $this->getCurrentUser()
], 'admin');
}
public function addProduct(): void
{
$this->requireAdmin();
$categories = $this->categoryModel->getActive();
$this->view('admin/products/form', [
'product' => null,
'categories' => $categories,
'action' => 'add',
'user' => $this->getCurrentUser()
], 'admin');
}
public function storeProduct(): void
{
$this->requireAdmin();
$data = [
'name' => $this->getPost('name'),
'category_id' => (int) $this->getPost('category_id'),
'description' => $this->getPost('description'),
'price' => (float) $this->getPost('price'),
'old_price' => $this->getPost('old_price') ? (float) $this->getPost('old_price') : null,
'sku' => $this->getPost('sku'),
'stock_quantity' => (int) $this->getPost('stock_quantity', 0),
'is_available' => $this->getPost('is_available') ? true : false,
'is_featured' => $this->getPost('is_featured') ? true : false,
'image_url' => $this->getPost('image_url'),
'color' => $this->getPost('color'),
'material' => $this->getPost('material'),
'card_size' => $this->getPost('card_size', 'small')
];
try {
$this->productModel->createProduct($data);
$this->redirect('/admin/products?message=' . urlencode('Товар успешно добавлен'));
} catch (\Exception $e) {
$this->redirect('/admin/products/add?error=' . urlencode($e->getMessage()));
}
}
public function editProduct(int $id): void
{
$this->requireAdmin();
$product = $this->productModel->find($id);
if (!$product) {
$this->redirect('/admin/products?error=' . urlencode('Товар не найден'));
return;
}
$categories = $this->categoryModel->getActive();
$this->view('admin/products/form', [
'product' => $product,
'categories' => $categories,
'action' => 'edit',
'user' => $this->getCurrentUser()
], 'admin');
}
public function updateProduct(int $id): void
{
$this->requireAdmin();
$data = [
'name' => $this->getPost('name'),
'category_id' => (int) $this->getPost('category_id'),
'description' => $this->getPost('description'),
'price' => (float) $this->getPost('price'),
'old_price' => $this->getPost('old_price') ? (float) $this->getPost('old_price') : null,
'stock_quantity' => (int) $this->getPost('stock_quantity', 0),
'is_available' => $this->getPost('is_available') ? true : false,
'image_url' => $this->getPost('image_url'),
'color' => $this->getPost('color'),
'material' => $this->getPost('material')
];
try {
$this->productModel->updateProduct($id, $data);
$this->redirect('/admin/products?message=' . urlencode('Товар обновлен'));
} catch (\Exception $e) {
$this->redirect('/admin/products/edit/' . $id . '?error=' . urlencode($e->getMessage()));
}
}
public function deleteProduct(int $id): void
{
$this->requireAdmin();
$this->productModel->update($id, ['is_available' => false]);
$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();
$categories = $this->categoryModel->getAllWithProductCount();
$this->view('admin/categories/index', [
'categories' => $categories,
'message' => $this->getQuery('message'),
'error' => $this->getQuery('error'),
'user' => $this->getCurrentUser()
], 'admin');
}
public function addCategory(): void
{
$this->requireAdmin();
$parentCategories = $this->categoryModel->getParent();
$this->view('admin/categories/form', [
'category' => null,
'parentCategories' => $parentCategories,
'action' => 'add',
'user' => $this->getCurrentUser()
], 'admin');
}
public function storeCategory(): void
{
$this->requireAdmin();
$data = [
'name' => $this->getPost('name'),
'parent_id' => $this->getPost('parent_id') ? (int) $this->getPost('parent_id') : null,
'description' => $this->getPost('description'),
'sort_order' => (int) $this->getPost('sort_order', 0),
'is_active' => $this->getPost('is_active') ? true : false
];
try {
$this->categoryModel->createCategory($data);
$this->redirect('/admin/categories?message=' . urlencode('Категория добавлена'));
} catch (\Exception $e) {
$this->redirect('/admin/categories/add?error=' . urlencode($e->getMessage()));
}
}
public function editCategory(int $id): void
{
$this->requireAdmin();
$category = $this->categoryModel->find($id);
if (!$category) {
$this->redirect('/admin/categories?error=' . urlencode('Категория не найдена'));
return;
}
$parentCategories = $this->categoryModel->getParent();
$this->view('admin/categories/form', [
'category' => $category,
'parentCategories' => $parentCategories,
'action' => 'edit',
'user' => $this->getCurrentUser()
], 'admin');
}
public function updateCategory(int $id): void
{
$this->requireAdmin();
$data = [
'name' => $this->getPost('name'),
'parent_id' => $this->getPost('parent_id') ? (int) $this->getPost('parent_id') : null,
'description' => $this->getPost('description'),
'sort_order' => (int) $this->getPost('sort_order', 0),
'is_active' => $this->getPost('is_active') ? true : false
];
try {
$this->categoryModel->updateCategory($id, $data);
$this->redirect('/admin/categories?message=' . urlencode('Категория обновлена'));
} catch (\Exception $e) {
$this->redirect('/admin/categories/edit/' . $id . '?error=' . urlencode($e->getMessage()));
}
}
public function deleteCategory(int $id): void
{
$this->requireAdmin();
$result = $this->categoryModel->safeDelete($id);
if ($result['deleted']) {
$this->redirect('/admin/categories?message=' . urlencode('Категория удалена'));
} else {
$msg = $result['reason'] === 'has_products'
? 'Категория скрыта (содержит товары)'
: 'Категория скрыта (имеет дочерние категории)';
$this->redirect('/admin/categories?message=' . urlencode($msg));
}
}
public function orders(): void
{
$this->requireAdmin();
$orders = $this->orderModel->getAllForAdmin();
$this->view('admin/orders/index', [
'orders' => $orders,
'user' => $this->getCurrentUser()
], 'admin');
}
public function orderDetails(int $id): void
{
$this->requireAdmin();
$order = $this->orderModel->getWithDetails($id);
if (!$order) {
$this->redirect('/admin/orders?error=' . urlencode('Заказ не найден'));
return;
}
$this->view('admin/orders/details', [
'order' => $order,
'user' => $this->getCurrentUser()
], 'admin');
}
public function updateOrderStatus(int $id): void
{
$this->requireAdmin();
$status = $this->getPost('status');
$this->orderModel->updateStatus($id, $status);
$this->redirect('/admin/orders/' . $id);
}
public function users(): void
{
$this->requireAdmin();
$users = $this->userModel->getAllPaginated();
$this->view('admin/users/index', [
'users' => $users,
'user' => $this->getCurrentUser()
], 'admin');
}
}