diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..2c0090b
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,29 @@
+# AETERNA MVC - Корневой .htaccess
+# Перенаправляет все запросы в public/
+
+
+ RewriteEngine On
+
+ # Если запрос к assets (статика) - перенаправляем в public/assets
+ RewriteCond %{REQUEST_URI} ^/cite_practica/assets/
+ RewriteRule ^assets/(.*)$ public/assets/$1 [L]
+
+ # Если запрос к старым img директориям - оставляем как есть
+ RewriteCond %{REQUEST_URI} ^/cite_practica/img/
+ RewriteRule ^img/(.*)$ img/$1 [L]
+
+ RewriteCond %{REQUEST_URI} ^/cite_practica/img2/
+ RewriteRule ^img2/(.*)$ img2/$1 [L]
+
+ # Если это существующий файл (для обратной совместимости) - пропускаем
+ RewriteCond %{REQUEST_FILENAME} -f
+ RewriteRule ^ - [L]
+
+ # Все остальное - в public/index.php
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteRule ^(.*)$ public/index.php [QSA,L]
+
+
+# Отключаем просмотр директорий
+Options -Indexes
+
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..75e71ec
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,34 @@
+FROM php:8.2-apache
+
+# Установка расширений PHP
+RUN apt-get update && apt-get install -y \
+ libpq-dev \
+ libzip-dev \
+ unzip \
+ && docker-php-ext-install pdo pdo_pgsql zip \
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/*
+
+# Включаем mod_rewrite
+RUN a2enmod rewrite
+
+# Копируем конфигурацию Apache
+COPY docker/apache/vhosts.conf /etc/apache2/sites-available/vhosts.conf
+COPY docker/apache/entrypoint.sh /usr/local/bin/entrypoint.sh
+RUN chmod +x /usr/local/bin/entrypoint.sh
+
+# Рабочая директория
+WORKDIR /var/www/html
+
+# Копируем приложение
+COPY . /var/www/html/
+
+# Устанавливаем права
+RUN chown -R www-data:www-data /var/www/html
+
+# Экспортируем порт
+EXPOSE 80
+
+# Точка входа
+ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
+
diff --git a/app/Controllers/AdminController.php b/app/Controllers/AdminController.php
new file mode 100644
index 0000000..3053536
--- /dev/null
+++ b/app/Controllers/AdminController.php
@@ -0,0 +1,374 @@
+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 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');
+ }
+}
+
diff --git a/app/Controllers/AuthController.php b/app/Controllers/AuthController.php
new file mode 100644
index 0000000..210794e
--- /dev/null
+++ b/app/Controllers/AuthController.php
@@ -0,0 +1,217 @@
+userModel = new User();
+ }
+
+ /**
+ * Форма входа
+ */
+ public function loginForm(): void
+ {
+ if ($this->isAuthenticated()) {
+ $this->redirect('/catalog');
+ }
+
+ $redirect = $this->getQuery('redirect', '/catalog');
+
+ $this->view('auth/login', [
+ 'redirect' => $redirect,
+ 'error' => $this->getFlash('error'),
+ 'success' => $this->getFlash('success')
+ ]);
+ }
+
+ /**
+ * Обработка входа
+ */
+ public function login(): void
+ {
+ $email = $this->getPost('email', '');
+ $password = $this->getPost('password', '');
+ $redirect = $this->getPost('redirect', '/catalog');
+
+ if (empty($email) || empty($password)) {
+ $this->json([
+ 'success' => false,
+ 'message' => 'Заполните все поля'
+ ]);
+ return;
+ }
+
+ $user = $this->userModel->authenticate($email, $password);
+
+ if (!$user) {
+ $this->json([
+ 'success' => false,
+ 'message' => 'Неверный email или пароль'
+ ]);
+ return;
+ }
+
+ // Устанавливаем сессию
+ $this->setSession($user);
+
+ $this->json([
+ 'success' => true,
+ 'redirect' => $redirect
+ ]);
+ }
+
+ /**
+ * Форма регистрации
+ */
+ public function registerForm(): void
+ {
+ if ($this->isAuthenticated()) {
+ $this->redirect('/catalog');
+ }
+
+ $this->view('auth/register', [
+ 'errors' => $_SESSION['registration_errors'] ?? [],
+ 'old' => $_SESSION['old_data'] ?? [],
+ 'success' => $_SESSION['registration_success'] ?? null
+ ]);
+
+ // Очищаем flash данные
+ unset($_SESSION['registration_errors']);
+ unset($_SESSION['old_data']);
+ unset($_SESSION['registration_success']);
+ }
+
+ /**
+ * Обработка регистрации
+ */
+ public function register(): void
+ {
+ $errors = [];
+
+ $fullName = trim($this->getPost('fio', ''));
+ $city = trim($this->getPost('city', ''));
+ $email = trim($this->getPost('email', ''));
+ $phone = trim($this->getPost('phone', ''));
+ $password = $this->getPost('password', '');
+ $confirmPassword = $this->getPost('confirm-password', '');
+ $privacy = $this->getPost('privacy');
+
+ // Валидация
+ if (empty($fullName) || strlen($fullName) < 3) {
+ $errors[] = 'ФИО должно содержать минимум 3 символа';
+ }
+
+ if (empty($city) || strlen($city) < 2) {
+ $errors[] = 'Введите корректное название города';
+ }
+
+ if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
+ $errors[] = 'Введите корректный email адрес';
+ }
+
+ if (empty($phone)) {
+ $errors[] = 'Введите номер телефона';
+ }
+
+ if (empty($password) || strlen($password) < 6) {
+ $errors[] = 'Пароль должен содержать минимум 6 символов';
+ }
+
+ if ($password !== $confirmPassword) {
+ $errors[] = 'Пароли не совпадают';
+ }
+
+ if (!$privacy) {
+ $errors[] = 'Необходимо согласие с условиями обработки персональных данных';
+ }
+
+ // Проверяем существование email
+ if (empty($errors) && $this->userModel->emailExists($email)) {
+ $errors[] = 'Пользователь с таким email уже существует';
+ }
+
+ if (!empty($errors)) {
+ $_SESSION['registration_errors'] = $errors;
+ $_SESSION['old_data'] = [
+ 'fio' => $fullName,
+ 'city' => $city,
+ 'email' => $email,
+ 'phone' => $phone
+ ];
+ $this->redirect('/register');
+ return;
+ }
+
+ // Создаем пользователя
+ try {
+ $userId = $this->userModel->register([
+ 'email' => $email,
+ 'password' => $password,
+ 'full_name' => $fullName,
+ 'phone' => $phone,
+ 'city' => $city
+ ]);
+
+ if (!$userId) {
+ throw new \Exception('Ошибка при создании пользователя');
+ }
+
+ // Получаем созданного пользователя
+ $user = $this->userModel->find($userId);
+
+ // Устанавливаем сессию
+ $this->setSession($user);
+
+ $_SESSION['registration_success'] = 'Регистрация прошла успешно!';
+ $this->redirect('/catalog');
+
+ } catch (\Exception $e) {
+ $_SESSION['registration_errors'] = [$e->getMessage()];
+ $_SESSION['old_data'] = [
+ 'fio' => $fullName,
+ 'city' => $city,
+ 'email' => $email,
+ 'phone' => $phone
+ ];
+ $this->redirect('/register');
+ }
+ }
+
+ /**
+ * Выход из системы
+ */
+ public function logout(): void
+ {
+ session_destroy();
+ session_start();
+
+ $this->redirect('/');
+ }
+
+ /**
+ * Установить сессию пользователя
+ */
+ private function setSession(array $user): void
+ {
+ $_SESSION['user_id'] = $user['user_id'];
+ $_SESSION['user_email'] = $user['email'];
+ $_SESSION['full_name'] = $user['full_name'];
+ $_SESSION['user_phone'] = $user['phone'] ?? '';
+ $_SESSION['user_city'] = $user['city'] ?? '';
+ $_SESSION['isLoggedIn'] = true;
+ $_SESSION['isAdmin'] = (bool) $user['is_admin'];
+ $_SESSION['login_time'] = time();
+ }
+}
+
diff --git a/app/Controllers/CartController.php b/app/Controllers/CartController.php
new file mode 100644
index 0000000..d3368cc
--- /dev/null
+++ b/app/Controllers/CartController.php
@@ -0,0 +1,218 @@
+cartModel = new Cart();
+ $this->productModel = new Product();
+ }
+
+ /**
+ * Страница корзины
+ */
+ public function index(): void
+ {
+ $this->requireAuth();
+
+ $user = $this->getCurrentUser();
+ $cartItems = $this->cartModel->getUserCart($user['id']);
+ $totals = $this->cartModel->getCartTotal($user['id']);
+
+ $this->view('cart/checkout', [
+ 'user' => $user,
+ 'cartItems' => $cartItems,
+ 'totalQuantity' => $totals['quantity'],
+ 'totalAmount' => $totals['amount']
+ ]);
+ }
+
+ /**
+ * Добавить товар в корзину
+ */
+ public function add(): void
+ {
+ if (!$this->isAuthenticated()) {
+ $this->json([
+ 'success' => false,
+ 'message' => 'Требуется авторизация'
+ ]);
+ return;
+ }
+
+ $productId = (int) $this->getPost('product_id', 0);
+ $quantity = (int) $this->getPost('quantity', 1);
+ $userId = $this->getCurrentUser()['id'];
+
+ if ($productId <= 0) {
+ $this->json([
+ 'success' => false,
+ 'message' => 'Неверный товар'
+ ]);
+ return;
+ }
+
+ // Проверяем наличие товара
+ $product = $this->productModel->find($productId);
+
+ if (!$product || !$product['is_available']) {
+ $this->json([
+ 'success' => false,
+ 'message' => 'Товар не найден'
+ ]);
+ return;
+ }
+
+ // Проверяем количество на складе
+ $cartItem = $this->cartModel->getItem($userId, $productId);
+ $currentQty = $cartItem ? $cartItem['quantity'] : 0;
+ $newQty = $currentQty + $quantity;
+
+ if ($newQty > $product['stock_quantity']) {
+ $this->json([
+ 'success' => false,
+ 'message' => 'Недостаточно товара на складе'
+ ]);
+ return;
+ }
+
+ // Добавляем в корзину
+ $result = $this->cartModel->addItem($userId, $productId, $quantity);
+
+ if ($result) {
+ $cartCount = $this->cartModel->getCount($userId);
+ $this->json([
+ 'success' => true,
+ 'cart_count' => $cartCount,
+ 'message' => 'Товар добавлен в корзину'
+ ]);
+ } else {
+ $this->json([
+ 'success' => false,
+ 'message' => 'Ошибка при добавлении в корзину'
+ ]);
+ }
+ }
+
+ /**
+ * Обновить количество товара
+ */
+ public function update(): void
+ {
+ if (!$this->isAuthenticated()) {
+ $this->json([
+ 'success' => false,
+ 'message' => 'Требуется авторизация'
+ ]);
+ return;
+ }
+
+ $productId = (int) $this->getPost('product_id', 0);
+ $quantity = (int) $this->getPost('quantity', 1);
+ $userId = $this->getCurrentUser()['id'];
+
+ if ($quantity <= 0) {
+ // Если количество 0 или меньше - удаляем
+ $this->cartModel->removeItem($userId, $productId);
+ $cartCount = $this->cartModel->getCount($userId);
+ $this->json([
+ 'success' => true,
+ 'cart_count' => $cartCount
+ ]);
+ return;
+ }
+
+ // Проверяем наличие на складе
+ $product = $this->productModel->find($productId);
+ if (!$product || $quantity > $product['stock_quantity']) {
+ $this->json([
+ 'success' => false,
+ 'message' => 'Недостаточно товара на складе'
+ ]);
+ return;
+ }
+
+ $result = $this->cartModel->updateQuantity($userId, $productId, $quantity);
+
+ if ($result) {
+ $totals = $this->cartModel->getCartTotal($userId);
+ $this->json([
+ 'success' => true,
+ 'cart_count' => $totals['quantity'],
+ 'total_amount' => $totals['amount']
+ ]);
+ } else {
+ $this->json([
+ 'success' => false,
+ 'message' => 'Ошибка при обновлении'
+ ]);
+ }
+ }
+
+ /**
+ * Удалить товар из корзины
+ */
+ public function remove(): void
+ {
+ if (!$this->isAuthenticated()) {
+ $this->json([
+ 'success' => false,
+ 'message' => 'Требуется авторизация'
+ ]);
+ return;
+ }
+
+ $productId = (int) $this->getPost('product_id', 0);
+ $userId = $this->getCurrentUser()['id'];
+
+ $result = $this->cartModel->removeItem($userId, $productId);
+
+ if ($result) {
+ $cartCount = $this->cartModel->getCount($userId);
+ $this->json([
+ 'success' => true,
+ 'cart_count' => $cartCount
+ ]);
+ } else {
+ $this->json([
+ 'success' => false,
+ 'message' => 'Ошибка при удалении'
+ ]);
+ }
+ }
+
+ /**
+ * Получить количество товаров в корзине
+ */
+ public function count(): void
+ {
+ if (!$this->isAuthenticated()) {
+ $this->json([
+ 'success' => true,
+ 'cart_count' => 0
+ ]);
+ return;
+ }
+
+ $userId = $this->getCurrentUser()['id'];
+ $count = $this->cartModel->getCount($userId);
+
+ $this->json([
+ 'success' => true,
+ 'cart_count' => $count
+ ]);
+ }
+}
+
diff --git a/app/Controllers/HomeController.php b/app/Controllers/HomeController.php
new file mode 100644
index 0000000..af41ca7
--- /dev/null
+++ b/app/Controllers/HomeController.php
@@ -0,0 +1,26 @@
+getCurrentUser();
+
+ $this->view('home/index', [
+ 'user' => $user,
+ 'isLoggedIn' => $this->isAuthenticated(),
+ 'isAdmin' => $this->isAdmin()
+ ]);
+ }
+}
+
diff --git a/app/Controllers/OrderController.php b/app/Controllers/OrderController.php
new file mode 100644
index 0000000..01c8a2a
--- /dev/null
+++ b/app/Controllers/OrderController.php
@@ -0,0 +1,125 @@
+orderModel = new Order();
+ $this->cartModel = new Cart();
+ }
+
+ /**
+ * Страница оформления заказа (корзина)
+ */
+ public function checkout(): void
+ {
+ $this->requireAuth();
+
+ $user = $this->getCurrentUser();
+ $cartItems = $this->cartModel->getUserCart($user['id']);
+ $totals = $this->cartModel->getCartTotal($user['id']);
+
+ $this->view('cart/checkout', [
+ 'user' => $user,
+ 'cartItems' => $cartItems,
+ 'totalQuantity' => $totals['quantity'],
+ 'totalAmount' => $totals['amount']
+ ]);
+ }
+
+ /**
+ * Создание заказа
+ */
+ public function create(): void
+ {
+ if (!$this->isAuthenticated()) {
+ $this->json([
+ 'success' => false,
+ 'message' => 'Требуется авторизация'
+ ]);
+ return;
+ }
+
+ $user = $this->getCurrentUser();
+ $cartItems = $this->cartModel->getUserCart($user['id']);
+
+ if (empty($cartItems)) {
+ $this->json([
+ 'success' => false,
+ 'message' => 'Корзина пуста'
+ ]);
+ return;
+ }
+
+ // Получаем данные заказа
+ $orderData = [
+ 'customer_name' => $this->getPost('full_name', $user['full_name']),
+ 'customer_email' => $this->getPost('email', $user['email']),
+ 'customer_phone' => $this->getPost('phone', $user['phone']),
+ 'delivery_address' => $this->getPost('address', ''),
+ 'delivery_region' => $this->getPost('region', ''),
+ 'postal_code' => $this->getPost('postal_code', ''),
+ 'delivery_method' => $this->getPost('delivery', 'courier'),
+ 'payment_method' => $this->getPost('payment', 'card'),
+ 'promo_code' => $this->getPost('promo_code', ''),
+ 'discount' => (float) $this->getPost('discount', 0),
+ 'delivery_price' => (float) $this->getPost('delivery_price', 2000),
+ 'notes' => $this->getPost('notes', '')
+ ];
+
+ // Валидация
+ if (empty($orderData['customer_name'])) {
+ $this->json([
+ 'success' => false,
+ 'message' => 'Укажите ФИО'
+ ]);
+ return;
+ }
+
+ if (empty($orderData['customer_phone'])) {
+ $this->json([
+ 'success' => false,
+ 'message' => 'Укажите телефон'
+ ]);
+ return;
+ }
+
+ if (empty($orderData['delivery_address'])) {
+ $this->json([
+ 'success' => false,
+ 'message' => 'Укажите адрес доставки'
+ ]);
+ return;
+ }
+
+ try {
+ $result = $this->orderModel->createFromCart($user['id'], $cartItems, $orderData);
+
+ $this->json([
+ 'success' => true,
+ 'order_id' => $result['order_id'],
+ 'order_number' => $result['order_number'],
+ 'message' => 'Заказ успешно оформлен'
+ ]);
+
+ } catch (\Exception $e) {
+ $this->json([
+ 'success' => false,
+ 'message' => $e->getMessage()
+ ]);
+ }
+ }
+}
+
diff --git a/app/Controllers/PageController.php b/app/Controllers/PageController.php
new file mode 100644
index 0000000..255a26a
--- /dev/null
+++ b/app/Controllers/PageController.php
@@ -0,0 +1,45 @@
+view('pages/services', [
+ 'user' => $this->getCurrentUser(),
+ 'isLoggedIn' => $this->isAuthenticated()
+ ]);
+ }
+
+ /**
+ * Страница доставки и оплаты
+ */
+ public function delivery(): void
+ {
+ $this->view('pages/delivery', [
+ 'user' => $this->getCurrentUser(),
+ 'isLoggedIn' => $this->isAuthenticated()
+ ]);
+ }
+
+ /**
+ * Страница гарантии
+ */
+ public function warranty(): void
+ {
+ $this->view('pages/warranty', [
+ 'user' => $this->getCurrentUser(),
+ 'isLoggedIn' => $this->isAuthenticated()
+ ]);
+ }
+}
+
diff --git a/app/Controllers/ProductController.php b/app/Controllers/ProductController.php
new file mode 100644
index 0000000..e2b34e5
--- /dev/null
+++ b/app/Controllers/ProductController.php
@@ -0,0 +1,102 @@
+productModel = new Product();
+ $this->categoryModel = new Category();
+ }
+
+ /**
+ * Каталог товаров
+ */
+ public function catalog(): void
+ {
+ $this->requireAuth();
+
+ $user = $this->getCurrentUser();
+ $isAdmin = $this->isAdmin();
+
+ // Получаем параметры фильтрации
+ $filters = [
+ 'category_id' => (int) $this->getQuery('category', 0),
+ 'search' => $this->getQuery('search', ''),
+ 'min_price' => (int) $this->getQuery('min_price', 0),
+ 'max_price' => (int) $this->getQuery('max_price', 1000000),
+ 'colors' => $this->getQuery('colors', []),
+ 'materials' => $this->getQuery('materials', [])
+ ];
+
+ $showAll = $isAdmin && $this->getQuery('show_all') === '1';
+
+ // Получаем данные
+ $categories = $this->categoryModel->getActive();
+ $products = $showAll
+ ? $this->productModel->getAllForAdmin(true)
+ : $this->productModel->getAvailable($filters);
+
+ $availableColors = $this->productModel->getAvailableColors();
+ $availableMaterials = $this->productModel->getAvailableMaterials();
+
+ // Подкатегории для выбранной категории
+ $subcategories = [];
+ if ($filters['category_id'] > 0) {
+ $subcategories = $this->categoryModel->getChildren($filters['category_id']);
+ }
+
+ $this->view('products/catalog', [
+ 'user' => $user,
+ 'isAdmin' => $isAdmin,
+ 'categories' => $categories,
+ 'subcategories' => $subcategories,
+ 'products' => $products,
+ 'filters' => $filters,
+ 'showAll' => $showAll,
+ 'availableColors' => $availableColors,
+ 'availableMaterials' => $availableMaterials,
+ 'success' => $this->getQuery('success'),
+ 'error' => $this->getQuery('error')
+ ]);
+ }
+
+ /**
+ * Страница товара
+ */
+ public function show(int $id): void
+ {
+ $this->requireAuth();
+
+ $product = $this->productModel->findWithCategory($id);
+
+ if (!$product || (!$product['is_available'] && !$this->isAdmin())) {
+ $this->redirect('/catalog?error=product_not_found');
+ return;
+ }
+
+ $similarProducts = $this->productModel->getSimilar(
+ $id,
+ $product['category_id']
+ );
+
+ $this->view('products/show', [
+ 'product' => $product,
+ 'similarProducts' => $similarProducts,
+ 'user' => $this->getCurrentUser(),
+ 'isAdmin' => $this->isAdmin()
+ ]);
+ }
+}
+
diff --git a/app/Core/App.php b/app/Core/App.php
new file mode 100644
index 0000000..812c62b
--- /dev/null
+++ b/app/Core/App.php
@@ -0,0 +1,163 @@
+router = new Router();
+ }
+
+ /**
+ * Получить экземпляр приложения
+ */
+ public static function getInstance(): ?self
+ {
+ return self::$instance;
+ }
+
+ /**
+ * Получить роутер
+ */
+ public function getRouter(): Router
+ {
+ return $this->router;
+ }
+
+ /**
+ * Инициализация приложения
+ */
+ public function init(): self
+ {
+ // Запускаем сессию
+ if (session_status() === PHP_SESSION_NONE) {
+ session_start();
+ }
+
+ // Регистрируем автозагрузчик
+ $this->registerAutoloader();
+
+ // Настраиваем обработку ошибок
+ $this->setupErrorHandling();
+
+ // Загружаем маршруты
+ $this->loadRoutes();
+
+ return $this;
+ }
+
+ /**
+ * Регистрация автозагрузчика классов
+ */
+ private function registerAutoloader(): void
+ {
+ spl_autoload_register(function ($class) {
+ // Преобразуем namespace в путь к файлу
+ $prefix = 'App\\';
+ $baseDir = dirname(__DIR__) . '/';
+
+ $len = strlen($prefix);
+ if (strncmp($prefix, $class, $len) !== 0) {
+ return;
+ }
+
+ $relativeClass = substr($class, $len);
+ $file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';
+
+ if (file_exists($file)) {
+ require $file;
+ }
+ });
+ }
+
+ /**
+ * Настройка обработки ошибок
+ */
+ private function setupErrorHandling(): void
+ {
+ $config = require dirname(__DIR__, 2) . '/config/app.php';
+
+ if ($config['debug'] ?? false) {
+ error_reporting(E_ALL);
+ ini_set('display_errors', '1');
+ } else {
+ error_reporting(0);
+ ini_set('display_errors', '0');
+ }
+
+ set_exception_handler(function (\Throwable $e) {
+ $this->handleException($e);
+ });
+
+ set_error_handler(function ($severity, $message, $file, $line) {
+ throw new \ErrorException($message, 0, $severity, $file, $line);
+ });
+ }
+
+ /**
+ * Обработка исключений
+ */
+ private function handleException(\Throwable $e): void
+ {
+ $config = require dirname(__DIR__, 2) . '/config/app.php';
+
+ http_response_code(500);
+
+ if ($config['debug'] ?? false) {
+ echo "
Ошибка приложения ";
+ echo "Сообщение: " . htmlspecialchars($e->getMessage()) . "
";
+ echo "Файл: " . htmlspecialchars($e->getFile()) . ":" . $e->getLine() . "
";
+ echo "" . htmlspecialchars($e->getTraceAsString()) . " ";
+ } else {
+ echo View::render('errors/500', [], 'main');
+ }
+ }
+
+ /**
+ * Загрузка маршрутов
+ */
+ private function loadRoutes(): void
+ {
+ $routesFile = dirname(__DIR__, 2) . '/config/routes.php';
+
+ if (file_exists($routesFile)) {
+ $router = $this->router;
+ require $routesFile;
+ }
+ }
+
+ /**
+ * Запуск приложения
+ */
+ public function run(): void
+ {
+ $uri = $_SERVER['REQUEST_URI'] ?? '/';
+ $method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
+
+ // Удаляем базовый путь, если он есть
+ $basePath = '/cite_practica';
+ if (strpos($uri, $basePath) === 0) {
+ $uri = substr($uri, strlen($basePath));
+ }
+
+ // Если URI пустой, делаем его корневым
+ if (empty($uri) || $uri === false) {
+ $uri = '/';
+ }
+
+ try {
+ $this->router->dispatch($uri, $method);
+ } catch (\Exception $e) {
+ $this->handleException($e);
+ }
+ }
+}
+
diff --git a/app/Core/Controller.php b/app/Core/Controller.php
new file mode 100644
index 0000000..125f52a
--- /dev/null
+++ b/app/Core/Controller.php
@@ -0,0 +1,148 @@
+ $_SESSION['user_id'] ?? null,
+ 'email' => $_SESSION['user_email'] ?? '',
+ 'full_name' => $_SESSION['full_name'] ?? '',
+ 'phone' => $_SESSION['user_phone'] ?? '',
+ 'city' => $_SESSION['user_city'] ?? '',
+ 'is_admin' => $_SESSION['isAdmin'] ?? false,
+ 'login_time' => $_SESSION['login_time'] ?? null
+ ];
+ }
+
+ /**
+ * Проверить, авторизован ли пользователь
+ */
+ protected function isAuthenticated(): bool
+ {
+ return isset($_SESSION['isLoggedIn']) && $_SESSION['isLoggedIn'] === true;
+ }
+
+ /**
+ * Проверить, является ли пользователь администратором
+ */
+ protected function isAdmin(): bool
+ {
+ return $this->isAuthenticated() && isset($_SESSION['isAdmin']) && $_SESSION['isAdmin'] === true;
+ }
+
+ /**
+ * Требовать авторизацию
+ */
+ protected function requireAuth(): void
+ {
+ if (!$this->isAuthenticated()) {
+ $redirect = urlencode($_SERVER['REQUEST_URI']);
+ $this->redirect("/login?redirect={$redirect}");
+ }
+ }
+
+ /**
+ * Требовать права администратора
+ */
+ protected function requireAdmin(): void
+ {
+ if (!$this->isAdmin()) {
+ $this->redirect('/login');
+ }
+ }
+
+ /**
+ * Получить POST данные
+ */
+ protected function getPost(?string $key = null, $default = null)
+ {
+ if ($key === null) {
+ return $_POST;
+ }
+ return $_POST[$key] ?? $default;
+ }
+
+ /**
+ * Получить GET данные
+ */
+ protected function getQuery(?string $key = null, $default = null)
+ {
+ if ($key === null) {
+ return $_GET;
+ }
+ return $_GET[$key] ?? $default;
+ }
+
+ /**
+ * Установить flash-сообщение
+ */
+ protected function setFlash(string $type, string $message): void
+ {
+ $_SESSION['flash'][$type] = $message;
+ }
+
+ /**
+ * Получить flash-сообщение
+ */
+ protected function getFlash(string $type): ?string
+ {
+ $message = $_SESSION['flash'][$type] ?? null;
+ unset($_SESSION['flash'][$type]);
+ return $message;
+ }
+}
+
diff --git a/app/Core/Database.php b/app/Core/Database.php
new file mode 100644
index 0000000..4aa2af6
--- /dev/null
+++ b/app/Core/Database.php
@@ -0,0 +1,110 @@
+connection = new \PDO($dsn, $config['username'], $config['password']);
+ $this->connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
+ $this->connection->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC);
+ $this->connection->exec("SET NAMES 'utf8'");
+ } catch (\PDOException $e) {
+ throw new \Exception("Ошибка подключения к базе данных: " . $e->getMessage());
+ }
+ }
+
+ public static function getInstance(): self
+ {
+ if (self::$instance === null) {
+ self::$instance = new self();
+ }
+ return self::$instance;
+ }
+
+ public function getConnection(): \PDO
+ {
+ return $this->connection;
+ }
+
+ /**
+ * Выполнить SELECT запрос
+ */
+ public function query(string $sql, array $params = []): array
+ {
+ $stmt = $this->connection->prepare($sql);
+ $stmt->execute($params);
+ return $stmt->fetchAll();
+ }
+
+ /**
+ * Выполнить SELECT запрос и получить одну запись
+ */
+ public function queryOne(string $sql, array $params = []): ?array
+ {
+ $stmt = $this->connection->prepare($sql);
+ $stmt->execute($params);
+ $result = $stmt->fetch();
+ return $result ?: null;
+ }
+
+ /**
+ * Выполнить INSERT/UPDATE/DELETE запрос
+ */
+ public function execute(string $sql, array $params = []): bool
+ {
+ $stmt = $this->connection->prepare($sql);
+ return $stmt->execute($params);
+ }
+
+ /**
+ * Получить ID последней вставленной записи
+ */
+ public function lastInsertId(): string
+ {
+ return $this->connection->lastInsertId();
+ }
+
+ /**
+ * Начать транзакцию
+ */
+ public function beginTransaction(): bool
+ {
+ return $this->connection->beginTransaction();
+ }
+
+ /**
+ * Подтвердить транзакцию
+ */
+ public function commit(): bool
+ {
+ return $this->connection->commit();
+ }
+
+ /**
+ * Откатить транзакцию
+ */
+ public function rollBack(): bool
+ {
+ return $this->connection->rollBack();
+ }
+
+ // Запрещаем клонирование и десериализацию
+ private function __clone() {}
+ public function __wakeup()
+ {
+ throw new \Exception("Десериализация Singleton запрещена");
+ }
+}
+
diff --git a/app/Core/Model.php b/app/Core/Model.php
new file mode 100644
index 0000000..8063e7a
--- /dev/null
+++ b/app/Core/Model.php
@@ -0,0 +1,180 @@
+db = Database::getInstance();
+ }
+
+ /**
+ * Найти запись по первичному ключу
+ */
+ public function find(int $id): ?array
+ {
+ $sql = "SELECT * FROM {$this->table} WHERE {$this->primaryKey} = ?";
+ return $this->db->queryOne($sql, [$id]);
+ }
+
+ /**
+ * Получить все записи
+ */
+ public function all(?string $orderBy = null): array
+ {
+ $sql = "SELECT * FROM {$this->table}";
+ if ($orderBy) {
+ $sql .= " ORDER BY {$orderBy}";
+ }
+ return $this->db->query($sql);
+ }
+
+ /**
+ * Найти записи по условию
+ */
+ public function where(array $conditions, ?string $orderBy = null): array
+ {
+ $where = [];
+ $params = [];
+
+ foreach ($conditions as $column => $value) {
+ $where[] = "{$column} = ?";
+ $params[] = $value;
+ }
+
+ $sql = "SELECT * FROM {$this->table} WHERE " . implode(' AND ', $where);
+
+ if ($orderBy) {
+ $sql .= " ORDER BY {$orderBy}";
+ }
+
+ return $this->db->query($sql, $params);
+ }
+
+ /**
+ * Найти одну запись по условию
+ */
+ public function findWhere(array $conditions): ?array
+ {
+ $where = [];
+ $params = [];
+
+ foreach ($conditions as $column => $value) {
+ $where[] = "{$column} = ?";
+ $params[] = $value;
+ }
+
+ $sql = "SELECT * FROM {$this->table} WHERE " . implode(' AND ', $where) . " LIMIT 1";
+ return $this->db->queryOne($sql, $params);
+ }
+
+ /**
+ * Создать новую запись
+ */
+ public function create(array $data): ?int
+ {
+ $columns = array_keys($data);
+ $placeholders = array_fill(0, count($data), '?');
+
+ $sql = sprintf(
+ "INSERT INTO %s (%s) VALUES (%s) RETURNING %s",
+ $this->table,
+ implode(', ', $columns),
+ implode(', ', $placeholders),
+ $this->primaryKey
+ );
+
+ $stmt = $this->db->getConnection()->prepare($sql);
+ $stmt->execute(array_values($data));
+
+ return (int) $stmt->fetchColumn();
+ }
+
+ /**
+ * Обновить запись
+ */
+ public function update(int $id, array $data): bool
+ {
+ $set = [];
+ $params = [];
+
+ foreach ($data as $column => $value) {
+ $set[] = "{$column} = ?";
+ $params[] = $value;
+ }
+ $params[] = $id;
+
+ $sql = sprintf(
+ "UPDATE %s SET %s WHERE %s = ?",
+ $this->table,
+ implode(', ', $set),
+ $this->primaryKey
+ );
+
+ return $this->db->execute($sql, $params);
+ }
+
+ /**
+ * Удалить запись
+ */
+ public function delete(int $id): bool
+ {
+ $sql = "DELETE FROM {$this->table} WHERE {$this->primaryKey} = ?";
+ return $this->db->execute($sql, [$id]);
+ }
+
+ /**
+ * Подсчитать количество записей
+ */
+ public function count(array $conditions = []): int
+ {
+ $sql = "SELECT COUNT(*) FROM {$this->table}";
+ $params = [];
+
+ if (!empty($conditions)) {
+ $where = [];
+ foreach ($conditions as $column => $value) {
+ $where[] = "{$column} = ?";
+ $params[] = $value;
+ }
+ $sql .= " WHERE " . implode(' AND ', $where);
+ }
+
+ $stmt = $this->db->getConnection()->prepare($sql);
+ $stmt->execute($params);
+ return (int) $stmt->fetchColumn();
+ }
+
+ /**
+ * Выполнить произвольный SQL запрос
+ */
+ protected function query(string $sql, array $params = []): array
+ {
+ return $this->db->query($sql, $params);
+ }
+
+ /**
+ * Выполнить произвольный SQL запрос и получить одну запись
+ */
+ protected function queryOne(string $sql, array $params = []): ?array
+ {
+ return $this->db->queryOne($sql, $params);
+ }
+
+ /**
+ * Выполнить произвольный SQL запрос (INSERT/UPDATE/DELETE)
+ */
+ protected function execute(string $sql, array $params = []): bool
+ {
+ return $this->db->execute($sql, $params);
+ }
+}
+
diff --git a/app/Core/Router.php b/app/Core/Router.php
new file mode 100644
index 0000000..44b1a03
--- /dev/null
+++ b/app/Core/Router.php
@@ -0,0 +1,155 @@
+routes[] = [
+ 'method' => strtoupper($method),
+ 'route' => $route,
+ 'controller' => $controller,
+ 'action' => $action
+ ];
+ return $this;
+ }
+
+ /**
+ * GET маршрут
+ */
+ public function get(string $route, string $controller, string $action): self
+ {
+ return $this->add('GET', $route, $controller, $action);
+ }
+
+ /**
+ * POST маршрут
+ */
+ public function post(string $route, string $controller, string $action): self
+ {
+ return $this->add('POST', $route, $controller, $action);
+ }
+
+ /**
+ * Найти маршрут по URL и методу
+ */
+ public function match(string $url, string $method): ?array
+ {
+ $method = strtoupper($method);
+ $url = $this->removeQueryString($url);
+ $url = trim($url, '/');
+
+ foreach ($this->routes as $route) {
+ if ($route['method'] !== $method) {
+ continue;
+ }
+
+ $pattern = $this->convertRouteToRegex($route['route']);
+
+ if (preg_match($pattern, $url, $matches)) {
+ // Извлекаем параметры из URL
+ $this->params = $this->extractParams($route['route'], $matches);
+
+ return [
+ 'controller' => $route['controller'],
+ 'action' => $route['action'],
+ 'params' => $this->params
+ ];
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Преобразовать маршрут в регулярное выражение
+ */
+ private function convertRouteToRegex(string $route): string
+ {
+ $route = trim($route, '/');
+
+ // Заменяем {param} на regex группу
+ $pattern = preg_replace('/\{([a-zA-Z_]+)\}/', '([^/]+)', $route);
+
+ return '#^' . $pattern . '$#';
+ }
+
+ /**
+ * Извлечь параметры из совпадений
+ */
+ private function extractParams(string $route, array $matches): array
+ {
+ $params = [];
+
+ // Находим все {param} в маршруте
+ preg_match_all('/\{([a-zA-Z_]+)\}/', $route, $paramNames);
+
+ foreach ($paramNames[1] as $index => $name) {
+ if (isset($matches[$index + 1])) {
+ $params[$name] = $matches[$index + 1];
+ }
+ }
+
+ return $params;
+ }
+
+ /**
+ * Удалить query string из URL
+ */
+ private function removeQueryString(string $url): string
+ {
+ if ($pos = strpos($url, '?')) {
+ $url = substr($url, 0, $pos);
+ }
+ return $url;
+ }
+
+ /**
+ * Получить параметры маршрута
+ */
+ public function getParams(): array
+ {
+ return $this->params;
+ }
+
+ /**
+ * Диспетчеризация запроса
+ */
+ public function dispatch(string $url, string $method): void
+ {
+ $match = $this->match($url, $method);
+
+ if ($match === null) {
+ http_response_code(404);
+ echo View::render('errors/404', ['url' => $url], 'main');
+ return;
+ }
+
+ $controllerClass = "App\\Controllers\\" . $match['controller'];
+ $action = $match['action'];
+
+ if (!class_exists($controllerClass)) {
+ throw new \Exception("Контроллер {$controllerClass} не найден");
+ }
+
+ $controller = new $controllerClass();
+
+ if (!method_exists($controller, $action)) {
+ throw new \Exception("Метод {$action} не найден в контроллере {$controllerClass}");
+ }
+
+ // Вызываем метод контроллера с параметрами
+ call_user_func_array([$controller, $action], $match['params']);
+ }
+}
+
diff --git a/app/Core/View.php b/app/Core/View.php
new file mode 100644
index 0000000..4e975f3
--- /dev/null
+++ b/app/Core/View.php
@@ -0,0 +1,184 @@
+ $_SESSION['user_id'] ?? null,
+ 'email' => $_SESSION['user_email'] ?? '',
+ 'full_name' => $_SESSION['full_name'] ?? '',
+ 'is_admin' => $_SESSION['isAdmin'] ?? false,
+ 'login_time' => $_SESSION['login_time'] ?? null
+ ];
+ }
+
+ /**
+ * Генерация URL
+ */
+ public static function url(string $path): string
+ {
+ return '/' . ltrim($path, '/');
+ }
+
+ /**
+ * Генерация URL для ассетов
+ */
+ public static function asset(string $path): string
+ {
+ return '/assets/' . ltrim($path, '/');
+ }
+}
+
diff --git a/app/Models/Cart.php b/app/Models/Cart.php
new file mode 100644
index 0000000..757c37c
--- /dev/null
+++ b/app/Models/Cart.php
@@ -0,0 +1,145 @@
+table} c
+ JOIN products p ON c.product_id = p.product_id
+ WHERE c.user_id = ? AND p.is_available = TRUE
+ ORDER BY c.created_at DESC";
+ return $this->query($sql, [$userId]);
+ }
+
+ /**
+ * Получить общую сумму корзины
+ */
+ public function getCartTotal(int $userId): array
+ {
+ $sql = "SELECT
+ SUM(c.quantity) as total_quantity,
+ SUM(c.quantity * p.price) as total_amount
+ FROM {$this->table} c
+ JOIN products p ON c.product_id = p.product_id
+ WHERE c.user_id = ? AND p.is_available = TRUE";
+ $result = $this->queryOne($sql, [$userId]);
+
+ return [
+ 'quantity' => (int) ($result['total_quantity'] ?? 0),
+ 'amount' => (float) ($result['total_amount'] ?? 0)
+ ];
+ }
+
+ /**
+ * Получить количество товаров в корзине
+ */
+ public function getCount(int $userId): int
+ {
+ $sql = "SELECT COALESCE(SUM(quantity), 0) as total FROM {$this->table} WHERE user_id = ?";
+ $result = $this->queryOne($sql, [$userId]);
+ return (int) $result['total'];
+ }
+
+ /**
+ * Добавить товар в корзину
+ */
+ public function addItem(int $userId, int $productId, int $quantity = 1): bool
+ {
+ // Проверяем, есть ли уже этот товар в корзине
+ $existing = $this->findWhere([
+ 'user_id' => $userId,
+ 'product_id' => $productId
+ ]);
+
+ if ($existing) {
+ // Увеличиваем количество
+ $newQuantity = $existing['quantity'] + $quantity;
+ return $this->update($existing['cart_id'], [
+ 'quantity' => $newQuantity,
+ 'updated_at' => date('Y-m-d H:i:s')
+ ]);
+ }
+
+ // Добавляем новую запись
+ return $this->create([
+ 'user_id' => $userId,
+ 'product_id' => $productId,
+ 'quantity' => $quantity
+ ]) !== null;
+ }
+
+ /**
+ * Обновить количество товара в корзине
+ */
+ public function updateQuantity(int $userId, int $productId, int $quantity): bool
+ {
+ $sql = "UPDATE {$this->table}
+ SET quantity = ?, updated_at = CURRENT_TIMESTAMP
+ WHERE user_id = ? AND product_id = ?";
+ return $this->execute($sql, [$quantity, $userId, $productId]);
+ }
+
+ /**
+ * Удалить товар из корзины
+ */
+ public function removeItem(int $userId, int $productId): bool
+ {
+ $sql = "DELETE FROM {$this->table} WHERE user_id = ? AND product_id = ?";
+ return $this->execute($sql, [$userId, $productId]);
+ }
+
+ /**
+ * Очистить корзину пользователя
+ */
+ public function clearCart(int $userId): bool
+ {
+ $sql = "DELETE FROM {$this->table} WHERE user_id = ?";
+ return $this->execute($sql, [$userId]);
+ }
+
+ /**
+ * Проверить, есть ли товар в корзине
+ */
+ public function hasItem(int $userId, int $productId): bool
+ {
+ $item = $this->findWhere([
+ 'user_id' => $userId,
+ 'product_id' => $productId
+ ]);
+ return $item !== null;
+ }
+
+ /**
+ * Получить товар из корзины
+ */
+ public function getItem(int $userId, int $productId): ?array
+ {
+ return $this->findWhere([
+ 'user_id' => $userId,
+ 'product_id' => $productId
+ ]);
+ }
+}
+
diff --git a/app/Models/Category.php b/app/Models/Category.php
new file mode 100644
index 0000000..54d659d
--- /dev/null
+++ b/app/Models/Category.php
@@ -0,0 +1,159 @@
+table}
+ WHERE is_active = TRUE
+ ORDER BY sort_order, name";
+ return $this->query($sql);
+ }
+
+ /**
+ * Получить родительские категории (без parent_id)
+ */
+ public function getParent(): array
+ {
+ $sql = "SELECT * FROM {$this->table}
+ WHERE parent_id IS NULL AND is_active = TRUE
+ ORDER BY sort_order, name";
+ return $this->query($sql);
+ }
+
+ /**
+ * Получить подкатегории
+ */
+ public function getChildren(int $parentId): array
+ {
+ $sql = "SELECT * FROM {$this->table}
+ WHERE parent_id = ? AND is_active = TRUE
+ ORDER BY sort_order, name";
+ return $this->query($sql, [$parentId]);
+ }
+
+ /**
+ * Получить категорию по slug
+ */
+ public function findBySlug(string $slug): ?array
+ {
+ return $this->findWhere(['slug' => $slug]);
+ }
+
+ /**
+ * Получить все категории с количеством товаров
+ */
+ public function getAllWithProductCount(): array
+ {
+ $sql = "SELECT c1.*, c2.name as parent_name,
+ (SELECT COUNT(*) FROM products p WHERE p.category_id = c1.category_id) as product_count
+ FROM {$this->table} c1
+ LEFT JOIN {$this->table} c2 ON c1.parent_id = c2.category_id
+ ORDER BY c1.sort_order, c1.name";
+ return $this->query($sql);
+ }
+
+ /**
+ * Создать категорию
+ */
+ public function createCategory(array $data): ?int
+ {
+ $slug = $this->generateSlug($data['name']);
+
+ return $this->create([
+ 'name' => $data['name'],
+ 'slug' => $slug,
+ 'parent_id' => $data['parent_id'] ?? null,
+ 'description' => $data['description'] ?? null,
+ 'sort_order' => $data['sort_order'] ?? 0,
+ 'is_active' => $data['is_active'] ?? true
+ ]);
+ }
+
+ /**
+ * Обновить категорию
+ */
+ public function updateCategory(int $id, array $data): bool
+ {
+ $updateData = [
+ 'name' => $data['name'],
+ 'slug' => $this->generateSlug($data['name']),
+ 'parent_id' => $data['parent_id'] ?? null,
+ 'description' => $data['description'] ?? null,
+ 'sort_order' => $data['sort_order'] ?? 0,
+ 'is_active' => $data['is_active'] ?? true,
+ 'updated_at' => date('Y-m-d H:i:s')
+ ];
+
+ return $this->update($id, $updateData);
+ }
+
+ /**
+ * Безопасное удаление категории
+ */
+ public function safeDelete(int $id): array
+ {
+ // Проверяем наличие товаров
+ $sql = "SELECT COUNT(*) as cnt FROM products WHERE category_id = ?";
+ $result = $this->queryOne($sql, [$id]);
+ $productCount = (int) $result['cnt'];
+
+ // Проверяем наличие дочерних категорий
+ $sql = "SELECT COUNT(*) as cnt FROM {$this->table} WHERE parent_id = ?";
+ $result = $this->queryOne($sql, [$id]);
+ $childCount = (int) $result['cnt'];
+
+ if ($productCount > 0 || $childCount > 0) {
+ // Скрываем вместо удаления
+ $this->update($id, ['is_active' => false]);
+ return [
+ 'deleted' => false,
+ 'hidden' => true,
+ 'reason' => $productCount > 0 ? 'has_products' : 'has_children'
+ ];
+ }
+
+ // Удаляем полностью
+ $this->delete($id);
+ return ['deleted' => true, 'hidden' => false];
+ }
+
+ /**
+ * Генерация slug из названия
+ */
+ private function generateSlug(string $name): string
+ {
+ $slug = mb_strtolower($name);
+
+ // Транслитерация
+ $transliteration = [
+ 'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd',
+ 'е' => 'e', 'ё' => 'yo', 'ж' => 'zh', 'з' => 'z', 'и' => 'i',
+ 'й' => 'y', 'к' => 'k', 'л' => 'l', 'м' => 'm', 'н' => 'n',
+ 'о' => 'o', 'п' => 'p', 'р' => 'r', 'с' => 's', 'т' => 't',
+ 'у' => 'u', 'ф' => 'f', 'х' => 'h', 'ц' => 'ts', 'ч' => 'ch',
+ 'ш' => 'sh', 'щ' => 'sch', 'ъ' => '', 'ы' => 'y', 'ь' => '',
+ 'э' => 'e', 'ю' => 'yu', 'я' => 'ya'
+ ];
+
+ $slug = strtr($slug, $transliteration);
+ $slug = preg_replace('/[^a-z0-9]+/', '-', $slug);
+ $slug = trim($slug, '-');
+
+ return $slug;
+ }
+}
+
diff --git a/app/Models/Order.php b/app/Models/Order.php
new file mode 100644
index 0000000..deb31bc
--- /dev/null
+++ b/app/Models/Order.php
@@ -0,0 +1,217 @@
+beginTransaction();
+
+ // Считаем итоговую сумму
+ $subtotal = 0;
+ foreach ($cartItems as $item) {
+ $subtotal += $item['price'] * $item['quantity'];
+ }
+
+ $discountAmount = (float) ($orderData['discount'] ?? 0);
+ $deliveryPrice = (float) ($orderData['delivery_price'] ?? 2000);
+ $finalAmount = $subtotal - $discountAmount + $deliveryPrice;
+
+ // Генерируем номер заказа
+ $orderNumber = 'ORD-' . date('Ymd-His') . '-' . rand(1000, 9999);
+
+ // Создаем заказ
+ $orderId = $this->create([
+ 'user_id' => $userId,
+ 'order_number' => $orderNumber,
+ 'customer_name' => $orderData['customer_name'],
+ 'customer_email' => $orderData['customer_email'],
+ 'customer_phone' => $orderData['customer_phone'],
+ 'delivery_address' => $orderData['delivery_address'],
+ 'delivery_region' => $orderData['delivery_region'] ?? null,
+ 'postal_code' => $orderData['postal_code'] ?? null,
+ 'delivery_method' => $orderData['delivery_method'] ?? 'courier',
+ 'payment_method' => $orderData['payment_method'] ?? 'card',
+ 'subtotal' => $subtotal,
+ 'discount_amount' => $discountAmount,
+ 'delivery_price' => $deliveryPrice,
+ 'final_amount' => $finalAmount,
+ 'promo_code' => $orderData['promo_code'] ?? null,
+ 'status' => 'pending',
+ 'notes' => $orderData['notes'] ?? null
+ ]);
+
+ if (!$orderId) {
+ throw new \Exception('Не удалось создать заказ');
+ }
+
+ // Добавляем товары в заказ
+ foreach ($cartItems as $item) {
+ $this->addOrderItem($orderId, $item);
+ }
+
+ // Уменьшаем количество товаров на складе
+ $productModel = new Product();
+ foreach ($cartItems as $item) {
+ $productModel->decreaseStock($item['product_id'], $item['quantity']);
+ }
+
+ // Очищаем корзину
+ $cartModel = new Cart();
+ $cartModel->clearCart($userId);
+
+ $db->commit();
+
+ return [
+ 'order_id' => $orderId,
+ 'order_number' => $orderNumber,
+ 'final_amount' => $finalAmount
+ ];
+
+ } catch (\Exception $e) {
+ $db->rollBack();
+ throw $e;
+ }
+ }
+
+ /**
+ * Добавить товар в заказ
+ */
+ private function addOrderItem(int $orderId, array $item): void
+ {
+ $sql = "INSERT INTO order_items
+ (order_id, product_id, product_name, quantity, product_price, total_price)
+ VALUES (?, ?, ?, ?, ?, ?)";
+
+ $totalPrice = $item['price'] * $item['quantity'];
+
+ $this->execute($sql, [
+ $orderId,
+ $item['product_id'],
+ $item['name'],
+ $item['quantity'],
+ $item['price'],
+ $totalPrice
+ ]);
+ }
+
+ /**
+ * Получить заказ с подробностями
+ */
+ public function getWithDetails(int $orderId): ?array
+ {
+ $sql = "SELECT o.*, u.email as user_email, u.full_name as user_full_name
+ FROM {$this->table} o
+ LEFT JOIN users u ON o.user_id = u.user_id
+ WHERE o.order_id = ?";
+
+ $order = $this->queryOne($sql, [$orderId]);
+
+ if ($order) {
+ $order['items'] = $this->getOrderItems($orderId);
+ }
+
+ return $order;
+ }
+
+ /**
+ * Получить товары заказа
+ */
+ public function getOrderItems(int $orderId): array
+ {
+ $sql = "SELECT oi.*, p.image_url
+ FROM order_items oi
+ LEFT JOIN products p ON oi.product_id = p.product_id
+ WHERE oi.order_id = ?";
+ return $this->query($sql, [$orderId]);
+ }
+
+ /**
+ * Получить заказы пользователя
+ */
+ public function getUserOrders(int $userId, int $limit = 50): array
+ {
+ $sql = "SELECT * FROM {$this->table}
+ WHERE user_id = ?
+ ORDER BY created_at DESC
+ LIMIT ?";
+ return $this->query($sql, [$userId, $limit]);
+ }
+
+ /**
+ * Получить все заказы для админки
+ */
+ public function getAllForAdmin(int $limit = 50): array
+ {
+ $sql = "SELECT o.*, u.email as user_email
+ FROM {$this->table} o
+ LEFT JOIN users u ON o.user_id = u.user_id
+ ORDER BY o.created_at DESC
+ LIMIT ?";
+ return $this->query($sql, [$limit]);
+ }
+
+ /**
+ * Обновить статус заказа
+ */
+ public function updateStatus(int $orderId, string $status): bool
+ {
+ $updateData = [
+ 'status' => $status,
+ 'updated_at' => date('Y-m-d H:i:s')
+ ];
+
+ if ($status === 'completed') {
+ $updateData['completed_at'] = date('Y-m-d H:i:s');
+ }
+
+ return $this->update($orderId, $updateData);
+ }
+
+ /**
+ * Получить статистику заказов
+ */
+ public function getStats(): array
+ {
+ $stats = [];
+
+ // Общее количество заказов
+ $result = $this->queryOne("SELECT COUNT(*) as cnt FROM {$this->table}");
+ $stats['total'] = (int) $result['cnt'];
+
+ // Выручка
+ $result = $this->queryOne(
+ "SELECT COALESCE(SUM(final_amount), 0) as revenue
+ FROM {$this->table} WHERE status = 'completed'"
+ );
+ $stats['revenue'] = (float) $result['revenue'];
+
+ // По статусам
+ $statuses = $this->query(
+ "SELECT status, COUNT(*) as cnt FROM {$this->table} GROUP BY status"
+ );
+ $stats['by_status'] = [];
+ foreach ($statuses as $s) {
+ $stats['by_status'][$s['status']] = (int) $s['cnt'];
+ }
+
+ return $stats;
+ }
+}
+
diff --git a/app/Models/Product.php b/app/Models/Product.php
new file mode 100644
index 0000000..3ebf4c7
--- /dev/null
+++ b/app/Models/Product.php
@@ -0,0 +1,239 @@
+table} p
+ LEFT JOIN categories c ON p.category_id = c.category_id
+ WHERE p.product_id = ?";
+ return $this->queryOne($sql, [$id]);
+ }
+
+ /**
+ * Получить доступные товары
+ */
+ public function getAvailable(array $filters = [], int $limit = 50): array
+ {
+ $sql = "SELECT p.*, c.name as category_name
+ FROM {$this->table} p
+ LEFT JOIN categories c ON p.category_id = c.category_id
+ WHERE p.is_available = TRUE";
+ $params = [];
+
+ // Фильтр по категории
+ if (!empty($filters['category_id'])) {
+ $sql .= " AND p.category_id = ?";
+ $params[] = $filters['category_id'];
+ }
+
+ // Фильтр по цене
+ if (!empty($filters['min_price'])) {
+ $sql .= " AND p.price >= ?";
+ $params[] = $filters['min_price'];
+ }
+ if (!empty($filters['max_price'])) {
+ $sql .= " AND p.price <= ?";
+ $params[] = $filters['max_price'];
+ }
+
+ // Фильтр по цветам
+ if (!empty($filters['colors']) && is_array($filters['colors'])) {
+ $placeholders = implode(',', array_fill(0, count($filters['colors']), '?'));
+ $sql .= " AND p.color IN ({$placeholders})";
+ $params = array_merge($params, $filters['colors']);
+ }
+
+ // Фильтр по материалам
+ if (!empty($filters['materials']) && is_array($filters['materials'])) {
+ $placeholders = implode(',', array_fill(0, count($filters['materials']), '?'));
+ $sql .= " AND p.material IN ({$placeholders})";
+ $params = array_merge($params, $filters['materials']);
+ }
+
+ // Поиск по названию/описанию
+ if (!empty($filters['search'])) {
+ $sql .= " AND (p.name ILIKE ? OR p.description ILIKE ?)";
+ $search = '%' . $filters['search'] . '%';
+ $params[] = $search;
+ $params[] = $search;
+ }
+
+ $sql .= " ORDER BY p.product_id ASC LIMIT ?";
+ $params[] = $limit;
+
+ return $this->query($sql, $params);
+ }
+
+ /**
+ * Получить все товары (включая недоступные) для админки
+ */
+ public function getAllForAdmin(bool $showAll = true): array
+ {
+ $sql = "SELECT p.*, c.name as category_name
+ FROM {$this->table} p
+ LEFT JOIN categories c ON p.category_id = c.category_id";
+
+ if (!$showAll) {
+ $sql .= " WHERE p.is_available = TRUE";
+ }
+
+ $sql .= " ORDER BY p.created_at DESC";
+
+ return $this->query($sql);
+ }
+
+ /**
+ * Получить похожие товары
+ */
+ public function getSimilar(int $productId, int $categoryId, int $limit = 3): array
+ {
+ $sql = "SELECT * FROM {$this->table}
+ WHERE category_id = ?
+ AND product_id != ?
+ AND is_available = TRUE
+ ORDER BY RANDOM()
+ LIMIT ?";
+ return $this->query($sql, [$categoryId, $productId, $limit]);
+ }
+
+ /**
+ * Получить доступные цвета
+ */
+ public function getAvailableColors(): array
+ {
+ $sql = "SELECT DISTINCT color FROM {$this->table}
+ WHERE color IS NOT NULL AND color != '' AND is_available = TRUE
+ ORDER BY color";
+ $result = $this->query($sql);
+ return array_column($result, 'color');
+ }
+
+ /**
+ * Получить доступные материалы
+ */
+ public function getAvailableMaterials(): array
+ {
+ $sql = "SELECT DISTINCT material FROM {$this->table}
+ WHERE material IS NOT NULL AND material != '' AND is_available = TRUE
+ ORDER BY material";
+ $result = $this->query($sql);
+ return array_column($result, 'material');
+ }
+
+ /**
+ * Создать товар
+ */
+ public function createProduct(array $data): ?int
+ {
+ $slug = $this->generateSlug($data['name']);
+ $sku = $data['sku'] ?? $this->generateSku($data['name']);
+
+ return $this->create([
+ 'category_id' => $data['category_id'],
+ 'name' => $data['name'],
+ 'slug' => $slug,
+ 'description' => $data['description'] ?? null,
+ 'price' => $data['price'],
+ 'old_price' => $data['old_price'] ?? null,
+ 'sku' => $sku,
+ 'stock_quantity' => $data['stock_quantity'] ?? 0,
+ 'is_available' => $data['is_available'] ?? true,
+ 'is_featured' => $data['is_featured'] ?? false,
+ 'image_url' => $data['image_url'] ?? null,
+ 'color' => $data['color'] ?? null,
+ 'material' => $data['material'] ?? null,
+ 'card_size' => $data['card_size'] ?? 'small'
+ ]);
+ }
+
+ /**
+ * Обновить товар
+ */
+ public function updateProduct(int $id, array $data): bool
+ {
+ $updateData = [
+ 'name' => $data['name'],
+ 'category_id' => $data['category_id'],
+ 'description' => $data['description'] ?? null,
+ 'price' => $data['price'],
+ 'old_price' => $data['old_price'] ?? null,
+ 'stock_quantity' => $data['stock_quantity'] ?? 0,
+ 'is_available' => $data['is_available'] ?? true,
+ 'image_url' => $data['image_url'] ?? null,
+ 'color' => $data['color'] ?? null,
+ 'material' => $data['material'] ?? null,
+ 'updated_at' => date('Y-m-d H:i:s')
+ ];
+
+ return $this->update($id, $updateData);
+ }
+
+ /**
+ * Уменьшить количество товара на складе
+ */
+ public function decreaseStock(int $productId, int $quantity): bool
+ {
+ $sql = "UPDATE {$this->table}
+ SET stock_quantity = stock_quantity - ?,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE product_id = ?";
+ return $this->execute($sql, [$quantity, $productId]);
+ }
+
+ /**
+ * Проверить наличие товара
+ */
+ public function checkStock(int $productId, int $quantity): bool
+ {
+ $product = $this->find($productId);
+ return $product && $product['is_available'] && $product['stock_quantity'] >= $quantity;
+ }
+
+ /**
+ * Генерация slug
+ */
+ private function generateSlug(string $name): string
+ {
+ $slug = mb_strtolower($name);
+
+ $transliteration = [
+ 'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd',
+ 'е' => 'e', 'ё' => 'yo', 'ж' => 'zh', 'з' => 'z', 'и' => 'i',
+ 'й' => 'y', 'к' => 'k', 'л' => 'l', 'м' => 'm', 'н' => 'n',
+ 'о' => 'o', 'п' => 'p', 'р' => 'r', 'с' => 's', 'т' => 't',
+ 'у' => 'u', 'ф' => 'f', 'х' => 'h', 'ц' => 'ts', 'ч' => 'ch',
+ 'ш' => 'sh', 'щ' => 'sch', 'ъ' => '', 'ы' => 'y', 'ь' => '',
+ 'э' => 'e', 'ю' => 'yu', 'я' => 'ya'
+ ];
+
+ $slug = strtr($slug, $transliteration);
+ $slug = preg_replace('/[^a-z0-9]+/', '-', $slug);
+
+ return trim($slug, '-');
+ }
+
+ /**
+ * Генерация артикула
+ */
+ private function generateSku(string $name): string
+ {
+ $prefix = strtoupper(substr(preg_replace('/[^a-zA-Z0-9]/', '', $name), 0, 6));
+ return 'PROD-' . $prefix . '-' . rand(100, 999);
+ }
+}
+
diff --git a/app/Models/User.php b/app/Models/User.php
new file mode 100644
index 0000000..baec07e
--- /dev/null
+++ b/app/Models/User.php
@@ -0,0 +1,153 @@
+findWhere(['email' => $email]);
+ }
+
+ /**
+ * Проверить пароль пользователя
+ */
+ public function verifyPassword(string $password, string $hash): bool
+ {
+ return password_verify($password, $hash);
+ }
+
+ /**
+ * Хешировать пароль
+ */
+ public function hashPassword(string $password): string
+ {
+ return password_hash($password, PASSWORD_DEFAULT);
+ }
+
+ /**
+ * Создать нового пользователя
+ */
+ public function register(array $data): ?int
+ {
+ $config = require dirname(__DIR__, 2) . '/config/app.php';
+
+ // Проверяем, является ли email администраторским
+ $isAdmin = in_array(strtolower($data['email']), $config['admin_emails'] ?? []);
+
+ return $this->create([
+ 'email' => $data['email'],
+ 'password_hash' => $this->hashPassword($data['password']),
+ 'full_name' => $data['full_name'],
+ 'phone' => $data['phone'] ?? null,
+ 'city' => $data['city'] ?? null,
+ 'is_admin' => $isAdmin,
+ 'is_active' => true
+ ]);
+ }
+
+ /**
+ * Авторизация пользователя
+ */
+ public function authenticate(string $email, string $password): ?array
+ {
+ $user = $this->findByEmail($email);
+
+ if (!$user) {
+ return null;
+ }
+
+ if (!$user['is_active']) {
+ return null;
+ }
+
+ if (!$this->verifyPassword($password, $user['password_hash'])) {
+ return null;
+ }
+
+ // Обновляем время последнего входа
+ $this->update($user['user_id'], [
+ 'last_login' => date('Y-m-d H:i:s')
+ ]);
+
+ return $user;
+ }
+
+ /**
+ * Получить активных пользователей
+ */
+ public function getActive(int $limit = 50): array
+ {
+ $sql = "SELECT * FROM {$this->table}
+ WHERE is_active = TRUE
+ ORDER BY created_at DESC
+ LIMIT ?";
+ return $this->query($sql, [$limit]);
+ }
+
+ /**
+ * Получить всех пользователей с пагинацией
+ */
+ public function getAllPaginated(int $limit = 50, int $offset = 0): array
+ {
+ $sql = "SELECT * FROM {$this->table}
+ ORDER BY created_at DESC
+ LIMIT ? OFFSET ?";
+ return $this->query($sql, [$limit, $offset]);
+ }
+
+ /**
+ * Проверить существование email
+ */
+ public function emailExists(string $email): bool
+ {
+ $user = $this->findByEmail($email);
+ return $user !== null;
+ }
+
+ /**
+ * Обновить профиль пользователя
+ */
+ public function updateProfile(int $userId, array $data): bool
+ {
+ $allowedFields = ['full_name', 'phone', 'city'];
+ $updateData = array_intersect_key($data, array_flip($allowedFields));
+ $updateData['updated_at'] = date('Y-m-d H:i:s');
+
+ return $this->update($userId, $updateData);
+ }
+
+ /**
+ * Изменить пароль
+ */
+ public function changePassword(int $userId, string $newPassword): bool
+ {
+ return $this->update($userId, [
+ 'password_hash' => $this->hashPassword($newPassword),
+ 'updated_at' => date('Y-m-d H:i:s')
+ ]);
+ }
+
+ /**
+ * Заблокировать/разблокировать пользователя
+ */
+ public function setActive(int $userId, bool $active): bool
+ {
+ return $this->update($userId, [
+ 'is_active' => $active,
+ 'updated_at' => date('Y-m-d H:i:s')
+ ]);
+ }
+}
+
diff --git a/app/Views/admin/categories/form.php b/app/Views/admin/categories/form.php
new file mode 100644
index 0000000..1c74a14
--- /dev/null
+++ b/app/Views/admin/categories/form.php
@@ -0,0 +1,56 @@
+
+
+= $isEdit ? 'Редактирование категории' : 'Добавление категории' ?>
+
+
+ Назад к списку
+
+
+
+
diff --git a/app/Views/admin/categories/index.php b/app/Views/admin/categories/index.php
new file mode 100644
index 0000000..2a36ed6
--- /dev/null
+++ b/app/Views/admin/categories/index.php
@@ -0,0 +1,60 @@
+
+
+
+= htmlspecialchars($message) ?>
+
+
+
+= htmlspecialchars($error) ?>
+
+
+
+
+
+ ID
+ Название
+ Родитель
+ Товаров
+ Порядок
+ Статус
+ Действия
+
+
+
+
+
+ = $category['category_id'] ?>
+ = htmlspecialchars($category['name']) ?>
+ = htmlspecialchars($category['parent_name'] ?? '-') ?>
+ = $category['product_count'] ?>
+ = $category['sort_order'] ?>
+
+
+ Активна
+
+ Скрыта
+
+
+
+
+
+
+
+
+
+
diff --git a/app/Views/admin/dashboard.php b/app/Views/admin/dashboard.php
new file mode 100644
index 0000000..f26c415
--- /dev/null
+++ b/app/Views/admin/dashboard.php
@@ -0,0 +1,45 @@
+
+
+Дашборд
+
+
+
+
= $stats['total_products'] ?>
+
Всего товаров
+
+
+
= $stats['active_products'] ?>
+
Активных товаров
+
+
+
= $stats['total_orders'] ?>
+
Заказов
+
+
+
= $stats['total_users'] ?>
+
Пользователей
+
+
+
= number_format($stats['revenue'], 0, '', ' ') ?> ₽
+
Выручка
+
+
+
+
+
diff --git a/app/Views/admin/orders/details.php b/app/Views/admin/orders/details.php
new file mode 100644
index 0000000..0e7dfd7
--- /dev/null
+++ b/app/Views/admin/orders/details.php
@@ -0,0 +1,74 @@
+
+
+
+ Назад к заказам
+
+
+Заказ = htmlspecialchars($order['order_number']) ?>
+
+
+
+Товары в заказе
+
+
+
+ Изображение
+ Товар
+ Цена
+ Кол-во
+ Сумма
+
+
+
+
+
+
+
+
+ = htmlspecialchars($item['product_name']) ?>
+ = View::formatPrice($item['product_price']) ?>
+ = $item['quantity'] ?>
+ = View::formatPrice($item['total_price']) ?>
+
+
+
+
+
diff --git a/app/Views/admin/orders/index.php b/app/Views/admin/orders/index.php
new file mode 100644
index 0000000..026b25c
--- /dev/null
+++ b/app/Views/admin/orders/index.php
@@ -0,0 +1,62 @@
+
+
+Заказы
+
+
+Заказы отсутствуют
+
+
+
+
+ № заказа
+ Дата
+ Покупатель
+ Сумма
+ Статус
+ Действия
+
+
+
+
+
+ = htmlspecialchars($order['order_number']) ?>
+ = View::formatDateTime($order['created_at']) ?>
+
+ = htmlspecialchars($order['customer_name']) ?>
+ = htmlspecialchars($order['user_email']) ?>
+
+ = View::formatPrice($order['final_amount']) ?>
+
+ '#ffc107',
+ 'processing' => '#17a2b8',
+ 'shipped' => '#007bff',
+ 'completed' => '#28a745',
+ 'cancelled' => '#dc3545'
+ ];
+ $statusNames = [
+ 'pending' => 'Ожидает',
+ 'processing' => 'Обработка',
+ 'shipped' => 'Отправлен',
+ 'completed' => 'Завершен',
+ 'cancelled' => 'Отменен'
+ ];
+ $color = $statusColors[$order['status']] ?? '#666';
+ $name = $statusNames[$order['status']] ?? $order['status'];
+ ?>
+
+ = $name ?>
+
+
+
+
+ Подробнее
+
+
+
+
+
+
+
+
diff --git a/app/Views/admin/products/form.php b/app/Views/admin/products/form.php
new file mode 100644
index 0000000..8afc33f
--- /dev/null
+++ b/app/Views/admin/products/form.php
@@ -0,0 +1,106 @@
+
+
+= $isEdit ? 'Редактирование товара' : 'Добавление товара' ?>
+
+
+ Назад к списку
+
+
+
+
diff --git a/app/Views/admin/products/index.php b/app/Views/admin/products/index.php
new file mode 100644
index 0000000..e3037be
--- /dev/null
+++ b/app/Views/admin/products/index.php
@@ -0,0 +1,75 @@
+
+
+
+
+
+= htmlspecialchars($message) ?>
+
+
+
+= htmlspecialchars($error) ?>
+
+
+
+
+
+
+
+ ID
+ Изображение
+ Название
+ Категория
+ Цена
+ На складе
+ Статус
+ Действия
+
+
+
+
+
+ = $product['product_id'] ?>
+
+
+
+ = htmlspecialchars($product['name']) ?>
+ = htmlspecialchars($product['category_name'] ?? 'Без категории') ?>
+ = View::formatPrice($product['price']) ?>
+ = $product['stock_quantity'] ?> шт.
+
+
+ Активен
+
+ Скрыт
+
+
+
+
+
+
+
+
+
+
diff --git a/app/Views/admin/users/index.php b/app/Views/admin/users/index.php
new file mode 100644
index 0000000..b59cd6c
--- /dev/null
+++ b/app/Views/admin/users/index.php
@@ -0,0 +1,43 @@
+
+
+Пользователи
+
+
+
+
+ ID
+ ФИО
+ Email
+ Телефон
+ Город
+ Роль
+ Регистрация
+ Последний вход
+
+
+
+
+
+ = $u['user_id'] ?>
+ = htmlspecialchars($u['full_name'] ?? '-') ?>
+ = htmlspecialchars($u['email']) ?>
+ = htmlspecialchars($u['phone'] ?? '-') ?>
+ = htmlspecialchars($u['city'] ?? '-') ?>
+
+
+
+ Админ
+
+
+
+ Пользователь
+
+
+
+ = $u['created_at'] ? View::formatDateTime($u['created_at']) : '-' ?>
+ = $u['last_login'] ? View::formatDateTime($u['last_login']) : 'Не было' ?>
+
+
+
+
+
diff --git a/app/Views/auth/login.php b/app/Views/auth/login.php
new file mode 100644
index 0000000..9d6c7a5
--- /dev/null
+++ b/app/Views/auth/login.php
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
diff --git a/app/Views/auth/register.php b/app/Views/auth/register.php
new file mode 100644
index 0000000..271c48e
--- /dev/null
+++ b/app/Views/auth/register.php
@@ -0,0 +1,95 @@
+
+
+
+
+
+
Ошибки регистрации:
+
+
+ = htmlspecialchars($error) ?>
+
+
+
+
+
+
+
+ = htmlspecialchars($success) ?>
+
+
+
+
+ Для доступа к каталогу и оформления заказов необходимо зарегистрироваться
+
+
+
+
+
AETERNA
+
+
Присоединяйтесь к нам
+
Создайте аккаунт чтобы получить доступ ко всем функциям:
+
+ Доступ к каталогу товаров
+ Добавление товаров в корзину
+ Оформление заказов
+ История покупок
+
+
+
+
+
+
+
+
diff --git a/app/Views/cart/checkout.php b/app/Views/cart/checkout.php
new file mode 100644
index 0000000..476219b
--- /dev/null
+++ b/app/Views/cart/checkout.php
@@ -0,0 +1,304 @@
+
+
+
+
+
+
+
+ Товары в корзине
+
+
+
+
+
+
+
+
+
+
+
+
+
+
= htmlspecialchars($item['name']) ?>
+
= View::formatPrice($item['price']) ?>
+
+
+ -
+ = $item['quantity'] ?>
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Оформление заказа
+
+
+
+
+
+
+
+ ПРИМЕНИТЬ
+
+
+
+
+
+
+
+ Товары, = $totalQuantity ?> шт.
+ = View::formatPrice($totalAmount) ?>
+
+
+ Скидка
+ 0 ₽
+
+
+ Доставка
+ 2 000 ₽
+
+
+ ИТОГО:
+ = View::formatPrice($totalAmount + 2000) ?>
+
+
+
+ ОФОРМИТЬ ЗАКАЗ
+
+
+
+ Даю согласие на обработку персональных данных
+
+
+
+
+
+
+
+
+
diff --git a/app/Views/errors/404.php b/app/Views/errors/404.php
new file mode 100644
index 0000000..3473f69
--- /dev/null
+++ b/app/Views/errors/404.php
@@ -0,0 +1,18 @@
+
+
+
+
404
+
Страница не найдена
+
+ К сожалению, запрошенная страница не существует или была перемещена.
+
+
+
+
diff --git a/app/Views/errors/500.php b/app/Views/errors/500.php
new file mode 100644
index 0000000..b45b2f6
--- /dev/null
+++ b/app/Views/errors/500.php
@@ -0,0 +1,15 @@
+
+
+
+
500
+
Внутренняя ошибка сервера
+
+ Произошла ошибка при обработке вашего запроса. Мы уже работаем над её устранением.
+
+
+
+
diff --git a/app/Views/home/index.php b/app/Views/home/index.php
new file mode 100644
index 0000000..2704a70
--- /dev/null
+++ b/app/Views/home/index.php
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+
+
+
+
ДОБАВЬТЕ ИЗЫСКАННОСТИ В СВОЙ ИНТЕРЬЕР
+
Мы создаем мебель, которая сочетает в себе безупречный дизайн, натуральные материалы, продуманный функционал, чтобы ваш день начинался и заканчивался с комфортом.
+
+
+
ПЕРЕЙТИ В КАТАЛОГ
+
+
ПЕРЕЙТИ В КАТАЛОГ
+
+
+
+
+
+
+
+
+
+
+
+
О НАС
+
Компания AETERNA - российский производитель качественной корпусной и мягкой мебели для дома и офиса. С 2015 года мы успешно реализуем проекты любой сложности, сочетая современные технологии, проверенные материалы и классическое мастерство.
+
+
+
+
+
+
+
Наша сеть включает 30+ российских фабрик, отобранных по строгим стандартам качества. Мы сотрудничаем исключительно с лидерами рынка, чья продукция доказала свое превосходство временем.
+
+
+
+
+
+
+
+
+
+
+
+
ГОТОВОЕ РЕШЕНИЕ ДЛЯ ВАШЕЙ ГОСТИНОЙ
+
УСПЕЙТЕ ЗАКАЗАТЬ СЕЙЧАС
+
+
Подробнее
+
+
+
+
+
+
+
+
+
+
+
+
30 000+
+
Довольных покупателей
+
+
+
4500+
+
Реализованных заказов
+
+
+
+
+
+
+
+
ОТВЕТЫ НА ВОПРОСЫ
+
+
+
1
+
+
Сколько времени занимает доставка?
+
Доставка готовых позиций занимает 1-3 дня. Мебель на заказ изготавливается от 14 до 45 рабочих дней.
+
+
+
+
2
+
+
Нужно ли вносить предоплату?
+
Да, для запуска заказа в производство необходима предоплата в размере 50-70% от стоимости.
+
+
+
+
3
+
+
Предоставляется ли рассрочка или кредит?
+
Да, мы предлагаем рассрочку на 6 или 12 месяцев без первоначального взноса.
+
+
+
+
4
+
+
Что делать, если мебель пришла с дефектом?
+
Сообщите нам в течение 7 дней, мы решим вопрос о бесплатной замене или ремонте.
+
+
+
+
Задать вопрос
+
+
+
diff --git a/app/Views/layouts/admin.php b/app/Views/layouts/admin.php
new file mode 100644
index 0000000..7dbed2b
--- /dev/null
+++ b/app/Views/layouts/admin.php
@@ -0,0 +1,75 @@
+
+
+
+
+
+ AETERNA - Админ-панель
+
+
+
+
+
+
+
+
+
+
+ = $content ?>
+
+
+
+
diff --git a/app/Views/layouts/main.php b/app/Views/layouts/main.php
new file mode 100644
index 0000000..ed19613
--- /dev/null
+++ b/app/Views/layouts/main.php
@@ -0,0 +1,68 @@
+
+
+
+
+
+ AETERNA - = $title ?? 'Мебель и Интерьер' ?>
+
+
+
+
+
+
+
+
+
+ = \App\Core\View::partial('header', ['user' => $user ?? null, 'isLoggedIn' => $isLoggedIn ?? false, 'isAdmin' => $isAdmin ?? false]) ?>
+
+
+ = $content ?>
+
+
+ = \App\Core\View::partial('footer') ?>
+
+
+
+
+
diff --git a/app/Views/pages/delivery.php b/app/Views/pages/delivery.php
new file mode 100644
index 0000000..b62746e
--- /dev/null
+++ b/app/Views/pages/delivery.php
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
Доставка и оплата
+
+
+
+
Способы доставки
+
+
+
Курьерская доставка
+
Доставка до квартиры/офиса в удобное для вас время
+
+ По Москве: 1-3 дня
+ Московская область: 2-5 дней
+ Регионы России: 5-14 дней
+
+
от 2 000 ₽
+
+
+
+
Самовывоз
+
Забрать заказ можно с нашего склада
+
Адрес: г. Москва, ул. Примерная, д. 1
+
Бесплатно
+
+
+
+
Бесплатная доставка при заказе от 50 000 ₽
+
+
+
+
+
Способы оплаты
+
+
+
Банковская карта
+
Visa, Mastercard, МИР - онлайн или при получении
+
+
+
+
Наличные
+
Оплата курьеру при получении заказа
+
+
+
+
Безналичный расчет
+
Для юридических лиц с выставлением счета
+
+
+
+
Рассрочка
+
Рассрочка на 6 или 12 месяцев без переплаты
+
+
+
+
+
+
Важная информация
+
+ При получении товара проверьте комплектность и целостность упаковки
+ В случае обнаружения повреждений составьте акт с курьером
+ Сохраняйте упаковку до окончания гарантийного срока
+
+
+
+
+
diff --git a/app/Views/pages/services.php b/app/Views/pages/services.php
new file mode 100644
index 0000000..7db1904
--- /dev/null
+++ b/app/Views/pages/services.php
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
Наши услуги
+
+
+
+
Дизайн-проект
+
+ Наши дизайнеры создадут уникальный проект интерьера с подбором мебели, которая идеально впишется в ваше пространство.
+
+
от 15 000 ₽
+
+
+
+
Замеры
+
+ Бесплатный выезд замерщика для точного определения размеров и особенностей вашего помещения.
+
+
Бесплатно
+
+
+
+
Сборка мебели
+
+ Профессиональная сборка мебели нашими специалистами с гарантией качества работ.
+
+
от 3 000 ₽
+
+
+
+
Подъем на этаж
+
+ Услуга подъема мебели на любой этаж, включая помещения без лифта.
+
+
от 500 ₽ за этаж
+
+
+
+
Вывоз старой мебели
+
+ Демонтаж и вывоз старой мебели для освобождения пространства перед доставкой новой.
+
+
от 2 000 ₽
+
+
+
+
Реставрация
+
+ Восстановление и обновление мебели: перетяжка, покраска, замена фурнитуры.
+
+
от 5 000 ₽
+
+
+
+
+
Нужна консультация?
+
Позвоните нам или оставьте заявку, и мы свяжемся с вами в ближайшее время
+
+7 (912) 999-12-23
+
+
+
+
diff --git a/app/Views/pages/warranty.php b/app/Views/pages/warranty.php
new file mode 100644
index 0000000..ebc0fe2
--- /dev/null
+++ b/app/Views/pages/warranty.php
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
Гарантия и возврат
+
+
+
+
Гарантийные обязательства
+
+
+
24 месяца
+
Гарантия на всю мебель
+
Мы уверены в качестве нашей продукции и предоставляем расширенную гарантию на все изделия
+
+
+
Гарантия распространяется на:
+
+ Производственные дефекты
+ Дефекты материалов
+ Неисправность механизмов
+ Отклонения в размерах
+
+
+
Гарантия не распространяется на:
+
+ Механические повреждения
+ Повреждения от влаги/огня
+ Неправильную эксплуатацию
+ Естественный износ
+
+
+
+
+
Возврат и обмен
+
+
+
14 дней на возврат товара надлежащего качества
+
+
+
Условия возврата:
+
+ Товар не был в употреблении
+ Сохранены товарный вид и упаковка
+ Сохранены все ярлыки и бирки
+ Есть документ, подтверждающий покупку
+
+
+
Как оформить возврат:
+
+ Свяжитесь с нами по телефону или email
+ Опишите причину возврата
+ Получите номер заявки на возврат
+ Отправьте товар или дождитесь курьера
+ Получите деньги в течение 10 дней
+
+
+
+
Мебель, изготовленная по индивидуальному заказу, обмену и возврату не подлежит
+
+
+
+
+
+
Остались вопросы?
+
Свяжитесь с нашей службой поддержки
+
+7 (912) 999-12-23
+
aeterna@mail.ru
+
+
+
+
diff --git a/app/Views/partials/footer.php b/app/Views/partials/footer.php
new file mode 100644
index 0000000..26f1230
--- /dev/null
+++ b/app/Views/partials/footer.php
@@ -0,0 +1,48 @@
+
+
diff --git a/app/Views/partials/header.php b/app/Views/partials/header.php
new file mode 100644
index 0000000..b7307a1
--- /dev/null
+++ b/app/Views/partials/header.php
@@ -0,0 +1,109 @@
+
+
+
diff --git a/app/Views/products/catalog.php b/app/Views/products/catalog.php
new file mode 100644
index 0000000..792a660
--- /dev/null
+++ b/app/Views/products/catalog.php
@@ -0,0 +1,195 @@
+
+
+
+
+
+
+
+
+
+
+ = htmlspecialchars($success) ?>
+
+
+
+
+
+
+ Панель управления каталогом
+
+
+
+
+
+
+ Добро пожаловать, = htmlspecialchars($user['full_name'] ?? $user['email']) ?> !
+
+
+ Администратор
+
+
+
+
+
+
+
+
+
+
+ Каталог мебели
+
+ (= count($products) ?> товаров)
+
+
+
+
+
+ Результаты поиска: "= htmlspecialchars($filters['search']) ?> "
+
+ Очистить
+
+
+
+
+
+
+
+
+ Товары не найдены
+
+
+
+
+
+
+
= htmlspecialchars($product['name']) ?>
+
= View::formatPrice($product['price']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/Views/products/show.php b/app/Views/products/show.php
new file mode 100644
index 0000000..75723d2
--- /dev/null
+++ b/app/Views/products/show.php
@@ -0,0 +1,210 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
= htmlspecialchars($product['name']) ?>
+
+
+
+ ★' : '☆ ';
+ }
+ ?>
+
+
(= $product['review_count'] ?? 0 ?> отзывов)
+
+
+
+ = View::formatPrice($product['price']) ?>
+ $product['price']): ?>
+ = View::formatPrice($product['old_price']) ?>
+
+ -= round(($product['old_price'] - $product['price']) / $product['old_price'] * 100) ?>%
+
+
+
+
+
+ 10) {
+ echo ' В наличии';
+ } elseif ($product['stock_quantity'] > 0) {
+ echo ' Осталось мало: ' . $product['stock_quantity'] . ' шт.';
+ } else {
+ echo ' Нет в наличии';
+ }
+ ?>
+
+
+
+
+ Артикул:
+ = $product['sku'] ?? 'N/A' ?>
+
+
+ Категория:
+ = htmlspecialchars($product['category_name'] ?? 'Без категории') ?>
+
+
+ На складе:
+ = $product['stock_quantity'] ?> шт.
+
+
+
+
+ = nl2br(htmlspecialchars($product['description'] ?? 'Описание отсутствует')) ?>
+
+
+ 0): ?>
+
+
+ -
+
+ +
+
+
+
+
+ В корзину
+
+
+ Купить сейчас
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Похожие товары
+
+
+
+
+
+
= htmlspecialchars($similar['name']) ?>
+
= View::formatPrice($similar['price']) ?>
+
+
+
+
+
+
+
+
+
+
diff --git a/config/app.php b/config/app.php
new file mode 100644
index 0000000..b2ff377
--- /dev/null
+++ b/config/app.php
@@ -0,0 +1,51 @@
+ 'AETERNA',
+
+ // Режим отладки
+ 'debug' => true,
+
+ // URL приложения
+ 'url' => 'http://localhost',
+
+ // Базовый путь (для Docker)
+ 'base_path' => '/cite_practica',
+
+ // Часовой пояс
+ 'timezone' => 'Europe/Moscow',
+
+ // Локаль
+ 'locale' => 'ru_RU',
+
+ // Email администраторов (получают права администратора при регистрации)
+ 'admin_emails' => [
+ 'admin@aeterna.ru',
+ 'administrator@aeterna.ru',
+ 'aeterna@mail.ru'
+ ],
+
+ // Настройки сессии
+ 'session' => [
+ 'lifetime' => 120, // минуты
+ 'secure' => false,
+ 'http_only' => true
+ ],
+
+ // Настройки доставки
+ 'delivery' => [
+ 'default_price' => 2000,
+ 'free_from' => 50000, // Бесплатная доставка от этой суммы
+ ],
+
+ // Промокоды
+ 'promo_codes' => [
+ 'SALE10' => ['type' => 'percent', 'value' => 10],
+ 'FREE' => ['type' => 'free_delivery', 'value' => 0],
+ ]
+];
+
diff --git a/config/database.php b/config/database.php
index d7e632b..03fad14 100644
--- a/config/database.php
+++ b/config/database.php
@@ -1,32 +1,14 @@
connection = new PDO(
- "pgsql:host=localhost;dbname=aeterna_db;",
- "postgres",
- "1234"
- );
- $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
- $this->connection->exec("SET NAMES 'utf8'");
- } catch(PDOException $e) {
- die("Ошибка подключения: " . $e->getMessage());
- }
- }
-
- public static function getInstance() {
- if (self::$instance == null) {
- self::$instance = new Database();
- }
- return self::$instance;
- }
-
- public function getConnection() {
- return $this->connection;
- }
-}
-?>
\ No newline at end of file
+/**
+ * Конфигурация базы данных
+ */
+return [
+ 'driver' => 'pgsql',
+ 'host' => '185.130.224.177',
+ 'port' => '5481',
+ 'database' => 'postgres',
+ 'username' => 'admin',
+ 'password' => '38feaad2840ccfda0e71243a6faaecfd',
+ 'charset' => 'utf8',
+];
diff --git a/config/routes.php b/config/routes.php
new file mode 100644
index 0000000..539d59a
--- /dev/null
+++ b/config/routes.php
@@ -0,0 +1,66 @@
+get('/', 'HomeController', 'index');
+$router->get('/home', 'HomeController', 'index');
+
+// ========== Авторизация ==========
+$router->get('/login', 'AuthController', 'loginForm');
+$router->post('/login', 'AuthController', 'login');
+$router->get('/register', 'AuthController', 'registerForm');
+$router->post('/register', 'AuthController', 'register');
+$router->get('/logout', 'AuthController', 'logout');
+
+// ========== Каталог и товары ==========
+$router->get('/catalog', 'ProductController', 'catalog');
+$router->get('/product/{id}', 'ProductController', 'show');
+
+// ========== Корзина ==========
+$router->get('/cart', 'CartController', 'index');
+$router->post('/cart/add', 'CartController', 'add');
+$router->post('/cart/update', 'CartController', 'update');
+$router->post('/cart/remove', 'CartController', 'remove');
+$router->get('/cart/count', 'CartController', 'count');
+
+// ========== Заказы ==========
+$router->get('/checkout', 'OrderController', 'checkout');
+$router->post('/order', 'OrderController', 'create');
+
+// ========== Статические страницы ==========
+$router->get('/services', 'PageController', 'services');
+$router->get('/delivery', 'PageController', 'delivery');
+$router->get('/warranty', 'PageController', 'warranty');
+
+// ========== Админ-панель ==========
+$router->get('/admin', 'AdminController', 'dashboard');
+
+// Управление товарами
+$router->get('/admin/products', 'AdminController', 'products');
+$router->get('/admin/products/add', 'AdminController', 'addProduct');
+$router->post('/admin/products/add', 'AdminController', 'storeProduct');
+$router->get('/admin/products/edit/{id}', 'AdminController', 'editProduct');
+$router->post('/admin/products/edit/{id}', 'AdminController', 'updateProduct');
+$router->post('/admin/products/delete/{id}', 'AdminController', 'deleteProduct');
+
+// Управление категориями
+$router->get('/admin/categories', 'AdminController', 'categories');
+$router->get('/admin/categories/add', 'AdminController', 'addCategory');
+$router->post('/admin/categories/add', 'AdminController', 'storeCategory');
+$router->get('/admin/categories/edit/{id}', 'AdminController', 'editCategory');
+$router->post('/admin/categories/edit/{id}', 'AdminController', 'updateCategory');
+$router->post('/admin/categories/delete/{id}', 'AdminController', 'deleteCategory');
+
+// Управление заказами
+$router->get('/admin/orders', 'AdminController', 'orders');
+$router->get('/admin/orders/{id}', 'AdminController', 'orderDetails');
+$router->post('/admin/orders/{id}/status', 'AdminController', 'updateOrderStatus');
+
+// Управление пользователями
+$router->get('/admin/users', 'AdminController', 'users');
+
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..8dab156
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,23 @@
+version: '3.8'
+
+services:
+ web:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ ports:
+ - "8080:80"
+ volumes:
+ - .:/var/www/html
+ - ./docker/apache/vhosts.conf:/etc/apache2/sites-available/000-default.conf
+ environment:
+ - APACHE_RUN_USER=www-data
+ - APACHE_RUN_GROUP=www-data
+ restart: unless-stopped
+ networks:
+ - aeterna-network
+
+networks:
+ aeterna-network:
+ driver: bridge
+
diff --git a/docker/apache/entrypoint.sh b/docker/apache/entrypoint.sh
new file mode 100644
index 0000000..513dba1
--- /dev/null
+++ b/docker/apache/entrypoint.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+set -e
+
+# Включаем mod_rewrite
+a2enmod rewrite
+
+# Копируем конфигурацию виртуального хоста
+cp /etc/apache2/sites-available/vhosts.conf /etc/apache2/sites-enabled/000-default.conf
+
+# Устанавливаем права
+chown -R www-data:www-data /var/www/html
+
+echo "Apache configured successfully"
+
+# Запускаем Apache в foreground режиме
+exec apache2-foreground
+
diff --git a/docker/apache/vhosts.conf b/docker/apache/vhosts.conf
new file mode 100644
index 0000000..7458492
--- /dev/null
+++ b/docker/apache/vhosts.conf
@@ -0,0 +1,25 @@
+
+ ServerAdmin admin@aeterna.local
+ DocumentRoot /var/www/html
+ ServerName localhost
+
+
+ Options Indexes FollowSymLinks
+ AllowOverride All
+ Require all granted
+
+
+ # Логирование
+ ErrorLog ${APACHE_LOG_DIR}/error.log
+ CustomLog ${APACHE_LOG_DIR}/access.log combined
+
+ # Кодировка по умолчанию
+ AddDefaultCharset UTF-8
+
+ # Типы файлов
+ AddType text/css .css
+ AddType text/less .less
+ AddType text/javascript .js
+ AddType image/svg+xml .svg
+
+
diff --git a/public/.htaccess b/public/.htaccess
new file mode 100644
index 0000000..62f97fb
--- /dev/null
+++ b/public/.htaccess
@@ -0,0 +1,33 @@
+# AETERNA MVC - Apache URL Rewrite Rules
+
+
+ RewriteEngine On
+
+ # Базовый путь приложения
+ RewriteBase /cite_practica/
+
+ # Если запрос к существующему файлу или директории - пропускаем
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+
+ # Все остальные запросы направляем на index.php
+ RewriteRule ^(.*)$ index.php [QSA,L]
+
+
+# Отключаем просмотр директорий
+Options -Indexes
+
+# Защита файлов конфигурации
+
+ Order allow,deny
+ Deny from all
+
+
+# Кодировка по умолчанию
+AddDefaultCharset UTF-8
+
+# Типы файлов
+AddType text/css .css
+AddType text/javascript .js
+AddType image/svg+xml .svg
+
diff --git a/public/index.php b/public/index.php
new file mode 100644
index 0000000..87b08c4
--- /dev/null
+++ b/public/index.php
@@ -0,0 +1,18 @@
+init()->run();
+
diff --git a/public/mixins.less b/public/mixins.less
new file mode 100644
index 0000000..934de15
--- /dev/null
+++ b/public/mixins.less
@@ -0,0 +1,85 @@
+// ===================================
+// === ПЕРЕМЕННЫЕ И МИКСИНЫ AETERNA ===
+// ===================================
+@color-primary: #617365;
+@color-secondary: #D1D1D1;
+@color-accent: #453227;
+@color-text-dark: #333;
+@color-text-light: #fff;
+@color-button: @color-accent;
+@color-beige: #A2A09A;
+
+@font-logo: 'Anek Kannada', sans-serif;
+@font-main: 'Anonymous Pro', monospace;
+@font-heading: 'Playfair Display', serif;
+
+@shadow-light: 0 5px 15px rgba(0, 0, 0, 0.2);
+@shadow-dark: 2px 2px 4px rgba(0, 0, 0, 0.3);
+
+.flex-center(@gap: 0) {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: @gap;
+}
+
+.flex-between() {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.flex-column() {
+ display: flex;
+ flex-direction: column;
+}
+
+.icon-base(@size: 18px, @hover-scale: 1.1) {
+ cursor: pointer;
+ transition: all 0.3s ease;
+ font-size: @size;
+ &:hover {
+ transform: scale(@hover-scale);
+ }
+}
+
+.image-overlay() {
+ position: absolute;
+ inset: 0;
+ .flex-center(15px);
+ flex-direction: column;
+ text-align: center;
+ background-color: rgba(0, 0, 0, 0.4);
+ padding: 20px;
+ color: @color-text-light;
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
+ transition: all 0.3s ease;
+}
+
+.menu-base() {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ width: 250px;
+ background: white;
+ border-radius: 8px;
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
+ padding: 15px;
+ z-index: 1000;
+ margin-top: 5px;
+ display: none;
+}
+
+.input-base() {
+ width: 100%;
+ padding: 12px 15px;
+ border: 1px solid #ccc;
+ background-color: #fff;
+ font-family: @font-main;
+ font-size: 14px;
+ outline: none;
+ transition: border-color 0.3s ease;
+ &:focus {
+ border-color: @color-primary;
+ }
+}
\ No newline at end of file
diff --git a/public/style_for_cite.less b/public/style_for_cite.less
new file mode 100644
index 0000000..3a52d85
--- /dev/null
+++ b/public/style_for_cite.less
@@ -0,0 +1,3178 @@
+@import "mixins.less";
+@import "стили_оформления.less";
+// =======================
+// === БАЗОВЫЕ СТИЛИ ===
+// =======================
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+html, body {
+ height: 100%;
+}
+
+html {
+ scroll-behavior: smooth;
+}
+
+body {
+ font-family: @font-main;
+ background-color: @color-secondary;
+ color: @color-text-dark;
+ line-height: 1.6;
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+}
+
+.container {
+ max-width: 1210px;
+ margin: 0 auto;
+ padding: 0 20px;
+}
+
+ul {
+ list-style: none;
+}
+
+a {
+ text-decoration: none;
+ color: inherit;
+ transition: all 0.3s ease;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ font-family: @font-heading;
+ margin: 0;
+}
+
+p, li, span {
+ font-family: @font-main;
+}
+
+// =======================
+// === КОМПОНЕНТЫ ===
+// =======================
+
+.logo, .footer-logo {
+ font: bold 32px/1 @font-logo;
+ letter-spacing: 2px;
+ text-shadow: @shadow-dark;
+ flex-shrink: 0;
+}
+
+.btn {
+ padding: 12px 30px;
+ border: none;
+ cursor: pointer;
+ font-size: 14px;
+ text-transform: uppercase;
+ transition: all 0.3s ease;
+ font-family: @font-main;
+
+ &.primary-btn {
+ background-color: @color-button;
+ color: @color-text-light;
+
+ &:hover {
+ background-color: lighten(@color-button, 10%);
+ transform: translateY(-2px);
+ box-shadow: @shadow-light;
+ }
+ }
+}
+
+.number-circle {
+ .flex-center();
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ background-color: @color-button;
+ color: @color-text-light;
+ font-size: 16px;
+ font-weight: bold;
+ flex-shrink: 0;
+}
+
+.breadcrumbs {
+ font-size: 14px;
+ margin-bottom: 20px;
+ color: #666;
+
+ a {
+ color: #666;
+ opacity: 0.7;
+ &:hover { opacity: 1; }
+ }
+
+ .current-page {
+ font-weight: bold;
+ color: @color-text-dark;
+ }
+}
+
+// =======================
+// === ШАПКА САЙТА ===
+// =======================
+.header {
+ background-color: @color-secondary;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
+ z-index: 1000;
+
+ &__top, &__bottom {
+ padding: 15px 0;
+ .container {
+ .flex-between();
+ gap: 20px;
+ }
+ }
+
+ &__bottom {
+ padding: 10px 0;
+ border-top: 1px solid rgba(0, 0, 0, 0.05);
+
+ .catalog-link.active-catalog {
+ background-color: rgba(0, 0, 0, 0.08);
+ pointer-events: none;
+ }
+ }
+
+ .search-catalog {
+ .flex-center();
+ border: 2px solid @color-text-dark;
+ background-color: #fff;
+ max-width: 600px;
+ width: 100%;
+ margin: 0 auto;
+ overflow: hidden;
+
+ .catalog-dropdown {
+ position: relative;
+ background-color: #f8f8f8;
+ padding: 10px 15px 10px 25px;
+ font-size: 18px;
+ cursor: pointer;
+ border-right: 1px solid @color-text-dark;
+ .flex-center(10px);
+ width: 200px;
+ flex-shrink: 0;
+
+ &__menu {
+ .menu-base();
+ li {
+ padding: 8px 0;
+ cursor: pointer;
+ transition: color 0.3s;
+ border-bottom: 1px solid #f0f0f0;
+ &:last-child { border-bottom: none; }
+ &:hover { color: @color-accent; }
+ }
+ }
+ &:hover &__menu { display: block; }
+ }
+
+ .search-box {
+ .flex-center();
+ padding: 0 15px;
+ flex-grow: 1;
+ position: relative;
+ font-size: 15px;
+
+ input {
+ border: none;
+ padding: 10px 30px 10px 0;
+ outline: none;
+ font-size: 16px;
+ width: 100%;
+ text-align: left;
+ }
+
+ .search-icon {
+ font-size: 20px;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ }}
+
+ &__icons--top {
+ .flex-center(15px);
+ flex-shrink: 0;
+ .icon { .icon-base(); font-size: 20px;}
+ }
+
+ .nav-list {
+ .flex-center(30px);
+ font-size: 18px;
+ a {
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
+ &:hover { text-shadow: @shadow-dark; }
+ &.active {
+ border-bottom: 2px solid @color-button;
+ padding-bottom: 5px;
+ text-shadow: @shadow-dark;
+ }
+ &[href="#footer"] {
+ cursor: pointer;
+ }
+ }
+ }
+
+ .catalog-link {
+ .flex-center(10px);
+ border-radius: 4px;
+ white-space: nowrap;
+ font-size: 18px;
+ padding: 10px 18px;
+ &:hover { background-color: rgba(0, 0, 0, 0.05); }
+ }
+
+ .header-phone {
+ font-weight: bold;
+ color: @color-button;
+ flex-shrink: 0;
+ }
+}
+
+// =======================
+// === ОСНОВНЫЕ СЕКЦИИ ===
+// =======================
+.hero {
+ padding: 15px 0;
+
+ &__content {
+ .flex-center(50px);
+ min-height: 60vh;
+ align-items: center;
+ }
+
+ &__image-block {
+ position: relative;
+ flex: 0 0 40%;
+ max-width: 600px;
+ height: 600px;
+ .flex-center();
+
+ .hero__circle {
+ position: absolute;
+ width: 450px;
+ height: 450px;
+ background-color: @color-primary;
+ border-radius: 50%;
+ z-index: 1;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+
+ .hero__img {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ z-index: 2;
+ }
+ }
+
+ &__text-block {
+ flex: 0 0 60%;
+ padding-left: 50px;
+
+ h1 {
+ font-size: 42px;
+ font-weight: normal;
+ margin-bottom: 25px;
+ line-height: 1.3;
+ }
+
+ .hero__usp-text {
+ position: relative;
+ padding-left: 50px;
+ margin-bottom: 35px;
+ line-height: 1.7;
+ .flex-center();
+ justify-content: flex-start;
+ min-height: 40px;
+ font-size: 16px;
+
+ &::before {
+ content: "✓";
+ position: absolute;
+ left: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 32px;
+ height: 32px;
+ border: 2px solid @color-button;
+ background-color: transparent;
+ color: @color-button;
+ border-radius: 50%;
+ .flex-center();
+ font-size: 16px;
+ font-weight: bold;
+ }
+ }
+
+ .btn.primary-btn {
+ margin: 25px 0 0 50px;
+ padding: 14px 35px;
+ font-size: 15px;
+ }
+ }
+}
+
+.advantages {
+ padding: 30px 0 40px;
+
+ &__header {
+ display: flex;
+ align-items: center;
+ gap: 50px;
+ margin-bottom: 40px;
+ h2 {
+ font-size: 32px;
+ font-weight: normal;
+ flex: 0 0 30%;
+ }
+ }
+
+ &__items {
+ flex: 0 0 70%;
+ display: flex;
+ gap: 30px;
+ }
+
+ .advantage-item {
+ flex: 1;
+ text-align: left;
+ position: relative;
+ padding-top: 30px;
+
+ &__number {
+ .number-circle();
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+
+ h4 {
+ font-weight: 600;
+ margin-bottom: 10px;
+ }
+ }
+}
+
+.promo-images {
+ display: flex;
+ gap: 20px;
+ margin-top: 50px;
+
+ .promo-image-col {
+ position: relative;
+ overflow: hidden;
+ border-radius: 8px;
+ flex: 1;
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: translateY(-5px);
+ box-shadow: @shadow-light;
+ .image-overlay-text { background-color: rgba(0, 0, 0, 0.6); }
+ img { transform: scale(1.05); }
+ .image-overlay-text h4,
+ .image-overlay-text .overlay-link { transform: translateY(0); }
+ }
+
+ img {
+ width: 100%;
+ height: 350px;
+ object-fit: cover;
+ display: block;
+ transition: transform 0.5s ease;
+ }
+
+ .image-overlay-text {
+ .image-overlay();
+ h4 {
+ font-size: 24px;
+ text-transform: uppercase;
+ line-height: 1.2;
+ margin-bottom: 15px;
+ transform: translateY(20px);
+ transition: transform 0.3s ease;
+ }
+ }
+
+ .overlay-link {
+ display: inline-block;
+ text-transform: uppercase;
+ font-weight: bold;
+ border-radius: 3px;
+ margin-top: 15px;
+ padding: 10px 25px;
+ background-color: @color-button;
+ color: @color-text-light;
+ font-size: 12px;
+ transform: translateY(20px);
+ transition: all 0.3s ease;
+
+ &:hover {
+ background-color: lighten(@color-button, 10%);
+ transform: translateY(-2px);
+ box-shadow: @shadow-light;
+ }
+ }
+ }
+}
+
+.about {
+ padding: 40px 0 80px;
+
+ &__content {
+ display: flex;
+ align-items: flex-start;
+ gap: 50px;
+ }
+
+ &__column {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ &--left { flex: 0 0 40%; margin-bottom: 30px; }
+ &--right {
+ flex: 0 0 60%;
+ .about__caption {
+ padding-right: 50px;
+ }
+ }
+ }
+
+ &__text-block {
+ margin-bottom: 30px;
+ h2 { margin-bottom: 15px; }
+ }
+
+ &__img {
+ width: 93%;
+ object-fit: cover;
+ display: block;
+ &--small { height: 300px; }
+ &--large { height: 450px; }
+ }
+
+ .text-justified {
+ text-align: justify;
+ color: #555;
+ }
+}
+
+.solutions {
+ padding: 0;
+ background-color: @color-secondary;
+
+ &-slider {
+ position: relative;
+ width: 100%;
+ max-width: 1200px;
+ margin: 40px auto;
+ border-radius: 8px;
+ overflow: hidden;
+
+ &__slides {
+ display: flex;
+ width: 200%;
+ height: 100%;
+ animation: slideLeftRight 10s infinite ease-in-out;
+ }
+
+ &__slide {
+ width: 50%;
+ flex-shrink: 0;
+ position: relative;
+ overflow: hidden;
+ transition: transform 0.5s ease, box-shadow 0.5s ease;
+
+ &:hover {
+ transform: scale(1.02);
+ box-shadow: 0 10px 25px rgba(0,0,0,0.3);
+ .solution-img {
+ transform: scale(1.05);
+ filter: brightness(0.8);
+ }
+ .solution-text-overlay {
+ opacity: 1;
+ transform: translateY(-5px);
+ }
+ .solution-image-link {
+ transform: translateX(-50%) translateY(-6px);
+ background-color: rgba(255,255,255,0.9);
+ color: @color-text-dark;
+ }
+ }
+ }
+
+ .solution-img {
+ width: 100%;
+ height: auto;
+ object-fit: cover;
+ display: block;
+ }
+
+ .solution-text-overlay {
+ position: absolute;
+ top: 15%;
+ left: 8%;
+ color: #493131;
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.6);
+ z-index: 2;
+ opacity: 0.9;
+ transition: opacity 0.5s ease, transform 0.5s ease;
+ h2 {
+ font-size: 35px;
+ text-transform: uppercase;
+ margin-bottom: 10px;
+ }
+ p {
+ font-size: 25px;
+ text-transform: uppercase;
+ }
+ }
+
+ .solution-image-link {
+ position: absolute;
+ bottom: 40px;
+ left: 50%;
+ transform: translateX(-50%);
+ padding: 12px 30px;
+ border: 2px solid @color-text-light;
+ color: #493131;
+ text-transform: uppercase;
+ font-size: 16px;
+ font-weight: bold;
+ background: transparent;
+ transition: 0.4s ease;
+ z-index: 2;
+ &:hover {
+ background: @color-text-light;
+ color: @color-text-dark;
+ transform: translateX(-50%) translateY(-2px);
+ }
+ }
+ }
+}
+
+@keyframes slideLeftRight {
+ 0%, 40% { transform: translateX(0); }
+ 50%, 90% { transform: translateX(-50%); }
+ 100% { transform: translateX(0); }
+}
+
+.stats {
+ padding: 0;
+ margin-top: 20px;
+
+ .container {
+ display: flex;
+ justify-content: flex-end;
+ }
+
+ &__items {
+ display: flex;
+ gap: 20px;
+ .stat-item {
+ text-align: left;
+ .stat-number {
+ font-size: 36px;
+ font-weight: bold;
+ color: @color-text-dark;
+ margin-bottom: 5px;
+ }
+ .stat-label { color: @color-text-dark; }
+ }
+ }
+}
+
+.faq {
+ padding: 50px 0;
+
+ h2 {
+ text-align: left;
+ font-size: 32px;
+ font-weight: normal;
+ margin-bottom: 40px;
+ }
+
+ &__items {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 40px 60px;
+ margin-bottom: 40px;
+ }
+
+ .faq-item {
+ flex: 0 0 calc(50% - 30px);
+ .flex-center(15px);
+ align-items: flex-start;
+ &__content h4 {
+ font-weight: 600;
+ margin-bottom: 10px;
+ }
+ }
+
+ .btn.primary-btn {
+ display: block;
+ width: 100%;
+ margin: 20px auto 80px;
+ }
+}
+
+// =======================
+// === СТИЛИ КАТАЛОГА ===
+// =======================
+.catalog-main {
+ padding: 30px 0 60px;
+ background-color: lighten(@color-secondary, 5%);
+}
+
+.catalog-wrapper {
+ display: flex;
+ gap: 20px;
+}
+
+.catalog-sidebar {
+ flex: 0 0 250px;
+ background-color: #fff;
+ padding: 20px;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
+ height: fit-content;
+}
+
+.filter-group {
+ margin-bottom: 30px;
+}
+
+.filter-title {
+ font-size: 16px;
+ font-weight: bold;
+ margin-bottom: 15px;
+ text-transform: uppercase;
+}
+
+.filter-list li {
+ padding: 5px 0;
+ font-size: 16px;
+ a {
+ color: #555;
+ transition: color 0.2s;
+ &:hover { color: @color-accent; }
+ &.active-category {
+ font-weight: bold;
+ color: @color-primary;
+ }
+ }
+}
+
+.price-range {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+ width: 100%;
+
+ .range-slider {
+ width: 100%;
+
+ input[type="range"] {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 100%;
+ height: 5px;
+ background: @color-primary;
+ border-radius: 5px;
+ outline: none;
+ margin: 0;
+
+ &::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 20px;
+ height: 20px;
+ background: @color-accent;
+ border: 2px solid #fff;
+ border-radius: 50%;
+ cursor: pointer;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: scale(1.1);
+ background: lighten(@color-accent, 10%);
+ }
+ }
+
+ &::-moz-range-thumb {
+ width: 20px;
+ height: 20px;
+ background: @color-accent;
+ border: 2px solid #fff;
+ border-radius: 50%;
+ cursor: pointer;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: scale(1.1);
+ background: lighten(@color-accent, 10%);
+ }
+ }
+ }
+ }
+
+ .price-display {
+ font-size: 14px;
+ font-weight: bold;
+ text-align: center;
+ color: @color-text-dark;
+ padding: 10px;
+ background: #f8f8f8;
+ border-radius: 4px;
+ }
+}
+
+.filter-options {
+ list-style: none;
+ li {
+ display: flex;
+ align-items: center;
+ padding: 4px 0;
+ font-size: 14px;
+ }
+ label {
+ margin-left: 10px;
+ cursor: pointer;
+ color: #555;
+ }
+ input[type="checkbox"] {
+ width: 15px;
+ height: 15px;
+ cursor: pointer;
+ accent-color: @color-primary;
+ &:checked + label {
+ font-weight: bold;
+ color: @color-primary;
+ }
+ }
+}
+
+.filter-apply-btn {
+ width: 100%;
+ margin-top: 20px;
+}
+
+.catalog-products {
+ flex-grow: 1;
+}
+
+.products-container {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 20px;
+}
+
+.product-card {
+ background-color: #fff;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ transition: transform 0.3s ease;
+ box-sizing: border-box;
+
+ &:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
+ .product-img { transform: scale(1.05); }
+ }
+}
+
+.product-image-container {
+ position: relative;
+ overflow: hidden;
+ margin-bottom: 0;
+ padding: 0;
+ height: 250px;
+ .flex-center();
+}
+
+.product-img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ display: block;
+ transition: transform 0.3s ease;
+ margin: 0;
+}
+
+.product-img1 {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+ transition: transform 0.3s ease;
+ margin: 0;
+}
+
+.product-discount {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ background-color: @color-button;
+ color: @color-text-light;
+ padding: 3px 8px;
+ font-size: 12px;
+ font-weight: bold;
+ z-index: 10;
+}
+
+.product-wishlist-icon {
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ color: #333;
+ font-size: 18px;
+ cursor: pointer;
+ transition: color 0.3s ease;
+ z-index: 10;
+ &:hover { color: @color-accent; }
+}
+
+.product-name {
+ font-size: 16px;
+ font-weight: bold;
+ margin-bottom: 5px;
+}
+
+.product-details {
+ font-size: 13px;
+ color: #777;
+ margin-bottom: 10px;
+ flex-grow: 1;
+}
+
+.product-price {
+ font-size: 18px;
+ font-weight: bold;
+ color: @color-button;
+}
+
+.product-card.small { flex: 0 0 300px; max-width: 300px; height: 200px; }
+.product-card.small1 { flex: 0 0 320px; max-width: 320px; height: 250px;width: 320px; }
+.product-card.large { flex: 0 0 580px; max-width: 580px; height: 380px; }
+.product-card.wide { flex: 0 0 240px; max-width: 240px; height: 250px; }
+.product-card.wide1 { flex: 0 0 350px; max-width: 350px; height: 250px; }
+.product-card.wide2 { flex: 0 0 560px; max-width: 560px; height: 260px; }
+.product-card.wide2_1 { flex: 0 0 560px; max-width: 560px; height: 260px; margin: -280px 0 0; }
+.product-card.wide3 {
+ flex: 0 0 320px; max-width: 320px; height: 540px;
+ .product-image-container { height: 580px; }
+}
+.product-card.wide4 {
+ flex: 0 0 545px; max-width: 545px; margin: -270px 0 0; height: 250px;
+ .product-image-container { padding: 0; justify-content: flex-start; }
+ .product-img { margin-left: 0; align-self: flex-start; object-position: left center; }
+}
+.product-card.tall { flex: 0 0 300px; max-width: 300px; margin: -180px 0 0; height: 430px; }
+.product-card.full-width { flex: 0 0 100%; margin: -20px 0 0; max-width: 900px; height: 300px;}
+
+.product-card.full-width {
+ flex: 0 0 100%;
+ max-width: 100%;
+ height: 300px;
+
+ .product-image-container {
+ height: 100%;
+ padding: 0;
+ margin: 0;
+
+ .product-img1 {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ margin: 0;
+ padding: 0;
+ }
+ }
+}
+
+.product-card.tall .product-image-container,
+.product-card.large .product-image-container { height: 430px; }
+
+// =======================
+// === СТРАНИЦА ТОВАРА ===
+// =======================
+.product__section {
+ display: flex;
+ gap: 0;
+ margin: 30px 0;
+}
+
+.product__gallery, .product__info {
+ flex: 1;
+}
+
+.product__main-image {
+ margin-bottom: 15px;
+}
+
+.product__image {
+ width: 500px;
+ height: 300px;
+ border-radius: 4px;
+}
+
+.product__thumbnails {
+ display: flex;
+ gap: 10px;
+}
+
+.product__thumbnail {
+ border: none;
+ background: none;
+ cursor: pointer;
+ padding: 0;
+}
+
+.product__thumbnail img {
+ width: 245px;
+ height: 150px;
+ object-fit: cover;
+ border-radius: 4px;
+}
+
+.product__title {
+ font-size: 30px;
+ margin-bottom: 35px;
+}
+
+.product__rating {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 30px;
+}
+
+.product__color-selector {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 40px;
+}
+
+.product__color-option {
+ width: 45px;
+ height: 45px;
+ border-radius: 50%;
+ border: 2px solid transparent;
+ cursor: pointer;
+ transition: transform 0.3s ease;
+
+ &:hover{
+ transform: translateY(-2px);
+ }
+}
+
+.product__color-option.active {
+ border-color: @color-primary;
+}
+
+.product__description {
+ margin-bottom: 65px;
+ line-height: 1.5;
+}
+
+.product__details-link {
+ display: inline-block;
+ margin-bottom: 20px;
+ color: @color-primary;
+ font-weight: bold;
+}
+
+.product__purchase {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 35px;
+}
+
+.product__price {
+ font-size: 24px;
+ font-weight: bold;
+}
+
+.product__quantity {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.product__qty-btn {
+ width: 30px;
+ height: 30px;
+ background: @color-button;
+ color: @color-text-light;
+ border: none;
+ border-radius: 50%;
+ cursor: pointer;
+ font-weight: bold;
+ transition: all 0.3s ease;
+
+ &:hover {
+ background: lighten(@color-button, 10%);
+ transform: scale(1.1);
+ }
+}
+
+.product__qty-value {
+ font-weight: bold;
+ min-width: 30px;
+ text-align: center;
+}
+
+.product__actions {
+ display: flex;
+ gap: 15px;
+}
+
+.product__btn {
+ flex: 1;
+ padding: 12px 20px;
+ border: none;
+ border-radius: 4px;
+ font-weight: bold;
+ cursor: pointer;
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow: @shadow-light;
+ }
+}
+
+.product__btn.primary {
+ background: @color-button;
+ color: @color-text-light;
+
+ &:hover {
+ background: lighten(@color-button, 10%);
+ }
+}
+
+.product__btn.secondary {
+ background: transparent;
+ border: 1px solid @color-button;
+ color: @color-button;
+
+ &:hover {
+ background: @color-button;
+ color: @color-text-light;
+ }
+}
+
+.similar {
+ margin: 60px 0;
+}
+
+.similar__title {
+ margin-bottom: 30px;
+ font-size: 28px;
+ font-weight: bold;
+}
+
+.similar__grid {
+ display: flex;
+ gap: 25px;
+ flex-wrap: wrap;
+ justify-content: space-between;
+}
+
+.similar__card {
+ flex: 0 0 calc(33.333% - 17px);
+ min-width: 320px;
+ background: @color-secondary;
+ border-radius: 12px;
+ overflow: hidden;
+ transition: all 0.3s ease;
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
+
+ &:hover {
+ transform: translateY(-8px);
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
+ }
+}
+
+.similar__card-image {
+ height: 300px;
+ overflow: hidden;
+ background: white;
+
+ img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ transition: transform 0.3s ease;
+
+ &:hover {
+ transform: scale(1.05);
+ }
+ }
+}
+
+.similar__card-content {
+ padding: 25px;
+}
+
+.similar__card-title {
+ font-weight: bold;
+ margin-bottom: 10px;
+ font-size: 20px;
+ color: @color-text-dark;
+}
+
+.similar__card-description {
+ font-size: 15px;
+ margin-bottom: 15px;
+ color: #666;
+ line-height: 1.5;
+}
+
+.similar__card-price {
+ font-weight: bold;
+ font-size: 22px;
+ color: @color-button;
+}
+
+@media (max-width: 1024px) {
+ .similar {
+ &__card {
+ flex: 0 0 calc(50% - 13px);
+ min-width: 280px;
+ }
+ }
+}
+
+@media (max-width: 768px) {
+ .similar {
+ &__grid {
+ justify-content: center;
+ }
+
+ &__card {
+ flex: 0 0 100%;
+ max-width: 400px;
+ }
+ }
+}
+
+// =======================
+// === КОРЗИНА И ЗАКАЗ ===
+// =======================
+.main__content {
+ display: flex;
+ gap: 40px;
+ margin: 30px 0;
+
+ .products {
+ flex: 1;
+ }
+
+ .order {
+ flex: 0 0 65%;
+ padding: 40px;
+
+ &__header {
+ .flex-between();
+ margin-bottom: 20px;
+ }
+
+ &__title {
+ font-family: @font-logo;
+ font-size: 28px;
+ color: @color-text-dark;
+ margin: 0;
+ }
+
+ &__total {
+ font-weight: bold;
+ color: @color-text-dark;
+ }
+
+ &__section {
+ margin-bottom: 25px;
+ }
+
+ &__section-title {
+ font-family: @font-logo;
+ margin-bottom: 15px;
+ font-size: 18px;
+ color: @color-text-dark;
+ }
+ }
+}
+
+.products {
+ &__title {
+ font-family: @font-logo;
+ margin-bottom: 20px;
+ font-size: 24px;
+ color: @color-text-dark;
+ }
+
+ &__list {
+ .flex-column();
+ gap: 20px;
+ }
+
+ &__item {
+ background-color: @color-secondary;
+ border-radius: 8px;
+ padding: 20px;
+ display: flex;
+ gap: 15px;
+ border: 1px solid @color-secondary;
+ transition: transform 0.3s ease;
+ align-items: flex-start;
+ position: relative;
+
+ &:hover {
+ transform: translateY(-2px);
+ }
+ }
+
+ &__image {
+ width: 300px;
+ height: 200px;
+ border-radius: 4px;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ }
+
+ .product-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+ transition: transform 0.3s ease;
+ margin: 0;
+ }
+
+ &__details {
+ flex: 1;
+ .flex-column();
+ justify-content: space-between;
+ align-items: flex-start;
+ min-height: 200px;
+ }
+
+ &__name {
+ font-weight: bold;
+ margin-bottom: 5px;
+ color: @color-accent;
+ font-size: 18px;
+ font-family: @font-main;
+ }
+
+ &__price {
+ font-weight: bold;
+ font-size: 18px;
+ margin-bottom: 15px;
+ color: @color-text-dark;
+ }
+
+ &__controls {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ margin-top: auto;
+ width: 100%;
+ justify-content: space-between;
+ }
+
+ &__quantity {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ }
+
+ &__qty-btn {
+ width: 30px;
+ height: 30px;
+ background-color: @color-text-dark;
+ color: @color-text-light;
+ border: none;
+ border-radius: 50%;
+ cursor: pointer;
+ .flex-center();
+ font-family: @font-main;
+ font-weight: bold;
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: scale(1.1);
+ }
+ }
+
+ &__qty-value {
+ font-weight: bold;
+ min-width: 30px;
+ text-align: center;
+ font-size: 16px;
+ }
+
+ &__cart-icon {
+ background-color: transparent;
+ color: @color-text-dark;
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ border: 2px solid @color-text-dark;
+ margin-left: 20px;
+
+ &:hover {
+ transform: scale(1.1);
+ }
+
+ i { font-size: 18px; }
+ }
+}
+
+.form {
+ &__group { margin-bottom: 15px; }
+ &__label {
+ display: block;
+ margin-bottom: 5px;
+ font-weight: bold;
+ color: #000000;
+ }
+ &__input {
+ width: 100%;
+ padding: 14px 16px;
+ border: 2px solid #ccc;
+ font-family: @font-main;
+ font-size: 15px;
+ transition: border-color 0.3s ease;
+
+ &:focus {
+ border-color: @color-primary;
+ }
+
+ &:hover {
+ border-color: darken(#ccc, 10%);
+ }
+ &::placeholder {
+ font-style: italic;
+ color: #999;
+ }
+ }
+ &__row {
+ display: flex;
+ gap: 20px;
+ justify-content: space-between;
+ }
+ &__input--half {
+ width: 100%;
+ }
+ &__radio-group {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 20px;
+ margin-top: 20px;
+ }
+ &__radio-label {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ color: @color-text-dark;
+ position: relative;
+ padding-left: 30px;
+ flex: 1;
+
+ &:hover {
+ .form__custom-radio {
+ border-color: lighten(@color-accent, 10%);
+ }
+ }
+ }
+ &__radio-input {
+ position: absolute;
+ opacity: 0;
+ cursor: pointer;
+ }
+ &__custom-radio {
+ position: absolute;
+ left: 0;
+ height: 20px;
+ width: 20px;
+ background-color: @color-secondary;
+ border: 2px solid @color-accent;
+ border-radius: 50%;
+ transition: border-color 0.3s ease;
+ }
+ &__radio-input:checked ~ &__custom-radio {
+ background-color: @color-accent;
+
+ &:after {
+ content: "";
+ position: absolute;
+ display: block;
+ top: 4px;
+ left: 4px;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: white;
+ }
+ }
+}
+
+.divider {
+ height: 1px;
+ background-color: #999;
+ margin: 20px 0;
+}
+
+.promo {
+ display: flex;
+ margin-bottom: 20px;
+
+ &__input {
+ flex: 1;
+ padding: 10px;
+ border: 1px solid #000;
+ background-color: @color-secondary;
+ font-family: @font-main;
+ height: auto;
+ min-height: 48px;
+
+ &:hover {
+ border-color: @color-primary;
+ }
+
+ &::placeholder {
+ font-style: italic;
+ color: #999;
+ }
+ }
+
+ &__btn {
+ background-color: @color-accent;
+ color: @color-secondary;
+ border: none;
+ padding: 10px 60px;
+ cursor: pointer;
+ font-family: @font-main;
+ font-size: 18px;
+ transition: all 0.3s ease;
+
+ &:hover {
+ background-color: lighten(@color-accent, 10%);
+ transform: translateY(-2px);
+ }
+ }
+}
+
+.summary {
+ margin-bottom: 20px;
+
+ &__item {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 10px;
+ color: @color-text-dark;
+
+ &.total {
+ font-weight: bold;
+ font-size: 18px;
+ padding-top: 10px;
+ margin-top: 10px;
+ }
+ }
+}
+
+.order-btn {
+ width: 100%;
+ background-color: @color-accent;
+ color: @color-secondary;
+ border: none;
+ padding: 15px;
+ border-radius: 4px;
+ font-size: 18px;
+ cursor: pointer;
+ margin-bottom: 10px;
+ font-family: @font-main;
+ transition: all 0.3s ease;
+
+ &:hover {
+ background-color: lighten(@color-accent, 10%);
+ transform: translateY(-2px);
+ }
+}
+
+.privacy {
+ display: flex;
+ gap: 8px;
+ font-size: 16px;
+ color: #666;
+ margin-bottom: 20px;
+
+ input[type="checkbox"] {
+ width: 18px;
+ height: 18px;
+ cursor: pointer;
+ }
+}
+
+.services {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ flex-wrap: wrap;
+ gap: 24px;
+ margin-bottom: 24px;
+
+ &__title {
+ font-family: @font-logo;
+ margin-bottom: 10px;
+ font-size: 18px;
+ color: @color-text-dark;
+ display: block;
+ width: 100%;
+ }
+
+ &__item {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 8px;
+ color: @color-text-dark;
+ width: 100%;
+ }
+}
+
+.cart-icon {
+ position: relative;
+}
+
+.cart-count {
+ position: absolute;
+ top: -8px;
+ right: -8px;
+ background: @color-accent;
+ color: @color-text-light;
+ border-radius: 50%;
+ width: 18px;
+ height: 18px;
+ font-size: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.form__input.error {
+ border-color: #ff4444;
+ box-shadow: 0 0 5px rgba(255, 68, 68, 0.3);
+}
+
+.empty-cart {
+ text-align: center;
+ padding: 40px;
+ color: #666;
+ font-size: 18px;
+}
+
+// =======================
+// === АВТОРИЗАЦИЯ ===
+// =======================
+.profile-page-main {
+ .flex-center();
+ min-height: 80vh;
+ padding: 40px 0;
+ background-color: lighten(@color-secondary, 5%);
+ z-index: 1;
+
+ .profile-container {
+ display: flex;
+ width: 100%;
+ max-width: 1000px;
+ min-height: 600px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
+ background-color: @color-text-light;
+ }
+
+ .profile-left-col {
+ flex: 0 0 35%;
+ background-color: @color-primary;
+ color: @color-text-light;
+ display: flex;
+ justify-content: flex-start;
+ align-items: flex-start;
+ padding: 40px;
+ .logo {
+ font-size: 32px;
+ font-weight: normal;
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.4);
+ color: @color-text-light;
+ }
+ }
+
+ .profile-right-col {
+ flex: 0 0 65%;
+ .flex-center();
+ padding: 40px;
+ .profile-form-block {
+ width: 100%;
+ max-width: 400px;
+ h2 {
+ font-size: 28px;
+ font-weight: normal;
+ margin-bottom: 40px;
+ text-align: left;
+ color: @color-text-dark;
+ }
+ }
+ }
+
+ .profile-form {
+ .input-group {
+ margin-bottom: 20px;
+ label {
+ display: block;
+ font-size: 12px;
+ font-weight: bold;
+ color: @color-text-dark;
+ margin-bottom: 5px;
+ text-transform: uppercase;
+ }
+ }
+
+ input[type="text"],
+ input[type="email"],
+ input[type="tel"] {
+ .input-base();
+ }
+
+ .password-link {
+ display: block;
+ text-align: left;
+ font-size: 13px;
+ color: @color-text-dark;
+ text-decoration: underline;
+ margin: 10px 0 20px;
+ &:hover {
+ color: @color-accent;
+ text-decoration: none;
+ }
+ }
+
+ .save-btn {
+ padding: 15px 30px;
+ border: none;
+ cursor: pointer;
+ font-size: 15px;
+ text-transform: uppercase;
+ transition: all 0.3s ease;
+ font-family: @font-main;
+ width: 100%;
+ margin-top: 20px;
+ background-color: @color-primary;
+ color: @color-text-light;
+
+ &:hover {
+ background-color: lighten(@color-primary, 10%);
+ transform: translateY(-2px);
+ box-shadow: @shadow-light;
+ }
+ }
+
+ .auth-actions {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 25px;
+ padding-top: 20px;
+ border-top: 1px solid #eee;
+
+ .auth-text {
+ font-size: 13px;
+ color: @color-text-dark;
+ }
+
+ .login-btn {
+ background-color: transparent;
+ color: @color-accent;
+ border: 1px solid @color-accent;
+ padding: 10px 25px;
+ font-size: 13px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+
+ &:hover {
+ background-color: @color-primary;
+ color: @color-text-light;
+ }
+ }
+ }
+ }
+}
+
+// =======================
+// === СЕКЦИЯ УСЛУГ ===
+// =======================
+.services-section {
+ padding: 60px 0;
+ background-color: @color-secondary;
+}
+
+.services__wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 30px;
+}
+
+.services__top-row {
+ display: flex;
+ gap: 30px;
+ justify-content: center;
+
+ @media (max-width: 768px) {
+ flex-direction: column;
+ align-items: center;
+ }
+}
+
+.service-card {
+ border-radius: 8px;
+ padding: 40px;
+ min-height: 200px;
+ text-align: center;
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: translateY(-5px);
+ box-shadow: @shadow-light;
+ }
+
+ &--green {
+ background: @color-primary;
+ color: @color-text-light;
+ flex: 1;
+ max-width: 450px;
+ }
+
+ &--beige {
+ background: @color-beige;
+ color: @color-text-light;
+ width: 100%;
+ max-width: 930px;
+ margin: 0 auto;
+ }
+
+ &__title {
+ font-family: @font-logo;
+ font-size: 24px;
+ font-weight: bold;
+ margin-bottom: 15px;
+ text-transform: uppercase;
+ }
+
+ &__text {
+ font-family: @font-main;
+ font-size: 16px;
+ line-height: 1.6;
+ margin: 0;
+ }
+}
+
+// =======================
+// === ФУТЕР ===
+// =======================
+.footer {
+ background-color: @color-primary;
+ color: black;
+ padding: 40px 0 10px;
+ position: relative;
+ z-index: 1000;
+
+ &::before {
+ content: '';
+ display: block;
+ position: absolute;
+ top: -80px;
+ left: 0;
+ width: 100%;
+ height: 1px;
+ visibility: hidden;
+ }
+
+ &__content {
+ display: flex;
+ gap: 20px;
+ padding-bottom: 30px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.2);
+ }
+
+ &__col {
+ flex: 1;
+ &--logo { flex: 1.5; }
+ h5 {
+ margin-bottom: 15px;
+ font-size: 14px;
+ text-transform: uppercase;
+ }
+ ul li {
+ margin-bottom: 8px;
+ a:hover { text-decoration: underline; }
+ }
+ .social-icons,
+ .payment-icons {
+ .flex-center(15px);
+ justify-content: flex-start;
+ margin-top: 10px;
+ }
+ .social-icons .icon {
+ .icon-base(20px, 1.1);
+ color: black;
+ &:hover { color: @color-accent; }
+ }
+ .payment-icons .pay-icon {
+ .icon-base(24px, 1.05);
+ color: black;
+ }
+ }
+
+ .copyright {
+ text-align: center;
+ font-size: 12px;
+ padding-top: 20px;
+ color: rgba(255, 255, 255, 0.6);
+ }
+}
+
+// =======================
+// === ДОСТАВКА ===
+// =======================
+.delivery-content {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 40px 20px;
+}
+
+.delivery-content h1 {
+ font-family: @font-logo;
+ font-size: 42px;
+ text-align: center;
+ margin-bottom: 50px;
+ color: #453227;
+}
+
+.delivery-section {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 30px;
+ margin-bottom: 60px;
+}
+
+.delivery-card {
+ background: white;
+ padding: 30px;
+ border-radius: 12px;
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
+ text-align: center;
+ transition: transform 0.3s ease;
+ flex: 1;
+ min-width: 350px;
+ max-width: 400px;
+}
+
+.delivery-card:hover {
+ transform: translateY(-5px);
+}
+
+.delivery-icon {
+ font-size: 48px;
+ color: #617365;
+ margin-bottom: 20px;
+}
+
+.delivery-card h3 {
+ font-family: @font-logo;
+ font-size: 24px;
+ margin-bottom: 20px;
+ color: #453227;
+}
+
+.delivery-details {
+ text-align: left;
+}
+
+.detail-item {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 12px;
+ padding-bottom: 12px;
+ border-bottom: 1px solid #f0f0f0;
+}
+
+.detail-label {
+ font-weight: bold;
+ color: #333;
+}
+
+.detail-value {
+ color: #617365;
+ text-align: right;
+}
+
+// =======================
+// === ГАРАНТИЯ ===
+// =======================
+.warranty-content {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 40px 20px;
+}
+
+.warranty-content h1 {
+ font-family: 'Anek Kannada', sans-serif;
+ font-size: 42px;
+ text-align: center;
+ margin-bottom: 50px;
+ color: #453227;
+}
+
+.warranty-overview {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 25px;
+ margin-bottom: 60px;
+}
+
+.warranty-card {
+ background: white;
+ padding: 30px;
+ border-radius: 12px;
+ text-align: center;
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
+ transition: transform 0.3s ease;
+ flex: 1;
+ min-width: 250px;
+ max-width: 280px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.warranty-card:hover {
+ transform: translateY(-5px);
+}
+
+.warranty-icon {
+ font-size: 48px;
+ color: #617365;
+ margin-bottom: 20px;
+}
+
+.warranty-card h3 {
+ font-family: 'Anek Kannada', sans-serif;
+ font-size: 20px;
+ margin-bottom: 15px;
+ color: #453227;
+}
+
+.warranty-period {
+ font-size: 24px;
+ font-weight: bold;
+ color: #617365;
+ margin-top: auto;
+}
+
+.coverage-section {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 40px;
+ margin-bottom: 60px;
+}
+
+.coverage-covered,
+.coverage-not-covered {
+ flex: 1;
+ min-width: 300px;
+}
+
+.coverage-section h2 {
+ font-family: 'Anek Kannada', sans-serif;
+ font-size: 24px;
+ margin-bottom: 25px;
+ color: #453227;
+}
+
+.coverage-list {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.coverage-item {
+ display: flex;
+ align-items: flex-start;
+ gap: 15px;
+ padding: 20px;
+ border-radius: 8px;
+ background: white;
+ box-shadow: 0 3px 10px rgba(0,0,0,0.1);
+}
+
+.coverage-item.covered i {
+ color: #28a745;
+ font-size: 20px;
+ margin-top: 2px;
+ flex-shrink: 0;
+}
+
+.coverage-item.not-covered i {
+ color: #dc3545;
+ font-size: 20px;
+ margin-top: 2px;
+ flex-shrink: 0;
+}
+
+.coverage-text {
+ flex: 1;
+}
+
+.coverage-item h4 {
+ font-family: 'Anek Kannada', sans-serif;
+ font-size: 16px;
+ margin-bottom: 5px;
+ color: #333;
+}
+
+.card {
+ min-height: 250px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: @color-text-light;
+ text-align: center;
+
+ &--green {
+ background: @color-primary;
+ flex: 0 1 450px;
+ max-width: 450px;
+ }
+
+ &--beige {
+ background: @color-beige;
+ color: @color-text-dark;
+ flex: 0 1 925px;
+ max-width: 925px;
+ }
+}
+
+.design-section {
+ display: flex;
+ justify-content: center;
+ margin-bottom: 40px;
+ .card { width: 100%; }
+}
+
+// =======================
+// === АДАПТИВНОСТЬ ===
+// =======================
+@media (max-width: 1240px) {
+ .catalog-wrapper { gap: 20px; }
+ .catalog-sidebar { flex: 0 0 200px; }
+ .products-container {
+ gap: 15px;
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ .product-card.small1 {
+ margin-top: 100px;
+ }
+
+ .product-card.small,
+ .product-card.small1,
+ .product-card.large,
+ .product-card.wide,
+ .product-card.wide1,
+ .product-card.wide2,
+ .product-card.wide2_1,
+ .product-card.wide4 {
+ flex: 0 0 calc(33.333% - 10px);
+ max-width: calc(33.333% - 10px);
+ height: 180px;
+ margin: 0;
+
+ .product-image-container {
+ height: 180px;
+ }
+ }
+
+ .product-card.wide3 {
+ flex: 0 0 calc(25% - 10px);
+ max-width: calc(25% - 10px);
+ height: 300px;
+ margin: 0;
+
+ .product-image-container {
+ height: 350px;
+ }
+ }
+
+ .product-card.tall {
+ flex: 0 0 calc(25% - 10px);
+ max-width: calc(25% - 10px);
+ height: 300px;
+ margin: 0;
+
+ .product-image-container {
+ height: 300px;
+ }
+ }
+
+ .product-card.full-width {
+ flex: 0 0 100%;
+ max-width: 100%;
+ height: 300px;
+ margin: 0;
+
+ .product-image-container {
+ height: 300px;
+ }
+ }
+
+ .product-card.small { order: 1; }
+ .product-card.large { order: 2; }
+ .product-card.wide { order: 3; }
+
+ .product-card.small1 { order: 11; }
+ .product-card.wide2 { order: 12; }
+ .product-card.wide2_1 { order: 13; }
+
+ .product-card.wide3 { order: 21; }
+ .product-card.tall { order: 22; }
+
+ .product-card.wide3 { order: 31; }
+
+ .product-card.full-width { order: 41; flex-basis: 100%; }
+
+ .main__content {
+ gap: 20px;
+ .products {
+ flex: 0 0 35%;
+ .products__image {
+ width: 250px;
+ height: 180px;
+ }
+ }
+ .order {
+ flex: 0 0 60%;
+ padding: 30px;
+
+ .order__title {
+ font-size: 24px;
+ }
+
+ .order__section-title {
+ font-size: 16px;
+ }
+ }
+ }
+
+ .solutions-slider {
+ &__slide {
+ .solution-text-overlay {
+ top: 10%;
+ left: 5%;
+ h2 {
+ font-size: 26px;
+ margin-bottom: 5px;
+ line-height: 1.2;
+ }
+ p {
+ font-size: 18px;
+ line-height: 1.2;
+ }
+ }
+ .solution-image-link {
+ bottom: 70px;
+ padding: 10px 25px;
+ font-size: 14px;
+ }
+ }
+ }
+
+ .product__image {
+ width: 350px;
+ height: 250px;
+ }
+
+ .product__thumbnail img {
+ width: 170px;
+ height: 120px;
+ }
+}
+
+@media (max-width: 1024px) {
+ .main__content {
+ gap: 25px;
+ .products {
+ flex: 0 0 30%;
+ .products__image {
+ width: 200px;
+ height: 150px;
+ }
+
+ .products__name {
+ font-size: 16px;
+ }
+
+ .products__price {
+ font-size: 16px;
+ }
+ }
+ .order {
+ flex: 0 0 60%;
+ padding: 25px;
+
+ .order__title {
+ font-size: 22px;
+ }
+
+ .form__input {
+ padding: 12px 14px;
+ font-size: 14px;
+ }
+
+ .promo__btn {
+ padding: 10px 40px;
+ font-size: 16px;
+ }
+ }
+ }
+}
+
+@media (max-width: 768px) {
+ .container { padding: 0 15px; }
+
+ .delivery-section {
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .delivery-card {
+ min-width: 100%;
+ max-width: 100%;
+ }
+
+ .delivery-content h1 {
+ font-size: 32px;
+ }
+
+ .warranty-overview {
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .warranty-card {
+ max-width: 100%;
+ width: 100%;
+ }
+
+ .coverage-section {
+ flex-direction: column;
+ }
+
+ .warranty-content h1 {
+ font-size: 32px;
+ }
+
+ .header__top .container,
+ .header__bottom .container,
+ .hero__content,
+ .advantages__header,
+ .about__content,
+ .advantages__items,
+ .promo-images,
+ .stats__items,
+ .faq__items,
+ .catalog-wrapper,
+ .main__content {
+ flex-direction: column;
+ gap: 30px;
+ }
+
+ .search-catalog {
+ order: 3;
+ width: 100%;
+ max-width: 100%;
+ }
+
+ .nav-list {
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 15px;
+ }
+
+ .hero {
+ &__image-block {
+ flex: none;
+ max-width: 400px;
+ height: 400px;
+ }
+ &__circle {
+ width: 380px;
+ height: 380px;
+ }
+ &__text-block {
+ flex: none;
+ padding-left: 0;
+ text-align: center;
+ h1 { font-size: 32px; }
+ .hero__usp-text {
+ padding-left: 0;
+ justify-content: center;
+ &::before { display: none; }
+ }
+ .btn.primary-btn { margin-left: 0; }
+ }
+ }
+
+ .advantages__header h2,
+ .faq h2 { font-size: 28px; }
+
+ .faq-item,
+ .stat-item {
+ flex: none;
+ .flex-center();
+ text-align: center;
+ }
+
+ .stats .container { justify-content: center; }
+ .catalog-dropdown__menu { width: 200px; }
+
+ .catalog-sidebar { width: 100%; flex: none; }
+ .products-container { gap: 15px; }
+
+ .product-card.small,
+ .product-card.small1,
+ .product-card.large,
+ .product-card.wide,
+ .product-card.wide1,
+ .product-card.wide2,
+ .product-card.wide2_1,
+ .product-card.wide3,
+ .product-card.wide4,
+ .product-card.tall,
+ .product-card.full-width {
+ flex: 0 0 100%;
+ max-width: 100%;
+ height: 250px;
+ margin: 0;
+
+ .product-image-container {
+ height: 200px;
+ }
+ }
+
+ .main__content {
+ flex-direction: column;
+ gap: 20px;
+
+ .products,
+ .order {
+ flex: 0 0 100%;
+ width: 100%;
+ }
+
+ .products {
+ .products__item {
+ flex-direction: column;
+ text-align: center;
+ gap: 15px;
+ }
+
+ .products__image {
+ width: 100%;
+ height: 200px;
+ justify-content: center;
+ }
+
+ .products__details {
+ min-height: auto;
+ align-items: center;
+ }
+
+ .products__controls {
+ justify-content: center;
+ margin-top: 15px;
+ }
+
+ .products__cart-icon {
+ margin-left: 0;
+ }
+ }
+
+ .order {
+ padding: 20px;
+
+ .order__title {
+ font-size: 20px;
+ text-align: center;
+ }
+
+ .order__total {
+ text-align: center;
+ }
+
+ .form__radio-group {
+ flex-direction: column;
+ gap: 15px;
+ }
+
+ .form__radio-label {
+ flex: none;
+ justify-content: flex-start;
+ }
+
+ .promo {
+ flex-direction: column;
+ gap: 10px;
+
+ &__btn {
+ width: 100%;
+ padding: 12px;
+ }
+ }
+
+ .order-btn {
+ padding: 12px;
+ font-size: 16px;
+ }
+
+ .services {
+ flex-direction: column;
+ align-items: center;
+ }
+ }
+ }
+
+ .product-image-container { height: 200px; }
+ .product-card.tall .product-image-container,
+ .product-card.large .product-image-container { height: 250px; }
+
+ .profile-page-main {
+ .profile-container {
+ flex-direction: column;
+ min-height: auto;
+ max-width: 100%;
+ box-shadow: none;
+ }
+ .profile-left-col {
+ flex: none;
+ width: 100%;
+ height: 100px;
+ .flex-center();
+ padding: 0;
+ }
+ .profile-right-col {
+ flex: none;
+ width: 100%;
+ padding: 30px 20px;
+ }
+ .profile-form-block { max-width: 100%; }
+ }
+
+ .form__row { flex-direction: column; }
+ .form__input--half { flex: 0 0 100%; max-width: 100%; }
+ .services { flex-direction: column; align-items: center; }
+
+ .services-section {
+ padding: 40px 0;
+ }
+
+ .service-card {
+ padding: 30px 20px;
+ min-height: 180px;
+
+ &--green,
+ &--beige {
+ max-width: 100%;
+ }
+
+ &__title {
+ font-size: 20px;
+ }
+
+ &__text {
+ font-size: 14px;
+ }
+ }
+ .solutions-slider {
+ margin: 20px auto;
+ &__slide {
+ .solution-text-overlay {
+ top: 8%;
+ left: 4%;
+ h2 {
+ font-size: 20px;
+ margin-bottom: 3px;
+ line-height: 1.1;
+ }
+ p {
+ font-size: 15px;
+ line-height: 1.1;
+ }
+ }
+ .solution-image-link {
+ bottom: 90px;
+ padding: 8px 20px;
+ font-size: 13px;
+ }
+ }
+ }
+}
+
+// Стили для ошибок полей
+.error-input {
+ border-color: #ff4444 !important;
+ box-shadow: 0 0 0 1px #ff4444;
+}
+
+.field-error {
+ color: #ff4444;
+ font-size: 12px;
+ margin-top: 5px;
+ margin-bottom: 10px;
+}
+
+// Стили для сообщений
+.message {
+ padding: 15px;
+ margin: 20px 0;
+ border-radius: 5px;
+ display: none;
+}
+
+.message.error {
+ background-color: #ffebee;
+ color: #c62828;
+ border: 1px solid #ffcdd2;
+}
+
+.message.success {
+ background-color: #e8f5e9;
+ color: #2e7d32;
+ border: 1px solid #c8e6c9;
+}
+
+// Добавьте в конец файла
+.access-denied {
+ text-align: center;
+ padding: 80px 20px;
+ background: white;
+ border-radius: 10px;
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
+ margin: 50px 0;
+
+ h2 {
+ color: #dc3545;
+ margin-bottom: 30px;
+ font-size: 28px;
+ }
+
+ p {
+ color: #666;
+ margin-bottom: 40px;
+ font-size: 18px;
+ line-height: 1.6;
+ }
+
+ .btn {
+ margin: 5px;
+ min-width: 200px;
+ }
+}
+// =======================
+// === ПРОФИЛЬ ПОЛЬЗОВАТЕЛЯ ===
+// =======================
+
+.user-profile-dropdown {
+ position: relative;
+ display: inline-block;
+
+ &__toggle {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ cursor: pointer;
+ padding: 8px 12px;
+ border-radius: 4px;
+ transition: all 0.3s ease;
+
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.05);
+ }
+
+ .user-avatar {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ background-color: @color-primary;
+ color: @color-text-light;
+ .flex-center();
+ font-weight: bold;
+ }
+
+ .user-info {
+ display: flex;
+ flex-direction: column;
+
+ .user-email {
+ font-size: 12px;
+ color: #666;
+ max-width: 150px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .user-status {
+ font-size: 10px;
+ padding: 2px 6px;
+ border-radius: 10px;
+ text-transform: uppercase;
+
+ &.admin {
+ background-color: #617365;
+ color: white;
+ }
+
+ &.user {
+ background-color: #28a745;
+ color: white;
+ }
+ }
+ }
+ }
+
+ &__menu {
+ .menu-base();
+ width: 220px;
+ top: 100%;
+ right: 0;
+
+ .user-details {
+ padding: 15px;
+ border-bottom: 1px solid #eee;
+
+ .user-name {
+ font-weight: bold;
+ margin-bottom: 5px;
+ }
+
+ .user-registered {
+ font-size: 11px;
+ color: #999;
+ }
+ }
+
+ ul {
+ padding: 10px 0;
+
+ li {
+ padding: 8px 15px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ transition: background-color 0.3s ease;
+
+ &:hover {
+ background-color: #f5f5f5;
+ }
+
+ &.logout {
+ color: #dc3545;
+ border-top: 1px solid #eee;
+ margin-top: 5px;
+ padding-top: 12px;
+
+ &:hover {
+ background-color: #ffe6e6;
+ }
+ }
+ }
+ }
+ }
+
+ &:hover &__menu {
+ display: block;
+ }
+}
+
+// =======================
+// === КАРТОЧКА ТОВАРА ===
+// =======================
+
+.product-image-container {
+ position: relative;
+ overflow: hidden;
+ margin-bottom: 0;
+ padding: 0;
+ height: 250px;
+ .flex-center();
+
+ &:hover {
+ .product-overlay-info {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ }
+}
+
+.product-overlay-info {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background: linear-gradient(to top, rgba(0,0,0,0.8), transparent);
+ color: white;
+ padding: 15px;
+ opacity: 0;
+ transform: translateY(10px);
+ transition: all 0.3s ease;
+
+ .product-overlay-name {
+ font-weight: bold;
+ font-size: 14px;
+ margin-bottom: 5px;
+ }
+
+ .product-overlay-price {
+ font-size: 16px;
+ font-weight: bold;
+
+ .old-price {
+ text-decoration: line-through;
+ font-size: 12px;
+ color: #ccc;
+ margin-right: 5px;
+ }
+
+ .current-price {
+ color: #ffd700;
+ }
+ }
+
+ .product-overlay-category {
+ font-size: 11px;
+ opacity: 0.8;
+ margin-top: 3px;
+ }
+
+ .product-overlay-stock {
+ font-size: 11px;
+ margin-top: 5px;
+
+ &.out-of-stock {
+ color: #ff6b6b;
+ }
+
+ i {
+ margin-right: 5px;
+ }
+ }
+}
+
+.product-card-details {
+ padding: 15px;
+ background: white;
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+
+ .product-card-name {
+ font-weight: bold;
+ font-size: 16px;
+ margin-bottom: 8px;
+ color: @color-text-dark;
+ }
+
+ .product-card-description {
+ font-size: 13px;
+ color: #777;
+ margin-bottom: 10px;
+ flex-grow: 1;
+ }
+
+ .product-card-attributes {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-bottom: 10px;
+
+ .attribute {
+ font-size: 11px;
+ background: #f5f5f5;
+ padding: 3px 8px;
+ border-radius: 12px;
+ color: #666;
+
+ i {
+ margin-right: 3px;
+ }
+ }
+ }
+
+ .product-card-price {
+ margin-bottom: 10px;
+
+ .old-price {
+ text-decoration: line-through;
+ font-size: 14px;
+ color: #999;
+ margin-right: 8px;
+ }
+
+ .current-price {
+ font-size: 18px;
+ font-weight: bold;
+ color: @color-button;
+ }
+ }
+
+ .add-to-cart-btn {
+ width: 100%;
+ padding: 8px;
+ font-size: 14px;
+ }
+
+ .admin-actions {
+ display: flex;
+ gap: 5px;
+ margin-top: 10px;
+
+ .admin-btn {
+ flex: 1;
+ font-size: 12px;
+ padding: 6px;
+
+ &.delete-btn {
+ background: #dc3545;
+
+ &:hover {
+ background: #c82333;
+ }
+ }
+ }
+ }
+}
+
+// =======================
+// === ПРОФИЛЬ ПОЛЬЗОВАТЕЛЯ ===
+// =======================
+
+.user-profile-dropdown {
+ position: relative;
+ display: inline-block;
+
+ .user-profile-toggle {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ cursor: pointer;
+ padding: 8px 12px;
+ border-radius: 4px;
+ transition: all 0.3s ease;
+
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.05);
+
+ .dropdown-arrow {
+ transform: rotate(180deg);
+ }
+ }
+
+ .user-avatar {
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #617365 0%, #453227 100%);
+ color: @color-text-light;
+ .flex-center();
+ font-weight: bold;
+ font-size: 16px;
+ box-shadow: 0 2px 5px rgba(0,0,0,0.2);
+ }
+
+ .user-info {
+ display: flex;
+ flex-direction: column;
+
+ .user-email {
+ font-size: 12px;
+ color: #666;
+ max-width: 120px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .user-status {
+ font-size: 10px;
+ padding: 2px 8px;
+ border-radius: 12px;
+ text-transform: uppercase;
+ font-weight: bold;
+ text-align: center;
+ margin-top: 2px;
+
+ &.admin {
+ background-color: #617365;
+ color: white;
+ border: 1px solid #617365;
+ }
+
+ &.user {
+ background-color: #28a745;
+ color: white;
+ border: 1px solid #28a745;
+ }
+ }
+ }
+
+ .dropdown-arrow {
+ font-size: 10px;
+ color: #666;
+ transition: transform 0.3s ease;
+ }
+ }
+
+ .user-profile-menu {
+ .menu-base();
+ width: 280px;
+ top: 100%;
+ right: 0;
+ margin-top: 10px;
+ padding: 0;
+ overflow: hidden;
+
+ .user-profile-header {
+ padding: 20px;
+ background: linear-gradient(135deg, #617365 0%, #453227 100%);
+ color: white;
+
+ .user-profile-name {
+ font-weight: bold;
+ margin-bottom: 8px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-size: 16px;
+ }
+
+ .user-profile-details {
+ small {
+ display: block;
+ opacity: 0.8;
+ margin-bottom: 5px;
+ font-size: 11px;
+
+ i {
+ margin-right: 5px;
+ width: 14px;
+ text-align: center;
+ }
+ }
+ }
+ }
+
+ .user-profile-links {
+ list-style: none;
+ padding: 10px 0;
+
+ li {
+ border-bottom: 1px solid #f0f0f0;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ a {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 20px;
+ color: #333;
+ transition: all 0.3s ease;
+
+ &:hover {
+ background-color: #f8f9fa;
+ color: @color-primary;
+ text-decoration: none;
+
+ i {
+ transform: scale(1.1);
+ }
+ }
+
+ i {
+ width: 20px;
+ text-align: center;
+ font-size: 14px;
+ color: #617365;
+ transition: transform 0.3s ease;
+ }
+
+ span {
+ flex-grow: 1;
+ }
+ }
+ }
+
+ .logout-item {
+ border-top: 2px solid #f0f0f0;
+ margin-top: 5px;
+
+ a {
+ color: #dc3545;
+
+ &:hover {
+ background-color: #ffe6e6;
+ color: #c82333;
+
+ i {
+ color: #dc3545;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ &:hover .user-profile-menu {
+ display: block;
+ animation: fadeIn 0.3s ease;
+ }
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+// Для мобильных устройств
+@media (max-width: 768px) {
+ .user-profile-dropdown {
+ .user-profile-toggle {
+ .user-info {
+ display: none;
+ }
+
+ .dropdown-arrow {
+ display: none;
+ }
+ }
+
+ .user-profile-menu {
+ width: 250px;
+ right: -50px;
+ }
+ }
+}
+// Добавьте в конец файла
+.unavailable-product {
+ position: relative;
+ opacity: 0.6;
+ filter: grayscale(0.7);
+
+ &::before {
+ content: "ТОВАР ЗАКОНЧИЛСЯ";
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background: rgba(0, 0, 0, 0.85);
+ color: white;
+ padding: 15px 25px;
+ border-radius: 5px;
+ font-weight: bold;
+ font-size: 16px;
+ text-align: center;
+ z-index: 100;
+ white-space: nowrap;
+ pointer-events: none;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
+ }
+
+ .product-name-overlay {
+ .name, .price {
+ color: #999 !important;
+ text-shadow: none !important;
+ }
+ }
+
+ .add-to-cart-btn {
+ display: none !important;
+ }
+
+ &:hover {
+ transform: none !important;
+ cursor: not-allowed;
+ }
+}
+
+.out-of-stock-badge {
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ background: #6c757d;
+ color: white;
+ padding: 5px 10px;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: bold;
+ z-index: 10;
+}
+
+// Для админ-таблицы
+.admin-table tr.unavailable {
+ background-color: #f8f9fa !important;
+ opacity: 0.7;
+
+ td {
+ color: #999;
+ }
+}
+
+.status-unavailable {
+ background-color: #6c757d !important;
+ color: white !important;
+}
\ No newline at end of file