From 53281ee422d47a2f891af58477a5dacff81fa465 Mon Sep 17 00:00:00 2001 From: Arsen Mirzaev Tatyano-Muradovich Date: Tue, 12 Oct 2021 13:17:30 +1000 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B1=D0=BB=D0=BE=D0=BA=D0=B0=20"=D0=90=D0=BD?= =?UTF-8?q?=D0=B0=D0=BB=D0=BE=D0=B3=D0=B8=D1=87=D0=BD=D1=8B=D0=B5=20=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D1=80=D1=8B"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../system/controllers/ProductController.php | 54 +-- .../system/controllers/SearchController.php | 254 +--------- mirzaev/skillparts/system/models/Edge.php | 24 +- mirzaev/skillparts/system/models/Product.php | 177 ++++++- .../system/models/ProductEdgeProduct.php | 6 +- mirzaev/skillparts/system/models/Search.php | 438 +++++++++++++++++- .../skillparts/system/views/search/index.php | 262 +++-------- 7 files changed, 719 insertions(+), 496 deletions(-) diff --git a/mirzaev/skillparts/system/controllers/ProductController.php b/mirzaev/skillparts/system/controllers/ProductController.php index 932596d..320a3c2 100644 --- a/mirzaev/skillparts/system/controllers/ProductController.php +++ b/mirzaev/skillparts/system/controllers/ProductController.php @@ -95,12 +95,7 @@ class ProductController extends Controller // Не существует товар к которому планируется соединение // Инициализация товара - $to = new Product(); - - // Запись артикула - $to->catn = (string) $target; - - if ($to->save()) { + if ($to = Product::writeEmpty((string) $target)) { // Удалось записать товар } else { // Не удалось записать товар @@ -116,19 +111,8 @@ class ProductController extends Controller } } - // Запись ребра - if ($edge = ProductEdgeProduct::writeSafe(Product::collectionName() . "/$from->_key", Product::collectionName() . "/$to->_key", data: ['type' => 'analogue'])) { - // Ребро сохранено - - // Запись в журнал о соединении - $from->journal('connect', [ - 'to' => Product::collectionName() . "/$to->_key" - ]); - - // Запись в журнал о соединении - $to->journal('connect', [ - 'from' => Product::collectionName() . "/$from->_key" - ]); + if (($edge = $from->synchronization($to)) instanceof ProductEdgeProduct) { + // Ребро создано // Запись в буфер возврата $return['alert'] = "Продукты успешно соединены ребром: $edge->_key"; @@ -202,37 +186,9 @@ class ProductController extends Controller if ($to = Product::searchByCatn($target)) { // Существует товар который нужно отсоединить - // Поиск ребра - if ($edge = @ProductEdgeProduct::searchByVertex(Product::collectionName() . "/$from->_key", Product::collectionName() . "/$to->_key", type: 'analogue')[0]) { - // Найдено ребро (from: $from, to: $to) - } else if ($edge = @ProductEdgeProduct::searchByVertex(Product::collectionName() . "/$to->_key", Product::collectionName() . "/$from->_key", type: 'analogue')[0]) { - // Найдено ребро (from: $to, to: $from) - } else { - // Не найдены ребра - - // Запись кода ответа - yii::$app->response->statusCode = 500; - - // Запись в буфер возврата - $return['alert'] = "Не удалось найти связь с этим товаром: $target"; - - // Переход в конец алгоритма - goto end; - } - - if ($edge->delete() > 0) { + if ($from->disconnect($to)) { // Удалось удалить ребро (связь) - // Запись в журнал о разъединении - $from->journal('disconnect', [ - 'to' => Product::collectionName() . "/$to->_key" - ]); - - // Запись в журнал о соединении - $to->journal('disconnect', [ - 'from' => Product::collectionName() . "/$from->_key" - ]); - // Запись в буфер возврата $return['alert'] = "Продукты успешно отсоединены"; } else { @@ -243,7 +199,7 @@ class ProductController extends Controller yii::$app->response->statusCode = 500; // Запись в буфер возврата - $return['alert'] = "Не удалось удалить ребро между $catn и $target"; + $return['alert'] = "Не удалось отсоединить $target от $catn"; // Переход в конец алгоритма goto end; diff --git a/mirzaev/skillparts/system/controllers/SearchController.php b/mirzaev/skillparts/system/controllers/SearchController.php index 6a24fd5..191f711 100644 --- a/mirzaev/skillparts/system/controllers/SearchController.php +++ b/mirzaev/skillparts/system/controllers/SearchController.php @@ -8,29 +8,15 @@ use yii; use yii\web\Controller; use yii\web\Response; -use app\models\Account; use app\models\Product; -use app\models\Supply; use app\models\Search; -use app\models\connection\Dellin; -use app\models\Settings; - -use Exception; - +/** + * @todo + * 1. Ограничение доступа + */ class SearchController extends Controller { - - - - - - - - - - - /** * @todo * 1. Сессию привязать к аккаунту и проверку по нему делать, иначе её можно просто сбрасывать @@ -161,222 +147,26 @@ class SearchController extends Controller ])) { // Данные найдены по поиску в полях Каталожного номера - foreach ($response as &$row) { - // Перебор продуктов + // Генерация данных для представления + $response = Search::content(products: $response); - // Поиск поставок привязанных к продуктам - $connections = Supply::searchByEdge( - from: 'product', - to: 'supply', - edge: 'supply_edge_product', - limit: 11, - direction: 'OUTBOUND', - subquery_where: [ - ['product._key' => $row['_key']], - ['supply.catn == product.catn'], - ['supply_edge_product.type' => 'connect'] - ], - where: 'supply._id == supply_edge_product[0]._from', - select: '{supply, supply_edge_product}' - ); - - // Инициализация буфера - $buffer_connections = []; - - if (count($connections) === 11) { - // Если в базе данных хранится много поставок - - // Инициализация - $row['overload'] = true; - } - - foreach ($connections as $key => &$connection) { - // Перебор поставок - - if (($cost = $cost['ЦенаЗаЕдиницу'] ?? $connection['supply_edge_product'][0]['onec']['Цены']['Цена']['ЦенаЗаЕдиницу']) < 1 - || ($amount = $connection['supply']['amnt'] ?? $connection['supply_edge_product'][0]['onec']['Количество']) < 1 - ) { - // Цена меньше единицы (подразумевается как ошибка) или количество меньше единицы - - // Скрыть из выдачи - unset($connections[$key]); - - continue; - } - - // Инициализация аккаунта - $connection['account'] = Account::searchBySupplyId($connection['supply_edge_product'][0]['_from']); - - // Инициализация продукта - $connection['product'] = Product::searchBySupplyId($connection['supply_edge_product'][0]['_from']); - - try { - // Инициализация данных геолокации - - try { - $from = (int) $connection['account']['opts']['delivery_from_terminal'] ?? empty(Settings::search()->delivery_from_default) ? 36 : (int) Settings::search()->delivery_from_default; - } catch (Exception $e) { - $from = empty(Settings::search()->delivery_from_default) ? 36 : (int) Settings::search()->delivery_from_default; - } - - try { - $to = (int) yii::$app->user->identity->opts['delivery_to_terminal'] ?? 36; - } catch (Exception $e) { - $to = 36; - } - - if ( - ($buffer_connection = $connection['product']['bffr']["$from-$to"] ?? false) - && time() < $buffer_connection['expires'] - ) { - // Найдены данные доставки в буфере - // и срок хранения не превышен, информация актуальна - - // Запись в буфер вывода - $connection['delivery'] = $buffer_connection['data']; - $connection['delivery']['type'] = 'auto'; - } else { - // Инициализация инстанции продукта в базе данных - $product = Product::searchByCatn($connection['product']['catn']); - - // Инициализация доставки Dellin (автоматическая) - $product->bffr = [ - "$from-$to" => [ - 'data' => $connection['delivery'] = Dellin::calcDeliveryAdvanced( - $from, - $to, - (int) ($connection['product']['wght'] ?? 0), - (int) ($connection['product']['dmns']['x'] ?? 0), - (int) ($connection['product']['dmns']['y'] ?? 0), - (int) ($connection['product']['dmns']['z'] ?? 0) - ), - 'expires' => time() + 86400 - ] - ] + ($product->bffr ?? []); - $connection['delivery']['type'] = 'auto'; - - // Отправка в базу данных - $product->update(); - } - } catch (Exception $e) { - $connection['delivery']['error'] = true; - - // var_dump($e->getMessage()); - // var_dump($e->getTrace()); - // var_dump($e->getFile()); - - // var_dump(json_decode($e->getMessage(), true)['errors']); - // die; - } - - // Инициализация цены (цена поставки + цена доставки + наша наценка) - $connection['cost'] = $cost + ($connection['delivery']['price']['all'] ?? $connection['delivery']['price']['one'] ?? 0) + ($settings['increase'] ?? 0); - - // Инициализация версии для рассчета доставки по воздуху - $buffer_delivery_avia = $connection; - - try { - // Инициализация данных геолокации - - if (($cost = $cost['ЦенаЗаЕдиницу'] ?? $buffer_delivery_avia['supply_edge_product'][0]['onec']['Цены']['Цена']['ЦенаЗаЕдиницу']) < 1 - || ($amount = $connection['supply']['amnt'] ?? $connection['supply_edge_product'][0]['onec']['Количество']) < 1 - ) { - // Цена меньше единицы (подразумевается как ошибка) или количество меньше единицы - // Этот код не будет выполняться, так как цена одна на обе позиции и аналогичная проверка выше уже есть - // Однако я это оставлю для возможных доработок в будущем - - // Скрыть из выдачи - unset($connections[$key]); - - continue; - } - - try { - $from = (int) $buffer_delivery_avia['account']['opts']['delivery_from_terminal'] ?? empty(Settings::search()->delivery_from_default) ? 36 : (int) Settings::search()->delivery_from_default; - } catch (Exception $e) { - $from = empty(Settings::search()->delivery_from_default) ? 36 : (int) Settings::search()->delivery_from_default; - } - - try { - $to = (int) yii::$app->user->identity->opts['delivery_to_terminal'] ?? 36; - } catch (Exception $e) { - $to = 36; - } - - if ( - ($buffer_connection = $buffer_delivery_avia['product']['bffr']["$from-$to-avia"] ?? false) - && time() < $buffer_connection['expires'] - ) { - // Найдены данные доставки в буфере - // и срок хранения не превышен, информация актуальна - - // Запись в буфер вывода - $buffer_delivery_avia['delivery'] = $buffer_connection['data']; - $buffer_delivery_avia['delivery']['type'] = 'avia'; - } else { - // Инициализация инстанции продукта в базе данных - $product = Product::searchByCatn($buffer_delivery_avia['product']['catn']); - - // Инициализация доставки Dellin (автоматическая) - $product->bffr = [ - "$from-$to-avia" => [ - 'data' => $buffer_delivery_avia['delivery'] = Dellin::calcDeliveryAdvanced( - $from, - $to, - (int) ($buffer_delivery_avia['product']['wght'] ?? 0), - (int) ($buffer_delivery_avia['product']['dmns']['x'] ?? 0), - (int) ($buffer_delivery_avia['product']['dmns']['y'] ?? 0), - (int) ($buffer_delivery_avia['product']['dmns']['z'] ?? 0), - avia: true - ), - 'expires' => time() + 86400 - ] - ] + ($product->bffr ?? []); - $buffer_delivery_avia['delivery']['type'] = 'avia'; - - // Отправка в базу данных - $product->update(); - } - } catch (Exception $e) { - $buffer_delivery_avia['delivery']['error'] = true; - - // echo '
';
-                                // var_dump($e->getMessage());
-                                // var_dump($e->getTrace());
-                                // var_dump($e->getFile());
-
-                                // var_dump(json_decode($e->getMessage(), true)['errors']);
-                                // die;
-                            }
-
-                            if (!isset($buffer_delivery_avia['delivery']['error']) || $buffer_delivery_avia['delivery']['error'] !== true) {
-                                // Если рассчиталась доставка самолётом
-
-                                // Инициализация цены (цена поставки + цена доставки + наша наценка)
-                                $buffer_delivery_avia['cost'] = $cost + ($buffer_delivery_avia['delivery']['price']['all'] ?? $buffer_delivery_avia['delivery']['price']['one'] ?? 0) + ($settings['increase'] ?? 0);
-
-                                // Запись в буфер
-                                $buffer_connections[] = $buffer_delivery_avia;
-                            }
-                        }
-
-                        // Запись обработанных данных
-                        $row['supplies'] = array_merge($connections, $buffer_connections);
-                    }
-
-                    // Запись ответа
-                    $return = [
-                        'panel' => $this->renderPartial('/search/panel', compact('response')),
-                        '_csrf' => yii::$app->request->getCsrfToken()
-                    ];
-
-                    if ((int) yii::$app->request->post('advanced')) {
-                        // Полноценный поиск
+                    if (yii::$app->request->isPost) {
+                        // POST-запрос
 
                         // Запись ответа
-                        $return['main'] = $this->renderPartial('/search/index', compact('response'));
-                        $return['hide'] = 1;
-                        $return['redirect'] = '/search?type=product&q=' . $query;
+                        $return = [
+                            'panel' => $this->renderPartial('/search/panel', compact('response', 'query')),
+                            '_csrf' => yii::$app->request->getCsrfToken()
+                        ];
+
+                        if ((int) yii::$app->request->post('advanced')) {
+                            // Полноценный поиск
+
+                            // Запись ответа
+                            $return['main'] = $this->renderPartial('/search/index', compact('response', 'query'));
+                            $return['hide'] = 1;
+                            $return['redirect'] = '/search?type=product&q=' . $query;
+                        }
                     }
                 } else {
                     // Данные не найдены
@@ -413,7 +203,7 @@ class SearchController extends Controller
 
                 $advanced = true;
 
-                return $this->render('/search/index', compact('response', 'timer', 'advanced'));
+                return $this->render('/search/index', compact('response', 'timer', 'advanced', 'query'));
             }
         }
 
diff --git a/mirzaev/skillparts/system/models/Edge.php b/mirzaev/skillparts/system/models/Edge.php
index 070a90d..7f4584a 100644
--- a/mirzaev/skillparts/system/models/Edge.php
+++ b/mirzaev/skillparts/system/models/Edge.php
@@ -112,7 +112,7 @@ abstract class Edge extends Document
      * @todo
      * 1. Удалить $type и оставить только $data
      */
-    public static function write(string $_from, string $_to, string $type, array $data = []): ?static
+    public static function write(string $_from, string $_to, string $type = '', array $data = []): ?static
     {
         // Инициализация
         $edge = new static;
@@ -159,19 +159,31 @@ abstract class Edge extends Document
     }
 
     /**
-     * Поиск рёбер
+     * Поиск рёбер по направлению
      */
     public static function search(string $target, string $direction = 'OUTBOUND', string $type = '', int $limit = 1): static|array|null
     {
-        if ($direction === 'OUTBOUND') {
-            $query = self::find()->where(['_from' => $target, 'type' => $type]);
-        } else if ($direction === 'INBOUND') {
-            $query = self::find()->where(['_to' => $target, 'type' => $type]);
+        if (str_contains($direction, 'OUTBOUND')) {
+            // Исходящие рёбра
+
+            $query = static::find()->where(['_from' => $target, 'type' => $type]);
+        } else if (str_contains($direction, 'INBOUND')) {
+            // Входящие рёбра
+
+            $query = static::find()->where(['_to' => $target, 'type' => $type]);
+        } else if (str_contains($direction, 'ANY')) {
+            // Исходящие и входящие рёбра
+
+            return static::search(target: $target, direction: 'OUTBOUND', type: $type, limit: $limit) + static::search(target: $target, direction: 'INBOUND', type: $type, limit: $limit);
         }
 
         if ($limit < 2) {
+            // Одно ребро или меньше
+
             return $query->one();
         } else {
+            // Несколько рёбер
+
             return $query->limit($limit)->all();
         }
     }
diff --git a/mirzaev/skillparts/system/models/Product.php b/mirzaev/skillparts/system/models/Product.php
index 36d5de7..56d152a 100644
--- a/mirzaev/skillparts/system/models/Product.php
+++ b/mirzaev/skillparts/system/models/Product.php
@@ -16,6 +16,8 @@ use DateTime;
 use DateTimeZone;
 
 use Exception;
+use phpDocumentor\Reflection\DocBlock\Tags\Var_;
+use stdClass;
 
 /**
  * Продукт (в ассортименте магазина)
@@ -596,10 +598,183 @@ class Product extends Document
      */
     public static function searchAnalogs(string $catn, int $limit = 30): ?array
     {
+        // Инициализация буфера возврата
+        $return = [];
+
         // Поиск ключей аналогов
         $analogs = ProductEdgeProduct::searchConnections(self::searchByCatn($catn)->_key, $limit);
 
+        foreach ($analogs as $analog) {
+            // Перебор найденных ключей (_key) аналогов
 
-        return [];
+            if ($analog = Product::searchById(self::collectionName() . "/$analog")) {
+                // Найден товар
+
+                // Запись в буфер вывода
+                $return[] = $analog;
+            } else {
+                // Не найден товар
+            }
+        }
+
+        return $return;
+    }
+
+    /**
+     * Синхронизация аналогов
+     *
+     * Связывает с товаром и связанными с ним товарами в одну группу
+     *
+     * @param self $to Цель
+     *
+     * @return array Созданные рёбра
+     */
+    public function synchronization(self $to): array
+    {
+        // Инициализация буфера сохранённых рёбер
+        $edges = [];
+
+        // Инициализация списка товаров в группе и буфера для связей "всех со всеми"
+        $products = $_products =  array_unique(array_merge($this->connections(), $to->connections(), [Product::collectionName() . "/$to->_key"]));
+
+        foreach ($products as $product) {
+            // Перебор связей для создания связей во всех товарах
+
+            // Удаление обрабатываемого товара из буферного списка товаров
+            unset($_products[array_search($product, $_products)]);
+
+            foreach ($_products as $_product) {
+                // Перебор товаров из буфера
+
+                if ($from = self::searchById($product)) {
+                    // Товар найден
+                } else {
+                    if ($from = Product::writeEmpty($product)) {
+                        // Удалось записать товар
+                    } else {
+                        // Не удалось записать товар
+
+                        continue;
+                    }
+                }
+
+                if ($to = self::searchById($_product)) {
+                    // Товар найден
+                } else {
+                    if ($to = Product::writeEmpty($_product)) {
+                        // Удалось записать товар
+                    } else {
+                        // Не удалось записать товар
+
+                        continue;
+                    }
+                }
+
+                if ($edge = $from->connect($to)) {
+                    // Ребро создано
+
+                    // Запись в буфер
+                    $edges[] = $edge;
+                }
+            }
+        }
+
+        return $edges;
+    }
+
+    /**
+     * Подключение аналога
+     *
+     * СВЯЗЫВАЕТ ТОЛЬКО ДВА ТОВАРА, ГРУППА ОСТАНЕТСЯ НЕИЗМЕННОЙ
+     *
+     * @param self $to Цель
+     */
+    public function connect(self $to): ?ProductEdgeProduct
+    {
+        if (ProductEdgeProduct::searchByVertex(Product::collectionName() . "/$this->_key", Product::collectionName() . "/$to->_key", limit: 1)) {
+            // Дубликат найден
+        } else if (ProductEdgeProduct::searchByVertex(Product::collectionName() . "/$to->_key", Product::collectionName() . "/$this->_key", limit: 1)) {
+            // Дубликат найден (наоборот)
+
+            // Вероятно эта проверка здесь не нужна, так как мы знаем входные данные
+        } else {
+            // Дубликаты не найдены
+
+            if ($edge = ProductEdgeProduct::write(Product::collectionName() . "/$this->_key", Product::collectionName() . "/$to->_key", data: ['type' => 'analogue'])) {
+                // Ребро сохранено
+
+                // Запись в журнал о соединении
+                $this->journal('connect analogue', [
+                    'to' => Product::collectionName() . "/$to->_key"
+                ]);
+
+                // Запись в журнал о соединении
+                $to->journal('connect analogue', [
+                    'from' => Product::collectionName() . "/$this->_key"
+                ]);
+
+                return $edge;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Отключение аналога
+     *
+     * @param self $to Цель
+     */
+    public function disconnect(self $to): bool
+    {
+        // Поиск ребра
+        if ($edge = @ProductEdgeProduct::searchByVertex(Product::collectionName() . "/$this->_key", Product::collectionName() . "/$to->_key", type: 'analogue')[0]) {
+            // Найдено ребро (from: $this, to: $to)
+        } else if ($edge = @ProductEdgeProduct::searchByVertex(Product::collectionName() . "/$to->_key", Product::collectionName() . "/$this->_key", type: 'analogue')[0]) {
+            // Найдено ребро (from: $to, to: $this)
+        } else {
+            // Не найдены ребра
+
+            return false;
+        }
+
+        if ($edge->delete() > 0) {
+            // Удалось удалить ребро (связь)
+
+            // Запись в журнал о разъединении
+            $this->journal('disconnect analogue', [
+                'to' => Product::collectionName() . "/$to->_key"
+            ]);
+
+            // Запись в журнал о соединении
+            $to->journal('disconnect analogue', [
+                'from' => Product::collectionName() . "/$this->_key"
+            ]);
+
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Найти все связанные товары
+     *
+     * @param int $limit Ограничение по максимальному значению
+     */
+    public function connections(int $limit = 100): array
+    {
+        // Инициализация буфера связанных товаров
+        $products = [];
+
+        foreach (ProductEdgeProduct::search(self::collectionName() . "/$this->_key", direction: 'ANY', type: 'analogue', limit: $limit) as $edge) {
+            // Перебор связей для создания списка товаров (вершин)
+
+            // Добавление товаров (вершин) в буфер (подразумевается, что без дубликатов)
+            if (!in_array($edge->_from, $products, true)) $products[] = $edge->_from;
+            if (!in_array($edge->_to, $products, true)) $products[] = $edge->_to;
+        }
+
+        return $products;
     }
 }
diff --git a/mirzaev/skillparts/system/models/ProductEdgeProduct.php b/mirzaev/skillparts/system/models/ProductEdgeProduct.php
index 6b77c0f..0a8b798 100644
--- a/mirzaev/skillparts/system/models/ProductEdgeProduct.php
+++ b/mirzaev/skillparts/system/models/ProductEdgeProduct.php
@@ -34,7 +34,7 @@ class ProductEdgeProduct extends Edge
             preg_match_all('/\w+\/([0-9]+)/', $edge->_to, $matches);
 
             // Запись артикула в буфер вывода
-            $return[] = $matches[1];
+            $return[] = $matches[1][0];
         }
 
         // Перерасчет ограничения по количеству
@@ -47,10 +47,10 @@ class ProductEdgeProduct extends Edge
             // Перебор найденных рёбер
 
             // Извлечение ключа
-            preg_match_all('/\w+\/([0-9]+)/', $edge->_to, $matches);
+            preg_match_all('/\w+\/([0-9]+)/', $edge->_from, $matches);
 
             // Запись артикула в буфер вывода
-            $return[] = $matches[1];
+            $return[] = $matches[1][0];
         }
 
         return $return;
diff --git a/mirzaev/skillparts/system/models/Search.php b/mirzaev/skillparts/system/models/Search.php
index c1a8b57..70c104b 100644
--- a/mirzaev/skillparts/system/models/Search.php
+++ b/mirzaev/skillparts/system/models/Search.php
@@ -5,9 +5,14 @@ declare(strict_types=1);
 namespace app\models;
 
 use yii;
-use yii\web\IdentityInterface;
 
 use app\models\traits\SearchByEdge;
+use app\models\connection\Dellin;
+use app\models\Settings;
+
+use datetime;
+use exception;
+use throwable;
 
 /**
  * Поиск
@@ -110,4 +115,435 @@ class Search extends Document
 
         return null;
     }
+
+
+    /**
+     * Поиск содержимого поиска
+     *
+     * @todo В будущем возможно заказ не только поставок реализовать
+     * Переписать реестр и проверку на дубликаты, не понимаю как они работают
+     */
+    public static function content(array $products, int $limit = 50, int $page = 1): Supply|int|array|null
+    {
+        // Инициализация буфера вывода
+        $response = $products;
+
+        // Генерация сдвига по запрашиваемым данным (пагинация)
+        $offset = $limit * ($page - 1);
+
+        foreach ($response as &$row) {
+            // Перебор продуктов
+
+            if ($row instanceof Product) {
+                // В массиве объект - инстанция товара
+
+                // Преобразование к массиву в буфер (унификация данных)
+                $_row = $row->attributes;
+            } else {
+                // В массиве не товар (подразумевается, что это массив с параметрами)
+
+                // Запись в буфер (унификация данных)
+                $_row = $row;
+            }
+
+            // Поиск поставок привязанных к продуктам
+            $connections = Supply::searchByEdge(
+                from: 'product',
+                to: 'supply',
+                edge: 'supply_edge_product',
+                limit: $limit,
+                offset: $offset,
+                direction: 'OUTBOUND',
+                subquery_where: [
+                    ['product._key' => $_row['_key']],
+                    ['supply.catn == product.catn'],
+                    ['supply_edge_product.type' => 'connect']
+                ],
+                where: 'supply._id == supply_edge_product[0]._from',
+                select: '{supply, supply_edge_product}'
+            );
+
+            // Инициализация буфера
+            $buffer_connections = [];
+
+            if (count($connections) === 11) {
+                // Если в базе данных хранится много поставок
+
+                // Инициализация
+                $_row['overload'] = true;
+            }
+
+            foreach ($connections as $key => &$connection) {
+                // Перебор поставок
+
+                if (($cost = $cost['ЦенаЗаЕдиницу'] ?? $connection['supply_edge_product'][0]['onec']['Цены']['Цена']['ЦенаЗаЕдиницу']) < 1
+                    || ($amount = $connection['supply']['amnt'] ?? $connection['supply_edge_product'][0]['onec']['Количество']) < 1
+                ) {
+                    // Цена меньше единицы (подразумевается как ошибка) или количество меньше единицы
+
+                    // Скрыть из выдачи
+                    unset($connections[$key]);
+
+                    continue;
+                }
+
+                // Инициализация аккаунта
+                $connection['account'] = Account::searchBySupplyId($connection['supply_edge_product'][0]['_from']);
+
+                // Инициализация продукта
+                $connection['product'] = Product::searchBySupplyId($connection['supply_edge_product'][0]['_from']);
+
+                try {
+                    // Инициализация данных геолокации
+
+                    try {
+                        $from = (int) $connection['account']['opts']['delivery_from_terminal'] ?? empty(Settings::search()->delivery_from_default) ? 36 : (int) Settings::search()->delivery_from_default;
+                    } catch (exception $e) {
+                        $from = empty(Settings::search()->delivery_from_default) ? 36 : (int) Settings::search()->delivery_from_default;
+                    }
+
+                    try {
+                        $to = (int) yii::$app->user->identity->opts['delivery_to_terminal'] ?? 36;
+                    } catch (Exception $e) {
+                        $to = 36;
+                    }
+
+                    if (
+                        ($buffer_connection = $connection['product']['bffr']["$from-$to"] ?? false)
+                        && time() < $buffer_connection['expires']
+                    ) {
+                        // Найдены данные доставки в буфере
+                        // и срок хранения не превышен, информация актуальна
+
+                        // Запись в буфер вывода
+                        $connection['delivery'] = $buffer_connection['data'];
+                        $connection['delivery']['type'] = 'auto';
+                    } else {
+                        // Инициализация инстанции продукта в базе данных
+                        $product = Product::searchByCatn($connection['product']['catn']);
+
+                        // Инициализация доставки Dellin (автоматическая)
+                        $product->bffr = [
+                            "$from-$to" => [
+                                'data' => $connection['delivery'] = Dellin::calcDeliveryAdvanced(
+                                    $from,
+                                    $to,
+                                    (int) ($connection['product']['wght'] ?? 0),
+                                    (int) ($connection['product']['dmns']['x'] ?? 0),
+                                    (int) ($connection['product']['dmns']['y'] ?? 0),
+                                    (int) ($connection['product']['dmns']['z'] ?? 0)
+                                ),
+                                'expires' => time() + 86400
+                            ]
+                        ] + ($product->bffr ?? []);
+                        $connection['delivery']['type'] = 'auto';
+
+                        // Отправка в базу данных
+                        $product->update();
+                    }
+                } catch (Exception $e) {
+                    $connection['delivery']['error'] = true;
+
+                    // var_dump($e->getMessage());
+                    // var_dump($e->getTrace());
+                    // var_dump($e->getFile());
+
+                    // var_dump(json_decode($e->getMessage(), true)['errors']);
+                    // die;
+                }
+
+                // Инициализация цены (цена поставки + цена доставки + наша наценка)
+                $connection['cost'] = $cost + ($connection['delivery']['price']['all'] ?? $connection['delivery']['price']['one'] ?? 0) + ($settings['increase'] ?? 0);
+
+                // Инициализация версии для рассчета доставки по воздуху
+                $buffer_delivery_avia = $connection;
+
+                try {
+                    // Инициализация данных геолокации
+
+                    if (($cost = $cost['ЦенаЗаЕдиницу'] ?? $buffer_delivery_avia['supply_edge_product'][0]['onec']['Цены']['Цена']['ЦенаЗаЕдиницу']) < 1
+                        || ($amount = $connection['supply']['amnt'] ?? $connection['supply_edge_product'][0]['onec']['Количество']) < 1
+                    ) {
+                        // Цена меньше единицы (подразумевается как ошибка) или количество меньше единицы
+                        // Этот код не будет выполняться, так как цена одна на обе позиции и аналогичная проверка выше уже есть
+                        // Однако я это оставлю для возможных доработок в будущем
+
+                        // Скрыть из выдачи
+                        unset($connections[$key]);
+
+                        continue;
+                    }
+
+                    try {
+                        $from = (int) $buffer_delivery_avia['account']['opts']['delivery_from_terminal'] ?? empty(Settings::search()->delivery_from_default) ? 36 : (int) Settings::search()->delivery_from_default;
+                    } catch (Exception $e) {
+                        $from = empty(Settings::search()->delivery_from_default) ? 36 : (int) Settings::search()->delivery_from_default;
+                    }
+
+                    try {
+                        $to = (int) yii::$app->user->identity->opts['delivery_to_terminal'] ?? 36;
+                    } catch (Exception $e) {
+                        $to = 36;
+                    }
+
+                    if (
+                        ($buffer_connection = $buffer_delivery_avia['product']['bffr']["$from-$to-avia"] ?? false)
+                        && time() < $buffer_connection['expires']
+                    ) {
+                        // Найдены данные доставки в буфере
+                        // и срок хранения не превышен, информация актуальна
+
+                        // Запись в буфер вывода
+                        $buffer_delivery_avia['delivery'] = $buffer_connection['data'];
+                        $buffer_delivery_avia['delivery']['type'] = 'avia';
+                    } else {
+                        // Инициализация инстанции продукта в базе данных
+                        $product = Product::searchByCatn($buffer_delivery_avia['product']['catn']);
+
+                        // Инициализация доставки Dellin (автоматическая)
+                        $product->bffr = [
+                            "$from-$to-avia" => [
+                                'data' => $buffer_delivery_avia['delivery'] = Dellin::calcDeliveryAdvanced(
+                                    $from,
+                                    $to,
+                                    (int) ($buffer_delivery_avia['product']['wght'] ?? 0),
+                                    (int) ($buffer_delivery_avia['product']['dmns']['x'] ?? 0),
+                                    (int) ($buffer_delivery_avia['product']['dmns']['y'] ?? 0),
+                                    (int) ($buffer_delivery_avia['product']['dmns']['z'] ?? 0),
+                                    avia: true
+                                ),
+                                'expires' => time() + 86400
+                            ]
+                        ] + ($product->bffr ?? []);
+                        $buffer_delivery_avia['delivery']['type'] = 'avia';
+
+                        // Отправка в базу данных
+                        $product->update();
+                    }
+                } catch (exception $e) {
+                    $buffer_delivery_avia['delivery']['error'] = true;
+
+                    // echo '
';
+                    // var_dump($e->getMessage());
+                    // var_dump($e->getTrace());
+                    // var_dump($e->getFile());
+
+                    // var_dump(json_decode($e->getMessage(), true)['errors']);
+                    // die;
+                }
+
+                if (!isset($buffer_delivery_avia['delivery']['error']) || $buffer_delivery_avia['delivery']['error'] !== true) {
+                    // Если рассчиталась доставка самолётом
+
+                    // Инициализация цены (цена поставки + цена доставки + наша наценка)
+                    $buffer_delivery_avia['cost'] = $cost + ($buffer_delivery_avia['delivery']['price']['all'] ?? $buffer_delivery_avia['delivery']['price']['one'] ?? 0) + ($settings['increase'] ?? 0);
+
+                    // Запись в буфер
+                    $buffer_connections[] = $buffer_delivery_avia;
+                }
+            }
+
+            // Запись обработанных данных
+            $_row['supplies'] = array_merge($connections, $buffer_connections);
+
+            // Запись из буфера
+            $row = $_row;
+        }
+
+        return $response;
+    }
+
+    /**
+     * Генерация HTML-кода с найденным товаром
+     *
+     * @param array $row Товар сгенерированный через Search::content()
+     * @param string|null $cpny Компания (сейчас пока что это $name - Название)
+     * @param string|null $covr Обложка
+     * @param string|null $catg Категория
+     * @param string|null $catn Артикул
+     * @param array $empties Реестр не найденных товаров
+     *
+     * @return string HTML-элемент с товаром
+     */
+    public static function generate(array &$row, string|null &$cpny = null, string|null &$covr = null, string|null &$catg = null, string|null &$catn = null, array &$empties = []): string
+    {
+        // Инициализация
+        extract($row);
+        $cpny ?? $cpny = 'Без названия';
+        // $dscr ?? $dscr = 'Описание';
+        $catg ?? $catg = 'Категория';
+        $catn ?? $catn = 'Неизвестно';
+        foreach ($imgs ?? [] as $img) {
+            // Перебор изображений для обложки
+
+            if ($img['covr'] ?? false) {
+                // Обложка найдена
+
+                $covr = $img['h150'];
+
+                break;
+            }
+        }
+
+        if (is_null($covr)) {
+            // Обложка не инициализирована
+
+            if (!$covr = $imgs[0]['h150'] ?? false) {
+                // Не удалось использовать первое изображение как обложку
+
+                // Запись обложки по умолчанию
+                $covr = '/img/covers/h150/product.png';
+            }
+        }
+
+        // Инициализация буфера с HTML поставок
+        $supplies_html = '';
+
+        // Инициализация блокировщика для пустого блока (на случай, если нет поставок, чтобы не было дубликатов вывода)
+        $empty_block = false;
+
+        // Инициализация счётчика поставок
+        $supplies_amount = count($row['supplies']);
+
+        // Инициализация указателя номера цикла
+        $supply_iterator = 1;
+
+        foreach (empty($row['supplies']) || count($row['supplies']) === 0 ? [null] : $row['supplies'] as $supply) {
+            // Инициализация модификатора класса
+            if ($supplies_amount > $supply_iterator) {
+                // Это не последняя строка с товаром и его поставками
+
+                $supply_class_modifier = 'mb-1';
+            } else {
+                // Это последняя строка с товаром и его поставками
+
+                $supply_class_modifier = '';
+            }
+
+            if (is_null($supply)) {
+                // Поставки отсутствуют
+
+                // Генерация данных об отсутствии заказов
+                goto no_supplies;
+            } else {
+                // Обычная обработка поставки
+
+                // Инициализация переменных
+                extract($supply);
+            }
+
+            // Инициализация цены
+            $price_raw = $cost;
+            $price = $price_raw . ' ' . $supply_edge_product[0]['onec']['Цены']['Цена']['Валюта'] ?? 'руб';
+
+            // Инициализация количества
+            $amount_raw = $amount =  $supply['amnt'] ?? $supply_edge_product[0]['onec']['Количество'];
+            if (empty($amount_raw) || $amount_raw < 1) {
+                // Уже не используется
+                $amount = 'Под заказ';
+            } else {
+                $amount .= ' шт';
+            }
+
+            if ($amount_raw < 1 || $price_raw < 1) {
+                // Нет в наличии или цена 0 рублей
+
+                // Поставки отстутвуют
+                no_supplies:
+
+                // Проверка на блокировку
+                if ($empty_block) continue;
+
+                $supplies_html .= <<
+                        
+                            
+                                Заказать поиск у оператора
+                            
+                        
+                    
+                HTML;
+
+                // Запись в список ненайденных
+                $empties[] = $catn;
+
+                // Запись блокировщика
+                $empty_block = true;
+
+                // Обновление счётчика
+                ++$supply_iterator;
+
+                continue;
+            }
+
+            // Инициализация доставки
+            if (isset($delivery['error']) || $delivery === '?') {
+                // Не удалось рассчитать доставку
+
+                // Инициализация типа доставки
+                $delivery_type = $delivery['type'] ?? 'auto';
+
+                // Инициализация индикатора
+                $delivery_icon = '';
+
+                // Инициализация времени
+                $delivery = '?';
+            } else {
+                // Удалось рассчитать доставку
+
+                // Инициализация типа доставки
+                $delivery_type = $delivery['type'] ?? 'auto';
+
+                // Инициализация индикатора
+                $delivery_icon = match ($delivery_type) {
+                    'avia' => '',
+                    default => ''
+                };
+
+                // Инициализация даты отправки
+                try {
+                    // Взять данные из "arrivalToOspSender" (Дата прибытия на терминал-отправитель)
+
+                    $delivery_send_date = datetime::createFromFormat('Y-m-d', $delivery['orderDates']['arrivalToOspSender'])->getTimestamp();
+                } catch (throwable $e) {
+                    // Взять данные из "pickup" (Дата передачи груза на адресе отправителя)
+
+                    $delivery_send_date = datetime::createFromFormat('Y-m-d', $delivery['orderDates']['pickup'])->getTimestamp();
+                }
+
+                // Инициализация времени доставки
+                try {
+                    // Доставка по воздуху (подразумевается), данные из "giveoutFromOspReceiver" (Дата и время, с которого груз готов к выдаче на терминале)
+
+                    $delivery_converted = datetime::createFromFormat('Y-m-d H:i:s', $delivery['orderDates']['giveoutFromOspReceiver'])->getTimestamp();
+                } catch (Throwable $e) {
+                    // Автоматическая доставка (подразумевается), данные из "arrivalToOspReceiver" (Дата прибытия натерминал-получатель)
+
+                    $delivery_converted = datetime::createFromFormat('Y-m-d', $delivery['orderDates']['arrivalToOspReceiver'])->getTimestamp();
+                }
+                $delivery = ceil(($delivery_converted - ($delivery_send_date ?? 0)) / 60 / 60 / 24) + 1;
+            }
+
+            // Инициализация индекса аккаунта
+            $index = $account['indx'] ?? 'Неизвестен';
+
+            // Генерация
+            $supplies_html .= <<
+                    $index
+                    $amount
+                    $delivery_icon $delivery дн
+                    $price
+                    
+                        
+                    
+                
+            HTML;
+
+            // Обновление счётчика
+            ++$supply_iterator;
+        }
+
+        return $supplies_html;
+    }
 }
diff --git a/mirzaev/skillparts/system/views/search/index.php b/mirzaev/skillparts/system/views/search/index.php
index 8594eed..88cb878 100644
--- a/mirzaev/skillparts/system/views/search/index.php
+++ b/mirzaev/skillparts/system/views/search/index.php
@@ -3,6 +3,7 @@
 declare(strict_types=1);
 
 use app\models\Product;
+use app\models\Search;
 
 ?>
 
@@ -27,218 +28,29 @@ use app\models\Product;
             
                 
 
-                    
-
- - +
+
+

Товары по запросу: ""

- - - $supply_iterator) { - // Это не последняя строка с товаром и его поставками - - $supply_class_modifier = 'mb-1'; - } else { - // Это последняя строка с товаром и его поставками - - $supply_class_modifier = ''; - } - - if (is_null($supply)) { - // Поставки отсутствуют - - // Генерация данных об отсутствии заказов - goto no_supplies; - } else { - // Обычная обработка поставки - - // Инициализация переменных - extract($supply); - } - - // Инициализация цены - $price_raw = $cost; - $price = $price_raw . ' ' . $supply_edge_product[0]['onec']['Цены']['Цена']['Валюта'] ?? 'руб'; - - // Инициализация количества - $amount_raw = $amount = $supply['amnt'] ?? $supply_edge_product[0]['onec']['Количество']; - if (empty($amount_raw) || $amount_raw < 1) { - // Уже не используется - $amount = 'Под заказ'; - } else { - $amount .= ' шт'; - } - - if ($amount_raw < 1 || $price_raw < 1) { - // Нет в наличии или цена 0 рублей - - // Поставки отстутвуют - no_supplies: - - // Проверка на блокировку - if ($empty_block) continue; - - $supplies_html .= << - - - Заказать поиск у оператора - - -
- HTML; - - // Запись в список ненайденных - $empties[] = $catn; - - // Запись блокировщика - $empty_block = true; - - // Обновление счётчика - ++$supply_iterator; - - continue; - } - - // Инициализация доставки - if (isset($delivery['error']) || $delivery === '?') { - // Не удалось рассчитать доставку - - // Инициализация типа доставки - $delivery_type = $delivery['type'] ?? 'auto'; - - // Инициализация индикатора - $delivery_icon = ''; - - // Инициализация времени - $delivery = '?'; - } else { - // Удалось рассчитать доставку - - // Инициализация типа доставки - $delivery_type = $delivery['type'] ?? 'auto'; - - // Инициализация индикатора - $delivery_icon = match ($delivery_type) { - 'avia' => '', - default => '' - }; - - // Инициализация даты отправки - try { - // Взять данные из "arrivalToOspSender" (Дата прибытия на терминал-отправитель) - - $delivery_send_date = DateTime::createFromFormat('Y-m-d', $delivery['orderDates']['arrivalToOspSender'])->getTimestamp(); - } catch (Throwable $e) { - // Взять данные из "pickup" (Дата передачи груза на адресе отправителя) - - $delivery_send_date = DateTime::createFromFormat('Y-m-d', $delivery['orderDates']['pickup'])->getTimestamp(); - } - - // Инициализация времени доставки - try { - // Доставка по воздуху (подразумевается), данные из "giveoutFromOspReceiver" (Дата и время, с которого груз готов к выдаче на терминале) - - $delivery_converted = DateTime::createFromFormat('Y-m-d H:i:s', $delivery['orderDates']['giveoutFromOspReceiver'])->getTimestamp(); - } catch (Throwable $e) { - // Автоматическая доставка (подразумевается), данные из "arrivalToOspReceiver" (Дата прибытия натерминал-получатель) - - $delivery_converted = DateTime::createFromFormat('Y-m-d', $delivery['orderDates']['arrivalToOspReceiver'])->getTimestamp(); - } - $delivery = ceil(($delivery_converted - ($delivery_send_date ?? 0)) / 60 / 60 / 24) + 1; - } - - // Инициализация индекса аккаунта - $index = $account['indx'] ?? 'Неизвестен'; - - // Генерация - $supplies_html .= << - $index - $amount - $delivery_icon $delivery дн - $price - - - -
- HTML; - - // Обновление счётчика - ++$supply_iterator; - ?> - - - - - - - -
+
@@ -249,14 +61,56 @@ use app\models\Product;
-->
-
- - + + +
+ + + + +

Аналогичные товары

+ + + +
+
+ + + +
+ +
+
+
+ + + + + +
+