шаг 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. 1. Установить Open Server Panel, Laragon или другой локальный сервер с PHP и MariaDB.
2. Направить корень сайта на папку `public/`. 2. Направить корень сайта на папку `public/`.
3. Проверить настройки базы в `.env`. 3. Проверить настройки базы в `.env`.
4. Открыть сайт в браузере. 4. Открыть `/install.php?token=rybstock-local-install` и применить схему базы.
5. Открыть сайт в браузере.
## Диагностика
Если после изменения файлов сайт показывает старый текст, открыть:
`/check.php`
Страница покажет реальный путь исполняемого файла, базу данных, количество таблиц и текущие верхние категории.
Если сервер смотрит не в `public/`, нужно настроить виртуальный хост, чтобы публичной была именно эта папка. Если сервер смотрит не в `public/`, нужно настроить виртуальный хост, чтобы публичной была именно эта папка.
+37 -1
View File
@@ -4,4 +4,40 @@ declare(strict_types=1);
require_once dirname(__DIR__) . '/app/bootstrap.php'; 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); extract($data, EXTR_SKIP);
$title = $title ?? 'Рыбсток';
$metaDescription = $metaDescription ?? '';
$metaKeywords = $metaKeywords ?? '';
$breadcrumbs = $breadcrumbs ?? [];
$showBackButton = $showBackButton ?? true;
require base_path('views/layouts/main.php'); 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); 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 customer_email_tokens;
DROP TABLE IF EXISTS site_settings; DROP TABLE IF EXISTS site_settings;
DROP TABLE IF EXISTS holidays; DROP TABLE IF EXISTS holidays;
DROP TABLE IF EXISTS redirects;
DROP TABLE IF EXISTS admins; DROP TABLE IF EXISTS admins;
CREATE TABLE admins ( CREATE TABLE admins (
@@ -43,9 +44,12 @@ CREATE TABLE categories (
parent_id BIGINT UNSIGNED NULL, parent_id BIGINT UNSIGNED NULL,
name VARCHAR(190) NOT NULL, name VARCHAR(190) NOT NULL,
slug VARCHAR(220) NOT NULL, slug VARCHAR(220) NOT NULL,
legacy_path VARCHAR(255) NULL,
description TEXT NULL, description TEXT NULL,
h1 VARCHAR(255) NULL,
seo_title VARCHAR(255) NULL, seo_title VARCHAR(255) NULL,
seo_description TEXT NULL, seo_description TEXT NULL,
seo_keywords TEXT NULL,
image_path VARCHAR(255) NULL, image_path VARCHAR(255) NULL,
sort_order INT NOT NULL DEFAULT 100, sort_order INT NOT NULL DEFAULT 100,
is_active TINYINT(1) NOT NULL DEFAULT 1, 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, updated_at DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id), PRIMARY KEY (id),
UNIQUE KEY uq_categories_slug (slug), UNIQUE KEY uq_categories_slug (slug),
UNIQUE KEY uq_categories_legacy_path (legacy_path),
KEY idx_categories_parent (parent_id), KEY idx_categories_parent (parent_id),
KEY idx_categories_active_sort (is_active, sort_order), KEY idx_categories_active_sort (is_active, sort_order),
CONSTRAINT fk_categories_parent CONSTRAINT fk_categories_parent
@@ -65,10 +70,15 @@ CREATE TABLE products (
category_id BIGINT UNSIGNED NULL, category_id BIGINT UNSIGNED NULL,
name VARCHAR(220) NOT NULL, name VARCHAR(220) NOT NULL,
slug VARCHAR(240) NOT NULL, slug VARCHAR(240) NOT NULL,
legacy_path VARCHAR(255) NULL,
sku VARCHAR(100) NULL, sku VARCHAR(100) NULL,
short_description TEXT NULL, short_description TEXT NULL,
description MEDIUMTEXT NULL, description MEDIUMTEXT NULL,
product_info 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, main_image_path VARCHAR(255) NULL,
base_unit ENUM('kg', 'g', 'l', 'ml', 'pcs', 'pack') NOT NULL DEFAULT 'kg', 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', 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, updated_at DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id), PRIMARY KEY (id),
UNIQUE KEY uq_products_slug (slug), UNIQUE KEY uq_products_slug (slug),
UNIQUE KEY uq_products_legacy_path (legacy_path),
UNIQUE KEY uq_products_sku (sku), UNIQUE KEY uq_products_sku (sku),
KEY idx_products_category (category_id), KEY idx_products_category (category_id),
KEY idx_products_public (is_published, is_available, sort_order), 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, used_count INT UNSIGNED NOT NULL DEFAULT 0,
is_active TINYINT(1) NOT NULL DEFAULT 1, is_active TINYINT(1) NOT NULL DEFAULT 1,
is_featured TINYINT(1) NOT NULL DEFAULT 0, 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, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, updated_at DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id), PRIMARY KEY (id),
UNIQUE KEY uq_promocodes_code (code), 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; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE promocode_targets ( CREATE TABLE promocode_targets (
@@ -368,4 +382,17 @@ CREATE TABLE holidays (
UNIQUE KEY uq_holidays_date (holiday_date) UNIQUE KEY uq_holidays_date (holiday_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) 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; SET FOREIGN_KEY_CHECKS = 1;
+261 -12
View File
@@ -3,21 +3,270 @@
SET NAMES utf8mb4; SET NAMES utf8mb4;
INSERT INTO categories (name, slug, sort_order, is_active) VALUES INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES
('Рыба', 'fish', 10, 1), (NULL, 'Рыба', 'ryba', 10, 1),
('Готовая продукция', 'gotovaya-produkciya', 20, 1), (NULL, 'Морепродукты', 'moreprodukty', 20, 1),
('Полуфабрикаты', 'polufabrikaty', 30, 1), (NULL, 'Фрукты, овощи, ягоды, грибы', 'frukty-ovoschi-yagody-griby', 30, 1),
('Мясо', 'myaso', 40, 1), (NULL, 'Икра', 'ikra', 40, 1),
('Сыры', 'syry', 50, 1), (NULL, 'Бакалея', 'bakaleya', 50, 1),
('Ягоды', 'yagody', 60, 1), (NULL, 'Полуфабрикаты', 'polufabrikaty', 60, 1),
('Овощи', 'ovoschi', 70, 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 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 VALUES
('Рыбсток7000', '300 руб. при заказе от 7000 руб.', 'fixed', 300.00, 7000.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), ('Рыбсток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); ('Рыбсток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 INSERT INTO site_settings (setting_key, setting_value, value_type, group_name) VALUES
('site_phone', '+7-977-736-33-63', 'string', 'contacts'), ('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-текст для страниц категорий. - SEO-текст для страниц категорий.
Для переноса со старого сайта у категорий есть поле `legacy_path`. Если старый URL отличается от нового slug, старый путь нужно сохранить и добавить 301-редирект в таблицу `redirects`.
Для SEO у категорий есть поля `h1`, `seo_title`, `seo_description`, `seo_keywords`.
### Товары ### Товары
Таблица `products` хранит основную карточку товара: Таблица `products` хранит основную карточку товара:
@@ -60,6 +65,10 @@
- доступность; - доступность;
- признаки "новинка", "хит", "акция". - признаки "новинка", "хит", "акция".
Для товаров также есть поле `legacy_path`, чтобы переносить старые ссылки без потери индексации.
Для SEO у товаров есть поля `h1`, `seo_title`, `seo_description`, `seo_keywords`.
### Фасовки ### Фасовки
Таблица `product_variants` хранит отдельные фасовки товара. Таблица `product_variants` хранит отдельные фасовки товара.
@@ -106,6 +115,13 @@
Если промокод действует на весь заказ, targets не нужны. Если промокод действует на весь заказ, targets не нужны.
Для отображения выгодных цен в каталоге у промокодов есть поля:
- `show_in_price_tiers` - показывать как градацию цены;
- `price_tier_label` - подпись для карточки товара.
Так можно показывать покупателю базовую цену, цену от 20 000 руб. наличными и цену от 50 000 руб. наличными.
### Доставка ### Доставка
Таблица `delivery_calculations` хранит результат расчета: Таблица `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; 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 { .page {
min-height: 100vh; min-height: 100vh;
display: grid; display: grid;
@@ -25,6 +69,10 @@ body {
background: #ffffff; background: #ffffff;
} }
.start-screen.wide {
width: min(1080px, 100%);
}
.eyebrow { .eyebrow {
margin: 0 0 8px; margin: 0 0 8px;
font-size: 14px; font-size: 14px;
@@ -42,6 +90,25 @@ p {
line-height: 1.55; 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 { .status-box {
display: flex; display: flex;
gap: 8px; gap: 8px;
@@ -78,3 +145,195 @@ p {
.primary-button:hover { .primary-button:hover {
background: #16492e; 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); declare(strict_types=1);
use App\Core\Database; use App\Controllers\HomeController;
require_once dirname(__DIR__) . '/app/bootstrap.php'; require_once dirname(__DIR__) . '/app/bootstrap.php';
$dbStatus = 'not_checked'; (new HomeController())->index();
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,
]);
+62
View File
@@ -12,6 +12,7 @@ $actualToken = $_GET['token'] ?? '';
$canRun = $isLocal && $expectedToken !== '' && hash_equals($expectedToken, (string) $actualToken); $canRun = $isLocal && $expectedToken !== '' && hash_equals($expectedToken, (string) $actualToken);
$result = null; $result = null;
$error = null; $error = null;
$stats = install_stats();
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$canRun) { if (!$canRun) {
@@ -22,6 +23,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$pdo = Database::pdo(); $pdo = Database::pdo();
run_sql_file($pdo, base_path('database/schema.sql')); run_sql_file($pdo, base_path('database/schema.sql'));
run_sql_file($pdo, base_path('database/seed.sql')); run_sql_file($pdo, base_path('database/seed.sql'));
$stats = install_stats();
$result = 'База данных установлена. Таблицы и стартовые данные созданы.'; $result = 'База данных установлена. Таблицы и стартовые данные созданы.';
} catch (Throwable $exception) { } catch (Throwable $exception) {
http_response_code(500); 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 function run_sql_file(PDO $pdo, string $path): void
{ {
if (!is_file($path)) { if (!is_file($path)) {
@@ -62,6 +91,39 @@ function run_sql_file(PDO $pdo, string $path): void
<h1>Установка базы</h1> <h1>Установка базы</h1>
<p>Эта страница применяет `database/schema.sql` и `database/seed.sql`. Таблицы будут пересозданы.</p> <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): ?> <?php if (!$isLocal): ?>
<div class="status-box danger">Установка доступна только при APP_ENV=local.</div> <div class="status-box danger">Установка доступна только при APP_ENV=local.</div>
<?php elseif (!$canRun): ?> <?php elseif (!$canRun): ?>
+19 -1
View File
@@ -4,16 +4,34 @@ declare(strict_types=1);
/** @var string $templatePath */ /** @var string $templatePath */
/** @var string $title */ /** @var string $title */
/** @var string $metaDescription */
/** @var string $metaKeywords */
/** @var array<int, array{title: string, url?: string}> $breadcrumbs */
/** @var bool $showBackButton */
?> ?>
<!doctype html> <!doctype html>
<html lang="ru"> <html lang="ru">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <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"> <link rel="stylesheet" href="/assets/css/app.css">
</head> </head>
<body> <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"> <main class="page">
<?php require $templatePath; ?> <?php require $templatePath; ?>
</main> </main>
+121 -4
View File
@@ -3,14 +3,131 @@
declare(strict_types=1); declare(strict_types=1);
/** @var string $dbStatus */ /** @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"> <section class="start-screen wide">
<p class="eyebrow">Старт проекта</p> <p class="eyebrow">Шаг 3</p>
<h1>Рыбсток</h1> <h1>Рыбсток</h1>
<p>Чистый PHP-проект подготовлен. Следующий шаг - схема базы данных и таблицы для каталога, фасовок, заказов и админки.</p> <p>Каталог открывается уже на главной странице, чтобы покупатель сразу видел ассортимент и не делал лишних переходов.</p>
<p class="muted">Версия сборки: <?= e($buildVersion) ?></p>
<div class="status-box"> <div class="status-box">
<span>MariaDB:</span> <span>MariaDB:</span>
<strong><?= htmlspecialchars($dbStatus, ENT_QUOTES, 'UTF-8') ?></strong> <strong><?= e($dbStatus) ?></strong>
</div> </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> </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. Категории ### 2.3. Категории
Основные категории для главного заголовка и меню: Основные категории должны строго соответствовать старому сайту, чтобы сохранить дерево, ссылки, индексацию и целостность переноса:
- Рыба; - Рыба;
- Готовая продукция; - Рыбное филе;
- Рыбные стейки;
- Охлажденная рыба;
- Малосольная рыба;
- Копченая рыба;
- Вяленая рыба;
- Морепродукты;
- Фрукты, овощи, ягоды, грибы;
- Икра;
- Бакалея;
- Полуфабрикаты; - Полуфабрикаты;
- Мясная гастрономия;
- Мясо; - Мясо;
- Сыры; - Сыры;
- Ягоды; - Молочная продукция.
- Овощи.
Нужны категории и подкатегории. Подкатегории визуально отделить от категорий, например более мягким цветом, меньшим шрифтом или отступом. Нужны категории и подкатегории. Подкатегории визуально отделить от категорий, например более мягким цветом, меньшим шрифтом или отступом. Для сохранения индексации нужны точные старые URL/slug каждой категории и товара; если они отличаются от текущих slug в seed-файле, их нужно заменить перед переносом.
## 3. Структура сайта ## 3. Структура сайта
@@ -64,6 +73,7 @@
- Заметное сообщение: "Работаем без предоплаты. Оплата только при получении товара." - Заметное сообщение: "Работаем без предоплаты. Оплата только при получении товара."
- Поиск по сайту. - Поиск по сайту.
- Быстрый переход в полный каталог. - Быстрый переход в полный каталог.
- Сам каталог должен быть уже открыт на главной странице, чтобы покупатель сразу видел товары и не делал лишних переходов.
- Понятная кнопка "Доставка и оплата". - Понятная кнопка "Доставка и оплата".
- Верхнее меню с категориями и подкатегориями. - Верхнее меню с категориями и подкатегориями.
@@ -629,4 +639,6 @@
- Шаг 1 выполнен: выбрана техническая связка на чистом PHP, создан стартовый каркас проекта, подключение к MariaDB через `.env`, базовые папки и файл `ШАГ_1_ТЕХНИЧЕСКОЕ_РЕШЕНИЕ.md`. - Шаг 1 выполнен: выбрана техническая связка на чистом PHP, создан стартовый каркас проекта, подключение к MariaDB через `.env`, базовые папки и файл `ШАГ_1_ТЕХНИЧЕСКОЕ_РЕШЕНИЕ.md`.
- Шаг 2 выполнен: создана SQL-схема базы `database/schema.sql`, стартовые данные `database/seed.sql`, страница установки `public/install.php` и документ `docs/ШАГ_2_БАЗА_ДАННЫХ.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`.
- Следующий шаг: сделать базовый роутинг, страницу полного каталога и первые формы админки для категорий/товаров.