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 = [
+ 'Товары',
+ 'Категории',
+ 'Фасовки',
+ 'Заказы',
+ 'Промокоды',
+ 'Акции',
+ 'Популярные товары',
+ 'Отзывы',
+ 'Настройки',
+ 'Прайс-лист',
+];
+?>
+
+
+
+
+
+ Админка Рыбсток
+
+
+
+
+
+
Админка
+
Рыбсток
+
Подготовлены основные разделы будущей панели управления.
+
+
+
+
= e($section) ?>
+
+
+
+
+
+
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`. Таблицы будут пересозданы.
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`.
+- Следующий шаг: сделать базовый роутинг, страницу полного каталога и первые формы админки для категорий/товаров.