diff --git a/README.md b/README.md index 8c2cfed..c66ac0f 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,15 @@ 1. Установить Open Server Panel, Laragon или другой локальный сервер с PHP и MariaDB. 2. Направить корень сайта на папку `public/`. 3. Проверить настройки базы в `.env`. -4. Открыть сайт в браузере. +4. Открыть `/install.php?token=rybstock-local-install` и применить схему базы. +5. Открыть сайт в браузере. + +## Диагностика + +Если после изменения файлов сайт показывает старый текст, открыть: + +`/check.php` + +Страница покажет реальный путь исполняемого файла, базу данных, количество таблиц и текущие верхние категории. Если сервер смотрит не в `public/`, нужно настроить виртуальный хост, чтобы публичной была именно эта папка. diff --git a/admin/index.php b/admin/index.php index 4a13bff..3b4abfc 100644 --- a/admin/index.php +++ b/admin/index.php @@ -4,4 +4,40 @@ declare(strict_types=1); require_once dirname(__DIR__) . '/app/bootstrap.php'; -echo 'RybStock admin panel will be created on the next steps.'; +$sections = [ + 'Товары', + 'Категории', + 'Фасовки', + 'Заказы', + 'Промокоды', + 'Акции', + 'Популярные товары', + 'Отзывы', + 'Настройки', + 'Прайс-лист', +]; +?> + + + + + + Админка Рыбсток + + + +
+
+

Админка

+

Рыбсток

+

Подготовлены основные разделы будущей панели управления.

+ +
+ +
+ +
+
+
+ + diff --git a/app/Controllers/HomeController.php b/app/Controllers/HomeController.php new file mode 100644 index 0000000..925c693 --- /dev/null +++ b/app/Controllers/HomeController.php @@ -0,0 +1,57 @@ +query('SELECT 1'); + $dbStatus = 'connected'; + + $categories = (new CategoryRepository())->tree(); + $productRepository = new ProductRepository(); + $catalogProducts = $productRepository->catalogPreview(24); + $popularProducts = $productRepository->popularPreview(10); + $settings = (new SettingRepository())->allKeyed(); + } catch (Throwable $exception) { + $dbStatus = app_env('APP_DEBUG', 'false') === 'true' + ? 'error: ' . $exception->getMessage() + : 'error'; + $notice = 'Если таблицы еще не созданы, откройте страницу установки базы.'; + } + + view('pages/home', [ + 'title' => 'Рыбсток - продукты и рыба с доставкой по Москве и Московской области', + 'metaDescription' => 'Рыбсток - интернет-магазин рыбы, морепродуктов, мяса, сыров, полуфабрикатов и продуктов с доставкой по Москве и Московской области.', + 'metaKeywords' => 'Рыбсток, рыба, морепродукты, мясо, сыры, полуфабрикаты, доставка продуктов Москва', + 'breadcrumbs' => [ + ['title' => 'Главная'], + ], + 'showBackButton' => false, + 'dbStatus' => $dbStatus, + 'categories' => $categories, + 'catalogProducts' => $catalogProducts, + 'popularProducts' => $popularProducts, + 'settings' => $settings, + 'notice' => $notice, + 'buildVersion' => '2026-06-04-02', + ]); + } +} diff --git a/app/Core/View.php b/app/Core/View.php index 4568ed5..19400a4 100644 --- a/app/Core/View.php +++ b/app/Core/View.php @@ -18,6 +18,12 @@ final class View extract($data, EXTR_SKIP); + $title = $title ?? 'Рыбсток'; + $metaDescription = $metaDescription ?? ''; + $metaKeywords = $metaKeywords ?? ''; + $breadcrumbs = $breadcrumbs ?? []; + $showBackButton = $showBackButton ?? true; + require base_path('views/layouts/main.php'); } } diff --git a/app/Repositories/BaseRepository.php b/app/Repositories/BaseRepository.php new file mode 100644 index 0000000..c82982a --- /dev/null +++ b/app/Repositories/BaseRepository.php @@ -0,0 +1,16 @@ +> + */ + public function active(): array + { + $statement = $this->pdo()->query( + 'SELECT id, parent_id, name, slug, legacy_path, h1, seo_title, seo_description, seo_keywords, image_path, sort_order + FROM categories + WHERE is_active = 1 + ORDER BY parent_id IS NOT NULL, sort_order, name' + ); + + return $statement->fetchAll(); + } + + /** + * @return array> + */ + public function tree(): array + { + $items = $this->active(); + $childrenByParent = []; + + foreach ($items as $item) { + $parentKey = $item['parent_id'] === null ? 0 : (int) $item['parent_id']; + $childrenByParent[$parentKey][] = $item; + } + + return $this->buildTree($childrenByParent, 0); + } + + /** + * @param array>> $childrenByParent + * @return array> + */ + private function buildTree(array $childrenByParent, int $parentId): array + { + $branch = []; + + foreach ($childrenByParent[$parentId] ?? [] as $item) { + $item['children'] = $this->buildTree($childrenByParent, (int) $item['id']); + $branch[] = $item; + } + + return $branch; + } + + /** + * @return array|null + */ + public function findBySlug(string $slug): ?array + { + $statement = $this->pdo()->prepare( + 'SELECT id, parent_id, name, slug, legacy_path, description, h1, seo_title, seo_description, seo_keywords, image_path + FROM categories + WHERE slug = :slug AND is_active = 1 + LIMIT 1' + ); + $statement->execute(['slug' => $slug]); + $category = $statement->fetch(); + + return $category ?: null; + } + + /** + * @return array> + */ + public function breadcrumbs(int $categoryId): array + { + $items = []; + $currentId = $categoryId; + + while ($currentId > 0) { + $statement = $this->pdo()->prepare( + 'SELECT id, parent_id, name, slug + FROM categories + WHERE id = :id + LIMIT 1' + ); + $statement->execute(['id' => $currentId]); + $category = $statement->fetch(); + + if (!$category) { + break; + } + + array_unshift($items, [ + 'title' => $category['name'], + 'url' => '/catalog/' . $category['slug'], + ]); + + $currentId = $category['parent_id'] === null ? 0 : (int) $category['parent_id']; + } + + array_unshift($items, [ + 'title' => 'Главная', + 'url' => '/', + ]); + + return $items; + } +} diff --git a/app/Repositories/ProductRepository.php b/app/Repositories/ProductRepository.php new file mode 100644 index 0000000..bd70317 --- /dev/null +++ b/app/Repositories/ProductRepository.php @@ -0,0 +1,103 @@ +> + */ + public function popularPreview(int $limit = 10): array + { + $limit = max(1, min($limit, 30)); + $sql = $this->previewSelectSql( + 'ORDER BY p.sales_count DESC, RAND()', + $limit + ); + + $statement = $this->pdo()->query($sql); + + return $this->attachPriceTiers($statement->fetchAll()); + } + + /** + * @return array> + */ + public function catalogPreview(int $limit = 24): array + { + $limit = max(1, min($limit, 60)); + $sql = $this->previewSelectSql( + 'ORDER BY p.sort_order, p.name', + $limit + ); + + $statement = $this->pdo()->query($sql); + + return $this->attachPriceTiers($statement->fetchAll()); + } + + private function previewSelectSql(string $orderBy, int $limit): string + { + return 'SELECT + p.id, + p.name, + p.slug, + p.legacy_path, + p.h1, + p.seo_title, + p.seo_description, + p.seo_keywords, + c.name AS category_name, + c.slug AS category_slug, + p.main_image_path, + p.base_unit, + p.package_display_mode, + p.sales_count, + v.id AS variant_id, + v.name AS variant_name, + v.unit, + v.package_quantity, + v.price_per_unit, + v.package_price + FROM products p + LEFT JOIN categories c ON c.id = p.category_id + LEFT JOIN product_variants v + ON v.id = ( + SELECT pv.id + FROM product_variants pv + WHERE pv.product_id = p.id + AND pv.is_published = 1 + AND pv.is_available = 1 + ORDER BY pv.is_default DESC, pv.sort_order, pv.id + LIMIT 1 + ) + WHERE p.is_published = 1 + AND p.is_available = 1 + ' . $orderBy . ' + LIMIT ' . $limit; + } + + /** + * @param array> $products + * @return array> + */ + private function attachPriceTiers(array $products): array + { + $priceTierService = new PriceTierService(); + + foreach ($products as &$product) { + $basePrice = $product['price_per_unit'] ?? $product['package_price'] ?? null; + $product['price_tiers'] = $basePrice === null + ? [] + : $priceTierService->forAmount((float) $basePrice); + } + + unset($product); + + return $products; + } +} diff --git a/app/Repositories/ProductVariantRepository.php b/app/Repositories/ProductVariantRepository.php new file mode 100644 index 0000000..b580e27 --- /dev/null +++ b/app/Repositories/ProductVariantRepository.php @@ -0,0 +1,27 @@ +> + */ + public function publishedForProduct(int $productId): array + { + $statement = $this->pdo()->prepare( + 'SELECT id, product_id, name, unit, package_quantity, step_quantity, price_per_unit, package_price, is_default + FROM product_variants + WHERE product_id = :product_id + AND is_published = 1 + AND is_available = 1 + ORDER BY is_default DESC, sort_order, id' + ); + + $statement->execute(['product_id' => $productId]); + + return $statement->fetchAll(); + } +} diff --git a/app/Repositories/PromocodeRepository.php b/app/Repositories/PromocodeRepository.php new file mode 100644 index 0000000..44add07 --- /dev/null +++ b/app/Repositories/PromocodeRepository.php @@ -0,0 +1,25 @@ +> + */ + public function priceTiers(): array + { + $statement = $this->pdo()->query( + 'SELECT code, title, discount_type, discount_value, min_order_amount, price_tier_label, payment_note + FROM promocodes + WHERE is_active = 1 + AND show_in_price_tiers = 1 + AND discount_type = "percent" + ORDER BY min_order_amount, discount_value' + ); + + return $statement->fetchAll(); + } +} diff --git a/app/Repositories/SettingRepository.php b/app/Repositories/SettingRepository.php new file mode 100644 index 0000000..a0c22a3 --- /dev/null +++ b/app/Repositories/SettingRepository.php @@ -0,0 +1,38 @@ + + */ + public function allKeyed(): array + { + $statement = $this->pdo()->query( + 'SELECT setting_key, setting_value, value_type + FROM site_settings + ORDER BY group_name, setting_key' + ); + + $settings = []; + + foreach ($statement->fetchAll() as $row) { + $settings[$row['setting_key']] = $this->castValue($row['setting_value'], $row['value_type']); + } + + return $settings; + } + + private function castValue(?string $value, string $type): mixed + { + return match ($type) { + 'number' => $value === null ? null : (float) $value, + 'bool' => $value === '1' || $value === 'true', + 'json' => $value === null ? null : json_decode($value, true), + default => $value, + }; + } +} diff --git a/app/Services/PriceTierService.php b/app/Services/PriceTierService.php new file mode 100644 index 0000000..03ce157 --- /dev/null +++ b/app/Services/PriceTierService.php @@ -0,0 +1,48 @@ +> + */ + public function forAmount(float $basePrice): array + { + $tiers = [[ + 'label' => 'Базовая цена', + 'min_order_amount' => 0.0, + 'discount_percent' => 0.0, + 'price' => round($basePrice, 2), + 'note' => 'Без промокода', + ]]; + + foreach ($this->repository()->priceTiers() as $promocode) { + $discount = (float) $promocode['discount_value']; + $tiers[] = [ + 'label' => $promocode['price_tier_label'] ?: $promocode['title'], + 'code' => $promocode['code'], + 'min_order_amount' => (float) $promocode['min_order_amount'], + 'discount_percent' => $discount, + 'price' => round($basePrice * (100 - $discount) / 100, 2), + 'note' => $promocode['payment_note'], + ]; + } + + return $tiers; + } + + private function repository(): PromocodeRepository + { + return $this->promocodes ?? new PromocodeRepository(); + } +} diff --git a/app/helpers.php b/app/helpers.php index 821459b..c2bed3e 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -26,3 +26,8 @@ function view(string $template, array $data = []): void { View::render($template, $data); } + +function e(?string $value): string +{ + return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); +} diff --git a/database/schema.sql b/database/schema.sql index baac768..b0c7894 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -22,6 +22,7 @@ DROP TABLE IF EXISTS customer_login_codes; DROP TABLE IF EXISTS customer_email_tokens; DROP TABLE IF EXISTS site_settings; DROP TABLE IF EXISTS holidays; +DROP TABLE IF EXISTS redirects; DROP TABLE IF EXISTS admins; CREATE TABLE admins ( @@ -43,9 +44,12 @@ CREATE TABLE categories ( parent_id BIGINT UNSIGNED NULL, name VARCHAR(190) NOT NULL, slug VARCHAR(220) NOT NULL, + legacy_path VARCHAR(255) NULL, description TEXT NULL, + h1 VARCHAR(255) NULL, seo_title VARCHAR(255) NULL, seo_description TEXT NULL, + seo_keywords TEXT NULL, image_path VARCHAR(255) NULL, sort_order INT NOT NULL DEFAULT 100, is_active TINYINT(1) NOT NULL DEFAULT 1, @@ -53,6 +57,7 @@ CREATE TABLE categories ( updated_at DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uq_categories_slug (slug), + UNIQUE KEY uq_categories_legacy_path (legacy_path), KEY idx_categories_parent (parent_id), KEY idx_categories_active_sort (is_active, sort_order), CONSTRAINT fk_categories_parent @@ -65,10 +70,15 @@ CREATE TABLE products ( category_id BIGINT UNSIGNED NULL, name VARCHAR(220) NOT NULL, slug VARCHAR(240) NOT NULL, + legacy_path VARCHAR(255) NULL, sku VARCHAR(100) NULL, short_description TEXT NULL, description MEDIUMTEXT NULL, product_info MEDIUMTEXT NULL, + h1 VARCHAR(255) NULL, + seo_title VARCHAR(255) NULL, + seo_description TEXT NULL, + seo_keywords TEXT NULL, main_image_path VARCHAR(255) NULL, base_unit ENUM('kg', 'g', 'l', 'ml', 'pcs', 'pack') NOT NULL DEFAULT 'kg', package_display_mode ENUM('auto', 'base_and_package', 'package_only') NOT NULL DEFAULT 'auto', @@ -86,6 +96,7 @@ CREATE TABLE products ( updated_at DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uq_products_slug (slug), + UNIQUE KEY uq_products_legacy_path (legacy_path), UNIQUE KEY uq_products_sku (sku), KEY idx_products_category (category_id), KEY idx_products_public (is_published, is_available, sort_order), @@ -165,11 +176,14 @@ CREATE TABLE promocodes ( used_count INT UNSIGNED NOT NULL DEFAULT 0, is_active TINYINT(1) NOT NULL DEFAULT 1, is_featured TINYINT(1) NOT NULL DEFAULT 0, + show_in_price_tiers TINYINT(1) NOT NULL DEFAULT 0, + price_tier_label VARCHAR(120) NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uq_promocodes_code (code), - KEY idx_promocodes_active (is_active, starts_at, ends_at) + KEY idx_promocodes_active (is_active, starts_at, ends_at), + KEY idx_promocodes_price_tiers (show_in_price_tiers, min_order_amount) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE promocode_targets ( @@ -368,4 +382,17 @@ CREATE TABLE holidays ( UNIQUE KEY uq_holidays_date (holiday_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CREATE TABLE redirects ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + old_path VARCHAR(255) NOT NULL, + new_path VARCHAR(255) NOT NULL, + http_code SMALLINT UNSIGNED NOT NULL DEFAULT 301, + is_active TINYINT(1) NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_redirects_old_path (old_path), + KEY idx_redirects_active (is_active) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + SET FOREIGN_KEY_CHECKS = 1; diff --git a/database/seed.sql b/database/seed.sql index 3c0e91e..a785c8f 100644 --- a/database/seed.sql +++ b/database/seed.sql @@ -3,21 +3,270 @@ SET NAMES utf8mb4; -INSERT INTO categories (name, slug, sort_order, is_active) VALUES -('Рыба', 'fish', 10, 1), -('Готовая продукция', 'gotovaya-produkciya', 20, 1), -('Полуфабрикаты', 'polufabrikaty', 30, 1), -('Мясо', 'myaso', 40, 1), -('Сыры', 'syry', 50, 1), -('Ягоды', 'yagody', 60, 1), -('Овощи', 'ovoschi', 70, 1); +INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES +(NULL, 'Рыба', 'ryba', 10, 1), +(NULL, 'Морепродукты', 'moreprodukty', 20, 1), +(NULL, 'Фрукты, овощи, ягоды, грибы', 'frukty-ovoschi-yagody-griby', 30, 1), +(NULL, 'Икра', 'ikra', 40, 1), +(NULL, 'Бакалея', 'bakaleya', 50, 1), +(NULL, 'Полуфабрикаты', 'polufabrikaty', 60, 1), +(NULL, 'Мясная гастрономия', 'myasnaya-gastronomiya', 70, 1), +(NULL, 'Мясо', 'myaso', 80, 1), +(NULL, 'Сыры', 'syry', 90, 1), +(NULL, 'Молочная продукция', 'molochnaya-produkciya', 100, 1); + +INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES +((SELECT id FROM categories c WHERE c.slug = 'ryba'), 'Красная рыба', 'krasnaya-ryba', 10, 1), +((SELECT id FROM categories c WHERE c.slug = 'ryba'), 'Белая рыба', 'belaya-ryba', 20, 1), +((SELECT id FROM categories c WHERE c.slug = 'ryba'), 'Охлажденная и живая рыба', 'ohlazhdennaya-i-zhivaya-ryba', 30, 1), +((SELECT id FROM categories c WHERE c.slug = 'ryba'), 'Рыбное филе', 'rybnoe-file', 40, 1), +((SELECT id FROM categories c WHERE c.slug = 'ryba'), 'Фарш рыбный', 'farsh-rybnyj', 50, 1), +((SELECT id FROM categories c WHERE c.slug = 'ryba'), 'Вяленая рыба', 'vyalenaya-ryba', 60, 1), +((SELECT id FROM categories c WHERE c.slug = 'ryba'), 'Малосольная рыба', 'malosolnaya-ryba', 70, 1), +((SELECT id FROM categories c WHERE c.slug = 'ryba'), 'Жареная рыба', 'zharenaya-ryba', 80, 1), +((SELECT id FROM categories c WHERE c.slug = 'ryba'), 'Копченая рыба', 'kopchenaya-ryba', 90, 1), +((SELECT id FROM categories c WHERE c.slug = 'ryba'), 'Пресервы и консервы', 'preservy-i-konservy', 100, 1), +((SELECT id FROM categories c WHERE c.slug = 'ryba'), 'Рыбные стейки', 'rybnye-stejki', 110, 1); + +INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES +((SELECT id FROM categories c WHERE c.slug = 'krasnaya-ryba'), 'Брюшки лосося', 'bryushki-lososya', 10, 1), +((SELECT id FROM categories c WHERE c.slug = 'krasnaya-ryba'), 'Горбуша', 'gorbushka', 20, 1), +((SELECT id FROM categories c WHERE c.slug = 'krasnaya-ryba'), 'Голец', 'golec', 30, 1), +((SELECT id FROM categories c WHERE c.slug = 'krasnaya-ryba'), 'Кета', 'keta', 40, 1), +((SELECT id FROM categories c WHERE c.slug = 'krasnaya-ryba'), 'Кижуч', 'kizhuch', 50, 1), +((SELECT id FROM categories c WHERE c.slug = 'krasnaya-ryba'), 'Лосось', 'losos', 60, 1), +((SELECT id FROM categories c WHERE c.slug = 'krasnaya-ryba'), 'Молоки лососевые', 'moloki-lososevye', 70, 1), +((SELECT id FROM categories c WHERE c.slug = 'krasnaya-ryba'), 'Нерка', 'nerka', 80, 1), +((SELECT id FROM categories c WHERE c.slug = 'krasnaya-ryba'), 'Семга', 'semga', 90, 1), +((SELECT id FROM categories c WHERE c.slug = 'krasnaya-ryba'), 'Форель', 'forel', 100, 1), +((SELECT id FROM categories c WHERE c.slug = 'krasnaya-ryba'), 'Чавыча', 'chavycha', 110, 1); + +INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Барабулька', 'barabulka', 10, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Белый Амур', 'belyj-amur', 20, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Вобла', 'vobla', 30, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Вомер', 'vomer', 40, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Густера', 'gustera', 50, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Дори', 'dori', 60, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Дорадо', 'dorado', 70, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Жерех', 'zhereh', 80, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Зубатка', 'zubatka', 90, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Игла рыба', 'igla-ryba', 100, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Камбала', 'kambala', 110, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Карп', 'karp', 120, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Карась', 'karas', 130, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Кефаль', 'kefal', 140, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Килька', 'kilka', 150, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Конгрио', 'kongrio', 160, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Корюшка', 'koryushka', 170, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Лакедра', 'lakedra', 180, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Лещ', 'lesch', 190, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Ледяная', 'ledyanaya', 200, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Лемонема', 'lemonema', 210, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Марлин', 'marlin', 220, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Макрурус', 'makrurus', 230, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Масляная', 'maslyanaya', 240, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Минтай', 'mintaj', 250, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Мойва', 'mojva', 260, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Молочная рыба Ханос', 'molochnaya-ryba-hanos', 270, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Навага', 'navaga', 280, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Омуль', 'omul', 290, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Осетр', 'osetr', 300, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Окунь', 'okun', 310, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Палтус', 'paltus', 320, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Пангасиус', 'pangasius', 330, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Пелядь', 'pelyad', 340, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Плотва', 'plotva', 350, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Пикша', 'piksha', 360, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Путассу', 'putassu', 370, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Рыбец', 'rybec', 380, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Сайда', 'sajda', 390, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Сазан', 'sazan', 400, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Салака', 'salaka', 410, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Сардина Иваси', 'sardina-ivasi', 420, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Сельдь', 'seld', 430, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Сибас', 'sibas', 440, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Сиг', 'sig', 450, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Синец', 'sinec', 460, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Скумбрия', 'skumbriya', 470, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Ставрида', 'stavrida', 480, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Сом', 'som', 490, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Судак', 'sudak', 500, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Тарань', 'taran', 510, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Терпуг', 'terpug', 520, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Тилапия', 'tilapiya', 530, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Толстолобик', 'tolstolobik', 540, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Треска', 'treska', 550, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Тунец', 'tunec', 560, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Тюлька', 'tyulka', 570, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Угольная треска', 'ugolnaya-treska', 580, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Хамса', 'hamsa', 590, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Хоки', 'hoki', 600, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Хек', 'hek', 610, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Чехонь', 'chehon', 620, 1), +((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Щука', 'schuka', 630, 1); + +INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES +((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе горбуши', 'file-gorbushi', 10, 1), +((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе кефали', 'file-kefali', 20, 1), +((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе кижуча', 'file-kizhucha', 30, 1), +((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе кеты', 'file-kety', 40, 1), +((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе масляной', 'file-maslyanoj', 50, 1), +((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе минтая', 'file-mintaya', 60, 1), +((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе окуня', 'file-okunya', 70, 1), +((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе палтуса', 'file-paltusa', 80, 1), +((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе пангасиуса', 'file-pangasiusa', 90, 1), +((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе пикши', 'file-pikshi', 100, 1), +((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе сазана', 'file-sazana', 110, 1), +((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе сельди', 'file-seldi', 120, 1), +((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе сайды', 'file-sajdy', 130, 1), +((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе семги', 'file-semgi', 140, 1), +((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе судака', 'file-sudaka', 150, 1), +((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе толстолобика', 'file-tolstolobika', 160, 1), +((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе тилапии', 'file-tilapii', 170, 1), +((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе трески', 'file-treski', 180, 1), +((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе форели', 'file-foreli', 190, 1), +((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе хека', 'file-heka', 200, 1), +((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе щуки', 'file-schuki', 210, 1); + +INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES +((SELECT id FROM categories c WHERE c.slug = 'kopchenaya-ryba'), 'Горячее копчение', 'goryachee-kopchenie', 10, 1), +((SELECT id FROM categories c WHERE c.slug = 'kopchenaya-ryba'), 'Холодное копчение', 'holodnoe-kopchenie', 20, 1), +((SELECT id FROM categories c WHERE c.slug = 'kopchenaya-ryba'), 'Копченые морепродукты', 'kopchenye-moreprodukty', 30, 1), +((SELECT id FROM categories c WHERE c.slug = 'kopchenaya-ryba'), 'Балыки', 'balyki', 40, 1), +((SELECT id FROM categories c WHERE c.slug = 'kopchenaya-ryba'), 'Брюшки и теша', 'bryushki-i-tesha', 50, 1), +((SELECT id FROM categories c WHERE c.slug = 'kopchenaya-ryba'), 'Копченое филе', 'kopchenoe-file', 60, 1), +((SELECT id FROM categories c WHERE c.slug = 'kopchenaya-ryba'), 'Рулеты', 'rulety', 70, 1), +((SELECT id FROM categories c WHERE c.slug = 'kopchenaya-ryba'), 'Шашлычки', 'shashlychki', 80, 1), +((SELECT id FROM categories c WHERE c.slug = 'kopchenaya-ryba'), 'Хребты', 'hrebty', 90, 1), +((SELECT id FROM categories c WHERE c.slug = 'kopchenaya-ryba'), 'Юкола', 'yukola', 100, 1); + +INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES +((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Краб', 'krab', 10, 1), +((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Кальмар', 'kalmar', 20, 1), +((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Вонголе', 'vongole', 30, 1), +((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Креветки', 'krevetki', 40, 1), +((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Мидии', 'midii', 50, 1), +((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Морской гребешок', 'morskoj-grebeshok', 60, 1), +((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Осьминог', 'osminog', 70, 1), +((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Морской коктейль', 'morskoj-koktejl', 80, 1), +((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Чука', 'chuka', 90, 1), +((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Угорь', 'ugor', 100, 1), +((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Для суши', 'dlya-sushi', 110, 1), +((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Раки', 'raki', 120, 1), +((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Другое', 'drugoe-moreprodukty', 130, 1); + +INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES +((SELECT id FROM categories c WHERE c.slug = 'krevetki'), 'Королевские и ванамей', 'korolevskie-i-vanamej', 10, 1), +((SELECT id FROM categories c WHERE c.slug = 'krevetki'), 'Северные', 'severnye-krevetki', 20, 1), +((SELECT id FROM categories c WHERE c.slug = 'krevetki'), 'Тигровые', 'tigrovye-krevetki', 30, 1), +((SELECT id FROM categories c WHERE c.slug = 'krevetki'), 'Очищенные', 'ochischennye-krevetki', 40, 1), +((SELECT id FROM categories c WHERE c.slug = 'krevetki'), 'В панировке', 'krevetki-v-panirovke', 50, 1), +((SELECT id FROM categories c WHERE c.slug = 'krevetki'), 'Лангустины', 'langustiny', 60, 1); + +INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES +((SELECT id FROM categories c WHERE c.slug = 'frukty-ovoschi-yagody-griby'), 'Фрукты', 'frukty', 10, 1), +((SELECT id FROM categories c WHERE c.slug = 'frukty-ovoschi-yagody-griby'), 'Овощи', 'ovoschi', 20, 1), +((SELECT id FROM categories c WHERE c.slug = 'frukty-ovoschi-yagody-griby'), 'Ягоды', 'yagody', 30, 1), +((SELECT id FROM categories c WHERE c.slug = 'frukty-ovoschi-yagody-griby'), 'Грибы', 'griby', 40, 1), +((SELECT id FROM categories c WHERE c.slug = 'frukty-ovoschi-yagody-griby'), 'Овощные и ягодные смеси', 'ovoschnye-i-yagodnye-smesi', 50, 1); + +INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES +((SELECT id FROM categories c WHERE c.slug = 'ikra'), 'Красная икра', 'krasnaya-ikra', 10, 1), +((SELECT id FROM categories c WHERE c.slug = 'ikra'), 'Икра сельди', 'ikra-seldi', 20, 1), +((SELECT id FROM categories c WHERE c.slug = 'ikra'), 'Икра минтая', 'ikra-mintaya', 30, 1), +((SELECT id FROM categories c WHERE c.slug = 'ikra'), 'Икра палтуса', 'ikra-paltusa', 40, 1), +((SELECT id FROM categories c WHERE c.slug = 'ikra'), 'Икра трески', 'ikra-treski', 50, 1), +((SELECT id FROM categories c WHERE c.slug = 'ikra'), 'Икра щуки', 'ikra-schuki', 60, 1), +((SELECT id FROM categories c WHERE c.slug = 'ikra'), 'Икра Масаго и Тобико', 'ikra-masago-i-tobiko', 70, 1), +((SELECT id FROM categories c WHERE c.slug = 'ikra'), 'Икра для самостоятельной засолки', 'ikra-dlya-samostoyatelnoj-zasolki', 80, 1), +((SELECT id FROM categories c WHERE c.slug = 'ikra'), 'Вяленая икра', 'vyalenaya-ikra', 90, 1); + +INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES +((SELECT id FROM categories c WHERE c.slug = 'bakaleya'), 'Снеки и орехи', 'sneki-i-orehi', 10, 1), +((SELECT id FROM categories c WHERE c.slug = 'bakaleya'), 'Овощная консервация', 'ovoschnaya-konservaciya', 20, 1), +((SELECT id FROM categories c WHERE c.slug = 'bakaleya'), 'Оливковое масло', 'olivkovoe-maslo', 30, 1), +((SELECT id FROM categories c WHERE c.slug = 'bakaleya'), 'Соусы и уксусы', 'sousy-i-uksusy', 40, 1), +((SELECT id FROM categories c WHERE c.slug = 'bakaleya'), 'Макаронные изделия', 'makaronnye-izdeliya', 50, 1), +((SELECT id FROM categories c WHERE c.slug = 'bakaleya'), 'Приправы и Специи', 'pripravy-i-specii', 60, 1), +((SELECT id FROM categories c WHERE c.slug = 'bakaleya'), 'Крупы и мука', 'krupy-i-muka', 70, 1), +((SELECT id FROM categories c WHERE c.slug = 'bakaleya'), 'Соль и сахар', 'sol-i-sahar', 80, 1), +((SELECT id FROM categories c WHERE c.slug = 'bakaleya'), 'Масло растительное', 'maslo-rastitelnoe', 90, 1), +((SELECT id FROM categories c WHERE c.slug = 'bakaleya'), 'Кофе', 'kofe', 100, 1); + +INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES +((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Рыбные котлеты', 'rybnye-kotlety', 10, 1), +((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Крабовые палочки', 'krabovye-palochki', 20, 1), +((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Кальмары и креветки в панировке', 'kalmary-i-krevetki-v-panirovke', 30, 1), +((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Наггетсы и стрипсы', 'naggetsy-i-stripsy', 40, 1), +((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Сырные и рыбные палочки', 'syrnye-i-rybnye-palochki', 50, 1), +((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Чебуреки', 'chebureki', 60, 1), +((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Мясные котлеты', 'myasnye-kotlety', 70, 1), +((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Супы', 'supy', 80, 1), +((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Фаршированная продукция', 'farshirovannaya-produkciya', 90, 1), +((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Полуфабрикаты из мяса и птицы', 'polufabrikaty-iz-myasa-i-pticy', 100, 1), +((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Блинчики и оладьи', 'blinchiki-i-oladi', 110, 1), +((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Овощные полуфабрикаты', 'ovoschnye-polufabrikaty', 120, 1), +((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Сырники', 'syrniki', 130, 1), +((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Пельмени и вареники', 'pelmeni-i-vareniki', 140, 1), +((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Другие полуфабрикаты', 'drugie-polufabrikaty', 150, 1), +((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Жареная рыба и котлеты', 'zharenaya-ryba-i-kotlety', 160, 1), +((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Колбаски и купаты', 'kolbaski-i-kupaty', 170, 1); + +INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES +((SELECT id FROM categories c WHERE c.slug = 'myasnaya-gastronomiya'), 'Из мяса птицы', 'iz-myasa-pticy', 10, 1), +((SELECT id FROM categories c WHERE c.slug = 'myasnaya-gastronomiya'), 'Из мяса свинины', 'iz-myasa-svininy', 20, 1), +((SELECT id FROM categories c WHERE c.slug = 'myasnaya-gastronomiya'), 'Из мяса говядины', 'iz-myasa-govyadiny', 30, 1); + +INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Говядина', 'govyadina', 10, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Мраморная говядина', 'mramornaya-govyadina', 20, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Свинина', 'svinina', 30, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Баранина', 'baranina', 40, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Буйвол', 'bujvol', 50, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Курица', 'kurica', 60, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Кролик', 'krolik', 70, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Индейка', 'indejka', 80, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Телятина', 'telyatina', 90, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Утка', 'utka', 100, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Гусь', 'gus', 110, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Цыплята Корнишоны', 'cyplyata-kornishony', 120, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Перепела', 'perepela', 130, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Субпродукты', 'subprodukty', 140, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Мясные полуфабрикаты', 'myasnye-polufabrikaty', 150, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Туши и полутуши', 'tushi-i-polutushi', 160, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Котлетное мясо', 'kotletnoe-myaso', 170, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Суповые наборы', 'supovye-nabory', 180, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Мясо для стейков', 'myaso-dlya-stejkov', 190, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Филе птицы', 'file-pticy', 200, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Мякоть говядины', 'myakot-govyadiny', 210, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Толстый и тонкий край', 'tolstyj-i-tonkij-kraj', 220, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Мясо на кости', 'myaso-na-kosti', 230, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Вырезка', 'vyrezka', 240, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Языки', 'yazyki', 250, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Фарш', 'farsh', 260, 1), +((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Охлажденное мясо', 'ohlazhdennoe-myaso', 270, 1); + +INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES +((SELECT id FROM categories c WHERE c.slug = 'syry'), 'Твердые', 'tverdye-syry', 10, 1), +((SELECT id FROM categories c WHERE c.slug = 'syry'), 'Полутвердые', 'polutverdye-syry', 20, 1), +((SELECT id FROM categories c WHERE c.slug = 'syry'), 'Рассольные сыры', 'rassolnye-syry', 30, 1), +((SELECT id FROM categories c WHERE c.slug = 'syry'), 'С плесенью', 'syry-s-plesenyu', 40, 1), +((SELECT id FROM categories c WHERE c.slug = 'syry'), 'Со вкусами', 'syry-so-vkusami', 50, 1), +((SELECT id FROM categories c WHERE c.slug = 'syry'), 'Элитные сыры', 'elitnye-syry', 60, 1), +((SELECT id FROM categories c WHERE c.slug = 'syry'), 'Мягкие сыры', 'myagkie-syry', 70, 1); + +INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES +((SELECT id FROM categories c WHERE c.slug = 'molochnaya-produkciya'), 'Молоко', 'moloko', 10, 1), +((SELECT id FROM categories c WHERE c.slug = 'molochnaya-produkciya'), 'Сливки', 'slivki', 20, 1), +((SELECT id FROM categories c WHERE c.slug = 'molochnaya-produkciya'), 'Сливочное масло', 'slivochnoe-maslo', 30, 1); INSERT INTO promocodes - (code, title, discount_type, discount_value, min_order_amount, applies_to, is_active, is_featured) + (code, title, discount_type, discount_value, min_order_amount, applies_to, is_active, is_featured, show_in_price_tiers, price_tier_label) VALUES - ('Рыбсток7000', '300 руб. при заказе от 7000 руб.', 'fixed', 300.00, 7000.00, 'order', 1, 1), - ('Рыбсток20000', '5% при заказе от 20000 руб.', 'percent', 5.00, 20000.00, 'order', 1, 1), - ('Рыбсток50000', '7% при заказе от 50000 руб.', 'percent', 7.00, 50000.00, 'order', 1, 1); + ('Рыбсток7000', '300 руб. при заказе от 7000 руб.', 'fixed', 300.00, 7000.00, 'order', 1, 1, 0, NULL), + ('Рыбсток20000', '5% при заказе от 20000 руб.', 'percent', 5.00, 20000.00, 'order', 1, 1, 1, 'Цена от 20 000 руб. наличными'), + ('Рыбсток50000', '7% при заказе от 50000 руб.', 'percent', 7.00, 50000.00, 'order', 1, 1, 1, 'Цена от 50 000 руб. наличными'); INSERT INTO site_settings (setting_key, setting_value, value_type, group_name) VALUES ('site_phone', '+7-977-736-33-63', 'string', 'contacts'), diff --git a/docs/ПЛАН_ПЕРЕНОСА_ТОВАРОВ.md b/docs/ПЛАН_ПЕРЕНОСА_ТОВАРОВ.md new file mode 100644 index 0000000..fd8a157 --- /dev/null +++ b/docs/ПЛАН_ПЕРЕНОСА_ТОВАРОВ.md @@ -0,0 +1,98 @@ +# План переноса товаров со старого сайта + +## Цель + +Перенести товары со старого сайта в новую базу без ручного копирования и без потери структуры, SEO и индексации. + +## Что нужно перенести + +Для каждого товара: + +- название; +- старая ссылка; +- новый slug; +- категория и подкатегория; +- описание; +- краткое описание; +- информация о товаре; +- `title`; +- `description`; +- `keywords`; +- H1, если он отличается от названия; +- фото; +- фасовки; +- цены по фасовкам; +- единицы измерения; +- доступность/публикация. + +Для каждой категории: + +- название; +- старая ссылка; +- новый slug; +- H1; +- `title`; +- `description`; +- `keywords`; +- текст категории; +- родительская категория; +- порядок в меню. + +## Возможные способы переноса + +### 1. Выгрузка CSV/Excel + +Лучший вариант, если старый сайт умеет выгружать товары. + +Нужны отдельные колонки для категорий, фасовок, цен, SEO и старых ссылок. + +### 2. Доступ к базе старого сайта + +Самый точный вариант, если можно получить дамп базы или доступ к phpMyAdmin. + +Можно написать миграционный PHP-скрипт, который перенесет категории, товары, фото, SEO и ссылки. + +### 3. Парсинг старого сайта + +Рабочий вариант, если нет выгрузки и доступа к базе. + +Скрипт обходит старые URL, собирает HTML, title, description, keywords, хлебные крошки, описание, фото и цены. + +Минус: зависит от верстки старого сайта, поэтому требует отдельного тестирования. + +### 4. Ручной Excel-шаблон + +Запасной вариант, если автоматический перенос невозможен. + +Мы готовим шаблон, заполняем его товарами, затем импортируем в новую базу. + +## Что уже заложено в новой базе + +- `categories.legacy_path` - старый URL категории; +- `products.legacy_path` - старый URL товара; +- `categories.seo_title`; +- `categories.seo_description`; +- `categories.seo_keywords`; +- `categories.h1`; +- `products.seo_title`; +- `products.seo_description`; +- `products.seo_keywords`; +- `products.h1`; +- таблица `redirects` для 301-редиректов; +- таблица `product_images`; +- таблица `product_variants`. + +## Отдельный этап работ + +Перенос товаров выделяется в отдельную задачу после базовой структуры каталога и страниц. + +Порядок: + +1. Получить источник данных старого сайта: выгрузку, базу или список URL. +2. Сопоставить старые категории с новой структурой. +3. Подготовить импортёр. +4. Сделать тестовый импорт 10-20 товаров. +5. Проверить категории, фото, цены, фасовки, SEO и старые ссылки. +6. Запустить полный импорт. +7. Сформировать 301-редиректы. +8. Проверить выборочно страницы товаров и категорий. diff --git a/docs/ШАГ_2_БАЗА_ДАННЫХ.md b/docs/ШАГ_2_БАЗА_ДАННЫХ.md index cc7959f..f0de7e0 100644 --- a/docs/ШАГ_2_БАЗА_ДАННЫХ.md +++ b/docs/ШАГ_2_БАЗА_ДАННЫХ.md @@ -31,6 +31,7 @@ - популярные товары; - настройки сайта; - праздничные/нерабочие дни. +- старые URL и 301-редиректы для сохранения индексации. ## Главная логика связей @@ -45,6 +46,10 @@ - меню каталога; - SEO-текст для страниц категорий. +Для переноса со старого сайта у категорий есть поле `legacy_path`. Если старый URL отличается от нового slug, старый путь нужно сохранить и добавить 301-редирект в таблицу `redirects`. + +Для SEO у категорий есть поля `h1`, `seo_title`, `seo_description`, `seo_keywords`. + ### Товары Таблица `products` хранит основную карточку товара: @@ -60,6 +65,10 @@ - доступность; - признаки "новинка", "хит", "акция". +Для товаров также есть поле `legacy_path`, чтобы переносить старые ссылки без потери индексации. + +Для SEO у товаров есть поля `h1`, `seo_title`, `seo_description`, `seo_keywords`. + ### Фасовки Таблица `product_variants` хранит отдельные фасовки товара. @@ -106,6 +115,13 @@ Если промокод действует на весь заказ, targets не нужны. +Для отображения выгодных цен в каталоге у промокодов есть поля: + +- `show_in_price_tiers` - показывать как градацию цены; +- `price_tier_label` - подпись для карточки товара. + +Так можно показывать покупателю базовую цену, цену от 20 000 руб. наличными и цену от 50 000 руб. наличными. + ### Доставка Таблица `delivery_calculations` хранит результат расчета: diff --git a/docs/ШАГ_3_PHP_КЛАССЫ.md b/docs/ШАГ_3_PHP_КЛАССЫ.md new file mode 100644 index 0000000..10ed4ce --- /dev/null +++ b/docs/ШАГ_3_PHP_КЛАССЫ.md @@ -0,0 +1,107 @@ +# Шаг 3. Первые PHP-классы и вывод данных + +## Что сделано + +Созданы первые классы для работы сайта на чистом PHP: + +- `app/Controllers/HomeController.php` +- `app/Repositories/BaseRepository.php` +- `app/Repositories/CategoryRepository.php` +- `app/Repositories/ProductRepository.php` +- `app/Repositories/ProductVariantRepository.php` +- `app/Repositories/SettingRepository.php` + +Главная страница больше не содержит бизнес-логику напрямую. Теперь `public/index.php` вызывает `HomeController`, а контроллер получает данные через репозитории. + +Также в базовый layout добавлены: + +- meta title; +- meta description; +- meta keywords; +- хлебные крошки; +- кнопка "Вернуться назад". + +## Что выводится на главной + +Главная страница теперь показывает: + +- статус подключения к MariaDB; +- категории из таблицы `categories`, строго по дереву старого сайта; +- вложенность категорий до третьего уровня; +- каталог товаров прямо на главной странице; +- настройки доставки из таблицы `site_settings`; +- блок популярных товаров из опубликованных товаров и фасовок. +- градации выгодной цены: базовая цена, цена от 20 000 руб. наличными, цена от 50 000 руб. наличными. + +Если таблицы еще не установлены, сайт не падает, а показывает сообщение с подсказкой. + +## Что добавлено в админку + +`admin/index.php` теперь показывает стартовую сетку разделов будущей панели: + +- Товары; +- Категории; +- Фасовки; +- Заказы; +- Промокоды; +- Акции; +- Популярные товары; +- Отзывы; +- Настройки; +- Прайс-лист. + +Это пока не полноценная админка, а основа навигации для следующих шагов. + +## Что добавлено для цен + +В `promocodes` добавлены поля: + +- `show_in_price_tiers`; +- `price_tier_label`. + +Создан `app/Services/PriceTierService.php`, который считает отображаемые цены для карточки товара: + +- базовая цена; +- минус 5% при заказе от 20 000 руб. за наличный расчет; +- минус 7% при заказе от 50 000 руб. за наличный расчет. + +Это не отменяет ввод промокода в корзине. Это только понятная подсказка в каталоге, чтобы человек заранее видел выгоду крупного заказа. + +## Что нужно проверить + +1. Если база еще не установлена, открыть `/install.php?token=rybstock-local-install`. +2. После установки открыть главную страницу. +3. На главной должны появиться стартовые категории из `database/seed.sql`. +4. Открыть `/admin/` и проверить стартовые разделы админки. + +Стартовые категории: + +- Рыба; +- Рыбное филе; +- Рыбные стейки; +- Охлажденная рыба; +- Малосольная рыба; +- Копченая рыба; +- Вяленая рыба; +- Морепродукты; +- Фрукты, овощи, ягоды, грибы; +- Икра; +- Бакалея; +- Полуфабрикаты; +- Мясная гастрономия; +- Мясо; +- Сыры; +- Молочная продукция. + +Важно: названия категорий уже приведены к старому дереву. Для полного сохранения индексации еще нужны точные старые URL категорий и товаров. В схеме для этого предусмотрены `legacy_path` и таблица `redirects`. + +## Следующий шаг + +Шаг 4: + +1. Сделать базовый роутинг для страниц сайта. +2. Создать страницу полного каталога. +3. Вывести категории и товары из базы. +4. Подготовить первые формы админки для категорий и товаров. + +Перенос товаров вынесен в отдельный документ: `docs/ПЛАН_ПЕРЕНОСА_ТОВАРОВ.md`. diff --git a/public/admin/index.php b/public/admin/index.php new file mode 100644 index 0000000..5e6862e --- /dev/null +++ b/public/admin/index.php @@ -0,0 +1,5 @@ +query('SHOW TABLES')->fetchAll(PDO::FETCH_COLUMN); + echo "tables_count: " . count($tables) . "\n"; + + if (in_array('categories', $tables, true)) { + echo "categories_count: " . $pdo->query('SELECT COUNT(*) FROM categories')->fetchColumn() . "\n"; + echo "root_categories:\n"; + + $statement = $pdo->query('SELECT id, name, slug FROM categories WHERE parent_id IS NULL ORDER BY sort_order LIMIT 30'); + foreach ($statement->fetchAll() as $category) { + echo "- {$category['name']} ({$category['slug']})\n"; + } + } else { + echo "categories_table: missing\n"; + } +} catch (Throwable $exception) { + echo "db_status: error\n"; + echo "error: " . $exception->getMessage() . "\n"; +} diff --git a/public/index.php b/public/index.php index 409fff0..5b3d783 100644 --- a/public/index.php +++ b/public/index.php @@ -2,22 +2,8 @@ declare(strict_types=1); -use App\Core\Database; +use App\Controllers\HomeController; require_once dirname(__DIR__) . '/app/bootstrap.php'; -$dbStatus = 'not_checked'; - -try { - Database::pdo()->query('SELECT 1'); - $dbStatus = 'connected'; -} catch (Throwable $exception) { - $dbStatus = app_env('APP_DEBUG', 'false') === 'true' - ? 'error: ' . $exception->getMessage() - : 'error'; -} - -view('pages/home', [ - 'title' => 'Рыбсток', - 'dbStatus' => $dbStatus, -]); +(new HomeController())->index(); diff --git a/public/install.php b/public/install.php index 7fee3e0..644b985 100644 --- a/public/install.php +++ b/public/install.php @@ -12,6 +12,7 @@ $actualToken = $_GET['token'] ?? ''; $canRun = $isLocal && $expectedToken !== '' && hash_equals($expectedToken, (string) $actualToken); $result = null; $error = null; +$stats = install_stats(); if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$canRun) { @@ -22,6 +23,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $pdo = Database::pdo(); run_sql_file($pdo, base_path('database/schema.sql')); run_sql_file($pdo, base_path('database/seed.sql')); + $stats = install_stats(); $result = 'База данных установлена. Таблицы и стартовые данные созданы.'; } catch (Throwable $exception) { http_response_code(500); @@ -32,6 +34,33 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } } +function install_stats(): array +{ + try { + $pdo = Database::pdo(); + $tables = $pdo->query('SHOW TABLES')->fetchAll(PDO::FETCH_COLUMN); + $categoryCount = in_array('categories', $tables, true) + ? (int) $pdo->query('SELECT COUNT(*) FROM categories')->fetchColumn() + : 0; + $firstCategories = in_array('categories', $tables, true) + ? $pdo->query('SELECT name FROM categories WHERE parent_id IS NULL ORDER BY sort_order LIMIT 12')->fetchAll(PDO::FETCH_COLUMN) + : []; + + return [ + 'ok' => true, + 'database' => app_env('DB_DATABASE', ''), + 'tables_count' => count($tables), + 'category_count' => $categoryCount, + 'first_categories' => $firstCategories, + ]; + } catch (Throwable $exception) { + return [ + 'ok' => false, + 'error' => $exception->getMessage(), + ]; + } +} + function run_sql_file(PDO $pdo, string $path): void { if (!is_file($path)) { @@ -62,6 +91,39 @@ function run_sql_file(PDO $pdo, string $path): void

Установка базы

Эта страница применяет `database/schema.sql` и `database/seed.sql`. Таблицы будут пересозданы.

+
+ Версия install.php: + 2026-06-04-02 +
+ +
+ Файл: + +
+ + +
+ База: + +
+
+ Таблиц: + +
+
+ Категорий: + +
+ +
+ Верхние категории: + +
+ + +
+ +
Установка доступна только при APP_ENV=local.
diff --git a/views/layouts/main.php b/views/layouts/main.php index 857eba5..8f85cac 100644 --- a/views/layouts/main.php +++ b/views/layouts/main.php @@ -4,16 +4,34 @@ declare(strict_types=1); /** @var string $templatePath */ /** @var string $title */ +/** @var string $metaDescription */ +/** @var string $metaKeywords */ +/** @var array $breadcrumbs */ +/** @var bool $showBackButton */ ?> - <?= htmlspecialchars($title ?? 'Рыбсток', ENT_QUOTES, 'UTF-8') ?> + <?= e($title) ?> + + + + + + +
+ + + + + +
+
diff --git a/views/pages/home.php b/views/pages/home.php index dd51198..566dddb 100644 --- a/views/pages/home.php +++ b/views/pages/home.php @@ -3,14 +3,131 @@ declare(strict_types=1); /** @var string $dbStatus */ +/** @var array> $categories */ +/** @var array> $catalogProducts */ +/** @var array> $popularProducts */ +/** @var array $settings */ +/** @var string|null $notice */ +/** @var string $buildVersion */ ?> -
-

Старт проекта

+
+

Шаг 3

Рыбсток

-

Чистый PHP-проект подготовлен. Следующий шаг - схема базы данных и таблицы для каталога, фасовок, заказов и админки.

+

Каталог открывается уже на главной странице, чтобы покупатель сразу видел ассортимент и не делал лишних переходов.

+

Версия сборки:

MariaDB: - +
+ + +
+ + +
+
+

Категории

+ +

Категории пока не найдены. Если схема уже применена, проверьте `database/seed.sql`.

+ + + +
+ +
+

Настройки доставки

+
+
+
Внутри МКАД бесплатно от
+
руб.
+
+
+
Внутри МКАД платно
+
руб.
+
+
+
За МКАД расчет
+
руб. + руб./км
+
+
+
Подъем
+
руб.
+
+
+
+
+ +
+
+
+

Каталог продукции

+

Товары на главной

+
+ Весь каталог +
+ + +

После наполнения базы товары будут отображаться здесь сразу на главной странице.

+ +
+ +
+

+

+

+ + за : руб. + + + руб. за фасовку + + +
    + +
  • + + руб. +
  • + +
+ +
+ +
+ +
+ +
+

Популярные товары

+ +

Товары появятся здесь после наполнения каталога и публикации фасовок.

+ +
+ +
+

+

+ + руб. + + +
    + +
  • + + руб. +
  • + +
+ +
+ +
+ +
diff --git a/views/partials/breadcrumbs.php b/views/partials/breadcrumbs.php new file mode 100644 index 0000000..ccbb878 --- /dev/null +++ b/views/partials/breadcrumbs.php @@ -0,0 +1,21 @@ + $breadcrumbs */ +?> + + + diff --git a/views/partials/category-tree.php b/views/partials/category-tree.php new file mode 100644 index 0000000..6a6d1e4 --- /dev/null +++ b/views/partials/category-tree.php @@ -0,0 +1,24 @@ +> $items */ +/** @var int $depth */ +?> +
    + +
  • + + + + +
  • + +
diff --git a/ПЛАН_ДЕЙСТВИЙ_РЫБСТОК.md b/ПЛАН_ДЕЙСТВИЙ_РЫБСТОК.md index 0036b68..58bff84 100644 --- a/ПЛАН_ДЕЙСТВИЙ_РЫБСТОК.md +++ b/ПЛАН_ДЕЙСТВИЙ_РЫБСТОК.md @@ -41,17 +41,26 @@ ### 2.3. Категории -Основные категории для главного заголовка и меню: +Основные категории должны строго соответствовать старому сайту, чтобы сохранить дерево, ссылки, индексацию и целостность переноса: - Рыба; -- Готовая продукция; +- Рыбное филе; +- Рыбные стейки; +- Охлажденная рыба; +- Малосольная рыба; +- Копченая рыба; +- Вяленая рыба; +- Морепродукты; +- Фрукты, овощи, ягоды, грибы; +- Икра; +- Бакалея; - Полуфабрикаты; +- Мясная гастрономия; - Мясо; - Сыры; -- Ягоды; -- Овощи. +- Молочная продукция. -Нужны категории и подкатегории. Подкатегории визуально отделить от категорий, например более мягким цветом, меньшим шрифтом или отступом. +Нужны категории и подкатегории. Подкатегории визуально отделить от категорий, например более мягким цветом, меньшим шрифтом или отступом. Для сохранения индексации нужны точные старые URL/slug каждой категории и товара; если они отличаются от текущих slug в seed-файле, их нужно заменить перед переносом. ## 3. Структура сайта @@ -64,6 +73,7 @@ - Заметное сообщение: "Работаем без предоплаты. Оплата только при получении товара." - Поиск по сайту. - Быстрый переход в полный каталог. +- Сам каталог должен быть уже открыт на главной странице, чтобы покупатель сразу видел товары и не делал лишних переходов. - Понятная кнопка "Доставка и оплата". - Верхнее меню с категориями и подкатегориями. @@ -629,4 +639,6 @@ - Шаг 1 выполнен: выбрана техническая связка на чистом PHP, создан стартовый каркас проекта, подключение к MariaDB через `.env`, базовые папки и файл `ШАГ_1_ТЕХНИЧЕСКОЕ_РЕШЕНИЕ.md`. - Шаг 2 выполнен: создана SQL-схема базы `database/schema.sql`, стартовые данные `database/seed.sql`, страница установки `public/install.php` и документ `docs/ШАГ_2_БАЗА_ДАННЫХ.md`. -- Следующий шаг: применить схему через `/install.php?token=rybstock-local-install`, затем сделать первые PHP-классы для чтения категорий, товаров, фасовок и настроек. +- Шаг 3 выполнен: созданы первые PHP-классы, репозитории, контроллер главной страницы, вывод категорий/настроек/популярных товаров, каталог на главной, категории старого сайта в `database/seed.sql`, SEO-поля, хлебные крошки, кнопка назад, градации выгодных цен по промокодам и документ `docs/ШАГ_3_PHP_КЛАССЫ.md`. +- Перенос товаров выделен в отдельную задачу: `docs/ПЛАН_ПЕРЕНОСА_ТОВАРОВ.md`. +- Следующий шаг: сделать базовый роутинг, страницу полного каталога и первые формы админки для категорий/товаров.