шаг 3 завершен

This commit is contained in:
Alisa
2026-06-04 00:27:17 +03:00
parent 420e67c9bd
commit 62ce8c4d01
27 changed files with 1565 additions and 42 deletions
+10 -1
View File
@@ -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/`, нужно настроить виртуальный хост, чтобы публичной была именно эта папка.
+37 -1
View File
@@ -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 = [
'Товары',
'Категории',
'Фасовки',
'Заказы',
'Промокоды',
'Акции',
'Популярные товары',
'Отзывы',
'Настройки',
'Прайс-лист',
];
?>
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Админка Рыбсток</title>
<link rel="stylesheet" href="/assets/css/app.css">
</head>
<body>
<main class="page">
<section class="start-screen wide">
<p class="eyebrow">Админка</p>
<h1>Рыбсток</h1>
<p>Подготовлены основные разделы будущей панели управления.</p>
<div class="admin-grid">
<?php foreach ($sections as $section): ?>
<div class="admin-tile"><?= e($section) ?></div>
<?php endforeach; ?>
</div>
</section>
</main>
</body>
</html>
+57
View File
@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Core\Database;
use App\Repositories\CategoryRepository;
use App\Repositories\ProductRepository;
use App\Repositories\SettingRepository;
use Throwable;
final class HomeController
{
public function index(): void
{
$dbStatus = 'not_checked';
$categories = [];
$catalogProducts = [];
$popularProducts = [];
$settings = [];
$notice = null;
try {
Database::pdo()->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',
]);
}
}
+6
View File
@@ -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');
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Core\Database;
use PDO;
abstract class BaseRepository
{
protected function pdo(): PDO
{
return Database::pdo();
}
}
+110
View File
@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
final class CategoryRepository extends BaseRepository
{
/**
* @return array<int, array<string, mixed>>
*/
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<int, array<string, mixed>>
*/
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<int, array<int, array<string, mixed>>> $childrenByParent
* @return array<int, array<string, mixed>>
*/
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<string, mixed>|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<int, array<string, string>>
*/
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;
}
}
+103
View File
@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Services\PriceTierService;
final class ProductRepository extends BaseRepository
{
/**
* @return array<int, array<string, mixed>>
*/
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<int, array<string, mixed>>
*/
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<int, array<string, mixed>> $products
* @return array<int, array<string, mixed>>
*/
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;
}
}
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
final class ProductVariantRepository extends BaseRepository
{
/**
* @return array<int, array<string, mixed>>
*/
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();
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
final class PromocodeRepository extends BaseRepository
{
/**
* @return array<int, array<string, mixed>>
*/
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();
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
final class SettingRepository extends BaseRepository
{
/**
* @return array<string, mixed>
*/
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,
};
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Repositories\PromocodeRepository;
final class PriceTierService
{
public function __construct(
private readonly ?PromocodeRepository $promocodes = null
) {
}
/**
* @return array<int, array<string, mixed>>
*/
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();
}
}
+5
View File
@@ -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');
}
+28 -1
View File
@@ -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;
+261 -12
View File
@@ -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'),
@@ -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. Проверить выборочно страницы товаров и категорий.
+16
View File
@@ -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` хранит результат расчета:
+107
View File
@@ -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`.
+5
View File
@@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
require_once dirname(__DIR__, 2) . '/admin/index.php';
+259
View File
@@ -10,6 +10,50 @@ body {
background: #f7f8f4;
}
.site-utility {
width: min(1080px, calc(100% - 48px));
display: flex;
gap: 14px;
align-items: center;
margin: 18px auto 0;
}
.back-button {
appearance: none;
border: 1px solid #cfd8c9;
border-radius: 8px;
padding: 10px 12px;
font: inherit;
font-size: 14px;
font-weight: 700;
color: #1d5d3b;
background: #ffffff;
cursor: pointer;
}
.back-button:hover {
border-color: #9fb194;
background: #f5f8f1;
}
.breadcrumbs {
display: flex;
flex-wrap: wrap;
gap: 7px;
align-items: center;
min-width: 0;
font-size: 14px;
color: #687466;
}
.breadcrumbs a {
color: #1d5d3b;
}
.breadcrumbs-separator {
color: #a3ad9d;
}
.page {
min-height: 100vh;
display: grid;
@@ -25,6 +69,10 @@ body {
background: #ffffff;
}
.start-screen.wide {
width: min(1080px, 100%);
}
.eyebrow {
margin: 0 0 8px;
font-size: 14px;
@@ -42,6 +90,25 @@ p {
line-height: 1.55;
}
h2 {
margin: 0 0 16px;
font-size: 22px;
}
h3 {
margin: 0 0 8px;
font-size: 17px;
}
a {
color: #1d5d3b;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.status-box {
display: flex;
gap: 8px;
@@ -78,3 +145,195 @@ p {
.primary-button:hover {
background: #16492e;
}
.content-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px;
margin-top: 24px;
}
.panel {
padding: 20px;
border: 1px solid #dfe5da;
border-radius: 8px;
background: #fbfcf8;
}
.products-panel {
margin-top: 18px;
}
.section-heading {
display: flex;
gap: 16px;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 18px;
}
.section-heading .eyebrow,
.section-heading h2 {
margin-bottom: 0;
}
.subtle-link {
flex: 0 0 auto;
font-size: 15px;
font-weight: 700;
}
.muted {
margin: 0;
font-size: 15px;
color: #687466;
}
.category-list {
display: grid;
gap: 10px;
margin: 0;
padding: 0;
list-style: none;
}
.category-list .category-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 8px 0 0;
padding: 0;
list-style: none;
}
.category-list li {
font-size: 16px;
font-weight: 700;
}
.category-list.depth-1 li {
font-size: 14px;
font-weight: 600;
}
.category-list.depth-2 {
padding-left: 10px;
}
.category-list.depth-2 li {
font-size: 13px;
font-weight: 400;
}
.settings-list {
display: grid;
gap: 12px;
margin: 0;
}
.settings-list div {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 12px;
align-items: baseline;
}
.settings-list dt {
color: #687466;
}
.settings-list dd {
margin: 0;
font-weight: 700;
}
.product-grid,
.admin-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 12px;
}
.product-card,
.admin-tile {
min-height: 96px;
padding: 16px;
border: 1px solid #dfe5da;
border-radius: 8px;
background: #ffffff;
}
.product-card p {
margin: 0 0 10px;
font-size: 14px;
color: #687466;
}
.product-card .product-category {
margin-bottom: 8px;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
color: #7a856f;
}
.price-line {
display: block;
margin-bottom: 6px;
font-size: 14px;
color: #2d372f;
}
.price-tiers {
display: grid;
gap: 6px;
margin: 12px 0 0;
padding: 10px 0 0;
border-top: 1px solid #e3e8de;
list-style: none;
}
.price-tiers li {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
align-items: baseline;
font-size: 12px;
}
.price-tiers span {
color: #687466;
}
.price-tiers strong {
color: #1d5d3b;
}
.admin-tile {
display: grid;
place-items: center;
min-height: 72px;
font-weight: 700;
text-align: center;
}
@media (max-width: 760px) {
.site-utility {
width: calc(100% - 32px);
display: grid;
margin-top: 12px;
}
.content-grid {
grid-template-columns: 1fr;
}
.settings-list div {
grid-template-columns: 1fr;
gap: 2px;
}
.section-heading {
display: grid;
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
use App\Core\Database;
require_once dirname(__DIR__) . '/app/bootstrap.php';
header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
echo "RybStock diagnostic\n";
echo "version: 2026-06-04-02\n";
echo "file: " . __FILE__ . "\n";
echo "base_path: " . base_path() . "\n";
echo "db: " . app_env('DB_DATABASE', '') . "\n";
echo "home_text_expected: Каталог открывается уже на главной странице\n";
echo "schema_mtime: " . date('Y-m-d H:i:s', filemtime(base_path('database/schema.sql'))) . "\n";
echo "seed_mtime: " . date('Y-m-d H:i:s', filemtime(base_path('database/seed.sql'))) . "\n";
try {
$pdo = Database::pdo();
echo "db_status: connected\n";
$tables = $pdo->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";
}
+2 -16
View File
@@ -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();
+62
View File
@@ -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
<h1>Установка базы</h1>
<p>Эта страница применяет `database/schema.sql` и `database/seed.sql`. Таблицы будут пересозданы.</p>
<div class="status-box">
<span>Версия install.php:</span>
<strong>2026-06-04-02</strong>
</div>
<div class="status-box">
<span>Файл:</span>
<strong><?= htmlspecialchars(__FILE__, ENT_QUOTES, 'UTF-8') ?></strong>
</div>
<?php if ($stats['ok'] ?? false): ?>
<div class="status-box">
<span>База:</span>
<strong><?= htmlspecialchars((string) $stats['database'], ENT_QUOTES, 'UTF-8') ?></strong>
</div>
<div class="status-box">
<span>Таблиц:</span>
<strong><?= htmlspecialchars((string) $stats['tables_count'], ENT_QUOTES, 'UTF-8') ?></strong>
</div>
<div class="status-box">
<span>Категорий:</span>
<strong><?= htmlspecialchars((string) $stats['category_count'], ENT_QUOTES, 'UTF-8') ?></strong>
</div>
<?php if (!empty($stats['first_categories'])): ?>
<div class="status-box">
<span>Верхние категории:</span>
<strong><?= htmlspecialchars(implode(', ', $stats['first_categories']), ENT_QUOTES, 'UTF-8') ?></strong>
</div>
<?php endif; ?>
<?php else: ?>
<div class="status-box danger"><?= htmlspecialchars((string) ($stats['error'] ?? 'Нет статистики'), ENT_QUOTES, 'UTF-8') ?></div>
<?php endif; ?>
<?php if (!$isLocal): ?>
<div class="status-box danger">Установка доступна только при APP_ENV=local.</div>
<?php elseif (!$canRun): ?>
+19 -1
View File
@@ -4,16 +4,34 @@ declare(strict_types=1);
/** @var string $templatePath */
/** @var string $title */
/** @var string $metaDescription */
/** @var string $metaKeywords */
/** @var array<int, array{title: string, url?: string}> $breadcrumbs */
/** @var bool $showBackButton */
?>
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars($title ?? 'Рыбсток', ENT_QUOTES, 'UTF-8') ?></title>
<title><?= e($title) ?></title>
<?php if ($metaDescription !== ''): ?>
<meta name="description" content="<?= e($metaDescription) ?>">
<?php endif; ?>
<?php if ($metaKeywords !== ''): ?>
<meta name="keywords" content="<?= e($metaKeywords) ?>">
<?php endif; ?>
<link rel="stylesheet" href="/assets/css/app.css">
</head>
<body>
<header class="site-utility">
<?php if ($showBackButton): ?>
<button class="back-button" type="button" onclick="history.length > 1 ? history.back() : location.href='/'">Вернуться назад</button>
<?php endif; ?>
<?php require base_path('views/partials/breadcrumbs.php'); ?>
</header>
<main class="page">
<?php require $templatePath; ?>
</main>
+121 -4
View File
@@ -3,14 +3,131 @@
declare(strict_types=1);
/** @var string $dbStatus */
/** @var array<int, array<string, mixed>> $categories */
/** @var array<int, array<string, mixed>> $catalogProducts */
/** @var array<int, array<string, mixed>> $popularProducts */
/** @var array<string, mixed> $settings */
/** @var string|null $notice */
/** @var string $buildVersion */
?>
<section class="start-screen">
<p class="eyebrow">Старт проекта</p>
<section class="start-screen wide">
<p class="eyebrow">Шаг 3</p>
<h1>Рыбсток</h1>
<p>Чистый PHP-проект подготовлен. Следующий шаг - схема базы данных и таблицы для каталога, фасовок, заказов и админки.</p>
<p>Каталог открывается уже на главной странице, чтобы покупатель сразу видел ассортимент и не делал лишних переходов.</p>
<p class="muted">Версия сборки: <?= e($buildVersion) ?></p>
<div class="status-box">
<span>MariaDB:</span>
<strong><?= htmlspecialchars($dbStatus, ENT_QUOTES, 'UTF-8') ?></strong>
<strong><?= e($dbStatus) ?></strong>
</div>
<?php if ($notice !== null): ?>
<div class="status-box danger"><?= e($notice) ?></div>
<?php endif; ?>
<div class="content-grid">
<section class="panel">
<h2>Категории</h2>
<?php if ($categories === []): ?>
<p class="muted">Категории пока не найдены. Если схема уже применена, проверьте `database/seed.sql`.</p>
<?php else: ?>
<?php
$items = $categories;
$depth = 0;
require base_path('views/partials/category-tree.php');
?>
<?php endif; ?>
</section>
<section class="panel">
<h2>Настройки доставки</h2>
<dl class="settings-list">
<div>
<dt>Внутри МКАД бесплатно от</dt>
<dd><?= e((string) ($settings['inside_mkad_free_from'] ?? '5000')) ?> руб.</dd>
</div>
<div>
<dt>Внутри МКАД платно</dt>
<dd><?= e((string) ($settings['inside_mkad_paid_price'] ?? '599')) ?> руб.</dd>
</div>
<div>
<dt>За МКАД расчет</dt>
<dd><?= e((string) ($settings['outside_mkad_base_price'] ?? '199')) ?> руб. + <?= e((string) ($settings['outside_mkad_km_price'] ?? '60')) ?> руб./км</dd>
</div>
<div>
<dt>Подъем</dt>
<dd><?= e((string) ($settings['lift_price'] ?? '390')) ?> руб.</dd>
</div>
</dl>
</section>
</div>
<section class="panel products-panel">
<div class="section-heading">
<div>
<p class="eyebrow">Каталог продукции</p>
<h2>Товары на главной</h2>
</div>
<a class="subtle-link" href="/catalog">Весь каталог</a>
</div>
<?php if ($catalogProducts === []): ?>
<p class="muted">После наполнения базы товары будут отображаться здесь сразу на главной странице.</p>
<?php else: ?>
<div class="product-grid">
<?php foreach ($catalogProducts as $product): ?>
<article class="product-card">
<p class="product-category"><?= e($product['category_name'] ?? 'Каталог') ?></p>
<h3><?= e($product['name']) ?></h3>
<p><?= e($product['variant_name'] ?? 'Выберите доступную фасовку') ?></p>
<?php if ($product['price_per_unit'] !== null): ?>
<span class="price-line">за <?= e($product['unit'] ?? 'ед.') ?>: <?= e((string) $product['price_per_unit']) ?> руб.</span>
<?php endif; ?>
<?php if ($product['package_price'] !== null): ?>
<strong><?= e((string) $product['package_price']) ?> руб. за фасовку</strong>
<?php endif; ?>
<?php if (!empty($product['price_tiers'])): ?>
<ul class="price-tiers">
<?php foreach ($product['price_tiers'] as $tier): ?>
<li>
<span><?= e($tier['label']) ?></span>
<strong><?= e((string) $tier['price']) ?> руб.</strong>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</article>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>
<section class="panel products-panel">
<h2>Популярные товары</h2>
<?php if ($popularProducts === []): ?>
<p class="muted">Товары появятся здесь после наполнения каталога и публикации фасовок.</p>
<?php else: ?>
<div class="product-grid">
<?php foreach ($popularProducts as $product): ?>
<article class="product-card">
<h3><?= e($product['name']) ?></h3>
<p><?= e($product['variant_name'] ?? 'Фасовка не выбрана') ?></p>
<?php if ($product['package_price'] !== null): ?>
<strong><?= e((string) $product['package_price']) ?> руб.</strong>
<?php endif; ?>
<?php if (!empty($product['price_tiers'])): ?>
<ul class="price-tiers">
<?php foreach ($product['price_tiers'] as $tier): ?>
<li>
<span><?= e($tier['label']) ?></span>
<strong><?= e((string) $tier['price']) ?> руб.</strong>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</article>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>
</section>
+21
View File
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/** @var array<int, array{title: string, url?: string}> $breadcrumbs */
?>
<?php if ($breadcrumbs !== []): ?>
<nav class="breadcrumbs" aria-label="Хлебные крошки">
<?php foreach ($breadcrumbs as $index => $item): ?>
<?php if ($index > 0): ?>
<span class="breadcrumbs-separator">/</span>
<?php endif; ?>
<?php if (!empty($item['url']) && $index < count($breadcrumbs) - 1): ?>
<a href="<?= e($item['url']) ?>"><?= e($item['title']) ?></a>
<?php else: ?>
<span><?= e($item['title']) ?></span>
<?php endif; ?>
<?php endforeach; ?>
</nav>
<?php endif; ?>
+24
View File
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/** @var array<int, array<string, mixed>> $items */
/** @var int $depth */
?>
<ul class="category-list depth-<?= e((string) $depth) ?>">
<?php foreach ($items as $category): ?>
<li>
<a href="/catalog/<?= e($category['slug']) ?>"><?= e($category['name']) ?></a>
<?php if (!empty($category['children'])): ?>
<?php
$previousItems = $items;
$items = $category['children'];
$depth++;
require __DIR__ . '/category-tree.php';
$depth--;
$items = $previousItems;
?>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
+18 -6
View File
@@ -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`.
- Следующий шаг: сделать базовый роутинг, страницу полного каталога и первые формы админки для категорий/товаров.