125 Commits

Author SHA1 Message Date
root
761db76970 Merge branch 'stable' of https://git.mirzaev.sexy/mirzaev/skillparts into stable 2023-06-20 08:56:37 +00:00
root
9ebb4385cb что-то старое и грустное 2023-06-20 08:55:16 +00:00
1918db868d Merge branch 'stable' of https://git.mirzaev.sexy/mirzaev/skillparts into stable 2022-12-17 00:44:04 +10:00
ec2d478a62 что это?* 2022-12-17 00:44:00 +10:00
root
bfb0fbe4de изменение метров в сантиметры 2022-11-17 07:25:30 +00:00
root
f57d7f6460 исправление ошибок 2022-11-17 07:09:56 +00:00
root
2488594d81 исправление ошибок 2022-11-17 07:08:25 +00:00
root
ebf21a0642 Добавлены производители в фильтр 2022-11-17 06:12:15 +00:00
root
c41e5d4c1c Мелкие доработки 2022-11-11 06:25:46 +00:00
root
b3f2ef72da Множественное исправление багов 2022-11-01 03:37:36 +00:00
root
d21c6ce0ed Merge branch 'stable' of https://git.mirzaev.sexy/mirzaev/skillparts into stable 2022-10-31 00:30:33 +00:00
root
a11a21d1ab исправление иморта деловых линий 2022-10-31 00:29:53 +00:00
root
c11cac01aa php 8.1 2022-10-29 00:27:08 +00:00
root
fc38285d06 крупное обновление всего 2022-10-29 00:20:33 +00:00
root
4fafe85639 залупа 2022-08-04 13:37:24 +00:00
Arsen Mirzaev Tatyano-Muradovich
3ec9b920d2 Починил редактирование товара 2022-06-25 05:13:34 +10:00
Arsen Mirzaev Tatyano-Muradovich
33cc3efc7b починил редактирование изображений и артикула 2022-06-22 11:44:37 +10:00
Arsen Mirzaev Tatyano-Muradovich
2c2e830f0a крупная обнова 2022-06-01 14:24:48 +10:00
Arsen Mirzaev Tatyano-Muradovich
e1fa1c1f5a Исправления 2022-04-14 09:59:17 +10:00
Arsen Mirzaev Tatyano-Muradovich
79773503eb исправления 2022-04-13 15:47:57 +10:00
Arsen Mirzaev Tatyano-Muradovich
68d26118ab Не выводить в аналогах пустые товары 2022-04-13 13:54:07 +10:00
Arsen Mirzaev Tatyano-Muradovich
ab97036822 настройки маршрутизации под производителя 2022-04-13 13:18:06 +10:00
Arsen Mirzaev Tatyano-Muradovich
4f80a465bc Переработа всего под производителя 2022-04-13 12:39:58 +10:00
Arsen Mirzaev Tatyano-Muradovich
e8454defc2 АНАЛоги работают 2022-03-23 12:59:31 +10:00
Arsen Mirzaev Tatyano-Muradovich
bcc60fcd41 Доработка отображения почты 2022-03-11 08:51:53 +10:00
Arsen Mirzaev Tatyano-Muradovich
675c6cbc39 Обрезка почты в сайдбаре 2022-03-11 08:49:35 +10:00
Arsen Mirzaev Tatyano-Muradovich
16ce1ccf59 кнопка создать склад 2022-03-11 08:47:23 +10:00
Arsen Mirzaev Tatyano-Muradovich
4da846825b Починил импорт + доставка в тот же город 2022-03-11 08:39:10 +10:00
Arsen Mirzaev Tatyano-Muradovich
82d6747425 пошел нахуй 2022-03-10 09:00:12 +10:00
Arsen Mirzaev Tatyano-Muradovich
a31214ad6a Починил поиск 2022-03-05 13:44:21 +10:00
Arsen Mirzaev Tatyano-Muradovich
c5e24d7838 Починил аналоги и поиск 2022-03-05 13:44:11 +10:00
Arsen Mirzaev Tatyano-Muradovich
856d4f1995 докинул конфиг 2022-03-01 11:53:33 +10:00
Arsen Mirzaev Tatyano-Muradovich
2f4f38b2aa удаление складов + доставка 2022-03-01 11:53:28 +10:00
Arsen Mirzaev Tatyano-Muradovich
4683cf51db пульверизатор 2022-02-17 09:49:55 +10:00
Arsen Mirzaev Tatyano-Muradovich
e941fbd782 Доработка 2022-02-07 14:50:21 +10:00
Arsen Mirzaev Tatyano-Muradovich
24f079f622 конфиг 2022-02-07 14:29:42 +10:00
Arsen Mirzaev Tatyano-Muradovich
4c91c3192a Активные и неактивные товары 2022-02-07 14:29:18 +10:00
Arsen Mirzaev Tatyano-Muradovich
a971aef392 Нормальное исправление 2022-02-02 16:05:24 +10:00
Arsen Mirzaev Tatyano-Muradovich
ab3c3b8b05 Исправление фильтра на загрузку системных файлов 2022-02-02 15:41:14 +10:00
Arsen Mirzaev Tatyano-Muradovich
8fda1af398 исправление 2022-01-31 15:00:13 +10:00
Arsen Mirzaev Tatyano-Muradovich
953f99245c Работа над сайтом 26 2022-01-31 14:55:51 +10:00
Arsen Mirzaev Tatyano-Muradovich
8abdaf4626 Топовый малыш 2022-01-25 14:46:25 +10:00
Arsen Mirzaev Tatyano-Muradovich
bc98da67ce ой уволили 2022-01-18 14:39:12 +10:00
Arsen Mirzaev Tatyano-Muradovich
70d2ae2935 Терминалы 2021-12-27 14:56:06 +10:00
Arsen Mirzaev Tatyano-Muradovich
913a67d400 Исправления 2021-12-27 14:38:07 +10:00
Arsen Mirzaev Tatyano-Muradovich
b08f7f7d0a Склады 2021-12-27 14:03:15 +10:00
Arsen Mirzaev Tatyano-Muradovich
7a4f12aa94 попа 2021-12-14 14:29:38 +10:00
Arsen Mirzaev Tatyano-Muradovich
9fb2e4dc90 Исправления ляляля хуй соси 2021-12-06 14:20:16 +10:00
Arsen Mirzaev Tatyano-Muradovich
3f0b342d48 ещё 2021-12-01 10:42:49 +10:00
Arsen Mirzaev Tatyano-Muradovich
6daa3112b6 Синтаксические исправления 2021-12-01 10:41:04 +10:00
Arsen Mirzaev Tatyano-Muradovich
8f32df518f Панели аккаунтов, товаров, поставок, прайсов 2021-11-29 11:00:50 +10:00
Arsen Mirzaev Tatyano-Muradovich
29ea6a9a79 небольшие исправления 2021-11-02 12:21:46 +10:00
Arsen Mirzaev Tatyano-Muradovich
1a1392d950 ПРОВЕРКА ПО CATN, PROD И ЦЕНЕ 2021-10-27 11:26:03 +10:00
Arsen Mirzaev Tatyano-Muradovich
1d06acf2e0 Исправление карты yandex 2021-10-14 16:20:10 +10:00
Leonid
e6944c9b6b yandex maps 2021-10-12 14:34:31 +10:00
Arsen Mirzaev Tatyano-Muradovich
2b194457f5 composer 2021-10-12 13:37:52 +10:00
Arsen Mirzaev Tatyano-Muradovich
95b5d920b9 Мелкая графическая доработка 2021-10-12 13:27:44 +10:00
Arsen Mirzaev Tatyano-Muradovich
53281ee422 Доработка блока "Аналогичные товары" 2021-10-12 13:17:30 +10:00
Arsen Mirzaev Tatyano-Muradovich
a235ed25ac Исправление ошибки в товаре + отключение отправки уведомлений при неаутентифицированной сессии 2021-10-08 16:33:38 +10:00
Arsen Mirzaev Tatyano-Muradovich
6bab42ac33 Небольшие исправления 2021-10-07 20:20:43 +10:00
Arsen Mirzaev Tatyano-Muradovich
f76f15b503 nginx 2021-10-07 20:08:41 +10:00
Arsen Mirzaev Tatyano-Muradovich
dbd2982115 nginx 2021-10-07 19:39:35 +10:00
Arsen Mirzaev Tatyano-Muradovich
2a90f36f0c Мелкое исправление вывода ошибок и удаление старых файлов 2021-10-07 09:18:28 +10:00
Arsen Mirzaev Tatyano-Muradovich
43272d051e Создание связей, удаление связей и удаление товаров 2021-10-04 08:25:22 +10:00
Arsen Mirzaev Tatyano-Muradovich
7e63c8be97 Загрузка из Excel 2021-09-28 07:22:17 +10:00
Arsen Mirzaev Tatyano-Muradovich
8c53872955 Принятие и отказ в регистрации 2021-09-22 11:38:53 +10:00
Arsen Mirzaev Tatyano-Muradovich
134ce8f162 Панель модератора 2021-09-16 10:58:47 +10:00
Arsen Mirzaev Tatyano-Muradovich
a36f62e510 Доработка панели модератора 2021-09-14 08:06:29 +10:00
Arsen Mirzaev Tatyano-Muradovich
3cb2aa1a15 Панель модератора для регистрации поставщиков 2021-09-07 06:39:21 +10:00
Arsen Mirzaev Tatyano-Muradovich
d9337944b1 Работа над сайтом 20 2021-08-30 06:41:19 +10:00
Arsen Mirzaev Tatyano-Muradovich
fab247b4bc Исправление ошибок 2021-08-16 15:22:24 +10:00
Arsen Mirzaev Tatyano-Muradovich
96d9a0307c Исправление ошибок 2021-08-16 04:12:22 +10:00
Arsen Mirzaev Tatyano-Muradovich
42d20fd313 Исправление ошибок 2021-08-16 04:03:51 +10:00
Arsen Mirzaev Tatyano-Muradovich
57710d8a96 Исправление ошибок 2021-08-16 04:00:32 +10:00
Arsen Mirzaev Tatyano-Muradovich
044d80ef3e Работа над сайтом 19 2021-08-16 03:55:36 +10:00
Arsen Mirzaev Tatyano-Muradovich
dc30b8c6bb Работа над сайтом 19 2021-08-16 03:53:25 +10:00
Arsen Mirzaev Tatyano-Muradovich
5e01240938 Работа над сайтом 228 2021-08-09 23:48:43 +10:00
Arsen Mirzaev Tatyano-Muradovich
b7c271141f Грамматическое исправление 2021-07-30 20:51:43 +10:00
Arsen Mirzaev Tatyano-Muradovich
d087256181 Обновление панели аутентификации и отображения товаров в заказах 2021-07-30 20:43:37 +10:00
Arsen Mirzaev Tatyano-Muradovich
8270eaed11 Исправления календаря 2021-07-28 15:03:18 +10:00
Arsen Mirzaev Tatyano-Muradovich
7647ca69d5 Доработка календаря (исправление) 2021-07-28 13:41:47 +10:00
Arsen Mirzaev Tatyano-Muradovich
b80b887a11 Доработка календаря 2021-07-28 13:41:21 +10:00
Arsen Mirzaev Tatyano-Muradovich
49ed9bf59e Исправление отображения иконок, а так же ошибки с заголовком referrer 2021-07-28 10:39:50 +10:00
Arsen Mirzaev Tatyano-Muradovich
a6d8ee5e59 Работа над сайтом 17 2021-07-27 19:59:56 +10:00
Arsen Mirzaev Tatyano-Muradovich
b2b5b0737c Докидываю web.php.example 2021-07-26 05:33:37 +10:00
Arsen Mirzaev Tatyano-Muradovich
5f3623a258 Работа над сайтом 16 2021-07-26 05:27:41 +10:00
Arsen Mirzaev Tatyano-Muradovich
177de6b3ef Работа над сайтом 16 2021-07-25 09:05:14 +10:00
Arsen Mirzaev Tatyano-Muradovich
b3b5111006 Работа над сайтом 15 2021-07-19 07:58:31 +10:00
Arsen Mirzaev Tatyano-Muradovich
bf821a9819 Работа над сайтом 14 2021-07-12 04:21:32 +10:00
Arsen Mirzaev Tatyano-Muradovich
2ca929e122 Работа над сайтом 14 2021-07-09 06:49:07 +10:00
Arsen Mirzaev Tatyano-Muradovich
26686374f0 Исправления 2021-07-07 07:05:23 +10:00
Arsen Mirzaev Tatyano-Muradovich
d5bb9137f7 Работа над сайтом 13 2021-07-07 06:56:04 +10:00
Arsen Mirzaev Tatyano-Muradovich
fc59eff6db Работа над сайтом 13 2021-07-07 06:54:33 +10:00
Arsen Mirzaev Tatyano-Muradovich
83295650c9 Работа над сайтом 12 2021-06-29 09:08:01 +10:00
Arsen Mirzaev Tatyano-Muradovich
c4035d8ad3 Улучшенная регистрация пользователя 2021-06-24 15:18:34 +10:00
Arsen Mirzaev Tatyano-Muradovich
cc1e7e7d66 Работа над сайтом 11 2021-06-21 09:21:10 +10:00
Arsen Mirzaev Tatyano-Muradovich
8c4ca42d3c Отправка на почту 2021-06-21 04:21:28 +10:00
Arsen Mirzaev Tatyano-Muradovich
cb315a9fcf Обновление панели заказов 2021-06-16 08:24:26 +10:00
Arsen Mirzaev Tatyano-Muradovich
9e120afc24 Откатываю вперёд 2021-06-15 08:59:53 +10:00
Arsen Mirzaev Tatyano-Muradovich
44ca2e9f92 bootsrap fix 2021-06-15 08:41:16 +10:00
Arsen Mirzaev Tatyano-Muradovich
0fa1c977ca пох 2021-06-15 08:11:29 +10:00
Arsen Mirzaev Tatyano-Muradovich
0d654ad790 Изменение полей в панели модератора + генерация счетов 2021-06-14 20:57:29 +10:00
Arsen Mirzaev Tatyano-Muradovich
4f898bf796 Доработка 2021-06-07 05:53:14 +10:00
Arsen Mirzaev Tatyano-Muradovich
21cec0c5b4 Изменение отображения в поиске 2021-06-07 03:49:13 +10:00
Arsen Mirzaev Tatyano-Muradovich
fab290eacd Проработка панели модератора 2021-06-01 09:03:33 +10:00
Arsen Mirzaev Tatyano-Muradovich
4f8a99d8e2 Создание панели модератора и доработка уведомлений 2021-05-25 06:06:55 +10:00
Arsen Mirzaev Tatyano-Muradovich
248f5470f9 Небольшие внешние изменения 2021-05-17 01:49:45 +10:00
Arsen Mirzaev Tatyano-Muradovich
7e5f39c8e9 Страницы поставщикам и покупателям переделаны + исправление доставки 2021-05-11 06:47:04 +10:00
Arsen Mirzaev Tatyano-Muradovich
5d9228ec1b Проверка подтверждения оферты через сессии 2021-05-04 08:20:21 +10:00
Arsen Mirzaev Tatyano-Muradovich
3cf9f24a20 Докидываю файл настроек 2021-05-04 08:01:27 +10:00
Arsen Mirzaev Tatyano-Muradovich
03b52d071d Оферта, доставка, ограничения, страницы покупателям 2021-05-04 07:59:37 +10:00
Arsen Mirzaev Tatyano-Muradovich
3ea7a9e1eb Докидываю 2021-04-25 20:09:54 +10:00
Arsen Mirzaev Tatyano-Muradovich
85e3d5f1bd Исправления 2021-04-25 20:09:39 +10:00
Arsen Mirzaev Tatyano-Muradovich
0a8c74a9a3 Псевдоанонимные идентификаторы аккаунтов и комментарий к заказу 2021-04-25 20:00:05 +10:00
Arsen Mirzaev Tatyano-Muradovich
ada91bd0b7 Забыл отправить шаблоны файлов настроек 2021-04-19 07:14:44 +10:00
Arsen Mirzaev Tatyano-Muradovich
7865cac5d4 Подключение к ДеловыеЛинии 2021-04-19 07:11:38 +10:00
Arsen Mirzaev Tatyano-Muradovich
d4bc3e2263 Исправления и начало добавления рассчёта доставки 2021-04-13 07:21:31 +10:00
Arsen Mirzaev Tatyano-Muradovich
fe0453f91b Исправления 2021-04-12 05:32:13 +10:00
Arsen Mirzaev Tatyano-Muradovich
a11a5da2e1 Доработка экспорта заказов 2021-04-12 05:28:46 +10:00
Arsen Mirzaev Tatyano-Muradovich
b44194d5a3 test -> dev 2021-04-11 11:50:32 +10:00
Arsen Mirzaev Tatyano-Muradovich
874d5c3f55 Исправление ошибок для импорта из 1С 2021-04-11 11:48:32 +10:00
Arsen Mirzaev Tatyano-Muradovich
b50049eb67 В контроллер скопипастил модель... Исправление 2021-04-11 04:08:09 +10:00
Arsen Mirzaev Tatyano-Muradovich
c169347687 Исправление контроллера ошибок и моделей продуктов 2021-04-11 04:03:32 +10:00
Arsen Mirzaev Tatyano-Muradovich
912b25bea2 Разделение индексного файла (наконец-то) 2021-04-11 03:42:10 +10:00
Arsen Mirzaev Tatyano-Muradovich
7ca19da826 Доработка страницы товаров и их администрирования 2021-04-04 23:54:34 +10:00
272 changed files with 54634 additions and 7074 deletions

View File

@@ -13,8 +13,9 @@
}
],
"require": {
"php": "^8.0.0",
"twbs/bootstrap": ">=4.5",
"php": "^8.1.0",
"ext-intl": "~8.0",
"twbs/bootstrap": "4.6.0",
"yiisoft/yii2": "2.*",
"yiisoft/yii2-bootstrap": ">=2.0.0",
"yiisoft/yii2-swiftmailer": ">=2.0.0",
@@ -26,20 +27,15 @@
"carono/yii2-1c-exchange": "^0.3.1",
"yiisoft/yii2-imagine": "*",
"mirzaev/yii2-arangodb": ">=2.1.x-dev",
"mirzaev/yii2-arangodb-sessions": ">=1.1.x-dev"
"mirzaev/yii2-arangodb-sessions": ">=1.1.x-dev",
"guzzlehttp/guzzle": "^7.3",
"phpoffice/phpspreadsheet": "^1.18",
"hflabs/dadata": "^20.12"
},
"require-dev": {
"codeception/codeception": ">=4.1",
"codeception/module-webdriver": ">=1.0.0",
"yiisoft/yii2-debug": ">=2.1.0",
"yiisoft/yii2-gii": ">=2.1.0",
"yiisoft/yii2-faker": ">=2.0.0",
"codeception/verify": ">=1.1.0",
"codeception/specify": ">=0.4.6",
"symfony/browser-kit": ">=2.7",
"codeception/module-filesystem": ">=1.0.0",
"codeception/module-yii2": ">=1.0.0",
"codeception/module-asserts": ">=1.0.0"
"yiisoft/yii2-faker": ">=2.0.0"
},
"autoload": {
"psr-4": {

5433
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1,3 @@
/import
/import/*
/invoices/*
/accounts/*

View File

@@ -1,22 +1,10 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace app\assets;
use yii;
use yii\web\AssetBundle;
use yii\web\View;
/**
* Main application asset bundle.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class AppAsset extends AssetBundle
{
public $basePath = '@webroot';
@@ -37,17 +25,24 @@ class AppAsset extends AssetBundle
'js/bootstrap/bootstrap.min.js',
'https://cdn.jsdelivr.net/bxslider/4.1.1/jquery.bxslider.min.js',
'https://unpkg.com/cookielib/src/cookie.min.js',
'https://api-maps.yandex.ru/2.1/?apikey=ff21ed7c-2d34-4f91-8d7f-2144ec3e4397&lang=ru_RU',
'js/moment.min.js',
'js/menu.js',
'js/main.js',
'js/account.js',
'js/search.js',
'js/notification.js',
'js/reinitialization.js'
'js/reinitialization.js',
'js/yandex/metrika.js',
'js/yandex/geolocation.js',
'https://www.googletagmanager.com/gtag/js?id=G-6XYKBJJWR4',
'js/google/analytics.js'
];
public $jsOptions = [
// 'position' => View::POS_HEAD
];
public $depends = [
'yii\web\YiiAsset',
'yii\web\YiiAsset'
// 'yii\bootstrap\BootstrapAsset'
];
}

View File

@@ -0,0 +1,36 @@
<?php
namespace app\commands;
use yii\console\Controller;
use yii\console\ExitCode;
use app\models\Account;
class AccountController extends Controller
{
/**
* Сгенерировать уникальные идентификаторы
*
* @param int|string $_key Ключ аккаунта (оставить пустым или отправить "all", если для всех)
* @param bool $init Параметр обозначающий изменение только для тех у кого ранее идентификатор задан не был (без перезаписи)
*/
public function actionGenerateIndex(int|string $_key = null, bool $init = true)
{
// Инициализация
$accounts = empty($_key) || strcasecmp($_key, 'all') === 0 ? Account::readAll() : [Account::searchById($_key)];
// Генерация
$amount = Account::generateIndexes($accounts, $init);
echo 'Обработано аккаунтов: ' . $amount;
if ($amount > 0) {
// Был успешно обработан минимум 1 аккаунт
return ExitCode::OK;
}
return ExitCode::UNSPECIFIED_ERROR;
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace app\commands;
use yii\console\Controller;
use yii\console\ExitCode;
use app\models\connection\Dellin;
class DellinController extends Controller
{
/**
* Импортировать города из ДеловыеЛинии
*/
public function actionImportCities()
{
if (Dellin::importCities()) {
return ExitCode::OK;
}
return ExitCode::UNSPECIFIED_ERROR;
}
/**
* Импортировать терминалы из ДеловыеЛинии
*/
public function actionImportTerminals(?int $account = null)
{
if (Dellin::importTerminals($account)) {
return ExitCode::OK;
}
return ExitCode::UNSPECIFIED_ERROR;
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace app\commands;
use yii\console\Controller;
use yii\console\ExitCode;
use moonland\phpexcel\Excel;
use app\models\Supply;
use app\models\File;
class ImportController extends Controller
{
/**
* Импортировать города из ДеловыеЛинии
*/
public function actionCities()
{
if (Dellin::importCities()) {
return ExitCode::OK;
}
return ExitCode::UNSPECIFIED_ERROR;
}
/**
* Импортировать терминалы из ДеловыеЛинии
*/
public function actionTerminals()
{
if (Dellin::importTerminals()) {
return ExitCode::OK;
}
return ExitCode::UNSPECIFIED_ERROR;
}
/**
* Импортировать поставки из файлов
*/
public function actionSupplies(int $amount = 3)
{
try {
$files = File::searchSuppliesNeededToLoad($amount);
if ($files > 0) {
// Найдены файлы
foreach ($files as $file) {
// Перебор файлов для загрузки
// Загрузка в базу данных
Supply::loadExcel($file);
}
}
} catch (exception $e) {
return ExitCode::UNSPECIFIED_ERROR;
}
return ExitCode::OK;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace app\commands;
use yii\console\Controller;
use yii\console\ExitCode;
use app\models\Supply;
use app\models\File;
class SuppliesController extends Controller
{
/**
* Импортировать города из ДеловыеЛинии
*/
public function actionImport(int $amount = 3)
{
if (Dellin::importCities($amount)) {
return ExitCode::OK;
}
return ExitCode::UNSPECIFIED_ERROR;
}
}

View File

@@ -0,0 +1,136 @@
<?php
namespace app\commands;
use yii\console\Controller;
use yii\console\ExitCode;
use app\models\Invoice;
use app\models\Product;
use app\models\ProductGroup;
use app\models\ImportEdgeSupply;
class TestController extends Controller
{
public function actionInvoice($buyer = 123123, $order = 0)
{
// Генерация счета
Invoice::generate($order, $this->renderPartial('/invoice/order/pattern', [
'buyer' => [
'id' => $buyer,
'info' => 'Неизвестно'
],
'order' => [
'id' => $order,
'date' => time(),
'entries' => [
[
'title' => 'Тестовое вхождение',
'amount' => [
'value' => 1,
'unit' => 'шт'
],
'cost' => [
'value' => 1000,
'unit' => 'руб'
],
'type' => 'supply'
],
[
'title' => 'Тестовое вхождение',
'amount' => [
'value' => 1,
'unit' => 'шт'
],
'cost' => [
'value' => 1000,
'unit' => 'руб'
],
'type' => 'supply'
],
[
'title' => 'Тестовое вхождение',
'amount' => [
'value' => 1,
'unit' => 'шт'
],
'cost' => [
'value' => 1000,
'unit' => 'руб'
],
'type' => 'supply'
],
[
'title' => 'Тестовое вхождение',
'amount' => [
'value' => 5,
'unit' => 'шт'
],
'cost' => [
'value' => 1000,
'unit' => 'руб'
],
'type' => 'supply'
]
]
]
]));
return ExitCode::OK;
}
public function actionAnalogs($_id = 'product/51987159')
{
return ExitCode::OK;
}
public function actionWriteAnalog($_id = 'product/51987159', $analog = 'product/12051485')
{
// Инициализация товара
$product = Product::searchById($_id);
// Инициализация аналога
$analog = Product::searchById($analog);
if (!$group = ProductGroup::searchByProduct($product)) {
// Не найдена группа товаров
// Запись новой группы
$group = ProductGroup::writeEmpty(active: true);
// Запись товара в группу
$group->writeProduct($product);
}
if ($_group = ProductGroup::searchByProduct($analog)) {
// Найдена друга группа у товара который надо добавить в группу
// Перенос всех участников (включая целевой товар)
return $group->transfer($_group);
} else {
// Не найдена группа у товара который надо добавить в группу
// Запись целевого товара в группу
return $group->writeProduct($analog);
}
return ExitCode::OK;
}
public function actionReadAnalog($_id = 'product/51987159')
{
var_dump((ProductGroup::searchByProduct(Product::searchById($_id))->searchProducts()));
return ExitCode::OK;
}
public function actionEdgeMax()
{
var_dump(ImportEdgeSupply::generateVersion());
return ExitCode::OK;
}
}

View File

@@ -2,7 +2,7 @@
declare(strict_types=1);
return [
$config = [
'id' => 'skillparts-console',
'basePath' => dirname(__DIR__),
'bootstrap' => ['log'],
@@ -34,4 +34,15 @@ return [
'class' => 'yii\faker\FixtureController',
],
]
];
];
if (YII_ENV_DEV) {
// configuration adjustments for 'dev' environment
$config['bootstrap'][] = 'gii';
$config['modules']['gii'] = [
'class' => 'yii\gii\Module',
];
}
return $config;

View File

@@ -1,7 +1,28 @@
<?php
return [
'adminEmail' => 'admin@example.com',
'senderEmail' => 'noreply@example.com',
'senderName' => 'Example.com mailer',
'captcha' => [
'suppliers' => [
'public' => null,
'secret' => null
]
],
'mail' => [
'system' => null,
'info' => null
],
'dellin' => [
'nickname' => null,
'password' => null,
'key' => null
],
'cdek' => [
'nickname' => null,
'password' => null,
'key' => null
],
'dadata' => [
'key' => null,
'secret' => null
]
];

View File

@@ -1,40 +0,0 @@
<?php
/**
* Application configuration shared by all test types
*/
return [
'id' => 'skillparts-tests',
'basePath' => dirname(__DIR__),
'aliases' => [
'@bower' => '@vendor/bower-asset',
'@npm' => '@vendor/npm-asset',
],
'language' => 'en-US',
'components' => [
'db' => require __DIR__ . '/test_db.php',
'mailer' => [
'useFileTransport' => true,
],
'assetManager' => [
'basePath' => __DIR__ . '/../web/assets',
],
'urlManager' => [
'showScriptName' => true,
],
'user' => [
'identityClass' => 'app\models\Account',
],
'request' => [
'cookieValidationKey' => 'test',
'enableCsrfValidation' => false,
// but if you absolutely need it set cookie domain to localhost
/*
'csrfCookie' => [
'domain' => 'localhost',
],
*/
],
],
'params' => require __DIR__ . '/params.php',
];

View File

@@ -1,6 +0,0 @@
<?php
$db = require __DIR__ . '/db.php';
// test database! Important not to run tests on production or development databases
$db['dsn'] = 'mysql:host=localhost;dbname=yii2basic_test';
return $db;

View File

@@ -4,6 +4,7 @@ $config = [
'id' => 'skillparts',
'basePath' => dirname(__DIR__),
'bootstrap' => ['log'],
'defaultRoute' => 'main',
'aliases' => [
'@vendor' => dirname(__DIR__) . '/../../../vendor',
'@bower' => '@vendor/bower-asset',
@@ -50,12 +51,30 @@ $config = [
'errorHandler' => [
'errorAction' => 'error',
],
'mailer' => [
'mail_info' => [
'class' => 'yii\swiftmailer\Mailer',
// send all mails to a file by default. You have to set
// 'useFileTransport' to false and configure a transport
// for the mailer to send real emails.
'useFileTransport' => true,
'useFileTransport' => false,
'transport' => [
'class' => 'Swift_SmtpTransport',
'host' => 'smtp.yandex.com',
'username' => 'info@skillparts.ru',
'password' => 'SkillParts_1337',
'port' => '465',
'encryption' => 'ssl',
],
],
'mail_system' => [
'class' => 'yii\swiftmailer\Mailer',
'useFileTransport' => false,
'transport' => [
'class' => 'Swift_SmtpTransport',
'host' => 'smtp.yandex.com',
'username' => 'system@skillparts.ru',
'password' => 'System01001010Null',
'port' => '465',
'encryption' => 'ssl',
],
],
'log' => [
'traceLevel' => YII_DEBUG ? 3 : 0,
@@ -75,9 +94,28 @@ $config = [
'class' => 'yii\rest\UrlRule',
'controller' => 'main'
],
'product/<catn:[^/]+>' => 'product/index',
'product/<catn:[^/]+>/<action:(write|edit|delete)>/<target:(title|catn|desc|image)>' => 'product/<action>-<target>',
'orders' => 'order/index'
'<_key:[0-9]+>' => 'account/index',
'<_key:[0-9]+>/<target:[^/]+>/<action:(read|edit|delete|regenerate)>' => 'account/<action>',
'<_key:[0-9]+>/files/<file:[^/]+>' => 'account/file',
'<_key:[0-9]+>/<action:(accept|decline|data)>' => 'account/<action>',
'product/<prod:[^/]+>/<catn:[^/]+>' => 'product/index',
'product/<prod:[^/]+>/<catn:[^/]+>/<action:(write|delete|connect|disconnect|status)>' => 'product/<action>',
'<section:(product|cart)>/<prod:[^/]+>/<catn:[^/]+>/<action:(read|write|edit|delete)>/<target:(title|catn|name|dscr|prod|dmns|wght|image|cover|comm)>' => '<section>/<action>-<target>',
'products/<action:(read)>/<stts:[^/]+>/' => 'product/<action>',
'profile/geolocation/<action:(init|write)>' => 'profile/geolocation-<action>',
'profile/panel/<panel:(suppliers)>/<block:(requests)>/<action:(search)>' => 'profile/panel-<panel>-<block>-<action>',
'profile/imports/<action:(delete)>' => 'profile/imports-<action>',
'profile/warehouses/<action:(write|delete|rename|close|open)>' => 'profile/warehouses-<action>',
'orders' => 'order/index',
'orders/<filter:[^/]+>' => 'order/index',
'orders/<catn:[^/]+>/<action:(accept)>' => 'order/<action>',
'orders/supply/<catn:[^/]+>/<action:(read|write|edit|delete)>' => 'order/supply-<action>',
'orders/supply/<catn:[^/]+>/<action:(read|write|edit|delete)>/<target:(stts|cost|time|comm)>' => 'order/supply-<action>-<target>',
'invoices/<order:[^/]+>' => 'invoice/index',
'invoices/<order:[^/]+>/<action:(download)>' => 'invoice/<action>',
'verify/send' => 'verify/send',
'verify/<vrfy:[^/]+>' => 'verify/index',
'terminals/<action:(read|write)>' => 'terminal/<action>'
],
],
@@ -85,11 +123,13 @@ $config = [
'modules' => [
'exchange' => [
'class' => 'carono\exchange1c\ExchangeModule',
'exchangeDocuments' => true,
'validateModelOnSave' => true,
'groupClass' => 'app\models\SupplyGroup',
'productClass' => 'app\models\Supply',
'offerClass' => 'app\models\SupplyEdgeProduct',
'partnerClass' => 'app\models\Account',
'documentClass' => 'app\models\Purchase',
'documentClass' => 'app\models\Order',
'auth' => function ($mail, $pswd) {
// Необходимо уничтожить AccountForm
// return (new \app\models\AccountForm())->authentication($mail, $pswd);
@@ -104,6 +144,120 @@ $config = [
]
],
'params' => require __DIR__ . '/params.php',
'on beforeAction' => function ($event) {
if (yii::$app->user->isGuest) {
// Гость
} else {
// Пользователь
// Подтверждение почты
if (yii::$app->user->identity->vrfy !== true) {
// Почта не подтверждена
if (!(str_starts_with(yii::$app->request->getPathInfo(), 'verify')
|| match (yii::$app->request->getPathInfo()) {
'policy', 'notification', 'identification' => true,
default => false
})) {
// Фильтрация страниц
if (yii::$app->request->isPost) {
// POST-запрос
yii::$app->response->format = Response::FORMAT_JSON;
yii::$app->response->statusCode = 401;
return [
'main' => yii::$app->controller->renderPartial('/account/verify'),
'redirect' => '/registration',
'_csrf' => yii::$app->request->getCsrfToken()
];
} else {
// Подразумевается как GET-запрос
// Переадресация на страницу указывающую на необходимость подтвердить почту
yii::$app->response->redirect('/verify')->send();
}
}
}
// Согласие с офертой
if (
!(isset(yii::$app->session['offer_buyer_accepted'])
&& yii::$app->session['offer_buyer_accepted'] === true)
&& (!isset(yii::$app->user->identity->acpt['buyer'])
|| yii::$app->user->identity->acpt['buyer'] === false)
) {
// Нет подтверждения офферты пользователя
if (!(str_starts_with(yii::$app->request->getPathInfo(), 'verify')
|| str_starts_with(yii::$app->request->getPathInfo(), 'offer')
|| match (yii::$app->request->getPathInfo()) {
'notification', 'identification' => true,
default => false
})) {
// Фильтрация страниц
if (yii::$app->request->isPost) {
// POST-запрос
yii::$app->response->format = Response::FORMAT_JSON;
yii::$app->response->statusCode = 401;
return [
'main' => yii::$app->controller->renderPartial('/offer/index'),
'redirect' => '/registration',
'_csrf' => yii::$app->request->getCsrfToken()
];
} else {
// Подразумевается как GET-запрос
// Переадресация на оферту
yii::$app->response->redirect('/offer')->send();
}
}
} else if (
(isset(yii::$app->user->identity->agnt)
&& yii::$app->user->identity->agnt === true)
&& !(isset(yii::$app->session['offer_supplier_accepted'])
&& yii::$app->session['offer_supplier_accepted'] === true)
&& (!isset(yii::$app->user->identity->acpt['supplier'])
|| yii::$app->user->identity->acpt['supplier'] === false)
) {
// Нет подтверждения офферты поставщика
if (!(str_starts_with(yii::$app->request->getPathInfo(), 'verify')
|| str_starts_with(yii::$app->request->getPathInfo(), 'offer')
|| match (yii::$app->request->getPathInfo()) {
'notification', 'identification' => true,
default => false
})) {
// Фильтрация страниц
if (yii::$app->request->isPost) {
// POST-запрос
yii::$app->response->format = Response::FORMAT_JSON;
yii::$app->response->statusCode = 401;
return [
'main' => $this->renderPartial('/offer/supplier'),
'redirect' => '/registration',
'_csrf' => yii::$app->request->getCsrfToken()
];
} else {
// Подразумевается как GET-запрос
// Переадресация на оферту
yii::$app->response->redirect('/offer/suppliers')->send();
}
}
}
}
}
];
if (YII_ENV_DEV) {
@@ -118,4 +272,4 @@ if (YII_ENV_DEV) {
];
}
return $config;
return $config;

View File

@@ -0,0 +1,484 @@
<?php
declare(strict_types=1);
namespace app\controllers;
use yii;
use yii\web\Controller;
use yii\web\Response;
use yii\web\Cookie;
use yii\filters\AccessControl;
use app\models\Account;
use app\models\AccountForm;
class AccountController extends Controller
{
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::class,
'rules' => [
[
'allow' => true,
'actions' => [
'file',
'data',
'restore',
'generate-password'
]
],
[
'allow' => true,
'roles' => ['@'],
'actions' => [
'index',
'edit',
'regenerate'
]
],
[
'allow' => true,
'actions' => [
'read',
'accept',
'decline'],
'matchCallback' => function ($rule, $action): bool {
if (
!yii::$app->user->isGuest
&& (yii::$app->user->identity->type === 'administrator'
|| yii::$app->user->identity->type === 'moderator')
) {
return true;
}
return false;
}
],
[
'allow' => false,
'roles' => ['?'],
'denyCallback' => [$this, 'accessDenied']
]
]
]
];
}
public function accessDenied()
{
// Инициализация
$cookies = yii::$app->response->cookies;
// Запись cookie с редиректом, который выполнится после авторизации
$cookies->add(new Cookie([
'name' => 'redirect',
'value' => yii::$app->request->pathInfo
]));
if (Yii::$app->request->isPost) {
// POST-запрос
// Настройка
Yii::$app->response->format = Response::FORMAT_JSON;
// Генерация ответа
Yii::$app->response->content = json_encode([
'main' => $this->renderPartial('/account/index'),
'redirect' => yii::$app->request->pathInfo,
'_csrf' => Yii::$app->request->getCsrfToken()
]);
} else if (Yii::$app->request->isGet) {
// GET-запрос
$this->redirect('/authentication');
}
}
public function actionIndex()
{
return $this->renderPartial('/accounts/index');
}
/**
* Подтверждение
*
* @param int $_key Идентификатор аккаунта
*/
public function actionAccept(string $_key)
{
if (yii::$app->user->isGuest) {
// Аккаунт не аутентифицирован
} else {
if (yii::$app->request->isPost) {
// AJAX-POST-запрос
if (
yii::$app->user->identity->type === 'administrator'
|| yii::$app->user->identity->type === 'moderator'
) {
// Запрос произведен уполномоченным
if ($account = Account::searchById(Account::collectionName() . '/' . $_key)) {
// Аккаунт найден
$account->type = 'user';
if ($account->update() > 0) {
// Удалось перезаписать данные в хранилище
// Запись в журнал
$account->journal('accepted into suppliers');
// Отправка письма с подтверждением аккаунта
$account->sendMailVerify();
// Настройка формата ответа
yii::$app->response->format = Response::FORMAT_JSON;
return true;
}
}
}
}
}
// Запись кода ответа
yii::$app->response->statusCode = 500;
return false;
}
/**
* Отказ
*
* @param int $_key Идентификатор аккаунта
*/
public function actionDecline(string $_key)
{
if (yii::$app->user->isGuest) {
// Аккаунт не аутентифицирован
} else {
if (yii::$app->request->isPost) {
// AJAX-POST-запрос
if (
yii::$app->user->identity->type === 'administrator'
|| yii::$app->user->identity->type === 'moderator'
) {
// Запрос произведен уполномоченным
if ($account = Account::searchById(Account::collectionName() . '/' . $_key)) {
// Аккаунт найден
// Отправка письма с отказом и причиной
$account->sendMailDecline(yii::$app->request->post('reason') ?? yii::$app->request->get('reason'));
// Настройка формата ответа
yii::$app->response->format = Response::FORMAT_JSON;
// Удаление аккаунта
return $account->delete() > 0;
}
}
}
}
// Запись кода ответа
yii::$app->response->statusCode = 500;
return false;
}
/**
* Запрос файла
*
* @param int $_key Идентификатор аккаунта
* @param string $file Путь до файла от каталога аккаунта
*/
public function actionFile(string $_key, string $file)
{
// Инициализация файла
$file = YII_PATH_PUBLIC . "/../assets/accounts/$_key/files/$file";
if (file_exists($file)) {
// Удалось найти файл
return $this->response->sendFile($file);
} else {
// Не удалось найти файл
// Запись кода ответа
yii::$app->response->statusCode = 500;
// Перенаправление на страницу аккаунта
return yii::$app->response->redirect("/$_key");
}
}
/**
* Редактирование параметра
*
* @param int $_key
* @param string $target
*
* @return void
*/
public function actionEdit(int $_key, string $target)
{
if (yii::$app->user->isGuest) {
// Аккаунт не аутентифицирован
} else {
if (yii::$app->request->isPost) {
// AJAX-POST-запрос
if (
$_key === yii::$app->user->identity->_key ||
(yii::$app->user->identity->type === 'administrator'
|| yii::$app->user->identity->type === 'moderator')
) {
// Запрос произведен с изменяемого аккаунта, либо уполномоченным
if ($account = Account::searchById(Account::collectionName() . '/' . $_key)) {
// Аккаунт найден
// Запись параметра
$account->{$target} = yii::$app->request->post('value') ?? yii::$app->request->get('value') ?? 'Неизвестно';
// Настройка формата ответа
yii::$app->response->format = Response::FORMAT_JSON;
return $account->update() > 0;
}
}
}
}
// Запись кода ответа
yii::$app->response->statusCode = 500;
return false;
}
/**
* Регенерация параметра
*
* @param int $_key Идентификатор
* @param string $target
*
* @return void
*/
public function actionRegenerate(int $_key, string $target)
{
if (yii::$app->user->isGuest) {
// Аккаунт не аутентифицирован
} else {
if (yii::$app->request->isPost) {
// AJAX-POST-запрос
if ($account = Account::searchById(Account::collectionName() . '/' . $_key)) {
// Аккаунт найден
if (
$account->_key === yii::$app->user->identity->_key ||
(yii::$app->user->identity->type === 'administrator'
|| yii::$app->user->identity->type === 'moderator')
) {
// Запрос произведен с изменяемого аккаунта, либо уполномоченным
if ($target === 'indx') {
// Запрошена регенерация индекса
// Настройка формата ответа
yii::$app->response->format = Response::FORMAT_JSON;
// Регенерация индекса
return Account::generateIndexes([$account], force: true);
}
}
}
}
}
// Запись кода ответа
yii::$app->response->statusCode = 500;
return false;
}
/**
* Информация
*
* @param int $_key Идентификатор
*/
public function actionData(int $_key)
{
if (yii::$app->request->isPost) {
// AJAX-POST-запрос
// Инициализация аккаунта (тот кто выполняет запрос)
$account = account::initAccount();
// Поиск данных об аккаунта (запрашиваемом)
$data = account::initAccount($_key)->getAttributes();
if ($account->isAdmin() || $account->isModer()) {
// Авторизован как работник
} else if ((int) $account->_key === $_key) {
// Авторизован как владелец аккаунта
unset(
$data['vrfy'],
$data['geol'],
$data['auth'],
$data['jrnl'],
$data['acpt'],
$data['pswd']
);
} else {
// Не авторизован
// Очистка от защищенных свойств
$data = [
'_key' => $data['_key'],
'indx' => $data['indx'],
'agnt' => $data['agnt'],
'type' => $data['type']
];
}
// Настройка формата ответа
yii::$app->response->format = Response::FORMAT_JSON;
return [
'data' => $data,
'_csrf' => Yii::$app->request->getCsrfToken()
];
}
}
/**
* Восстановление пароля
*
* @return string
*/
public function actionRestore()
{
// Инициализация
$model = new AccountForm(yii::$app->request->post('AccountForm'));
$type = yii::$app->request->post('type') ?? yii::$app->request->get('type');
$target = yii::$app->request->post('target') ?? yii::$app->request->get('target');
// Фильтрация
$target = match ($target) {
'panel' => 'panel',
'main' => 'main',
default => 'main'
};
// Рендер для всплывающей панели
$panel = $target === 'panel';
if (yii::$app->request->isPost) {
// AJAX-POST-запрос
// Настройка кода ответа
yii::$app->response->format = Response::FORMAT_JSON;
if (yii::$app->user->isGuest || $model->validate()) {
// Аккаунт не аутентифицирован и проверка пройдена
// Отправка запроса на генерацию пароля и запись ответа
$return = [
'status' => Account::restoreSend($model->mail),
'_csrf' => yii::$app->request->getCsrfToken()
];
return $return;
} else {
// Аккаунт аутентифицирован
// Настройка кода ответа
yii::$app->response->statusCode = 400;
return [
'redirect' => '/',
'_csrf' => yii::$app->request->getCsrfToken()
];
}
}
}
/**
* Генерация нового пароля
*
* @return string
*/
public function actionGeneratePassword(string $id, string $key)
{
if ($account = Account::searchById(Account::collectionName() . "/$id")) {
// Найден аккаунт
if ($account->chpk === $key) {
// Ключи совпадают
// Инициализация буфера пароля
$old = $account->pswd;
// Генерация пароля
$account->restoreGenerate();
if ($account->pswd !== $old) {
// Успешно сгенерирован новый пароль
// Инициализация формы аутентификации
$form = new AccountForm;
// Запись параметров
$form->mail = $account->mail;
$form->pswd = $account->pswd;
// Аутентификация
$form->authentication();
}
}
}
// Перенаправление на главную страницу
$this->redirect('/');
}
/**
* Генерация нового пароля
*
* @return string
*/
public function actionRead(int $page = 1): string|array|null
{
if (yii::$app->request->isPost) {
// POST-запрос
// Инициализация входных параметров
$amount = yii::$app->request->post('amount') ?? yii::$app->request->get('amount') ?? 20;
$order = yii::$app->request->post('order') ?? yii::$app->request->get('order') ?? ['DESC'];
// Инициализация cookie
$cookies = yii::$app->response->cookies;
// Чтение аккаунтов
$accounts = Account::read(limit: $amount, page: $page, order: $order);
// Запись формата ответа
yii::$app->response->format = Response::FORMAT_JSON;
return [
'accounts' => $this->renderPartial('/account/list', compact('accounts', 'amount', 'page')),
'_csrf' => yii::$app->request->getCsrfToken()
];
}
return false;
}
}

View File

@@ -3,7 +3,8 @@
namespace app\controllers;
use app\models\AccountForm;
use app\models\Order;
use app\models\Notification;
use yii;
use yii\web\Controller;
use yii\web\Response;
@@ -11,6 +12,7 @@ use yii\filters\AccessControl;
use Throwable;
use Exception;
use yii\bootstrap\ActiveForm;
class AuthenticationController extends Controller
{
@@ -35,32 +37,45 @@ class AuthenticationController extends Controller
public function actionIndex()
{
if (yii::$app->request->isAjax) {
// Инициализация
$model = new AccountForm(yii::$app->request->post('AccountForm'));
$type = yii::$app->request->post('type') ?? yii::$app->request->get('type');
$target = yii::$app->request->post('target') ?? yii::$app->request->get('target');
$model->scenario = $model::SCENARIO_AUTHENTICATION;
// Фильтрация
$target = match ($target) {
'panel' => 'panel',
'main' => 'main',
default => 'main'
};
// Рендер для всплывающей панели
$panel = $target === 'panel';
if (yii::$app->request->isPost) {
// AJAX-POST-запрос
// Инициализация
$model = new AccountForm(yii::$app->request->post('AccountForm'));
$model->scenario = $model::SCENARIO_AUTHENTICATION;
// Настройка кода ответа
yii::$app->response->format = Response::FORMAT_JSON;
if (!yii::$app->user->isGuest || $model->authentication()) {
// Аккаунт аутентифицирован
// Создание сессии
// yii::$app->session->open();
// Отправка уведомления
Notification::_write('Вы аутентифицированы с устройства ' . $_SERVER['HTTP_USER_AGENT'] . ' ' . $_SERVER['REMOTE_ADDR'], true, yii::$app->user->identity->_key, Notification::TYPE_NOTICE);
// Инициализация
$notifications_button = $this->renderPartial('/notification/button');
$notifications_panel_full = true;
$notifications_panel = $this->renderPartial('/notification/panel', compact('notifications_panel_full'));
$notifications_panel = $this->renderPartial('/notification/panel', ['notifications_panel_full' => true]);
$cart_button = $this->renderPartial('/cart/button', ['cart_amount' => Order::count(supplies: true)]);
// Запись ответа
$return = [
'menu' => $this->renderPartial('/account/panel/authenticated', compact(
'notifications_button',
'notifications_panel',
'notifications_panel_full'
'cart_button'
)),
'_csrf' => yii::$app->request->getCsrfToken()
];
@@ -71,21 +86,13 @@ class AuthenticationController extends Controller
// Запись ответа
$return['redirect'] = '/' . $cookies['redirect'];
try {
if (empty($return['main'] = $this->renderPartial($return['redirect']))) {
throw new Exception('Представление найдено, но вернуло пустой результат');
}
} catch (Throwable $t) {
$return['main'] = $this->renderPartial($return['redirect'] . '/index');
}
// Генерация и запись
// $controller = 'app\\controllers\\' . ucfirst($cookies['redirect']) . 'Controller';
// $action = 'action' . ucfirst($cookies['redirect_action']);
// $return['main'] = (new $controller())->$action();
// Очистка cookie
unset(yii::$app->response->cookies['redirect']);
yii::$app->response->format = Response::FORMAT_HTML;
// Переадресация
return $this->redirect($return['redirect']);
} else {
// Не найдено cookie с переадресацией
@@ -102,10 +109,11 @@ class AuthenticationController extends Controller
} else {
// Аккаунт не аутентифицирован
// Настройка кода ответа
yii::$app->response->statusCode = 400;
return [
'main' => $this->renderPartial('/account/index', compact('model')),
$target => $this->renderPartial('/account/index', compact('model', 'panel')),
'redirect' => '/authentication',
'_csrf' => yii::$app->request->getCsrfToken()
];
@@ -115,7 +123,7 @@ class AuthenticationController extends Controller
if (!yii::$app->user->isGuest) {
yii::$app->response->redirect('/');
} else {
return $this->render('/account/index');
return $this->render('/account/index', compact('model', 'panel'));
}
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace app\controllers;
use yii\web\Controller;
class BuyersController extends Controller
{
public function actionIndex()
{
return $this->renderPartial('/buyers/index');
}
}

View File

@@ -4,44 +4,133 @@ declare(strict_types=1);
namespace app\controllers;
use app\models\Account;
use yii;
use yii\filters\AccessControl;
use yii\web\Controller;
use yii\web\Response;
use yii\web\Cookie;
use app\models\Product;
use app\models\Order;
use app\models\OrderEdgeSupply;
use app\models\Notification;
use Exception;
class CartController extends Controller
{
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::class,
'rules' => [
[
'allow' => true,
'roles' => ['@'],
'actions' => [
'index',
'edit-comm',
'count'
]
],
[
'allow' => false,
'roles' => ['?'],
'denyCallback' => [$this, 'accessDenied']
]
]
]
];
}
public function accessDenied()
{
// Инициализация
$cookies = yii::$app->response->cookies;
// Запись cookie с редиректом, который выполнится после авторизации
$cookies->add(new Cookie([
'name' => 'redirect',
'value' => yii::$app->request->pathInfo
]));
if (yii::$app->request->isPost) {
// POST-запрос
// Настройка
yii::$app->response->format = Response::FORMAT_JSON;
// Генерация ответа
yii::$app->response->content = json_encode([
'main' => $this->renderPartial('/account/index'),
'redirect' => '/cart',
'_csrf' => yii::$app->request->getCsrfToken()
]);
} else if (yii::$app->request->isGet) {
// GET-запрос
$this->redirect('/authentication');
}
}
/**
* Страница: "Корзина"
*
* @see $this->behaviors Доступ только аутентифицированным
*/
public function actionIndex(): string|array|null
public function actionIndex(): string|array|null|Response
{
// Инициализация
$page = yii::$app->request->get('page') ?? yii::$app->request->post('page') ?? 1;
$account = yii::$app->user;
$account = Account::initAccount();
// Поиск корзины (текущего заказа)
$model = Order::search();
$data = Order::searchSmart()[0] ?? null;
if (empty($model)) {
if (empty($data['order'])) {
// Корзина не инициализирована
// Инициализация
$model = new Order();
$model->save() or throw new Exception('Не удалось инициализировать заказ');
$data['order'] = new Order();
// Подключение
$model->connect($account);
if ($data['order']->save()) {
// Удалось инициализировать заказ
// Подключение заказа к аккаунту
$data['order']->connect($account);
} else {
throw new Exception('Не удалось инициализировать заказ');
}
}
// Инициализация содержимого корзины
$supplies = $model->content(10, $page);
$data['supplies'] = $data['order']->supplies(10, $page);
// Инициализация данных списка для выбора терминала
$delivery_to_terminal_list = $account->genListTerminalsTo();
$array_unshift_in_start = function (array &$array, string|int $key, mixed $value) {
$array = array_reverse($array, true);
$array[$key] = $value;
return $array = array_reverse($array, true);
};
$array_write_default_value = function (array &$array, string $key = 'Выберите', string $value = 'Выберите') use ($array_unshift_in_start) {
if (isset($array[$key])) {
// Смещение или ассоциация найдена
// Деинициализация
unset($array[$key]);
// Инициализация
$array_unshift_in_start($array, $key, $value);
}
};
// Сортировка по алфавиту
asort($delivery_to_terminal_list);
// Перемещение в начало массива значения "Выберите"
$array_write_default_value($delivery_to_terminal_list);
if (yii::$app->request->isPost) {
// POST-запрос
@@ -49,13 +138,103 @@ class CartController extends Controller
yii::$app->response->format = Response::FORMAT_JSON;
return [
'main' => $this->renderPartial('index', compact('model', 'supplies')),
'main' => $this->renderPartial('index', compact('account', 'data', 'delivery_to_terminal_list')),
'title' => 'Корзина',
'redirect' => '/cart',
'_csrf' => yii::$app->request->getCsrfToken()
];
}
return $this->render('index', compact('model', 'supplies'));
return $this->render('index', compact('account', 'data', 'delivery_to_terminal_list'));
}
public function actionEditComm(string $catn, string $prod): array|string|null
{
// Инициализация
$return = [
'_csrf' => yii::$app->request->getCsrfToken()
];
if (is_null($catn) || is_null($prod)) {
// Не получен артикул
yii::$app->response->statusCode = 500;
goto end;
}
if ($edges = OrderEdgeSupply::searchBySupplyCatnAndProd($catn, $prod, Order::searchByType()[0]['order'])) {
// Рёбра найдены (связи заказа с поставкой)
// Инициализация
$amount = 0;
foreach ($edges as $edge) {
// Перебор рёбер (связей заказа с поставкой)
// Инициализация
$text = yii::$app->request->post('text') ?? yii::$app->request->get('text') ?? 'Комментарий к заказу';
$comm = $edge->comm ?? null;
$edge->comm = empty($text) ? 'Комментарий к заказу' : $text;
if ($edge->save()) {
// Ребро обновлено
// Запись в журнал
$edge->journal('update', ['comm' => ['from' => $comm, 'to' => $edge->comm]]);
// Обновление счётчика
++$amount;
// Запись в буфер ответа
$return['comm'] = $edge->comm;
}
}
if ($amount > 0) {
// Удалось записать минимум 1 связь с поставкой
Notification::_write("Обновлён комментарий к товару $catn ($amount шт)");
} else {
// Не удалось записать минимум 1 связь с поставкой
Notification::_write("Не удалось обновить комментарий к товару $catn");
}
}
/**
* Конец алгоритма
*/
end:
if (yii::$app->request->isPost) {
// POST-запрос
yii::$app->response->format = Response::FORMAT_JSON;
return $return;
}
if ($model = Product::searchByCatnAndProd($catn, $prod)) {
return $this->render('index', compact('model'));
} else {
return $this->redirect('/');
}
}
public function actionCount(): array|string|null
{
if (yii::$app->request->isPost) {
// POST-запрос
// Настройка типа ответа
yii::$app->response->format = Response::FORMAT_JSON;
return [
'button' => $this->renderPartial('/cart/button', ['cart_amount' => Order::count(supplies: true)]),
'_csrf' => yii::$app->request->getCsrfToken()
];
}
}
}

View File

@@ -7,7 +7,7 @@ use yii\web\Controller;
class ErrorController extends Controller
{
public function actionIndex()
public function actionIndex(): string
{
$exception = Yii::$app->errorHandler->exception;
@@ -15,21 +15,26 @@ class ErrorController extends Controller
// Исключение не выброшено
// Запись кода ошибки
$statusCode = $exception->statusCode;
$code = $exception->statusCode ?? $exception->getCode() ?? 0;
// Запись названия ошибки
$name = match ($exception->statusCode) {
$title = match ($code) {
404 => '404 (Не найдено)',
default => $exception->getName()
default => '500 (Ошибка сервера)'
};
// Запись сообщения об ошибке
$message = match ($exception->statusCode) {
$description = match ($code) {
404 => 'Страница не найдена',
default => $exception->getMessage()
};
return $this->render('/error', compact('exception', 'statusCode', 'name', 'message'));
return $this->render('/error', compact('exception', 'code', 'title', 'description'));
}
}
public static function throw(string $title, string $description): string {
return yii::$app->controller->render('/error', compact('title', 'description'));
}
}

View File

@@ -9,6 +9,7 @@ use yii\web\Controller;
use yii\web\Response;
use app\models\AccountForm;
use app\models\Order;
class IdentificationController extends Controller
{
@@ -38,14 +39,15 @@ class IdentificationController extends Controller
// Инициализация
$notifications_button = $this->renderPartial('/notification/button');
$notifications_panel_full = true;
$notifications_panel = $this->renderPartial('/notification/panel', compact('notifications_panel_full'));
$notifications_panel = $this->renderPartial('/notification/panel', ['notifications_panel_full' => true]);
$cart_button = $this->renderPartial('/cart/button', ['cart_amount' => Order::count(supplies: true)]);
// Запись ответа
$return = [
'menu' => $this->renderPartial('/account/panel/authenticated', compact(
'notifications_button',
'notifications_panel'
'notifications_panel',
'cart_button'
)),
'_csrf' => yii::$app->request->getCsrfToken()
];

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace app\controllers;
use yii;
use yii\web\Controller;
use app\models\Order;
class InvoiceController extends Controller
{
public function actionIndex(int $order)
{
if ($order = Order::searchById(Order::collectionName() . '/' . $order)) {
return $this->renderPartial('/invoice/order/pattern', [
'account' => yii::$app->user->identity,
'order' => [
'id' => $order->_key,
'date' => $order->date ?? time() // @todo доделать
]
]);
}
}
public function actionDownload(int $order)
{
// Инициализация файла
$file = YII_PATH_PUBLIC . '/../assets/invoices/' . $order . '/invoice.xlsx';
if (file_exists($file)) {
// Удалось найти файл
return $this->response->sendFile($file);
} else {
// Не удалось найти файл
// Запись кода ответа
yii::$app->response->statusCode = 500;
return yii::$app->response->redirect('/orders');
}
}
}

View File

@@ -92,7 +92,7 @@ class NotificationController extends Controller
* @param bool $new Активация проверки на то, что уведомление не получено
* @param bool $count Посчитать
*/
$search = function (bool $new = false, bool $count = false) use ($model, $account, $type, $let, $limit): array|int {
$search = function (bool $new = false, bool $count = false) use ($model, $account, $type, $let, $limit): array|int|null|Notification {
if ($count) {
// Запрошен подсчёт непрочитанных уведомлений
@@ -164,7 +164,7 @@ class NotificationController extends Controller
goto end;
}
foreach ($notifications as $notification) {
foreach (is_array($notifications) ? $notifications : [$notifications] as $notification) {
// Перебор найденных уведомлений
if ($preload) {
@@ -174,7 +174,7 @@ class NotificationController extends Controller
}
// Запись ребра: ПОЛЬЗОВАТЕЛЬ -> УВЕДОМЛЕНИЕ (о том, что уведомление прочитано)
AccountEdgeNotification::write(yii::$app->user->id, $notification->readId(), $type);
AccountEdgeNotification::write($account->readId(), $notification->readId(), $type);
}
if (yii::$app->request->post('last')) {
@@ -184,7 +184,7 @@ class NotificationController extends Controller
$notification = $notifications[0];
$return['popup'] = [
'html' => $this->renderPartial('popup', compact('model', 'notification')),
'html' => $this->renderPartial('popup', compact('model', 'notification', 'account')),
'id' => 'popup/' . $notification->readId()
];
} else if (yii::$app->request->post('stream')) {

View File

@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace app\controllers;
use yii;
use yii\filters\AccessControl;
use yii\web\Cookie;
use yii\web\Response;
use yii\web\Controller;
class OfferController extends Controller
{
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::class,
'rules' => [
[
'allow' => true,
'actions' => ['index', 'suppliers'],
],
[
'allow' => true,
'roles' => ['@'],
'actions' => ['accept', 'accept-suppliers']
],
[
'allow' => false,
'roles' => ['?'],
'denyCallback' => [$this, 'accessDenied']
]
]
]
];
}
public function accessDenied()
{
// Инициализация
$cookies = yii::$app->response->cookies;
// Запись cookie с редиректом, который выполнится после авторизации
$cookies->add(new Cookie([
'name' => 'offer',
'value' => yii::$app->request->pathInfo
]));
if (yii::$app->request->isPost) {
// POST-запрос
// Настройка
yii::$app->response->format = Response::FORMAT_JSON;
// Генерация ответа
yii::$app->response->content = json_encode([
'redirect' => '/authentication',
'_csrf' => yii::$app->request->getCsrfToken()
]);
} else if (Yii::$app->request->isGet) {
// GET-запрос
$this->redirect('/authentication');
}
}
public function actionIndex()
{
return $this->render('/offer/index');
}
public function actionSuppliers()
{
return $this->render('/offer/supplier');
}
public function actionAccept()
{
// Инициализация
yii::$app->user->identity->acpt ?? yii::$app->user->identity->acpt = [];
// Запись
yii::$app->user->identity->acpt += ['buyer' => true];
if (yii::$app->user->identity->save()) {
// Удалось записать данные
// Запись в сессию
yii::$app->session['offer_buyer_accepted'] = true;
yii::$app->response->redirect('/');
}
}
public function actionAcceptSuppliers()
{
// Инициализация
yii::$app->user->identity->acpt ?? yii::$app->user->identity->acpt = [];
yii::$app->user->identity->acpt += ['supplier' => true];
if (yii::$app->user->identity->save()) {
// Удалось записать данные
// Запись в сессию
yii::$app->session['offer_supplier_accepted'] = true;
yii::$app->response->redirect('/');
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace app\controllers;
use yii\web\Controller;
use app\models\Terminal;
class PartnersController extends Controller
{
public function actionIndex()
{
$terminals = Terminal::read(500);
return $this->render('/partners/index', compact('terminals'));
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace app\controllers;
use yii\web\Controller;
class PolicyController extends Controller
{
public function actionIndex()
{
return $this->render('/policy/index');
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -10,52 +10,65 @@ use yii\web\Controller;
use yii\web\Response;
use app\models\AccountForm;
use app\models\Order;
use yii\bootstrap\ActiveForm;
class RegistrationController extends Controller
{
// public function behaviors()
// {
// return [
// 'access' => [
// 'class' => AccessControl::class,
// 'rules' => [
// [
// 'allow' => true,
// 'roles' => ['?'],
// ]
// ],
// ]
// ];
// }
public function actionIndex()
{
// Инициализация
$model = new AccountForm(yii::$app->request->post('AccountForm') ?? yii::$app->request->get('AccountForm'));
$type = yii::$app->request->post('type') ?? yii::$app->request->get('type');
$target = yii::$app->request->post('target') ?? yii::$app->request->get('target');
$model->scenario = $model::SCENARIO_REGISTRATION;
// Фильтрация
$target = match ($target) {
'panel' => 'panel',
'main' => 'main',
default => 'main'
};
// Рендер для всплывающей панели
$panel = $target === 'panel';
if (yii::$app->request->isPost) {
// POST-запрос
yii::$app->response->format = Response::FORMAT_JSON;
if (!yii::$app->user->isGuest || $model->registration()) {
if ($type === 'registration' && (!yii::$app->user->isGuest || $model->registration())) {
// Данные прошли проверку и аккаунт был создан
// Аутентификация
$model->scenario = $model::SCENARIO_AUTHENTICATION;
$model->authentication();
if (!$model->authentication()) {
// Не удалось аутентифицироваться
yii::$app->response->statusCode = 401;
$model->scenario = $model::SCENARIO_REGISTRATION;
return [
$target => $this->renderPartial('/account/index', compact('model', 'panel') + ['registration' => true]),
'redirect' => '/registration',
'_csrf' => yii::$app->request->getCsrfToken()
];
}
// Инициализация
$notifications_button = $this->renderPartial('/notification/button');
$notifications_panel_full = true;
$notifications_panel = $this->renderPartial('/notification/panel', compact('notifications_panel_full'));
$notifications_panel = $this->renderPartial('/notification/panel', ['notifications_panel_full' => true]);
$cart_button = $this->renderPartial('/cart/button', ['cart_amount' => Order::count(supplies: true)]);
// Запись ответа
$return = [
'menu' => $this->renderPartial('/account/panel/authenticated', compact(
'notifications_button',
'notifications_panel'
'notifications_panel',
'cart_button'
)),
'_csrf' => yii::$app->request->getCsrfToken()
];
@@ -65,10 +78,14 @@ class RegistrationController extends Controller
// Запись ответа
$return['redirect'] = '/' . $cookies['redirect'];
$return['main'] = $this->renderPartial($return['redirect'] . '/index');
// Очистка cookie
unset(yii::$app->response->cookies['redirect']);
yii::$app->response->format = Response::FORMAT_HTML;
// Переадресация
return $this->redirect($return['redirect']);
} else {
// Не найдено cookie с переадресацией
@@ -88,7 +105,7 @@ class RegistrationController extends Controller
yii::$app->response->statusCode = 400;
return [
'main' => $this->renderPartial('/account/index', compact('model')),
$target => $this->renderPartial('/account/index', compact('model', 'panel') + ['registration' => true]),
'redirect' => '/registration',
'_csrf' => yii::$app->request->getCsrfToken()
];
@@ -98,7 +115,7 @@ class RegistrationController extends Controller
if (!yii::$app->user->isGuest) {
yii::$app->response->redirect('/');
} else {
return $this->render('/account/index', compact('model'));
return $this->render('/account/index', compact('model', 'panel') + ['registration' => true]);
}
}
}

View File

@@ -9,22 +9,29 @@ use yii\web\Controller;
use yii\web\Response;
use app\models\Product;
use app\models\Supply;
use app\models\Search;
/**
* @todo
* 1. Ограничение доступа
*/
class SearchController extends Controller
{
/**
* @todo Сессию привязать к аккаунту и проверку по нему делать, иначе её можно просто сбрасывать
* @todo
* 1. Сессию привязать к аккаунту и проверку по нему делать, иначе её можно просто сбрасывать
* 2. Пагинация
*/
public function actionIndex(): array|string
{
// Инициализация параметров
$auth_only = false;
$account = yii::$app->user->identity;
if ($auth_only && yii::$app->user->isGuest) {
// Если активирован режим "Поиск только аутентифицированным" и запрос пришел не от аутентифицированного
// Запись кода ответа: 401 (необходима авторизация)
yii::$app->response->statusCode = 401;
@@ -44,7 +51,7 @@ class SearchController extends Controller
yii::$app->response->format = Response::FORMAT_JSON;
return [
'panel' => $this->renderPartial('/search/panel', ['history' => true]),
'panel' => $this->renderPartial('/search/panel', compact('account') + ['history' => true]),
'_csrf' => yii::$app->request->getCsrfToken()
];
}
@@ -59,7 +66,7 @@ class SearchController extends Controller
$timer = 0;
// Период пропуска запросов (в секундах)
$period = 5;
$period = 3;
$keep_connect = false;
$sanction = false;
$sanction_condition = ($session['last_request'] + $period - time()) < $period;
@@ -99,7 +106,7 @@ class SearchController extends Controller
$return = [
'timer' => $timer,
'panel' => $this->renderPartial('/search/loading'),
'search_line_window_show' => 1,
'_csrf' => yii::$app->request->getCsrfToken()
];
} else {
@@ -128,49 +135,42 @@ class SearchController extends Controller
$limit = yii::$app->request->isPost ? 10 : 20;
if ($response = Product::searchByPartialCatn($query, $limit, ['catn' => 'catn', '_key' => '_key'])) {
if ($response = Product::searchByPartialCatn($query, 'active', $limit, [
'_key' => '_key',
'catn' => 'catn',
'prod' => 'prod',
// Баг с названием DESC
'dscr' => 'dscr',
'catg' => 'catg',
'imgs' => 'imgs',
'name' => 'name',
'prod' => 'prod',
'dmns' => 'dmns',
'stts' => 'stts'
])) {
// Данные найдены по поиску в полях Каталожного номера
foreach ($response as &$row) {
// Перебор продуктов
// Генерация данных для представления
$response = Search::content(products: $response);
// Поиск поставок привязанных к продуктам
$row['supplies'] = 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_edge_product[0]'
);
if (count($row['supplies']) === 11) {
// Если в базе данных хранится много поставок
// Инициализация
$row['overload'] = true;
}
}
// Запись ответа
$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')),
'search_line_window_show' => 1,
'_csrf' => yii::$app->request->getCsrfToken()
];
if ((int) yii::$app->request->post('advanced')) {
// Полноценный поиск
// Запись ответа
$return['main'] = $this->renderPartial('/search/index', compact('response', 'query'));
$return['search_line_window_show'] = 1;
$return['redirect'] = '/search?type=product&q=' . $query;
}
}
} else {
// Данные не найдены
@@ -190,6 +190,7 @@ class SearchController extends Controller
return $return ?? [
'panel' => $this->renderPartial('/search/panel'),
'search_line_window_show' => 1,
'_csrf' => yii::$app->request->getCsrfToken()
];
} else {
@@ -205,7 +206,9 @@ class SearchController extends Controller
goto keep_connect_wait;
}
return $this->render('/search/index', compact('response', 'timer'));
$advanced = true;
return $this->render('/search/index', compact('response', 'timer', 'advanced', 'query'));
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace app\controllers;
use yii;
use yii\web\Controller;
use yii\web\UploadedFile;
use app\models\Request;
use app\models\Account;
class SuppliersController extends Controller
{
public function actionIndex()
{
return $this->renderPartial('/suppliers/index');
}
public function actionRequest()
{
return $this->renderPartial('/suppliers/request');
}
/**
* @todo Сделать отправку только назначенным модераторам
*/
public function actionRequestSend()
{
// Инициализация данных запроса
$request = yii::$app->request->post('Request') ?? yii::$app->request->get('Request');
// Запись поставщика
Account::writeSupplier($request['name'], $request['phon'], $request['mail'], $file = UploadedFile::getInstance(new Request($request), 'file'));
yii::$app->mail_system->compose()
->setFrom(yii::$app->params['mail']['system'])
->setTo(yii::$app->params['mail']['info'])
->setSubject('Регистрация поставщика')
->setHtmlBody($this->renderPartial('/mails/supplier', $request))
->attach($file->tempName, ['fileName' => $file->name])
->send();
return $this->renderPartial('/suppliers/requested');
}
}

View File

@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace app\controllers;
use yii;
use yii\web\Controller;
use yii\web\Response;
use yii\web\HttpException;
use yii\web\UploadedFile;
use yii\filters\AccessControl;
use app\models\Product;
use app\models\Settings;
use app\models\SupplyEdgeProduct;
use app\models\Supply;
use app\models\Account;
use app\models\Notification;
use app\models\OrderEdgeSupply;
use Exception;
class SupplyController extends Controller
{
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::class,
'rules' => [
[
'allow' => true,
'actions' => [
'index',
]
],
[
'allow' => true,
'roles' => ['@'],
'actions' => []
],
[
'allow' => true,
'actions' => [
'read'
],
'matchCallback' => function ($rule, $action): bool {
if (
!yii::$app->user->isGuest
&& (yii::$app->user->identity->type === 'administrator'
|| yii::$app->user->identity->type === 'moderator')
) return true;
return false;
}
],
[
'allow' => false,
'roles' => ['?'],
'denyCallback' => [$this, 'accessDenied']
]
]
]
];
}
/**
* Чтение поставок
*
* @param int $page Страница
*
* @return string|array|null
*/
public function actionRead(int $page = 1): string|array|null
{
if (yii::$app->request->isPost) {
// POST-запрос
// Инициализация входных параметров
$amount = yii::$app->request->post('amount') ?? yii::$app->request->get('amount') ?? 20;
$order = yii::$app->request->post('order') ?? yii::$app->request->get('order') ?? ['DESC'];
// Инициализация cookie
$cookies = yii::$app->response->cookies;
// Чтение поставок
$supplies = Supply::read(limit: $amount, page: $page, order: $order);
// Запись формата ответа
yii::$app->response->format = Response::FORMAT_JSON;
return [
'supplies' => $this->renderPartial('/supply/list', compact('supplies', 'amount', 'page')),
'_csrf' => yii::$app->request->getCsrfToken()
];
}
return false;
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace app\controllers;
use yii;
use yii\web\Controller;
use yii\web\Response;
use app\models\Terminal;
class TerminalController extends Controller
{
public function actionRead(): ?array
{
// Инициализация входных параметров
$amount = (int) yii::$app->request->post('amount') ?? yii::$app->request->get('amount');
if ($amount < 501) {
// Пройдена проверка
if (yii::$app->request->isPost) {
// POST-запрос
// Запись формата ответа
yii::$app->response->format = Response::FORMAT_JSON;
return [
'terminals' => Terminal::read($amount),
'_csrf' => yii::$app->request->getCsrfToken()
];
}
} else {
// Не пройдена проверка
// Запись кода ответа
yii::$app->response->statusCode = 500;
}
return false;
}
}

View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace app\controllers;
use yii;
use yii\web\Controller;
use yii\web\Response;
use app\models\Account;
class VerifyController extends Controller
{
public function actionIndex(string $vrfy = null): string|Response
{
if (isset($vrfy)) {
// Подтверждение регистрации
if (Account::verification($vrfy, auth: true)) {
// Успешно подтверждена регистрация
return $this->redirect('/profile');
}
return ErrorController::throw('Ошибка подтверждения', 'Код не совпадает с тем, что мы отправили вам на почту, либо регистрация уже была подтверждена.\n Свяжитесь с администрацией');
} else {
// Простой запрос
if (yii::$app->user->isGuest) {
// Пользователь не аутентифицирован
return yii::$app->response->redirect('/registration');
} else {
// Пользователь аутентифицирован
if (yii::$app->user->identity->vrfy === true) {
// Регистрация аккаунта уже подтверждена
// Генерация хеша пароля
yii::$app->user->identity->pswd = yii::$app->security->generatePasswordHash(yii::$app->user->identity->pswd);
// Запись в хранилище
yii::$app->user->identity->update();
if (yii::$app->request->isPost) {
// POST-запрос
// Запись формата ответа
yii::$app->response->format = Response::FORMAT_JSON;
return [
'main' => $this->renderPartial('/profile/index'),
'title' => 'Профиль',
'redirect' => '/profile',
'_csrf' => yii::$app->request->getCsrfToken()
];
} else {
// GET-запрос (подразумевается)
return $this->render('/profile/index');
}
} else {
// Регистрация аккаунта ещё не подтверждена
if (yii::$app->request->isPost) {
// POST-запрос
// Запись формата ответа
yii::$app->response->format = Response::FORMAT_JSON;
return $this->genPostVerify();
} else {
// GET-запрос (подразумевается)
return $this->render('/account/verify');
}
}
}
}
}
/**
* Отправить запрос на активацию
*
* @return string
*/
public function actionSend(): string|array
{
if (!yii::$app->user->isGuest) {
// Пользователь аутентифицирован
// Регенерация кода подтверждения
yii::$app->user->identity->verifyRegenerate();
// Отправка кода подтверждения на почту
yii::$app->user->identity->sendMailVerify();
if (yii::$app->request->isPost) {
// POST-запрос
// Запись формата ответа
yii::$app->response->format = Response::FORMAT_JSON;
return $this->genPostVerify();
} else {
// GET-запрос (подразумевается)
return $this->render('/account/verify');
}
}
}
/**
* Генерация данных для POST-запроса с переадресацией на страницу подтверждения
*/
function genPostVerify(): array {
return [
'main' => $this->renderPartial('/account/verify'),
'title' => 'Подтверждение аккаунта',
'redirect' => '/account/verify',
'_csrf' => yii::$app->request->getCsrfToken()
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
use mirzaev\yii2\arangodb\Migration;
class m210502_102203_create_terminal_collection extends Migration
{
public function up()
{
/**
* @param string Название коллекции
* @param array Тип коллекции (2 - документ, 3 - ребро)
*/
$this->createCollection('terminal', ['type' => 2]);
}
public function down()
{
$this->dropCollection('terminal');
}
}

View File

@@ -0,0 +1,20 @@
<?php
use mirzaev\yii2\arangodb\Migration;
class m210502_102358_create_dellin_collection extends Migration
{
public function up()
{
/**
* @param string Название коллекции
* @param array Тип коллекции (2 - документ, 3 - ребро)
*/
$this->createCollection('dellin', ['type' => 2]);
}
public function down()
{
$this->dropCollection('dellin');
}
}

View File

@@ -0,0 +1,20 @@
<?php
use mirzaev\yii2\arangodb\Migration;
class m210510_180939_create_request_collection extends Migration
{
public function up()
{
/**
* @param string Название коллекции
* @param array Тип коллекции (2 - документ, 3 - ребро)
*/
$this->createCollection('request', ['type' => 2]);
}
public function down()
{
$this->dropCollection('request');
}
}

View File

@@ -0,0 +1,20 @@
<?php
use mirzaev\yii2\arangodb\Migration;
class m210512_121658_create_cdek_collection extends Migration
{
public function up()
{
/**
* @param string Название коллекции
* @param array Тип коллекции (2 - документ, 3 - ребро)
*/
$this->createCollection('cdek', ['type' => 2]);
}
public function down()
{
$this->dropCollection('cdek');
}
}

View File

@@ -0,0 +1,20 @@
<?php
use mirzaev\yii2\arangodb\Migration;
class m211123_114511_create_import_collection extends Migration
{
public function up()
{
/**
* @param string Название коллекции
* @param array Тип коллекции (2 - документ, 3 - ребро)
*/
$this->createCollection('import', ['type' => 2]);
}
public function down()
{
$this->dropCollection('import');
}
}

View File

@@ -0,0 +1,20 @@
<?php
use mirzaev\yii2\arangodb\Migration;
class m211123_120136_create_import_edge_supply_collection extends Migration
{
public function up()
{
/**
* @param string Название коллекции
* @param array Тип коллекции (2 - документ, 3 - ребро)
*/
$this->createCollection('import_edge_supply', ['type' => 3]);
}
public function down()
{
$this->dropCollection('import_edge_supply');
}
}

View File

@@ -0,0 +1,20 @@
<?php
use mirzaev\yii2\arangodb\Migration;
class m211123_173801_create_import_edge_account_collection extends Migration
{
public function up()
{
/**
* @param string Название коллекции
* @param array Тип коллекции (2 - документ, 3 - ребро)
*/
$this->createCollection('import_edge_account', ['type' => 3]);
}
public function down()
{
$this->dropCollection('import_edge_account');
}
}

View File

@@ -0,0 +1,20 @@
<?php
use mirzaev\yii2\arangodb\Migration;
class m211221_183410_create_warehouse_collection extends Migration
{
public function up()
{
/**
* @param string Название коллекции
* @param array Тип коллекции (2 - документ, 3 - ребро)
*/
$this->createCollection('warehouse', ['type' => 2]);
}
public function down()
{
$this->dropCollection('warehouse');
}
}

View File

@@ -0,0 +1,20 @@
<?php
use mirzaev\yii2\arangodb\Migration;
class m211221_183447_create_warehouse_edge_import_collection extends Migration
{
public function up()
{
/**
* @param string Название коллекции
* @param array Тип коллекции (2 - документ, 3 - ребро)
*/
$this->createCollection('warehouse_edge_import', ['type' => 3]);
}
public function down()
{
$this->dropCollection('warehouse_edge_import');
}
}

View File

@@ -0,0 +1,20 @@
<?php
use mirzaev\yii2\arangodb\Migration;
class m211221_193454_create_account_edge_warehouse_collection extends Migration
{
public function up()
{
/**
* @param string Название коллекции
* @param array Тип коллекции (2 - документ, 3 - ребро)
*/
$this->createCollection('account_edge_warehouse', ['type' => 3]);
}
public function down()
{
$this->dropCollection('account_edge_warehouse');
}
}

View File

@@ -0,0 +1,20 @@
<?php
use mirzaev\yii2\arangodb\Migration;
class m220808_185553_create_file_collection extends Migration
{
public function up()
{
/**
* @param string Название коллекции
* @param array Тип коллекции (2 - документ, 3 - ребро)
*/
$this->createCollection('file', ['type' => 2]);
}
public function down()
{
$this->dropCollection('file');
}
}

View File

@@ -0,0 +1,20 @@
<?php
use mirzaev\yii2\arangodb\Migration;
class m220817_210350_create_import_edge_file_collection extends Migration
{
public function up()
{
/**
* @param string Название коллекции
* @param array Тип коллекции (2 - документ, 3 - ребро)
*/
$this->createCollection('import_edge_file', ['type' => 3]);
}
public function down()
{
$this->dropCollection('import_edge_file');
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -10,4 +10,79 @@ class AccountEdgeOrder extends Edge
{
return 'account_edge_order';
}
/**
* Свойства
*/
public function attributes(): array
{
return array_merge(
parent::attributes(),
[
'stts'
]
);
}
/**
* Метки свойств
*/
public function attributeLabels(): array
{
return array_merge(
parent::attributeLabels(),
[
'stts' => 'Статус'
]
);
}
/**
* Правила
*/
public function rules(): array
{
return array_merge(
parent::rules(),
[
[
[
'stts'
],
'string'
]
]
);
}
/**
* Перед сохранением
*
* @todo Подождать обновление от ебаного Yii2 и добавить
* проверку типов передаваемых параметров
*/
public function beforeSave($data): bool
{
if (parent::beforeSave($data)) {
if ($this->isNewRecord) {
$this->stts = 'current';
}
return true;
}
return false;
}
/**
* Поиск по идентификатору заказа
*
* @param string $order_id Идентификатор записи заказа в базе данных
* @param int $limit Ограничение
*
* @return array|null
*/
public static function searchByOrder(string $order_id, int $limit = 1): ?array
{
return self::find()->where(['_to' => $order_id])->limit($limit)->all();
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace app\models;
use app\models\Account;
/**
* Связь аккаунтов и складов
*/
class AccountEdgeWarehouse extends Edge
{
/**
* Имя коллекции
*/
public static function collectionName(): string
{
return 'account_edge_warehouse';
}
}

View File

@@ -8,6 +8,7 @@ use yii;
use yii\base\Model;
use app\models\Account;
use Exception;
/**
* AccountForm is the model behind the login form.
@@ -24,6 +25,8 @@ class AccountForm extends Model
public $mail;
public $pswd;
public $auto = false;
public $rept;
public $pols;
private $account = false;
@@ -32,14 +35,45 @@ class AccountForm extends Model
{
return [
// Обязательные поля
[['mail', 'pswd'], 'required', 'message' => 'Заполните поле'],
[
[
'mail',
],
'required',
'message' => 'Заполните поле'
],
// Обязательные поля для аутентификации
[
[
'pswd',
],
'required',
'message' => 'Заполните поле',
'on' => self::SCENARIO_AUTHENTICATION
],
// Функция "Запомнить меня"
['auto', 'boolean', 'on' => self::SCENARIO_AUTHENTICATION],
[
'auto',
'boolean',
'on' => self::SCENARIO_AUTHENTICATION
],
// Проверка почты,
['mail', 'email', 'message' => 'Проверьте почту'],
['mail', 'validateMail', 'on' => self::SCENARIO_REGISTRATION],
[
'mail',
'email',
'message' => 'Проверьте почту'
],
[
'mail',
'validateMail',
'on' => self::SCENARIO_REGISTRATION
],
// Проверка пароля
['pswd', 'validatePassword', 'on' => self::SCENARIO_AUTHENTICATION]
[
'pswd',
'validatePassword',
'on' => self::SCENARIO_AUTHENTICATION
]
];
}
@@ -48,7 +82,8 @@ class AccountForm extends Model
return [
'mail' => 'Почта',
'pswd' => 'Пароль',
'auto' => '<i class="fas fa-lock"></i>'
'auto' => '<i class="fas fa-unlock"></i>',
'pols' => 'Политика конфедециальности'
];
}
@@ -59,6 +94,12 @@ class AccountForm extends Model
$account = $this->getAccount();
if (is_null($account)) {
// Не удалось проверить аккаунт
return;
}
if (!$account || $account->validateMail($this->mail)) {
// Проверка не пройдена
@@ -74,10 +115,38 @@ class AccountForm extends Model
$account = $this->getAccount();
if (!$account || !$account->validatePassword($this->pswd)) {
// Проверка не пройдена
if (is_null($account)) {
// Не удалось проверить аккаунт
$this->addError($attribute, 'Проверьте пароль');
return;
}
if ($account) {
// Удалось инициализировать аккаунт
try {
if (!$account->validatePasswordWithHash($this->pswd)) {
// Не пройдена проверка с хешем
throw new exception;
}
} catch (Exception $e) {
// Не пройдена проверка с хешем
try {
if (!$account->validatePasswordWithoutHash($this->pswd)) {
// Не пройдена проверка с паролем
throw new exception;
}
} catch (Exception $e) {
// Проверка без хеша не пройдена
$this->addError($attribute, 'Проверьте пароль');
}
}
} else {
$this->addError($attribute, 'Не удалось идентифицировать аккаунт');
}
}
}
@@ -93,6 +162,9 @@ class AccountForm extends Model
$this->pswd = $pswd;
}
// Регистронезависимая почта
$this->mail = mb_strtolower($this->mail);
if (isset($this->mail, $this->pswd) && $this->validate()) {
// Проверка пройдена
@@ -111,15 +183,33 @@ class AccountForm extends Model
// Инициализация нового аккаунта
$this->account = new Account();
if (isset($this->mail, $this->pswd) && $this->validate()) {
if ($this->validate()) {
// Проверка пройдена
// Запись параметров
$this->account->mail = $this->mail;
$this->account->pswd = yii::$app->security->generatePasswordHash($this->pswd);
// $this->account->pswd = yii::$app->security->generatePasswordHash(Account::passwordGenerate());
$this->account->pswd = $this->pswd = Account::passwordGenerate();
if (($account = Account::findByMail($this->mail)) || isset($account) && $account->vrfy !== true) {
// Аккаунт найден, но не подтверждён
// Удаление аккаунта (сейчас его создадим снова)
$account->delete();
}
// Регистрация
return $this->account->save();
if ($this->account->save()) {
// Успешно завершена регистрация или обновлены данные не до конца зарегистрировавшегося пользователя
// Генерация индекса
Account::generateIndexes([$this->account]);
// Отправка письма для подтверждения почты
$this->account->sendMailVerify();
return true;
}
}
return false;

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace app\models;
use CdekSDK2\Client;
class Cdek extends Document
{
public static function collectionName(): string
{
return 'cdek';
}
public function attributes(): array
{
return array_merge(
parent::attributes(),
[
'data'
]
);
}
public function attributeLabels(): array
{
return array_merge(
parent::attributeLabels(),
[
'data' => 'Данные ДеловыеЛинии',
]
);
}
/**
* Поиск по идентификатору города
*/
public static function searchByCityId(string $id): ?static
{
return static::findOne(['data["id"]' => $id]);
}
}

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace app\models;
use ArangoDBClient\Document as ArangoDBDocument;
class Dellin extends Document
{
public static function collectionName(): string
{
return 'dellin';
}
public function attributes(): array
{
return array_merge(
parent::attributes(),
[
'data'
]
);
}
public function attributeLabels(): array
{
return array_merge(
parent::attributeLabels(),
[
'data' => 'Данные ДеловыеЛинии',
]
);
}
/**
* Поиск по идентификатору города
*
* @param string $id Идентификатор города
*/
public static function searchByCityId(string $id): ?static
{
return static::findOne(['data["id"]' => $id]);
}
/**
* Поиск по КЛАДР-коду города
*
* @param string $id Код КЛАДР города
*/
public static function searchByCityKladr(string $code): ?static
{
return static::findOne(['data["code"]' => $code]);
}
/**
* Поиск по идентификатору терминала
*
* @param int $id Идентификатор терминала
* @param bool $terminal_data_only Запрос только данных терминала
*/
public static function searchByTerminalId(int $id, bool $terminal_data_only = false): bool|static|array|null|ArangoDBDocument
{
if ($terminal_data_only) {
return static::find()->foreach(['terminal' => self::collectionName() . '.data["terminals"]["terminal"]'])->where(['terminal["id"] == "' . $id . '"'])->select('terminal')->createCommand()->execute()->getAll()[0];
}
return static::find()->foreach(['terminal' => self::collectionName() . '.data["terminals"]["terminal"]'])->where(['terminal["id"] == "' . $id . '"'])->one();
}
}

View File

@@ -20,7 +20,9 @@ abstract class Document extends ActiveRecord
*/
public static function collectionName(): string
{
return throw new Exception('Не инициализировано название коллекции');
throw new Exception('Не инициализировано название коллекции');
return 'document';
}
/**
@@ -62,15 +64,19 @@ abstract class Document extends ActiveRecord
* @todo Подождать обновление от ебаного Yii2 и добавить
* проверку типов передаваемых параметров
*/
public function beforeSave($data): bool
public function beforeSave($create): bool
{
if (parent::beforeSave($data)) {
if (parent::beforeSave($create)) {
// Пройдена родительская проверка
if ($this->isNewRecord) {
// Новая запись
// Запись в журнал
$this->jrnl = array_merge(
[[
'date' => time(),
'account' => yii::$app->user->id,
'account' => yii::$app->user->id ?? 'system',
'action' => 'create'
]],
$this->jrnl ?? []
@@ -106,10 +112,12 @@ abstract class Document extends ActiveRecord
[array_merge(
[
'date' => $time = time(),
'account' => yii::$app->user->id,
'account' => yii::$app->user->id ?? 'system',
'action' => $action
],
...$data
[
'data' => $data
]
)]
);
@@ -125,6 +133,15 @@ abstract class Document extends ActiveRecord
return isset($this->_key) && static::collectionName() ? static::collectionName() . '/' . $this->_key : null;
}
/**
* Поиск
*/
public static function search(array $where, $limit = 1): array
{
return static::find()->where($where)->limit($limit)->all();
}
/**
* Поиск по идентификатору
*/
@@ -133,19 +150,29 @@ abstract class Document extends ActiveRecord
return static::findOne(['_id' => $_id]);
}
public static function readLast(): ?static
public static function readLast(): static|null|bool
{
return static::find()->orderBy(['DESC'])->one();
}
/**
* Чтение всех записей
*
* @deprecated
*/
public static function readAll(): array
{
return static::find()->all();
}
/**
* Чтение записей по максимальному ограничению
*/
public static function read(?array $where = [], int $limit = 100, int $page = 1, ?array $order = null): array
{
return static::find()->where($where)->orderby($order)->limit($limit)->offset(($page - 1) * $limit)->all();
}
/**
* Чтение количества записей
*/
@@ -157,12 +184,61 @@ abstract class Document extends ActiveRecord
/**
* Проверка на то, что в свойство передан массив
*/
public function arrayValidator(string $attribute, array $params = null): bool
public function arrayValidator(string $attribute, array $params = null): void
{
if (is_array($this->$attribute)) {
return true;
return;
} else {
$this->addError($attribute, 'Передан не массив');
}
return false;
$this->addError($attribute, 'Не пройдена проверка: "arrayValidator"');
}
/**
* Проверка на то, что в свойство передан массив и он хранит циферные значения
*/
public function arrayWithNumbersValidator(string $attribute, array $params = null): void
{
try {
if (is_array($this->$attribute)) {
foreach ($this->$attribute as $value) {
if (!(bool) preg_match('/^[0-9\.]*$/m', $value)) {
$this->addError($attribute, 'В массиве найдены запрещённые символы');
}
}
return;
} else {
$this->addError($attribute, 'Передан не массив');
}
} catch (Exception $e) {
$this->addError($attribute, $e->getMessage());
}
$this->addError($attribute, 'Не пройдена проверка: "arrayWithNumbersValidator"');
}
/**
* Конвертировать _id в _key
*/
private static function keyFromId(string $_id): ?string
{
preg_match_all('/\/([0-9]+)$/m', $_id, $mathes);
return $mathes[1][0] ?? null;
}
/**
* Статический вызов
*
* @param string $name
* @param array $args
*/
public static function __callStatic(string $name, array $args): mixed {
return match ($name) {
'keyFromId' => self::keyFromId(...$args)
};
}
}

View File

@@ -81,13 +81,21 @@ abstract class Edge extends Document
* Записать (с проверкой на существование)
*
* Создаст ребро только в том случае, если его аналога не существует
*
* @param string $_from Идентификатор отправителя (_id)
* @param string $_from Идентификатор получетеля (_id)
* @param string $type Дополнительное поле - тип взаимосвязи
* @param string $data Дополнительные данные
*
* @todo
* 1. Удалить $type и оставить только $data
*/
public static function writeSafe(string $_from, string $_to, string $type = '', array $data = []): ?static
{
if ($edge = self::searchByVertex($_from, $_to, limit: 1)) {
// Найдено в базе данных
return $edge;
return $edge[0];
}
return self::write($_from, $_to, $type, $data);
@@ -95,8 +103,16 @@ abstract class Edge extends Document
/**
* Записать
*
* @param string $_from Идентификатор отправителя (_id)
* @param string $_from Идентификатор получетеля (_id)
* @param string $type Дополнительное поле - тип взаимосвязи @deprecated
* @param string $data Дополнительные данные
*
* @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;
@@ -104,7 +120,7 @@ abstract class Edge extends Document
// Настройка
$edge->_from = $_from;
$edge->_to = $_to;
$edge->type = $type;
if (isset($type)) $edge->type = $type;
foreach ($data as $key => $value) {
if (is_int($key)) {
@@ -123,8 +139,14 @@ abstract class Edge extends Document
/**
* Поиск ребра по его вершинам
*
* @param string $_from Идентификатор исходящей вершины
* @param string $_to Идентификатор входящей вершины
* @param string|null $type deprecated
* @param int $limit Ограничение по количеству
* @param array|null $filter Фильтр (where())
*/
public static function searchByVertex(string $_from, string $_to, string|null $type = null, int $limit = 1): array|null
public static function searchByVertex(string $_from, string $_to, string|null $type = null, int $limit = 1, array|null $filter = null): array|null
{
$query = self::find()->where([
'_from' => $_from,
@@ -135,24 +157,64 @@ abstract class Edge extends Document
$query->where(['type' => $type]);
}
if (isset($filter)) {
$query->where($filter);
}
return $query->limit($limit)->all();
}
/**
* Поиск рёбер
* Поиск рёбер по направлению
*/
public static function search(string $target, string $direction = 'OUTBOUND', string $type = '', int $limit = 1): static|array|null
public static function searchByDirection(string $_from, string $direction = 'OUTBOUND', string $type = '', array $where = [], int $limit = 1, int $page = 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')) {
// Исходящие рёбра
if (isset($where)) {
// Получен параметр $where
// Реинициализация
$where = array_merge($where + ['_from' => $_from]);
} else {
// Не получен параметр $where
// Реинициализация
$where = array_merge(['_from' => $_from, 'type' => $type]);
}
$query = static::find()->where($where);
} else if (str_contains($direction, 'INBOUND')) {
// Входящие рёбра
if (isset($where)) {
// Получен параметр $where
// Реинициализация
$where = array_merge($where + ['_to' => $_from]);
} else {
// Не получен параметр $where
// Реинициализация
$where = array_merge(['_to' => $_from, 'type' => $type]);
}
$query = static::find()->where($where);
} else if (str_contains($direction, 'ANY')) {
// Исходящие и входящие рёбра
return static::searchByDirection(_from: $_from, direction: 'OUTBOUND', type: $type, where: $where, limit: $limit, page: $page) + static::searchByDirection(_from: $_from, direction: 'INBOUND', type: $type, where: $where, limit: $limit, page: $page);
}
if ($limit < 2) {
// Одно ребро или меньше
return $query->one();
} else {
return $query->limit($limit)->all();
// Несколько рёбер
return $query->limit($limit)->offset($limit * ($page - 1))->all();
}
}
}

View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace app\models;
use ArangoDBClient\Document as ArangoDBDocument;
use app\models\traits\SearchByEdge;
use app\models\ImportEdgeFile;
class File extends Document
{
use SearchByEdge;
public static function collectionName(): string
{
return 'file';
}
public function attributes(): array
{
return array_merge(
parent::attributes(),
[
'type',
'path',
'name',
'user',
'stts',
'meta'
]
);
}
public function attributeLabels(): array
{
return array_merge(
parent::attributeLabels(),
[
'type' => 'Тип файла',
'path' => 'Относительный от хранилища путь до файла',
'name' => 'Название файла',
'user' => 'Пользователь управляющий файлом',
'stts' => 'Статус',
'meta' => 'Метаданные'
]
);
}
/**
* Перед сохранением
*
* @todo Подождать обновление от ебаного Yii2 и добавить
* проверку типов передаваемых параметров
*/
public function beforeSave($data): bool
{
if (parent::beforeSave($data)) {
if ($this->isNewRecord) {
if ($this->type = 'supplies excel') {
// Список поставок
$this->stts = 'needed to load';
}
}
return true;
}
return false;
}
public static function searchSuppliesNeededToLoad(int $amount = 3): array
{
return static::find()->where(['stts' => 'needed to load'])->limit($amount)->all();
}
/**
* Поиск по инстанции импорта
*
* @param Import $import Инстанция импорта
*/
public static function searchByImport(Import $import): ?File
{
return new File(self::searchByEdge(
from: 'import',
to: 'file',
subquery_where: [
[
'import._id' => $import->readId()
]
],
where: 'import_edge_file[0]._id != null',
select: 'file',
limit: 1
)[0]) ?? null;
}
}

View File

@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace app\models;
use yii;
use app\models\traits\SearchByEdge;
use app\models\Account;
/**
* Импорт поставок
*
* Хранит в себе связи с поставками которые были загружены вместе
*/
class Import extends Document
{
use SearchByEdge;
/**
* Имя коллекции
*/
public static function collectionName(): string
{
return 'import';
}
/**
* Свойства
*/
public function attributes(): array
{
return array_merge(
parent::attributes(),
[
'name',
'file'
]
);
}
/**
* Метки свойств
*/
public function attributeLabels(): array
{
return array_merge(
parent::attributeLabels(),
[
'name' => 'Название',
'file' => 'Файл'
]
);
}
/**
* Правила
*/
public function rules(): array
{
return array_merge(
parent::rules(),
[
[
[
'file',
'name'
],
'string'
]
]
);
}
/**
* Поиск по складу
*
* @param Warehouse $warehouse Инстанция склада
* @param int $limit Ограничение по максимальному количеству
*
* @return array Инстанции импортов
*/
public static function searchByWarehouse(Warehouse $warehouse, int $limit = 10): ?array
{
return self::searchByEdge(
from: 'warehouse',
to: 'import',
edge: 'warehouse_edge_import',
direction: 'INBOUND',
subquery_where: [
['warehouse_edge_import._from' => $warehouse->readId()],
['warehouse_edge_import.type' => 'loaded']
],
where: 'warehouse_edge_import[0] != null',
limit: $limit
);
}
/**
* Поиск по поставке
*
* @param Supply $supply Поставка
* @param int $limit Ограничение по максимальному количеству
*
* @return array Инстанции импортов
*/
public static function searchBySupply(Supply $supply, int $limit = 10): ?array
{
return self::searchByEdge(
from: 'supply',
to: 'import',
edge: 'import_edge_supply',
direction: 'OUTBOUND',
subquery_where: [
['import_edge_supply._to' => $supply->readId()],
['import_edge_supply.type' => 'imported']
],
where: 'import_edge_supply[0] != null',
limit: $limit
);
}
/**
* Поиск по файлу
*
* @param File $file Файл
*
* @return Import|null Инстанция импорта
*/
public static function searchByFile(File $file): ?Import
{
return self::searchByEdge(
from: 'file',
to: 'import',
edge: 'import_edge_file',
direction: 'OUTBOUND',
subquery_where: [
['import_edge_file._to' => $file->readId()],
['import_edge_supply.type' => 'connected']
],
where: 'import_edge_supply[0] != null',
limit: 1
)[0] ?? null;
}
}

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace app\models;
use app\models\Account;
/**
* Связь инстанций импорта с поставками
*/
class ImportEdgeAccount extends Edge
{
/**
* Имя коллекции
*/
public static function collectionName(): string
{
return 'import_edge_account';
}
/**
* Свойства
*/
public function attributes(): array
{
return array_merge(
parent::attributes(),
[
]
);
}
/**
* Метки свойств
*/
public function attributeLabels(): array
{
return array_merge(
parent::attributeLabels(),
[
]
);
}
/**
* Правила
*/
public function rules(): array
{
return array_merge(
parent::rules(),
[
]
);
}
/**
* Поиск по аккаунту
*
* @param Account $account Аккаунт
* @param int $limit Ограничение по максимальному количеству
*/
public static function searchByAccount(Account $account, int $limit = 1): array
{
$account = Account::initAccount($account);
return static::find()->where(['_from' => $account->collectionName() . "$account->_key"])->limit($limit)->all();
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace app\models;
use app\models\File;
use app\models\Import;
/**
* Связь инстанций импорта с поставками
*/
class ImportEdgeFile extends Edge
{
/**
* Имя коллекции
*/
public static function collectionName(): string
{
return 'import_edge_file';
}
/**
* Свойства
*/
public function attributes(): array
{
return array_merge(
parent::attributes(),
[
]
);
}
/**
* Метки свойств
*/
public function attributeLabels(): array
{
return array_merge(
parent::attributeLabels(),
[
]
);
}
/**
* Правила
*/
public function rules(): array
{
return array_merge(
parent::rules(),
[
]
);
}
/**
* Поиск по файлу
*
* @param File $file Файл
* @param int $limit Ограничение по максимальному количеству
*/
public static function searchByFile(File $file, int $limit = 1): array
{
return static::find()->where(['_to' => $file->readId(), 'type' => 'connected'])->limit($limit)->all();
}
/**
* Поиск по инстанции импорта
*
* @param Import $import Инстанция импорта
* @param int $limit Ограничение по максимальному количеству
*/
public static function searchByImport(Import $import, int $limit = 1): array
{
return static::find()->where(['_from' => $import->readId(), 'type' => 'connected'])->limit($limit)->all();
}
}

View File

@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace app\models;
use mirzaev\yii2\arangodb\Query;
/**
* Связь инстанций импорта с поставками
*/
class ImportEdgeSupply extends Edge
{
/**
* Имя коллекции
*/
public static function collectionName(): string
{
return 'import_edge_supply';
}
/**
* Свойства
*/
public function attributes(): array
{
return array_merge(
parent::attributes(),
[
'vrsn'
]
);
}
/**
* Метки свойств
*/
public function attributeLabels(): array
{
return array_merge(
parent::attributeLabels(),
[
'vrsn' => 'Версия'
]
);
}
/**
* Правила
*/
public function rules(): array
{
return array_merge(
parent::rules(),
[
[
[
'vrsn'
],
'integer',
'message' => '{attribute} должен быть числом.'
]
]
);
}
/**
* Перед сохранением
*
* @todo Подождать обновление от ебаного Yii2 и добавить
* проверку типов передаваемых параметров
*/
public function beforeSave($data): bool
{
if (parent::beforeSave($data)) {
if ($this->isNewRecord) {
}
return true;
}
return false;
}
/**
* Поиск максимальной версии
*
* Ищет максимальную версию у поставок
*
* @param Supply $supply Поставка
*
* @return int Версия, если найдена
*/
public static function searchMaxVersion(Supply $supply): ?int
{
return static::find()->execute("RETURN MAX(
FOR import_edge_supply IN import_edge_supply
FILTER (import_edge_supply._to == 'supply/$supply->_key')
LIMIT 0,999
RETURN import_edge_supply.vrsn
)")[0] ?? null;
}
/**
* Поиск по поставке
*
* @param Supply $supply
*
* @return static|null
*/
public static function searchBySupply(Supply $supply): ?static
{
return static::find()->where(['_to' => $supply->readId()])->one() ?? null;
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace app\models;
use yii;
use yii\base\Model;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
// use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader;
// use PhpOffice\PhpSpreadsheet\Writer\Html;
use PhpOffice\PhpSpreadsheet\Reader\Html as HtmlReader;
use Exception;
class Invoice extends Model
{
public static function generate(string|int $order_key, string $html): void
{
// Инициализация директории
$dir = YII_PATH_PUBLIC . '/../assets/invoices/' . $order_key;
// Сохранение на диск
if (!file_exists($dir) && !mkdir($dir, 0775, true)) throw new Exception('Не удалось записать директорию:' . $dir);
$reader = new HtmlReader();
$spreadsheet = $reader->loadFromString($html);
$writer = new Xlsx($spreadsheet);
$writer->save($dir . '/invoice.xlsx');
// $reader = new XlsxReader();
// $reader->setReadDataOnly(true);
// $spreadsheet = $reader->load('../views/invoice/order/original.xlsx');
// $writer = new Html($spreadsheet);
// $writer->save('hello world.html');
}
}

View File

@@ -29,15 +29,25 @@ class Notification extends Document
const SCENARIO_TRUSTED_CREATE = 'create';
/**
* Тип уведомления: памятка
* Уведомление: "памятка"
*/
const TYPE_NOTICE = 'notice';
/**
* Тип уведомления: предупреждение
* Уведомление: "предупреждение"
*/
const TYPE_WARNING = 'warning';
/**
* Уведомление: "ошибка"
*/
const TYPE_ERROR = 'error';
/**
* Уведомление для модераторов: "новый заказ"
*/
const TYPE_MODERATOR_ORDER_NEW = 'new order';
/**
* Цель для отправки уведомления
*
@@ -129,7 +139,7 @@ class Notification extends Document
*/
public function write(): self|array|null
{
return $this::_write($this->text, $this->html, $this->account, $this->type);
return $this::_write($this->text, $this->html, $this->account ?? null, $this->type);
}
/**
@@ -137,14 +147,16 @@ class Notification extends Document
*
* @param string $html Содержимое уведомления (HTML или текст)
* @param bool|string|null $html Содержимое уведомления (HTML или текст)
* @param string $account Получатель уведомления
* @param string $account Получатель уведомления (_key или "@...")
* @param string $type Тип уведомления
*
* @todo Намного удобнее будет заменить _key на _id, чтобы из рёбер сразу получать аккаунт без лишних операций
*/
public static function _write(string $text, bool|string|null $html = false, string $account = null, string $type = self::TYPE_NOTICE): self|array|null
public static function _write(string $text, bool|string|null $html = false, string $account = '', string $type = self::TYPE_NOTICE): self|array|null
{
// Инициализация
$model = new self;
$account or $account = yii::$app->user->identity->_key ?? throw new Exception('Не удалось инициализировать получателя');
$receiver = Account::initAccount($account)->_key ?? $account ?? throw new Exception('Не удалось инициализировать получателя');
if ((bool) (int) $html) {
// Получен текст в формете HTML-кода
@@ -156,7 +168,7 @@ class Notification extends Document
$text = htmlspecialchars(strip_tags($text ?? null));
$model->html = <<<HTML
<p class="my-2 mx-3">$text</p>
<p>$text</p>
HTML;
}
@@ -164,7 +176,7 @@ class Notification extends Document
// Уведомление записано
// Инициализация получателей и создание ребра
self::searchReceiverAndConnect($model, $account, $type);
self::searchReceiverAndConnect($model, $receiver, $type);
}
return null;
@@ -174,20 +186,20 @@ class Notification extends Document
* Поиск получателя
*
* @param self $model Уведомление
* @param string $text Необработанный текст
* @param string $targets Необработанный текст с получателями
* @param string $type Тип уведомления
*/
protected static function searchReceiverAndConnect(self $model, string $text, string $type = self::TYPE_NOTICE): AccountEdgeNotification|array|null
protected static function searchReceiverAndConnect(self $model, string $targets, string $type = self::TYPE_NOTICE): AccountEdgeNotification|array|null
{
// Инициализация
$return = [];
// Конвертация
$accounts = array_map('trim', explode(self::$delimiter, $text));
$accounts = array_map('trim', explode(self::$delimiter, $targets));
foreach ($accounts as $account) {
if (in_array('@all', $accounts, true)) {
// Найден флаг обозначающий отправку всем пользователям
// Всем пользователям
// Инициализация
$return = [];
@@ -195,11 +207,64 @@ class Notification extends Document
foreach (Account::readAll() as $account) {
// Перебор всех аккаунтов
// Запись ребра: УВЕДОМЛЕНИЕ -> АККАУНТ
$return[] = AccountEdgeNotification::writeSafe($model->readId(), $account->readId(), $type);
}
} else if (
in_array('@authorized', $accounts, true)
|| in_array('@authorizeds', $accounts, true)
|| in_array('@authorised', $accounts, true)
|| in_array('@authoriseds', $accounts, true)
|| in_array('@auth', $accounts, true)
|| in_array('@autheds', $accounts, true)
) {
// Всем авторизованным (админам и модераторам)
// Инициализация
$return = [];
foreach (Account::readAllAuthorizeds() as $account) {
// Перебор всех аккаунтов
// Запись ребра: УВЕДОМЛЕНИЕ -> АККАУНТ
$return[] = AccountEdgeNotification::writeSafe($model->readId(), $account->readId(), $type);
}
} else if (
in_array('@administrator', $accounts, true)
|| in_array('@administrators', $accounts, true)
|| in_array('@admin', $accounts, true)
|| in_array('@admins', $accounts, true)
) {
// Администраторам
// Инициализация
$return = [];
foreach (Account::readAllAdministrators() as $account) {
// Перебор всех аккаунтов
// Запись ребра: УВЕДОМЛЕНИЕ -> АККАУНТ
$return[] = AccountEdgeNotification::writeSafe($model->readId(), $account->readId(), $type);
}
} else if (
in_array('@moderator', $accounts, true)
|| in_array('@moderators', $accounts, true)
|| in_array('@moder', $accounts, true)
|| in_array('@moders', $accounts, true)
) {
// Модераторам
// Инициализация
$return = [];
foreach (Account::readAllModerators() as $account) {
// Перебор всех аккаунтов
// Запись ребра: УВЕДОМЛЕНИЕ -> АККАУНТ
$return[] = AccountEdgeNotification::writeSafe($model->readId(), $account->readId(), $type);
}
} else if (in_array('@test', $accounts, true)) {
// Найден флаг обозначающий тестирование (отправка самому себе)
// Тестирование (отправка самому себе)
$return[] = AccountEdgeNotification::writeSafe($model->readId(), yii::$app->user->id, $type);
} else {
@@ -216,4 +281,19 @@ class Notification extends Document
return $return ? $return : null;
}
/**
* Конвертация типа уведомления в версию для отображения
*
* @param string|null $type Тип уведомления
*
* @return string
*/
public function genTypeToRussian(string $type = null): string {
return match($type ?? $this->type) {
'notice' => 'Уведомление',
'warning' => 'Предупреждение',
'error' => 'Ошибка'
};
}
}

View File

@@ -5,10 +5,14 @@ declare(strict_types=1);
namespace app\models;
use yii;
use yii\web\User as Account;
use app\models\traits\SearchByEdge;
use carono\exchange1c\controllers\ApiController;
use carono\exchange1c\interfaces\DocumentInterface;
use app\models\connection\Dellin;
use Exception;
/**
@@ -17,14 +21,14 @@ use Exception;
* @see Account Заказчик
* @see Supply Поставки для заказа
*/
class Order extends Document
class Order extends Document implements DocumentInterface
{
use SearchByEdge;
/**
* Поставки для записи
*/
public array $supplies;
public array|int $supplies;
/**
* Имя коллекции
@@ -42,7 +46,9 @@ class Order extends Document
return array_merge(
parent::attributes(),
[
'stts'
'ocid',
'stts',
'sync'
]
);
}
@@ -55,7 +61,9 @@ class Order extends Document
return array_merge(
parent::attributeLabels(),
[
'stts' => 'Статус'
'ocid' => 'Идентификатор 1C',
'stts' => 'Статус',
'sync' => 'Статус синхронизации с 1C'
]
);
}
@@ -69,14 +77,14 @@ class Order extends Document
parent::rules(),
[
[
'stts',
'string',
'message' => '{attribute} должен быть строкой'
'sync',
'boolean',
'message' => '{attribute} должен иметь логическое значение'
],
[
'stts',
'sync',
'default',
'value' => 'preparing'
'value' => false
]
]
);
@@ -88,39 +96,35 @@ class Order extends Document
public function connect(Account $account): ?AccountEdgeOrder
{
// Запись ребра: АККАУНТ -> ЗАКАЗ
return AccountEdgeOrder::write($account->id, $this->readId(), 'current') ?? throw new Exception('Не удалось инициализировать ребро: АККАУНТ -> ЗАКАЗ');
return AccountEdgeOrder::write($account->readId(), $this->readId(), data: ['stts' => 'current']) ?? throw new Exception('Не удалось инициализировать ребро: АККАУНТ -> ЗАКАЗ');
}
/**
* Запись товара
* Запись товара к заказу
*
* $supply = [ Supply $supply, int $amount = 1 ]
*
* @param Supply|array $supply Поставка
* @param string $supply_id Идентификатор поставки
* @param string $delivery_type Тип доставки
* @param int $amount Количество
* @param Account $trgt Заказчик
*
* @return int Количество записанных поставок
*
* @todo Создать параметр разделителя для администрации
*/
public function writeSupply(Supply|string|array $supply, Account $trgt = null): int
public function writeSupply(string $supply_id, string $delivery_type, int $amount = 1, Account $trgt = null): int
{
// Инициализация
$trgt ?? $trgt = yii::$app->user ?? throw new Exception('Не удалось инициализировать заказчика');
if ($supply instanceof Supply) {
// Передана инстанция класса поставки или второй элемент массива не является числом
// Унификация входных данных
$supply = [$supply->catn => 1];
}
$trgt ?? $trgt = yii::$app->user->identity ?? throw new Exception('Не удалось инициализировать заказчика');
// Проверка корзины
if (is_null($this->_key)) {
// Корзина не инициализирована
// Инициализация
// Инициализация корзины
if (!$this->save()) {
// Инициализация заказа не удалась
// Инициализация корзины (активного заказа) не удалась
throw new Exception('Ошибка при записи заказа в базу данных');
}
@@ -134,53 +138,58 @@ class Order extends Document
}
// Инициализация
$amount = 0;
$amount_buffer = 0;
foreach (is_array($supply) ? $supply : [$supply => 1] as $supply_raw => $amount_raw) {
// Перебор поставок
// Обработка поставок
for ($i = 0; $i < $amount; $i++) {
// Создание рёбер соразмерно запросу (добавление нескольких продуктов в корзину)
for ($i = 0; $i < $amount_raw; $i++) {
// Создание рёбер соразмерно запросу (добавление нескольких продуктов в корзину)
// Запись ребра: ЗАКАЗ -> ПОСТАВКА
if (!$supply_model = Supply::searchById($supply_id) or !$order_edge_supply = OrderEdgeSupply::write($this->readId(), $supply_model->readId(), 'write')) {
// Поставка не найдена или запись ребра не удалась
// Запись ребра: ЗАКАЗ -> ПОСТАВКА
if (!$supply_model = Supply::searchByCatn($supply_raw) or !OrderEdgeSupply::write($this->readId(), $supply_model->readId(), 'write')) {
// Поставка не найдена или запись ребра не удалась
continue;
} else {
// Ребро создано (товар подключен к заказу)
continue;
} else {
// Ребро создано (товар подключен к заказу)
// Обновление счётчика добавленных товаров
$amount_buffer++;
// Постинкрементация счётчика добавленных товаров
$amount++;
// Запись типа доставки
$order_edge_supply->dlvr = [
'type' => $delivery_type
];
$order_edge_supply->update();
// Запись в журнал
$this->journal('write', ['target' => $supply_model->readId()]);
}
// Запись в журнал
$this->journal('write', ['target' => $supply_model->readId()]);
}
}
if ($amount === 0) {
if ($amount_buffer === 0) {
// Отправка уведомления
self::notification('Неудачная попытка добавить товар в корзину');
} else if ($amount === 1) {
} else if ($amount_buffer === 1) {
// Отправка уведомления
self::notification('Товар ' . $supply_model->catn . ' добавлен в корзину');
} else {
// Отправка уведомления
self::notification('Добавлено ' . $amount . ' товаров в корзину');
self::notification('Добавлено ' . $amount_buffer . ' товаров в корзину');
}
return $amount;
return $amount_buffer;
}
/**
* Удаление поставки
*
* @param Supply|string|array $supply Товары
* @param Supply|array $supply Товары
*
* @return int Количество удалённых рёбер
*
* @todo Доделать
*/
public function deleteSupply(Supply|string|array $supply): int
public function deleteSupply(Supply|array $supply): int
{
// Инициализация
$amount = 0;
@@ -189,37 +198,61 @@ class Order extends Document
// Передана инстанция класса поставки или второй элемент массива не является числом
// Унификация входных данных
$supply = [$supply->catn => 1];
$supply = [
$supply->catn => [
'auto' => 1
]
];
}
foreach (is_array($supply) ? $supply : [$supply => 1] as $catn => $amount_raw) {
// Перебор товаров
foreach ($supply as $catn => $data) {
// Перебор целей
if ($supply = Supply::searchByCatn($catn)) {
foreach (OrderEdgeSupply::searchByVertex($this->readId(), $supply->readId(), limit: $amount_raw) as $edge) {
// Перебор рёбер до продукта (если товаров в заказе несколько)
var_dump('ок');
// Удаление
$edge->delete();
foreach ($data as $type => $delete_amount) {
// Перебор данных цели
// Запись в журнал
$this->journal('delete', ['target' => $supply->readId()]);
var_dump('да');
if ($supply = Supply::searchByCatn($catn)) {
// Поставка найдена
// Постинкрементация счётчика удалённых рёбер
$amount++;
$edges = OrderEdgeSupply::searchByVertex($this->readId(), $supply->readId(), limit: $delete_amount, filter: ['order_edge_supply.dlvr.type == \'' . $type . '\'']);
for (; count($edges) > $amount; $amount++) {
var_dump(count($edges), $amount, $delete_amount);
var_dump(PHP_EOL);
// Удаление из базы данных
$edges[$amount]->delete();
// Запись в журнал
$this->journal(
'delete',
[
'target' => [
$supply->readId() => $type
]
]
);
}
}
}
}
// Генерация вставки текста с типом доставки
$type = Supply::DeliveryToRussian($type);
if ($amount === 0) {
// Отправка уведомления
self::notification('Неудачная попытка удалить товар из корзины');
self::notification('Не удалось удалить товар из корзины');
} else if ($amount === 1) {
// Отправка уведомления
self::notification('Товар ' . $supply->catn . ' удалён из корзины');
self::notification("Товар $supply->catn c $type удалён из корзины");
} else {
// Отправка уведомления
self::notification('Удалено ' . $amount . ' товаров из корзины');
self::notification("Удалено $amount товаров из корзины");
}
return $amount;
@@ -227,58 +260,183 @@ class Order extends Document
/**
* Поиск заказа
*
* @todo Привести в порядок
*/
public static function search(Account $account = null, string $type = 'current', int $limit = 1, int $page = 1, string $select = null): self|array|null
{
// Инициализация
$account or $account = yii::$app->user ?? throw new Exception('Не удалось инициализировать пользователя');
public static function searchSmart(
Account|string $account = null,
string $stts = 'current',
string|null $search = null,
int $limit = 1,
int $page = 1,
string|null $select = null,
bool $supplies = false,
int|null $from = null,
int|null $to = null,
bool $count = false,
bool $debug = false
): int|array|null {
// Инициализация аккаунта
if (empty($account)) {
// Не получен аккаунт
// Генерация сдвига по запрашиваемым данным (пагинация)
$offset = $limit * ($page - 1);
if (strcasecmp($type, 'all') !== 0) {
// Если не указан параметр поиска всех заказов
$where_type = [
'account_edge_order.type' => $type
$subquery_where = [
[
'account._id' => Account::initAccount()->readId()
]
];
} else if ($account instanceof Account) {
// Получен аккаунт
if (Account::isMinimalAuthorized(Account::initAccount())) {
$subquery_where = [
[
'account._id' => $account->readId()
]
];
} else {
throw new Exception('У вас нет прав на обработку другого пользователя');
}
} else if (str_contains($account, '@all')) {
// Получен запрос на обработку всех аккаунтов
$subquery_where = [];
} else {
$where_type = [];
throw new Exception('Не удалось инициализировать пользователя');
}
$return = self::searchByEdge(
// Инициализация типа заказа
if (strcasecmp($stts, '@all') !== 0) {
// Если не указан поиск всех заказов
$subquery_where[] = [
'account_edge_order.stts' => $stts
];
}
// Инициализация сдвига по запрашиваемым данным (пагинация)
$offset = $limit * ($page - 1);
// Инициализация фильтрации
if (isset($from, $to)) {
// Задан период
// Инициализация логики
$foreach = [
['edge' => 'account_edge_order'],
['jrnl' => 'order.jrnl']
];
// Инициализация фильтра
$where = "edge._to == order._id && jrnl.action == 'requested' && jrnl.date >= $from && jrnl.date <= $to";
} else {
// Ничего не задано
// Инициализация логики
$foreach = ['edge' => 'account_edge_order'];
// Инициализация фильтра
$where = "edge._to == order._id";
}
// Поиск заказов в базе данных
$orders = self::searchByEdge(
from: 'account',
to: 'order',
subquery_where: [
[
'account._id' => $account->id
],
$where_type
],
foreach: ['edge' => 'account_edge_order'],
where: 'edge._to == order._id',
subquery_where: $subquery_where,
foreach: $foreach,
where: $where,
limit: $limit,
offset: $offset,
sort: ['DESC'],
select: $select,
direction: 'INBOUND'
direction: 'INBOUND',
count: !$supplies && $count,
debug: $debug
);
return $limit === 1 ? $return[0] ?? null : $return;
if ($debug) {
var_dump($orders);
die;
}
if (!$supplies && $count) {
// Запрошен подсчет заказов
return $orders;
}
// Инициализация буфера возврата
$return = [];
// Инициализация архитектуры буфера вывода
foreach ($orders ?? [null] as $key => $order) {
// Перебор заказов
// Запись в буфер возврата
$return[$key]['order'] = $order;
}
if ($supplies) {
// Запрошен поиск поставок
foreach ($return as $key => &$container) {
// Перебор заказов
if ($container['order'] instanceof Order) {
// Инстанция заказа
// Инициализация заказа
$order = $container['order'];
} else {
// Массив с заказом (подразумевается)
// Инициализация настроек
$config = $container['order'];
unset($config['_id'], $config['_rev'], $config['_id']);
// Инициализация заказа
$order = new Order($config);
}
// Чтение полного содержания
$return[$key]['supplies'] = $order->supplies($limit, $page, $search, count: $count);
if ($count) {
// Запрошен подсчет поставок (переделать под подсчёт)
return $return[$key]['supplies'];
}
}
}
return $return;
}
/**
* Поиск содержимого заказа
* Поиск содержимых поставок заказа
*
* @todo В будущем возможно заказ не только поставок реализовать
* Переписать реестр и проверку на дубликаты, не понимаю как они работают
*/
public function content(int $limit = 1, int $page = 1): Supply|array|null
public function supplies(int $limit = 1, int $page = 1, string|null $search = null, bool $count = false): Supply|int|array|null
{
// Инициализация аккаунта
$account = Account::initAccount();
// Генерация сдвига по запрашиваемым данным (пагинация)
$offset = $limit * ($page - 1);
// Поиск рёбер: ЗАКАЗ -> ПОСТАВКА
$supplies = Supply::searchByEdge(
if (!empty($search)) {
// Передан поиск по продуктам в заказах
// Запись ограничения по максимальному значению
$limit = 9999;
}
// Поиск по рёбрам: ЗАКАЗ -> ПОСТАВКА
$connections = Supply::searchByEdge(
from: 'order',
to: 'supply',
edge: 'order_edge_supply',
@@ -287,77 +445,307 @@ class Order extends Document
'order._id' => $this->readId()
]
],
foreach: ['edge' => 'order_edge_supply'],
where: 'edge._to == supply._id',
where: 'order_edge_supply != []',
limit: $limit,
offset: $offset,
direction: 'INBOUND'
filterStart: ['catn' => $search],
direction: 'INBOUND',
select: '{order_edge_supply}',
count: $count
);
// Инициализация реестра дубликатов
$registry = [];
if ($count) {
// Подсчёт запрошен
// Подсчёт и перестройка массива для очистки от дубликатов
foreach ($supplies as $key => &$supply) {
// Перебор поставок
if (in_array($supply->catn, $registry)) {
// Если данная поставка найдена в реестре
// Удаление
unset($supplies[$key]);
// Пропуск итерации
continue;
}
// Инициализация
$amount = 0;
// Повторный перебор для поиска дубликатов
foreach ($supplies as &$supply4check) {
if ($supply == $supply4check) {
// Найден дубликат
// Постинкрементация счётчика
$amount++;
// Запись в реестр
$registry[] = $supply4check->catn;
}
}
// Запись количества для заказа
$supply->amnt = $amount;
return $connections;
}
// Поиск стоимости для каждой поставки
foreach ($supplies as $key => &$supply) {
// Перебор поставок
// Инициализация реестра дубликатов
$supplies = [];
// Чтение стоимости
$cost = $supply->readCost();
foreach ($connections as $key => &$connection) {
// Перебор объектов для заказа
if ($cost < 1) {
// Если стоимость равна нулю (явная ошибка)
foreach ($connection['order_edge_supply'] as $edge) {
// Перебор связанных поставок
// Удаление из базы данных
$this->deleteSupply($supply->readId());
// Инициализация связанной с заказом поставки
$supply = Supply::searchById($edge['_to']);
// Удаление из списка
unset($supplies[$key]);
// Инициализация поставщика в буфере
if (empty($supplies[$supply->prod][$supply->catn][$edge['dlvr']['type']])) $supplies[$supply->prod][$supply->catn][$edge['dlvr']['type']] = [
'supply' => $supply,
'account' => Account::searchBySupplyId($supply->readId()),
'product' => Product::searchBySupplyId($supply->readId()),
'currency' => 'руб',
'amount' => 1
];
else ++$supplies[$supply->prod][$supply->catn][$edge['dlvr']['type']]['amount'];
// Пропуск итерации
continue;
// Инициализация буфера с обрабатываемой поставкой
$buffer = &$supplies[$supply->prod][$supply->catn][$edge['dlvr']['type']];
// Запись ребра
$buffer['edge'] = $edge;
if (empty($buffer['supply']->cost) || $buffer['supply']->cost < 1) {
// Если стоимость не найдена или равна нулю (явная ошибка)
// Удаление из базы данных
$this->deleteSupply($buffer['supply']);
// Инициализация стоимости товара для уведомления (чтобы там не было NULL)
$cost = $buffer['supply']->cost ?? 0;
// Отправка уведомлений покупателю
Notification::_write("Стоимость товара $supply->catn равна $cost", account: $account->_key, type: Notification::TYPE_ERROR);
Notification::_write("Товар $supply->catn удалён", account: $account->_key, type: Notification::TYPE_ERROR);
// Отправка уведомления поставщику
// Отправка уведомления модератору
// Удаление из списка
unset($connections[$key]);
// Выход из цикла
break;
}
try {
// Инициализация данных геолокации
try {
$from = (int) $buffer['account']['opts']['delivery_from_terminal'] ?? empty(Settings::searchActive()->delivery_from_default) ? 36 : (int) Settings::searchActive()->delivery_from_default;
} catch (Exception $e) {
$from = empty(Settings::searchActive()->delivery_from_default) ? 36 : (int) Settings::searchActive()->delivery_from_default;
}
try {
$to = (int) yii::$app->user->identity->opts['delivery_to_terminal'] ?? 36;
} catch (Exception $e) {
$to = 36;
}
if (($buffer_connection = $buffer['product']['bffr']["$from-$to-" . $edge['dlvr']['type']] ?? false) && time() < $buffer_connection['expires']) {
// Найдены данные доставки в буфере и их срок хранения не превышен, информация актуальна
// Запись в буфер вывода
$buffer['delivery'] = $buffer_connection['data'];
} else {
// Инициализация инстанции продукта в базе данных (реинициализация под ActiveRecord)
$product = Product::searchByCatnAndProd($buffer['product']['catn'], $buffer['product']['prod']);
// Инициализация доставки Dellin (автоматическая)
$product->bffr = ($product->bffr ?? []) + [
"$from-$to-" . $edge['dlvr']['type'] => [
'data' => $buffer['delivery'] = Dellin::calcDeliveryAdvanced(
$from,
$to,
(int) ($buffer['product']['wght'] ?? 0),
(int) ($buffer['product']['dmns']['x'] ?? 0),
(int) ($buffer['product']['dmns']['y'] ?? 0),
(int) ($buffer['product']['dmns']['z'] ?? 0),
avia: $edge['dlvr']['type'] === 'avia'
),
'expires' => time() + 86400
]
];
// Отправка в базу данных
$product->update();
}
// Запись цены (цена поставки + цена доставки + наша наценка)
$buffer['cost'] = ($supply->cost ?? $supply->onec['Цены']['Цена']['ЦенаЗаЕдиницу'] ?? throw new exception('Не найдена цена товара')) + ($buffer['delivery']['price']['all'] ?? $buffer['delivery']['price']['one'] ?? 0) + ($settings['increase'] ?? 0) ?? 0;
} catch (Exception $e) {
$buffer['delivery'] = null;
}
}
// Запись цены
$supply->cost = $cost['ЦенаЗаЕдиницу'] . ' ' . $cost['Валюта'];
}
return $supplies;
}
/**
* Проверка на то, что все поставки подтверждены
*
* @param array $order_edge_supply Поставки
*
* @return bool Статус подтверждения всех поставок (true если все и false если хотя бы одна из них не подтверждена)
*/
public static function checkSuppliesStts(array $order_edge_supply): bool
{
if (isset($order_edge_supply['_key'])) {
// Получено ребро: ЗАКАЗ -> ПОСТАВКА
if (empty($order_edge_supply['stts']) || $order_edge_supply['stts'] === 'processing' || $order_edge_supply['stts'] === 'requested') {
// Найдена неподтверждённая поставка
return false;
}
return true;
} else if (isset($order_edge_supply[0]['_key'])) {
// Получен массив (подразумевается, что с рёбрами: ЗАКАЗ -> ПОСТАВКА)
foreach ($order_edge_supply as $edge) {
// Перебор поставок
if (empty($edge['stts']) || $edge['stts'] === 'processing' || $edge['stts'] === 'requested') {
// Найдена неподтверждённая поставка
return false;
}
}
return true;
}
return false;
}
/**
* @return DocumentInterface[]
*/
public static function findDocuments1c(): ?array
{
// yii::$app->on(ApiController::EVENT_AFTER_EXPORT_ORDERS, self::afterExport1c());
$orders = self::searchByEdge(
from: 'account',
to: 'order',
edge: 'account_edge_order',
direction: 'INBOUND',
subquery_where: 'account_edge_order.type == "processed"',
where: ['sync' => false]
);
foreach ($orders as &$order) {
// Перебор заказов
// Запись о том, что синхронизация проведена
$order->sync = true;
$order->update();
}
return $orders;
}
/**
* @return OfferInterface[]
*/
public function getOffers1c(): mixed
{
// Инициализация
$supplies = [];
foreach ($this->jrnl as $key => $jrnl) {
// Перебор журнала
// if (isset($supplies[$key]) && $supplies[$key]['id'] !== $jrnl['target']) {
// // Запись уже существует и идентификаторы не совпадают
// $key .= '_du'
// // Реинициализация
// $supplies[$key]['id'] = $jrnl['target'];
// } else {
// // Инициализация
// }
if (($jrnl['action'] ?? null) === 'write') {
// Найдено событие записи товара к заказу
$supplies[$key]['id'] = $jrnl['target'];
$supplies[$key]['amount'] ?? $supplies[$key]['amount'] = 0;
++$supplies[$key]['amount'];
} else if (($jrnl['action'] ?? null) === 'delete') {
// Найдено событие удаления товара из заказа
$supplies[$key]['id'] = $jrnl['target'];
$supplies[$key]['amount'] ?? $supplies[$key]['amount'] = 0;
--$supplies[$key]['amount'];
}
}
// file_put_contents('supplies.txt', print_r($supplies, true));
if (count($supplies) > 0) {
// Поставки были записаны
// Инициализация
$supplies_buffer = [];
foreach ($supplies as $id => $supply) {
// Перебор поставок
if ($supply['amount'] < 1) {
continue;
}
if ($response = Supply::searchById($supply['id'])) {
// Поставка найдена в базе данных
$supplies_buffer[] = $response;
}
}
return $supplies_buffer;
}
// file_put_contents('AAAAAAAAAAAAAAAAAAA.txt', print_r(1, true));
return [];
}
/**
* Неизвестно для чего
*/
public function getRequisites1c(): void
{
// return true;
}
/**
* Получить контрагента (пользователя от лица которого происходит операция)
*
* @return PartnerInterface
*/
public function getPartner1c(): Account
{
return yii::$app->user->identity ?? throw new Exception('Не удалось идентифицировать пользователя');
}
public function getExportFields1c($context = null)
{
return [];
}
/**
* Возвращаем имя поля в базе данных, в котором хранится ID из 1с
*
* @return string
*/
public static function getIdFieldName1c()
{
return 'ocid';
}
public function setRaw1cData($cml, $object): void
{
}
protected static function afterExport1c(): void
{
file_put_contents('afterExport1c.txt', print_r(1, true));
}
/**
* Отправка уведомления
*/
@@ -366,4 +754,58 @@ class Order extends Document
// Отправка
return Notification::_write($text, type: $type);
}
/**
* Посчитать количство заказов или количество поставок в заказах
*
* @param int $limit Ограничение
* @param bool $supplies Считать поставки в активном заказе (иначе - заказы)
*
* @return int Количество
*/
public static function count(int $limit = 500, bool $supplies = false): int
{
return (int) self::searchSmart(supplies: $supplies, stts: $supplies ? 'current' : '@all', limit: $limit, count: true);
}
/**
* Генерация ярлыка на русском языке для статуса заказа
*
* @param string|null $status Статус заказа
*
* @return string Ярлык статуса на русском языке
*/
public static function statusToRussian(?string $status = 'processing'): string
{
return match ($status) {
'processing' => 'Обрабатывается',
'requested' => 'Запрошен',
'accepted' => 'Ожидается отправка',
'going' => 'Доставляется',
'ready' => 'Готов к выдаче',
'completed' => 'Завершен',
'reserved' => 'Резервирован',
default => 'Обрабатывается'
};
}
/**
* Генерация списка статусов на русском языке
*
* @param string|null $active Активный статус, который выведется первым в списке
*
* @return string Лист статусов на русском языке
*/
public static function statusListInRussian(?string $active = 'processing'): array
{
return [(empty($active) ? 'processing' : $active) => static::statusToRussian($active)] + [
'processing' => 'Обрабатывается',
'requested' => 'Запрошен',
'accepted' => 'Ожидается отправка',
'going' => 'Доставляется',
'ready' => 'Готов к выдаче',
'completed' => 'Завершен',
'reserved' => 'Резервирован'
];
}
}

View File

@@ -6,8 +6,164 @@ namespace app\models;
class OrderEdgeSupply extends Edge
{
/**
* Имя коллекции
*/
public static function collectionName(): string
{
return 'order_edge_supply';
}
/**
* Свойства
*/
public function attributes(): array
{
return array_merge(
parent::attributes(),
[
'comm',
'cost',
'time',
'stts',
'dlvr'
]
);
}
/**
* Метки свойств
*/
public function attributeLabels(): array
{
return array_merge(
parent::attributeLabels(),
[
'comm' => 'Комментарий',
'cost' => 'Цена',
'time' => 'Время',
'stts' => 'Статус',
'dlvr' => 'Доставка'
]
);
}
/**
* Правила
*/
public function rules(): array
{
return array_merge(
parent::rules(),
[
[
'comm',
'string',
'length' => [0, 300],
'message' => '{attribute} должен быть строкой от 0 до 300 символов'
],
[
'stts',
'string',
'length' => [0, 15],
'message' => '{attribute} должен быть строкой от 0 до 15 символов'
],
[
'cost',
'integer',
'message' => '{attribute} должна быть числом'
],
[
'time',
'integer',
'message' => '{attribute} должно быть числом'
],
[
'dlvr',
'arrayValidator',
'message' => '{attribute} должен быть массивом'
]
]
);
}
/**
* Поиск поставки по артикулу
*
* @param string $catn Артикул
* @param Order $order Заказ
* @param int $limit Максимальное количество
*
* @return array Поставки
*
* @deprecated
*/
public static function searchBySupplyCatn(string $catn, Order $order = null, int $limit = 10): array
{
if ($supply = Supply::searchByCatn($catn, 1)) {
// Поставка найдена
if (isset($order)) {
// Поиск только по определённому заказу
return self::find()->where(['_from' => $order->readId(), '_to' => $supply->readId()])->limit($limit)->all();
}
return self::find()->where(['_to' => $supply->readId()])->limit($limit)->all();
}
return [];
}
/**
* Поиск по данным поставки
*
* @param string $catn Артикул
* @param Order $order Заказ
* @param int $limit Максимальное количество
*
* @return array Поставки
*/
public static function searchBySupplyData(string $catn, string $prod, ?string $delivery = null, ?Order $order = null, int $limit = 10): array
{
if ($supply = Supply::searchByCatnAndProd($catn, $prod)) {
// Поставка найдена
if (isset($order)) {
// Поиск только по определённому заказу
if (isset($delivery)) {
// Поиск только по определённым типам доставки
return self::find()->where(['_from' => $order->readId(), '_to' => $supply->readId(), 'order_edge_supply.dlvr.type' => $delivery])->limit($limit)->all();
}
return self::find()->where(['_from' => $order->readId(), '_to' => $supply->readId()])->limit($limit)->all();
}
return self::find()->where(['_to' => $supply->readId()])->limit($limit)->all();
}
return [];
}
/**
* Генерация ярлыка на русском языке для статуса заказа
*
* @param string|null $status Статус заказа
*
* @return string Ярлык статуса на русском языке
*/
public static function statusToRussian(?string $status = 'processing'): string
{
return match ($status) {
'requested' => 'Запрошен',
'accepted' => 'Ожидается отправка',
'going' => 'Доставляется',
'completed' => 'Завершен',
'processing' => 'Обрабатывается',
default => 'Обрабатывается'
};
}
}

View File

@@ -4,12 +4,10 @@ declare(strict_types=1);
namespace app\models;
use yii;
use yii\web\UploadedFile;
use yii\imagine\Image;
use app\models\traits\SearchByEdge;
use moonland\phpexcel\Excel;
/**
@@ -43,20 +41,24 @@ class Product extends Document
const SCENARIO_WRITE = 'write';
/**
* Файл .excel для импорта товаров
* Аккаунт
*
* Используется для управления администратором от лица пользователя
*/
public Excel|string|array|null $file_excel = null;
public Account|string|null $account = null;
/**
* Файл .excel для импорта товаров
*
* Универсальный, когда неизвестно на какую позицию загружать каталог
*/
public Excel|UploadedFile|string|null $file_excel = null;
/**
* Изображение для импорта
*/
public UploadedFile|string|array|null $file_image = null;
/**
* Группа в которой состоит товар
*/
public ProductGroup|null $group = null;
/**
* Имя коллекции
*/
@@ -75,12 +77,15 @@ class Product extends Document
[
'catn',
'name',
'desc',
'ocid',
// В библеотеке есть баг на название DESC (неизвестно в моей или нет)
'dscr',
'prod',
'dmns',
'wght',
'imgs',
'time',
'oemn',
'cost'
'bffr',
'stts'
]
);
}
@@ -95,21 +100,25 @@ class Product extends Document
[
'catn' => 'Каталожный номер (catn)',
'name' => 'Название (name)',
'desc' => 'Описание (desc)',
'ocid' => 'Идентификатор 1C (ocid)',
'dscr' => 'Описание (dscr)',
'prod' => 'Производитель (prod)',
'dmns' => 'Габариты (dmns)',
'wght' => 'Вес (wght)',
'imgs' => 'Изображения (imgs)',
'time' => 'Срок доставки (time)',
'oemn' => 'OEM номера (oemn)',
'cost' => 'Стоимость (cost)',
'bffr' => 'Буфер',
'stts' => 'Статус',
'file_excel' => 'Документ (file_excel)',
'file_image' => 'Изображение (file_image)',
'group' => 'Группа (group)'
'account' => 'Аккаунт'
]
);
}
/**
* Правила
*
* @todo Правило для всех трёх габаритов
*/
public function rules(): array
{
@@ -130,25 +139,48 @@ class Product extends Document
],
[
[
'oemn',
'imgs'
'prod',
'name'
],
'arrayValidator',
'message' => '{attribute} должен быть массивом.'
'string',
'length' => [2, 80],
'message' => '{attribute} должен быть строкой от 3 до 80 символов'
],
[
'file_excel',
'required',
'message' => 'Заполните поля: {attribute}',
'on' => self::SCENARIO_IMPORT_EXCEL
'imgs',
'arrayValidator',
'message' => '{attribute} должен быть массивом'
],
[
'dscr',
'string',
'length' => [3, 256],
'message' => '{attribute} должен быть строкой от 3 до 256 символов'
],
[
'dmns',
'arrayWithNumbersValidator',
'message' => '{attribute} должен быть массивом и хранить циферные значения'
],
[
'wght',
'integer',
'min' => 0,
'max' => 30000,
'message' => '{attribute} должен иметь значение от 0 до 30000'
],
[
'stts',
'string',
'length' => [4, 20],
'message' => '{attribute} должен быть строкой от 4 до 20 символов'
],
[
'file_excel',
'file',
'skipOnEmpty' => false,
'skipOnEmpty' => true,
'extensions' => 'xlsx',
'checkExtensionByMimeType' => false,
'maxFiles' => 5,
'maxSize' => 1024 * 1024 * 30,
'wrongExtension' => 'Разрешены только документы в формате: ".xlsx"',
'message' => 'Проблема при чтении документа',
@@ -164,7 +196,7 @@ class Product extends Document
'file_image',
'file',
'skipOnEmpty' => false,
'extensions' => ['jpg', 'jpeg', 'png', 'gif', 'webp'],
'extensions' => ['jpg', 'jpeg', 'png', 'gif', 'webp', 'jfif'],
'checkExtensionByMimeType' => true,
'maxFiles' => 10,
'maxSize' => 1024 * 1024 * 30,
@@ -177,88 +209,45 @@ class Product extends Document
}
/**
* Инициализация продукта
* Перед сохранением
*
* @param string $catn Артикул, каталожный номер
* @todo Подождать обновление от ебаного Yii2 и добавить
* проверку типов передаваемых параметров
*/
public static function initEmpty(string $catn): self|array
public function beforeSave($create): bool
{
$oemn = self::searchOemn($catn);
if (parent::beforeSave($create)) {
// Пройдена родительская проверка
if (count($oemn) === 1) {
// Передан только один артикул
if ($this->isNewRecord) {
// Новая запись
if ($model = self::searchByCatn($catn)) {
// Продукт уже существует
return $model;
$this->stts = $this->stts ?? 'inactive';
}
// Запись пустого продукта
return self::writeEmpty($catn);
return true;
}
// Инициализация
$models = [];
foreach ($oemn as $catn) {
// Перебор всех найденных артикулов
if ($model = self::searchByCatn($catn)) {
// Продукт уже существует
continue;
}
// Запись
if ($model = self::writeEmpty($catn)) {
// Записано
// Запись в массив сохранённых моделей
$models[] = $model;
}
}
return $models;
return false;
}
/**
* Запись пустого продукта
* Запись пустого товара
*/
public static function writeEmpty(string $catn): ?self
public static function writeEmpty(string $catn, string $prod = 'Неизвестный', bool $active = false): ?self
{
// Инициализация
$model = new self;
// Настройки
$model->catn = $catn;
$model->prod = $prod;
$model->stts = $active ? 'active' : 'inactive';
// Запись
return $model->save() ? $model : null;
}
/**
* Поиск OEM номеров
*
* @param string $oemn Необработанная строка с OEM-номерами
* @param string $delimiters Разделители
*
* @todo НЕ ЗАБЫТЬ СДЕЛАТЬ НАСТРОЙКУ РАЗДЕЛИТЕЛЕЙ
*
* @return array OEM-номера
*/
public static function searchOemn(string $oemn, string $delimiters = '\s\+\/,'): array
{
// Инициализация
$catn = [];
// Конвертация
preg_match_all("/[^$delimiters]+/", $oemn, $catn);
return $catn[0];
}
/**
* Импорт изображений
*
@@ -266,12 +255,12 @@ class Product extends Document
*/
public function importImages(): int
{
// Инициализация
$amount = 0;
if ($this->validate()) {
// Проверка пройдена
// Инициализация
$amount = 0;
foreach ($this->file_image as $file) {
// Перебор обрабатываемых изображений
@@ -285,7 +274,6 @@ class Product extends Document
};
}
if (!file_exists(YII_PATH_PUBLIC . $catalog_h150 = '/img/products/' . $this->_key . '/h150')) {
// Директория для обложек изображений продукта не найдена
@@ -307,16 +295,19 @@ class Product extends Document
Image::resize(YII_PATH_PUBLIC . $catalog . '/' . $file->baseName . '.' . $file->extension, 150, 150)
->save(YII_PATH_PUBLIC . $catalog_h150 . '/' . $file->baseName . '.' . $file->extension, ['quality' => 80]);
// Инициализация
$this->imgs ?? $this->imgs = [];
// Запись в базу данных
$this->imgs = array_merge(
$this->imgs ?? [],
$this->imgs,
[[
'covr' => count($this->imgs) === 0 ? true : false,
'orig' => $catalog . '/' . $file->baseName . '.' . $file->extension,
'h150' => $catalog_h150 . '/' . $file->baseName . '.' . $file->extension
]]
);
$this->scenario = self::SCENARIO_WRITE;
if ($this->save()) {
@@ -328,94 +319,23 @@ class Product extends Document
}
}
return $amount;
}
if ($this->hasErrors()) {
// Получены ошибки
/**
* Импорт товаров
*
* На данный момент обрабатывает только импорт из
* файлов с расширением .excel
*/
public function importExcel(): bool
{
// Инициализация
$data = [];
$amount = 0;
foreach ($this->getErrors() as $attribute => $errors) {
// Перебор атрибутов
if ($this->validate()) {
foreach ($this->file_excel as $file) {
// Перебор файлов
foreach ($errors as $error) {
// Перебор ошибок атрибутов
// Инициализация
$dir = '../assets/import/' . date('Y_m_d#H-i', time()) . '/excel/';
$label = $this->getAttributeLabel($attribute);
// Сохранение на диск
if (!file_exists($dir)) {
mkdir($dir, 0775, true);
}
$file->saveAs($path = $dir . $file->baseName . '.' . $file->extension);
$data[] = Excel::import($path, [
'setFirstRecordAsKeys' => true,
'setIndexSheetByName' => true,
]);
}
foreach ($data as $data) {
// Перебор конвертированных файлов
if (count($data) < 1) {
// Не найдены строки с товарами
$this->addError('erros', 'Не удалось найти данные товаров');
} else {
// Перебор найденных товаров
foreach ($data as $doc) {
// Перебор полученных документов
// Сохранение в базе данных
$product = new static($doc);
$product->scenario = $product::SCENARIO_WRITE;
if ($product->validate()) {
// Проверка пройдена
// Запись документа
$product->save();
// Постинкрементация счётчика
$amount++;
// Запись группы
// $group = static::class . 'Group';
// (new $group())->writeMember($product, $this->group);
} else {
// Проверка не пройдена
foreach ($product->errors as $attribute => $error) {
$this->addError($attribute, $error);
}
}
}
Notification::_write("$label: $error", type: Notification::TYPE_ERROR);
}
}
// Деинициализация
$this->file_excel = '';
static::afterImportExcel($amount);
return true;
}
$this->addError('erros', 'Неизвестная ошибка');
static::afterImportExcel($amount);
return false;
return $amount;
}
/**
@@ -426,7 +346,7 @@ class Product extends Document
*
* @todo Переделать нормально
*/
public static function searchByCatn(string $catn, int $limit = 1, array $select = []): static|array|null
public static function searchByCatn(string $catn, int $limit = 999, array $select = []): static|array|null
{
if ($limit <= 1) {
return static::findOne(['catn' => $catn]);
@@ -450,19 +370,21 @@ class Product extends Document
}
/**
* Поиск по каталожному номеру (через представления)
* Поиск по каталожному номеру и производителю
*
* Ищет продукт и возвращает его,
* либо выполняет поиск через представление
*
* @todo Переделать нормально
*/
public static function searchByPartialCatn(string $catn, int $limit = 1, array $select = []): static|array|null
public static function searchByCatnAndProd(string $catn, string $prod, int $limit = 1, array $select = []): static|array|null
{
if ($limit <= 1) {
return static::findOne(['catn' => $catn, 'prod' => $prod]);
}
$query = self::find()
->for('product')
->in('product_search')
->search(['catn' => $catn])
->where(['catn' => $catn, 'prod' => $prod])
->limit($limit)
->select($select)
->createCommand()
@@ -479,40 +401,274 @@ class Product extends Document
}
/**
* Вызывается после загрузки поставок из excel-документа
* Поиск по каталожному номеру (через представления)
*
* @param int $amount Количество
* Ищет продукт и возвращает его,
* либо выполняет поиск через представление
*
* @todo Переделать нормально
*/
public static function afterImportExcel(int $amount = 0): bool
public static function searchByPartialCatn(string $catn, string $stts = 'active', int $limit = 1, array $select = []): static|array|null
{
// Инициализация
$model = new Notification;
$date = date('H:i d.m.Y', time());
$query = self::find()
->for('product')
->in('product_search')
->where(['stts' => $stts])
->search(<<<AQL
SEARCH ANALYZER(
LEVENSHTEIN_MATCH(
product.catn,
"$catn",
3,
true
)
OR STARTS_WITH(
product.catn,
"$catn"
),
"::text_en_no_stem")
AQL)
->limit($limit)
->orderBy('BM25(product) DESC')
->select($select)
->createCommand()
->execute()
->getAll();
// Настройка
$model->text = yii::$app->controller->renderPartial('/notification/system/afterImportExcel', compact('amount', 'date'));
$model->type = $model::TYPE_NOTICE;
foreach ($query as &$attribute) {
// Приведение всех свойств в массив и очистка от лишних данных
// Отправка
return (bool) $model->write();
$attribute = $attribute->getAll();
}
return $query;
}
/**
* Вызывается после загрузки поставок из 1С
* Найти по идентификатору поставки
*
* @param int $amount Количество
* @param string|null $_id Идентификатор поставки
*
* @return array|null Товар (Product)
*/
public static function afterImportOnec(): bool
public static function searchBySupplyId(string $_id): ?array
{
return static::searchByEdge(
from: 'supply',
to: 'product',
edge: 'supply_edge_product',
direction: 'INBOUND',
subquery_where: [
[
'supply_edge_product._from == "' . $_id . '"'
],
[
'supply_edge_product._to == product._id'
]
],
subquery_select: 'product',
where: 'supply_edge_product[0]._id != null',
limit: 1,
select: 'supply_edge_product[0]'
)[0] ?? null;
}
/**
* Подключение аналога
*
* @param Product $target Товар который надо подключить
*
* @return ProductEdgeProductGroup|null Ребро между товаром и группой, если создалось
*/
public function connect(Product $product): ?ProductEdgeProductGroup
{
if (!$group = ProductGroup::searchByProduct($this)) {
// Не найдена группа товаров
// Запись новой группы
$group = ProductGroup::writeEmpty(active: true);
// Запись товара в группу
$group->writeProduct($this);
}
if ($_group = ProductGroup::searchByProduct($product)) {
// Найдена другая группа у товара который надо добавить в группу
// Перенос всех участников (включая целевой товар)
return $group->transfer($_group);
} else {
// Не найдена группа у товара который надо добавить в группу
// Запись целевого товара в группу
return $group->writeProduct($product);
}
return null;
}
/**
* Отключение аналога
*
* @return bool Статус выполнения
*/
public function disconnect(): bool
{
if ($group = ProductGroup::searchByProduct($this)) {
// Найдена группа товаров
// Удаление из группы
$group->deleteProduct($this);
return true;
} else {
// Не найдена группа товаров
// Заебись
return true;
}
return false;
}
/**
* Проверка на уникальность
*
* @return bool|static Товар, если найден
*
* @todo
* 1. Обработка дубликатов
*/
public function validateForUniqueness(): bool|static
{
if ($supplies = self::search(['catn' => $this->catn, 'prod' => $this->prod], limit: 100)) {
// Найдены поставки с таким же артикулом (catn) и производителем (prod)
// if (count($supplies) > 1) throw new exception ('В базе данных имеется более чем один дубликат', 500);
if (count($supplies) > 1) return false;
// Запись обрабатываемой поставки
$supply = $supplies[0];
// Возврат (найден дубликат в базе данных)
return $supply;
}
// Возврат (подразумевается отсутствие дубликатов в базе данных)
return false;
}
/**
* Инициализация продукта
*
* @param string $catn Артикул, каталожный номер
*/
public static function initEmpty(string $catn, string $prod): Supply|array
{
$oemn = self::searchOemn($catn);
if (count($oemn) === 1) {
// Передан только один артикул
if ($model = Product::searchByCatnAndProd($catn, $prod)) {
// Продукт уже существует
return $model;
}
// Запись пустого продукта
return Product::writeEmpty($catn, $prod);
}
// Инициализация
$model = new Notification;
$date = date('H:i d.m.Y', time());
$models = [];
// Настройка
$model->text = yii::$app->controller->renderPartial('/notification/system/afterImportOnec', compact('amount', 'date'));
$model->type = $model::TYPE_NOTICE;
foreach ($oemn as $catn) {
// Перебор всех найденных артикулов
// Отправка
return (bool) $model->write();
if ($model = Product::searchByCatnAndProd($catn, $prod)) {
// Продукт уже существует
continue;
}
// Запись
if ($model = Product::writeEmpty($catn, $prod)) {
// Записано
// Запись в массив сохранённых моделей
$models[] = $model;
}
}
return $models;
}
/**
* Активация
*
* @return bool Статус выполнения
*/
public function activate(): bool
{
$this->stts = 'active';
if ($this->update() > 0) return true;
return false;
}
/**
* Найти товары по группе
*
* @param string|null $_id Идентификатор группы
*
* @return array|null Товары (Product)
*/
public static function searchByProductGroup(string $_id): ?array
{
return static::searchByEdge(
from: 'product_group',
to: 'product',
edge: 'product_edge_product_group',
direction: 'INBOUND',
subquery_where: [
[
'product_edge_product_group._from == "' . $_id . '"'
]
],
subquery_select: 'product',
where: 'product_edge_product_group[0]._id != null',
limit: 1,
select: 'product_edge_product_group[0]'
)[0];
}
/**
* Инициализация обложки для товара
*
* Ищет логотип в нужной категории (размере) для выбранного производителя
*
* @param string $prod Производитель
* @param int $size Размерная группа
*
* @return string Относительный путь до изображения от публичной корневой папки
*/
public static function cover(string $prod, int $size = 150): string
{
if ($size === 0) $size = '';
else $size = "h$size/";
// Инициализация пути
$path = "/img/covers/$size" . strtolower($prod);
// Поиск файла и возврат
if (file_exists(YII_PATH_PUBLIC . DIRECTORY_SEPARATOR . $path . '.jpg')) return $path . '.jpg';
else if (file_exists(YII_PATH_PUBLIC . DIRECTORY_SEPARATOR . $path . '.jpeg')) return $path . '.jpeg';
else if (file_exists(YII_PATH_PUBLIC . DIRECTORY_SEPARATOR . $path . '.png')) return $path . '.png';
// Возврат изображения по умолчанию
return "/img/covers/$size" . 'product.png';
}
}

View File

@@ -10,4 +10,49 @@ class ProductEdgeProduct extends Edge
{
return 'product_edge_product';
}
/**
* Найти все соединения
*
* @param string $_key Ключ товара
* @param int $limit Ограничение по количеству
*
* @return array|null Найденные соединения (массив с ключами - "_key")
*/
public static function searchConnections(string $_key, int $limit = 30): ?array
{
// Инициализация буфера возврата
$return = [];
// Поиск аналогов
$edges = self::find()->where(['_from' => Product::collectionName() . "/$_key"])->limit($limit)->all();
foreach ($edges as $edge) {
// Перебор найденных рёбер
// Извлечение ключа
preg_match_all('/\w+\/([0-9]+)/', $edge->_to, $matches);
// Запись артикула в буфер вывода
$return[] = $matches[1][0];
}
// Перерасчет ограничения по количеству
$limit -= count($edges);
// Поиск аналогов (с обратной привязкой)
$edges = self::find()->where(['_to' => Product::collectionName() . "/$_key"])->limit($limit)->all();
foreach ($edges as $edge) {
// Перебор найденных рёбер
// Извлечение ключа
preg_match_all('/\w+\/([0-9]+)/', $edge->_from, $matches);
// Запись артикула в буфер вывода
$return[] = $matches[1][0];
}
return $return;
}
}

View File

@@ -10,4 +10,17 @@ class ProductEdgeProductGroup extends Edge
{
return 'product_edge_product_group';
}
/**
* Поиск по товару
*
* @param Product $product Товар
* @param int $amount Ограничение по максимальному количеству
*
* @return null|self Ребро, если найдено
*/
public static function searchByProduct(Product $product, int $limit = 1): ?self
{
return self::find()->where(['_from' => $product->readId()])->limit($limit)->all()[0] ?? null;
}
}

View File

@@ -4,15 +4,19 @@ declare(strict_types=1);
namespace app\models;
use app\models\traits\SearchByEdge;
use carono\exchange1c\interfaces\GroupInterface;
use Zenwalker\CommerceML\Model\Group;
/**
* Группировка продуктов
* Группировка продуктов для соединения их в аналоги
*/
class ProductGroup extends Document implements GroupInterface
{
use SearchByEdge;
/**
* Имя коллекции
*/
@@ -29,7 +33,7 @@ class ProductGroup extends Document implements GroupInterface
return array_merge(
parent::attributes(),
[
'name'
'stts'
]
);
}
@@ -42,7 +46,7 @@ class ProductGroup extends Document implements GroupInterface
return array_merge(
parent::attributeLabels(),
[
'name' => 'Название (name)'
'stts' => 'Статус'
]
);
}
@@ -55,23 +59,152 @@ class ProductGroup extends Document implements GroupInterface
return array_merge(
parent::rules(),
[
// [
// 'name',
// 'required',
// 'message' => 'Заполните поле: {attribute}'
// ]
[
'stts',
'string',
'length' => [4, 20],
'message' => '{attribute} должен быть строкой от 4 до 20 символов'
]
]
);
}
/**
* Запись пустой группы
*
* @param bool $active Статус активации
*
* @return self Группа товаров, если создана
*/
public static function writeEmpty(bool $active = false): ?self
{
// Инициализация
$model = new self;
// Настройки
$model->stts = $active ? 'active' : 'inactive';
// Запись
return $model->save() ? $model : null;
}
/**
* Запись члена группы
*
* @deprecated
*/
public function writeMember(Product $member): ProductEdgeProductGroup
{
return ProductEdgeProductGroup::write($member->readId(), $this->readId(), 'member');
}
/**
* Запись товара в группу
*
* @param Product $product Товар
*/
public function writeProduct(Product $product): ?ProductEdgeProductGroup
{
// Запись товара в группу
$edge = ProductEdgeProductGroup::write($product->readId(), $this->readId(), data: ['type' => 'member']);
// Запись в журнал
$this->journal('write member', [
'product' => $product->readId()
]);
return $edge;
}
/**
* Удаление товара из группы
*
* @param Product $product Товар
*
* @return void
*/
public function deleteProduct(Product $product): void
{
// Удаление товара из группы (подразумевается, что будет только одно)
foreach (ProductEdgeProductGroup::searchByVertex($product->readId(), $this->readId(), filter: ['type' => 'member']) as $edge) $edge->delete();
// Запись в журнал
$this->journal('delete member', [
'product' => $product->readId()
]);
}
/**
* Найти рёбра до товаров
*
* @param int $limit Ограничение по максимальному количеству
*/
public function searchEdges(int $limit = 100, int $page = 1): ?array
{
return ProductEdgeProductGroup::searchByDirection($this->readId(), 'INBOUND', where: ['type' => 'member'], limit: $limit, page: $page);
}
/**
* Прочитать связанные товары
*
* @param int $limit Ограничение по максимальному количеству
*/
public function searchProducts(int $limit = 100, int $page = 1): ?array
{
// Инициализация буфера товаров
$products = [];
foreach ($this->searchEdges($limit, $page) as $edge) {
// Перебор рёбер
$products[] = Product::searchById($edge->_from);
}
return $products;
}
/**
* Перенос членов группы из другой
*
* @param ProductGroup $group Группа из которой нужен перенос
*
* @return null|int Количество перенесённых товаров, если произведён перенос
*/
public function transfer(ProductGroup $group): ?int
{
// Проверка на то, что запрошен перенос "из себя в себя"
if ($this->readId() === $group->readId()) return null;
// Инициализация счётчика записанных товаров
$transfered = 0;
// Перенос
foreach ($group->searchProducts() as $product) if ($this->writeProduct($product)) ++$transfered;
// Деактивация целевой группы (пустой)
$group->deactivate();
// Запись в журнал
$this->journal('transfer', [
'from' => $group->readId()
]);
return $transfered;
}
/**
* Деактивация
*
* @return bool Статус выполнения
*/
public function deactivate(): bool
{
$this->stts = 'inactive';
if ($this->update() > 0) return true;
return false;
}
/**
* Запись рёбер групп
*
@@ -171,4 +304,29 @@ class ProductGroup extends Document implements GroupInterface
{
return static::findOne(['onec_id' => $onec_id]);
}
/**
* Найти по идентификатору товара
*
* @param Product $product Товар
*
* @return self|null Группа (ProductGroup)
*/
public static function searchByProduct(Product $product): ?self
{
return static::searchByEdge(
from: 'product',
to: 'product_group',
edge: 'product_edge_product_group',
direction: 'INBOUND',
subquery_where: [
[
'product_edge_product_group._from == "' . $product->readId() . '"'
]
],
subquery_select: 'product_group',
where: 'product_edge_product_group[0]._id != null',
limit: 1
)[0] ?? null;
}
}

View File

@@ -1,66 +0,0 @@
<?php
declare(strict_types=1);
namespace app\models;
use carono\exchange1c\interfaces\DocumentInterface;
class Purchase extends Document implements DocumentInterface
{
public static function collectionName(): string
{
return 'purchase';
}
/**
* @return DocumentInterface[]
*/
public static function findDocuments1c(): ?array
{
return self::find()->andWhere(['status_id' => 2])->all();
}
/**
* @return OfferInterface[]
*/
public function getOffers1c(): mixed
{
return true;
}
public function getRequisites1c(): mixed
{
return true;
}
/**
* Получаем контрагента у документа
*
* @return PartnerInterface
*/
public function getPartner1c(): Account
{
// !!!!!!!!!!!!!!!!!!!
return $this->user ?? new Account;
}
public function getExportFields1c($context = null)
{
return [];
}
/**
* Возвращаем имя поля в базе данных, в котором хранится ID из 1с
*
* @return string
*/
public static function getIdFieldName1c()
{
return 'onec["Ид"]';
}
public function setRaw1cData($cml, $object): void
{
}
}

View File

@@ -1,13 +0,0 @@
<?php
declare(strict_types=1);
namespace app\models;
class PurchaseEdgeSupply extends Edge
{
public static function collectionName(): string
{
return 'purchase_edge_supply';
}
}

View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace app\models;
use yii;
use yii\web\UploadedFile;
use yii\imagine\Image;
use app\models\traits\SearchByEdge;
use moonland\phpexcel\Excel;
use Exception;
/**
* Запрос (регистрация поставщика)
*
* Представляет собой набор данных от поставщика для регистрации
*/
class Request extends Document
{
/**
* Файл с данными ("Карточка предприятия")
*/
public string|array|null $file = null;
/**
* Имя коллекции
*/
public static function collectionName(): string
{
return 'request';
}
/**
* Свойства
*/
public function attributes(): array
{
return array_merge(
parent::attributes(),
[
'name',
'phon',
'mail',
'file'
]
);
}
/**
* Метки свойств
*/
public function attributeLabels(): array
{
return array_merge(
parent::attributeLabels(),
[
'name' => 'ФИО',
'phon' => 'Телефон',
'mail' => 'Почта',
'file' => 'Карточка предприятия'
]
);
}
/**
* Правила
*
* @todo Правило для всех трёх габаритов
*/
public function rules(): array
{
return array_merge(
parent::rules(),
[
[
[
'file',
'mail'
],
'required',
'message' => 'Обязательные поля: {attribute}'
],
[
'file',
'file',
'skipOnEmpty' => false,
// 'extensions' => 'xlsx',
'checkExtensionByMimeType' => false,
'maxFiles' => 1,
'maxSize' => 1024 * 1024 * 30,
// 'wrongExtension' => 'Разрешены только документы в формате: ".xlsx"',
'message' => 'Проблема при чтении документа'
]
]
);
}
}

View File

@@ -5,9 +5,15 @@ declare(strict_types=1);
namespace app\models;
use yii;
use yii\web\IdentityInterface;
use app\models\traits\SearchByEdge;
use app\models\Dellin as DellinModel;
use app\models\Settings;
use app\models\connection\Dellin;
use datetime;
use exception;
use throwable;
/**
* Поиск
@@ -35,6 +41,7 @@ class Search extends Document
parent::attributes(),
[
'text',
'type',
'ipv4',
'head'
]
@@ -50,6 +57,7 @@ class Search extends Document
parent::attributeLabels(),
[
'text' => 'Текст',
'type' => 'Тип',
'ipv4' => 'IPv4',
'head' => 'Заголовки'
]
@@ -86,9 +94,10 @@ class Search extends Document
* Запись
*
* @param string $text Текст запроса
* @param string $type Тип запроса
* @param Account|null $account Пользователь совершивший запрос
*/
public static function write(string $text, Account|null $account = null): ?self
public static function write(string $text, string $type = 'general', Account|null $account = null): ?self
{
// Инициализация
$vertex = new self;
@@ -96,6 +105,7 @@ class Search extends Document
// Настройки
$vertex->text = $text;
$vertex->type = $type;
if ($vertex->save()) {
// Поиск записан
@@ -106,4 +116,472 @@ 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) === 100) {
// Если в базе данных хранится много поставок
// Инициализация
$_row['overload'] = true;
}
foreach ($connections as $key => &$connection) {
// Перебор поставок
if ($cost = $connection['supply']['cost'] < 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 {
// Доставка "auto"
try {
$from = (int) (Warehouse::searchBySupply(Supply::searchByCatnAndProd($connection['supply']['catn'], $connection['supply']['prod']))[0]->trmn ?? Settings::searchActive()?->delivery_from_default ?? 36);
} catch (exception $e) {
$from = empty(Settings::searchActive()->delivery_from_default) ? 36 : (int) Settings::searchActive()->delivery_from_default;
}
try {
$to = (int) yii::$app->user->identity->opts['delivery_to_terminal'] ?? 36;
} catch (Exception $e) {
$to = 36;
}
if (DellinModel::searchByTerminalId($from)->data['cityID'] === DellinModel::searchByTerminalId($to)->data['cityID']) {
// Доставка в пределах города
$connection['delivery'] = [
'price' => [
'all' => 1
],
'ready' => 1,
'type' => 'auto'
];
goto skip_avia;
}
// Доставка в другие города
// Инициализация буфера доставки
$buffer_connection = $connection['product']['bffr']["$from-$to"] ?? null;
if (isset($buffer_connection) && !empty($buffer_connection['data']) && time() < $buffer_connection['expires'] ?? 0) {
// Найдены данные доставки в буфере
// и срок хранения не превышен, информация актуальна
// Запись в буфер вывода
$connection['delivery'] = $buffer_connection['data'];
$connection['delivery']['type'] = 'auto';
} else {
// Инициализация инстанции продукта в базе данных
$product = Product::searchByCatnAndProd($connection['product']['catn'], $connection['product']['prod']);
if ($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)
)) {
// Получены данные доставки
// Инициализация доставки Dellin (автоматическая)
$product->bffr = [
"$from-$to" => [
'data' => $connection['delivery'],
'expires' => time() + 86400
]
] + ($product->bffr ?? []);
// Отправка в базу данных
$product->update();
}
// Запись типа доставки
$connection['delivery']['type'] = 'auto';
}
} 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;
} finally {
// echo $connection['delivery']['price']['all'];
// Инициализация цены (цена поставки + цена доставки + наша наценка)
$connection['cost'] = $cost + ($connection['delivery']['price']['all'] ?? $connection['delivery']['price']['one'] ?? 0) + ($settings['increase'] ?? 0);
}
// Инициализация версии для рассчета доставки по воздуху
$buffer_delivery_avia = $connection;
try {
// Доставка "avia"
if ($cost = $connection['supply']['cost'] < 1) {
// Цена меньше единицы (подразумевается как ошибка)
// Этот код не будет выполняться, так как цена одна на обе позиции и аналогичная проверка выше уже есть
// Однако я это оставлю для возможных доработок в будущем
// Скрыть из выдачи
unset($connections[$key]);
continue;
}
try {
$from = (int) (Warehouse::searchBySupply(Supply::searchByCatnAndProd($connection['supply']['catn'], $connection['supply']['prod']))[0]->trmn ?? Settings::searchActive()?->delivery_from_default ?? 36);
} catch (Exception $e) {
$from = empty(Settings::searchActive()->delivery_from_default) ? 36 : (int) Settings::searchActive()->delivery_from_default;
}
try {
$to = (int) yii::$app->user->identity->opts['delivery_to_terminal'] ?? 36;
} catch (Exception $e) {
$to = 36;
}
// Доставка в другие города
// Инициализация буфера доставки
$buffer_connection = $connection['product']['bffr']["$from-$to-avia"] ?? null;
if (isset($buffer_connection) && !empty($buffer_connection['data']) && time() < $buffer_connection['expires'] ?? 0) {
// Найдены данные доставки в буфере
// и срок хранения не превышен, информация актуальна
// Запись в буфер вывода
$buffer_delivery_avia['delivery'] = $buffer_connection['data'];
$buffer_delivery_avia['delivery']['type'] = 'avia';
} else {
// Инициализация инстанции продукта в базе данных
$product = Product::searchByCatnAndProd($buffer_delivery_avia['product']['catn'], $connection['product']['prod']);
if ($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
)) {
// Получены данные доставки
// Инициализация доставки Dellin (автоматическая)
$product->bffr = [
"$from-$to-avia" => [
'data' => $buffer_delivery_avia['delivery'],
'expires' => time() + 86400
]
] + ($product->bffr ?? []);
// Отправка в базу данных
$product->update();
}
// Запись типа доставки
$buffer_delivery_avia['delivery']['type'] = 'avia';
}
} catch (exception $e) {
$buffer_delivery_avia['delivery']['error'] = true;
// var_dump($e->getMessage());
// var_dump($e->getTrace());
// var_dump($e->getFile());
// var_dump(json_decode($e->getMessage(), true)['errors']);
// die;
} finally {
if (!isset($buffer_delivery_avia['delivery']['error']) || $buffer_delivery_avia['delivery']['error'] !== true) {
// Если рассчиталась доставка самолётом
// echo $buffer_delivery_avia['delivery']['price']['all']; die;
// Инициализация цены (цена поставки + цена доставки + наша наценка)
$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;
}
}
// Пропуск доставки "avia"
skip_avia:
}
// Запись обработанных данных
$_row['supplies'] = array_merge($connections, $buffer_connections);
// Запись из буфера
$row = $_row;
}
return $response;
}
/**
* Генерация HTML-кода с найденным товаром
*
* Я сам в ахуе, переделывать не буду
*
* @param array $row Товар сгенерированный через Search::content()
* @param string|null $cover Обложка
* @param array $list Реестр найденных товаров
* @param bool $analogs Запрошены аналоги (не выведет пустые товары)
*
* @return string HTML-элемент с товаром
*/
public static function generate(array &$row, string|null &$cover = null, array &$list = [], bool $analogs = false): string
{
foreach ($row['imgs'] ?? [] as &$img) {
// Перебор изображений для обложки
if ($img['covr'] ?? false) {
// Найдена обложка
$cover = $img['h150'];
break;
}
}
if (is_null($cover)) {
// Обложка не инициализирована
if (!$cover = $imgs[0]['h150'] ?? false) {
// Не удалось использовать первое изображение как обложку
// Запись обложки по умолчанию
$cover = Product::cover($row['prod'], 150);
}
}
// Инициализация буфера с HTML поставок
$supplies_html = '';
// Инициализация блокировщика для пустого блока (на случай, если нет поставок, чтобы не было дубликатов вывода)
$empty_block = false;
// Инициализация счётчика поставок
$supplies_amount = count($row['supplies'] ?? []);
// Инициализация указателя номера цикла
$supply_iterator = 1;
foreach (empty($row['supplies']) || $supplies_amount === 0 ? [null] : $row['supplies'] as &$supply) {
// Перебор поставок
// Запись в список найденных
$list[$row['prod']] = [$row['catn']] + (isset($list[$row['prod']]) ? $list[$row['prod']] : []);
// Инициализация модификатора класса
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 = ($supply['cost'] ?? 0) + ($cost ?? 0);
// $price = $price_raw . ' ' . $supply_edge_product[0]['onec']['Цены']['Цена']['Валюта'] ?? 'руб';
$price = $price_raw . ' руб';
// Инициализация количества
// $amount_raw = $amount = $supply['amnt'] ?? $supply_edge_product[0]['onec']['Количество'] ?? 0;
// Инициализация цены и её представления
$amount_raw = $amount = count(@SupplyEdgeProduct::searchByVertex(@Supply::collectionName() . '/' . $supply['_key'], @Product::searchByCatnAndProd($supply['catn'], $supply['prod'])->readId(), limit: 999)) ?? 1;
$amount .= ' шт';
if ($amount_raw < 1 || $price_raw < 1) {
// Количество 0 или цена 0
// Поставки отстутвуют
no_supplies:
// Проверка на блокировку или запрошены аналоги
if ($empty_block || $analogs) continue;
$supplies_html .= <<<HTML
<div class="row $supply_class_modifier m-0 h-100 text-right">
<a class="col-auto ml-auto my-auto text-dark" href="/order/new/custom" role="button" onclick="return false;">
<small>
Заказать поиск у оператора
</small>
</a>
</div>
HTML;
// Запись блокировщика
$empty_block = true;
// Обновление счётчика
++$supply_iterator;
continue;
}
// Инициализация доставки
if (isset($delivery['error']) || $delivery === '?') {
// Не удалось рассчитать доставку
// Инициализация типа доставки
$delivery_type = $delivery['type'] ?? 'auto';
// Инициализация индикатора
$delivery_icon = '<i class="mr-1 fas fa-truck"></i>';
// Инициализация времени
$delivery = '?';
} else {
// Удалось рассчитать доставку
// Инициализация типа доставки
$delivery_type = $delivery['type'] ?? 'auto';
// Инициализация индикатора
$delivery_icon = match ($delivery_type) {
'avia' => '<i class="mr-1 fas fa-plane"></i>',
default => '<i class="mr-1 fas fa-truck"></i>'
};
if ($delivery['ready'] ?? false) {
// Указана дата готовности к получению
// Инициализация доставки
$delivery = $delivery['ready'];
} else {
// Не указана дата готовности к получению
// Инициализация даты отправки
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 .= <<<HTML
<div class="row $supply_class_modifier m-0 text-right">
<small class="ml-auto col-1 ml-2 my-auto pl-2 pr-0">$index</small>
<small class="col-1 my-auto pl-2 pr-0 text-center">$amount</small>
<small class="col-auto mr-2 my-auto pl-2 pr-0 text-left" title="Ориентировочно">$delivery_icon $delivery дн</small>
<b class="col-auto my-auto my-auto text-center">$price</b>
<a class="col-1 ml-0 py-2 text-dark d-flex button_white rounded" title="Добавить {$row['catn']} в корзину" role="button" onclick="return cart_write('{$supply['_id']}', '$delivery_type');">
<i class="fas fa-cart-arrow-down pr-1 m-auto"></i>
</a>
</div>
HTML;
// Обновление счётчика
++$supply_iterator;
}
return $supplies_html;
}
}

View File

@@ -26,7 +26,10 @@ class Settings extends Document
parent::attributes(),
[
'search_period',
'search_connect_keep'
'search_connect_keep',
'delivery_from_default',
'addition_global',
'delivery_addition_global'
]
);
}
@@ -40,7 +43,10 @@ class Settings extends Document
parent::attributeLabels(),
[
'search_period' => 'Поисковый период',
'search_connect_keep' => 'Режим удержания'
'search_connect_keep' => 'Режим удержания',
'delivery_from_default' => 'Место отправки поставки по умолчанию',
'addition_global' => 'Глобальная наценка',
'delivery_addition_global' => 'Глобальная надбавка к доставке'
]
);
}
@@ -55,14 +61,17 @@ class Settings extends Document
[
[
[
'search_period'
'search_period',
'delivery_from_default',
'delivery_addition_global'
],
'integer',
'message' => '{attribute} должен хранить цифровое значение'
],
[
[
'search_connect_keep'
'search_connect_keep',
'addition_global'
],
'string',
'message' => '{attribute} должен хранить строковый тип'
@@ -70,4 +79,14 @@ class Settings extends Document
]
);
}
/**
* Найти активную запись с настройками
*
* @todo Доделать
*/
public static function searchActive(): ?self
{
return static::findOne(['active' => true]);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,10 +5,10 @@ declare(strict_types=1);
namespace app\models;
use app\models\traits\Xml2Array;
use carono\exchange1c\interfaces\OfferInterface;
use Zenwalker\CommerceML\Model\Offer;
class SupplyEdgeProduct extends Edge implements OfferInterface
use carono\exchange1c\interfaces\OfferInterface;
class SupplyEdgeProduct extends Edge implements OfferInterface
{
use Xml2Array;
@@ -69,6 +69,17 @@ class SupplyEdgeProduct extends Edge implements OfferInterface
return self::findOne([self::getIdFieldName1c() => $ocid]);
}
/**
* Поиск по идентификатору поставки
*
* @param string $_id Идентификатор поставки
* @param int $limit Ограничение по максимальному количеству
*/
public static function searchBySupplyId(string $_id, int $limit = 1): array
{
return self::find()->where(['_from' => $_id])->limit($limit)->all();
}
/**
* Название поля в котором хранится ID из 1C
*/

View File

@@ -4,10 +4,11 @@ declare(strict_types=1);
namespace app\models;
use carono\exchange1c\interfaces\GroupInterface;
/**
* Группировка поставок
*/
class SupplyGroup extends ProductGroup
class SupplyGroup extends ProductGroup implements GroupInterface
{
/**
* Имя коллекции
@@ -16,4 +17,9 @@ class SupplyGroup extends ProductGroup
{
return 'supply_group';
}
public static function createTree1c($groups): Document|null {
return null;
}
}

View File

@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace app\models;
/**
* Терминалы выдачи
*/
class Terminal extends Document
{
public static function collectionName(): string
{
return 'terminal';
}
public function attributes(): array
{
return array_merge(
parent::attributes(),
[
'name',
'cntr',
'city',
'strt',
'hous',
'offs',
'comm',
'dell',
'hndl'
]
);
}
public function rules(): array
{
return array_merge(
parent::rules(),
[
[
[
'name',
'cntr',
'city',
'strt',
'hous',
'offs',
'comm'
],
'string'
],
[
[
'dell',
'hndl'
],
'integer'
]
]
);
}
public function attributeLabels(): array
{
return array_merge(
parent::attributeLabels(),
[
'name' => 'Название',
'cntr' => 'Страна',
'city' => 'Город',
'strt' => 'Улица',
'hous' => 'Дом',
'offs' => 'Офис',
'comm' => 'Комментарий',
'dell' => 'Терминал ДеловыеЛинии для рассчётов доставки',
'hndl' => 'Количество дней для обработки после получения от ДеловыеЛинии'
]
);
}
/**
* Поиск по индентификатору терминала ДеловыеЛинии
*
* @param string $dell Идентификатор терминала ДеловыеЛинии
* @param int $limit Максимальное количество результатов поиска
*
* @return array|null Терминалы SkillParts, если найдены
*
* @todo Сделать привязку терминалов SkillParts к нескольким терминалам ДеловыеЛинии и переделать под это поиск
*/
public static function searchByDellinTerminalId(string $dell, int $limit = 1): ?array
{
return self::find()->where(['dell' => $dell])->limit($limit)->all();
}
}

View File

@@ -0,0 +1,287 @@
<?php
declare(strict_types=1);
namespace app\models;
use app\models\traits\SearchByEdge;
/**
* Склад
*
* Хранит в себе связи с инстанциями поставок, а от них и связи со всеми поставками
*/
class Warehouse extends Document
{
use SearchByEdge;
/**
* Имя коллекции
*/
public static function collectionName(): string
{
return 'warehouse';
}
/**
* Свойства
*/
public function attributes(): array
{
return array_merge(
parent::attributes(),
[
'name',
'addr',
'trmn',
'actv',
'open'
]
);
}
/**
* Метки свойств
*/
public function attributeLabels(): array
{
return array_merge(
parent::attributeLabels(),
[
'name' => 'Название',
'addr' => 'Адрес',
'trmn' => 'Терминал',
'actv' => 'Активность',
'open' => 'Доступность'
]
);
}
/**
* Правила
*/
public function rules(): array
{
return array_merge(
parent::rules(),
[
[
[
'name',
'addr'
],
'string'
],
[
'actv',
'boolean'
]
]
);
}
/**
* Запись по аккаунту
*
* @param Account|null $account Аккаунт
*
* @return static|null Записанный склад
*/
public static function writeByAccount(Account|null $account = null): ?static
{
// Инициализация аккаунта
$account = Account::initAccount($account);
// Инициализация склада
$warehouse = new static;
// Запись параметров склада
$warehouse->actv = true;
if ($warehouse->save()) {
// Удалось записать склад в базу данных
// Инициализация ребра: АККАУНТ -> СКЛАД
AccountEdgeWarehouse::writeSafe($account->readId(), $warehouse->readId(), data: ['type' => 'connected']);
return $warehouse;
}
return null;
}
/**
* Найти по аккаунту
*
* @param Account|null $account Аккаунт
* @param int $limit Ограничение по максимальному количеству
*
* @return mixed Склады
*
* @deprecated
*/
public static function searchByAccount(Account|null $account = null, int $limit = 10): mixed
{
if ($account = Account::initAccount($account)) {
// Инициализирован аккаунт
return static::searchByEdge(
from: 'account',
to: 'warehouse',
edge: 'account_edge_warehouse',
direction: 'INBOUND',
subquery_where: [
[
'account_edge_warehouse._from == "' . $account->readId() . '"'
],
[
'account_edge_warehouse.type == "connected"'
]
],
where: 'account_edge_warehouse[0] != null',
limit: $limit
);
}
return null;
}
/**
* Найти по поставке
*
* @param Supply $supply Поставка
* @param int $limit Ограничение по максимальному количеству
*
* @return mixed Склады
*/
public static function searchBySupply(Supply $supply, int $limit = 10): mixed
{
return static::searchByImport(Import::searchBySupply($supply, limit: 1)[0], $limit);
}
/**
* Найти по инстанции поставки
*
* @param Import $import Инстанция поставки
* @param int $limit Ограничение по максимальному количеству
*
* @return mixed Склады
*/
public static function searchByImport(Import $import, int $limit = 10): mixed
{
return self::searchByEdge(
from: 'import',
to: 'warehouse',
edge: 'warehouse_edge_import',
direction: 'OUTBOUND',
subquery_where: [
['warehouse_edge_import._to' => $import->readId()],
['warehouse_edge_import.type' => 'loaded']
],
where: 'warehouse_edge_import[0] != null',
limit: $limit
);
}
/**
* Инициализация с записью
*
* Читает все склады привязанные к аккаунту, либо создает новый склад
*
* @param Account|null $account Аккаунт
* @param int $limit — Ограничение по максимальному количеству
*
* @return array Склады
*/
public static function initWithWrite(Account|null $account = null, int $limit = 10): array
{
if ($account = Account::initAccount($account)) {
// Инициализирован аккаунт
if ($warehouses = static::searchByAccount($account, $limit)) {
// Найдены склады
return $warehouses;
}
return [static::writeByAccount()];
}
return [];
}
/**
* Генерация списка терминалов из ДеловыеЛинии для отправителя
*
* Актуальное (выбранное, активное) значение записывается первым
*
* @param array Необработанный список терминалов
*/
public function genListTerminalsFrom(): array
{
// Инициализация
$list = [];
$cities = Dellin::read(limit: 9999, order: ['dellin.data.name' => 'DESC']);
foreach ($cities as $city) {
// Перебор городов
foreach ($city->data['terminals']['terminal'] as $termial) {
// Перебор терминалов
if (in_array($termial['id'], $list, true)) {
// Если встретился дубликат (исполняется очень часто)
continue;
}
// Запись
$list[$termial['id']] = $city->data['name'] . ' (' . $termial['address'] . ')';
}
}
return $this->syncListWithSettings($list, 'trmn');
}
/**
* Синхронизация списка вариантов параметра с текущим значением из настроек
*
* @param array &$list Список
* @param string $var Название параметра
*
* @return array Сортированный список
*/
protected function syncListWithSettings(array &$list, string $var): array
{
// Инициализация текущего значения параметра в начале массива
if (isset($this->$var)) {
// Параметр найден в настройках аккаунта
if (isset($list[$this->$var])) {
// Найдено совпадение сохранённого параметра с полученным списком из поставок
// Буфер для сохранения параметра
$buffer = $list[$this->$var];
// Удаление параметра
unset($list[$this->$var]);
// Сохранение параметра в начале массива
$list = [$this->$var => $buffer] + $list;
} else {
// Совпадение не найдено
// Сохранение параметра из данных аккаунта в начале массива
$list = [$this->$var => $this->$var] + $list;
}
} else {
// Параметр $var не найден в настройках аккаунта
// Сохранение параметра из данных аккаунта в начале массива
$list = ['Город отправления' => 'Город отправления'] + $list;
}
return $list;
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace app\models;
use app\models\Account;
/**
* Связь складов с инстанциями импортов
*/
class WarehouseEdgeImport extends Edge
{
/**
* Имя коллекции
*/
public static function collectionName(): string
{
return 'warehouse_edge_import';
}
/**
* Поиск по складу
*
* @param Warehouse $warehouse Склад
* @param int $limit Ограничение по максимальному количеству
*
* @return array Связи склада и инстанций поставок
*
* @deprecated Бесполезно
*/
public static function searchByWarehouse(Warehouse $warehouse, int $limit = 1): array
{
return static::find()->where(['_from' => $warehouse->readId()])->limit($limit)->all();
}
/**
* Поиск по инстанции импорта
*
* @param Import $import Инстанция импорта
*
* @return array Связи склада и инстанций поставок
*/
public static function searchByImport(Import $import): array
{
return static::find()->where(['_to' => $import->readId()])->limit(1)->all();
}
}

View File

@@ -0,0 +1,489 @@
<?php
declare(strict_types=1);
namespace app\models\connection;
use yii;
use yii\base\Model;
use app\models\Dellin as DellinModel;
use app\models\Product;
use app\models\Account;
use app\models\Settings;
use GuzzleHttp\Client as Guzzle;
use GuzzleHttp\Exception\ClientException as GuzzleException;
use DateTime;
use DateTimeZone;
use Exception;
class Dellin extends Model
{
/**
* Инстанция браузера
*/
public static Guzzle $browser;
/**
* Сессия аккаунта
*/
public static string $session;
public function __construct($config = [])
{
parent::__construct($config);
self::$browser = new Guzzle([
'base_uri' => 'https://api.dellin.ru/'
]);
self::authorization();
}
// /**
// * Поиск городов
// *
// * @return array|null Найденные города
// */
// public function searchCities(): ?array
// {
// $this->onReady(function () {
// // Запрос городов
// $request = $this->browser->post('/v2/public/kladr.json', [
// 'json' => [
// 'appkey' => yii::$app->params['dellin']['key'],
// ]
// ])
// });
// }
/**
* Рассчет доставки (расширенный)
*
* Рассчет нескольких товаров идет через простое перемножение результатов доставки одного товара
* В API всегда идет рассчет для одного товара, так было решено
*
* @param int $from Идентификатор терминала Dellin
* @param int $to Идентификатор терминала Dellin
* @param int $weight Вес (кг)
* @param int $x Ширина (cм)
* @param int $y Высота (cм)
* @param int $z Длинна (cм)
* @param int $amount Количество
* @param Account|int|null $account Аккаунт
*
* @return string
*
* @todo Загружать помимо терминалов ещё и адреса, чтобы доделать доставку малогабаритных грузов
* Разрабраться с параметрами 0,54м * 0,39м * 0,39м (0.082134м) и 0.1 куб метр в чем разница
*/
public static function calcDeliveryAdvanced(int $from, int $to, int $weight, int $x, int $y, int $z, int $amount = 1, bool $avia = false, Account|int|null $account = null): array
{
return self::handle(function () use ($from, $to, $weight, $x, $y, $z, $amount, $avia, $account) {
// Всё готово к работе
$account = Account::initAccount($account);
// Инициализация
$from = DellinModel::searchByTerminalId($from, terminal_data_only: true);
$to = DellinModel::searchByTerminalId($to, terminal_data_only: true);
// Значения по умолчанию, если указан 0
if (empty($x) || $x === 0) $x = 25;
if (empty($y) || $y === 0) $y = 40;
if (empty($z) || $z === 0) $z = 25;
if (empty($weight) || $weight === 0) $weight = 300;
// Конвертация из сантиметров в метры
$x /= 100;
$y /= 100;
$z /= 100;
// Вычисление самой крупной стороны, так как ДеловыеЛинии имеют ограничения на все три поля и у длинны оно больше всех
if ($x > $z && $x > $y) {
// "X" больше всех
// Инициализация
$width = $z;
$height = $y;
$length = $x;
} else if ($y > $x && $y > $z) {
// "Y" больше всех
// Инициализация
$width = $x;
$height = $z;
$length = $y;
} else {
// "Z" больше всех
// Инициализация
$width = $x;
$height = $y;
$length = $z;
}
// Инициализация
$query = [];
// Рассчёт типа доставки
if (
!$avia
&& $weight <= 30
&& ($length <= 0.54 && $width <= 0.39 && $height <= 0.39)
&& $length * $width * $height <= 0.1
) {
// Доставка категории "small"
$query['delivery']['deliveryType']['type'] = 'small';
$query['delivery']['derival']['variant'] = 'address';
$query['delivery']['derival']['address']['search'] = $from->fullAddress;
$query['delivery']['derival']['time']['worktimeStart'] = '08:00';
$query['delivery']['derival']['time']['worktimeEnd'] = '20:00';
$query['delivery']['arrival']['variant'] = 'address';
$query['delivery']['arrival']['address']['search'] = $to->fullAddress;
$query['delivery']['arrival']['time']['worktimeStart'] = '08:00';
$query['delivery']['arrival']['time']['worktimeEnd'] = '20:00';
} else {
// Доставка категории "auto"
if ($avia) {
// Рассчет для доставки по воздуху
// Ограничение на минимальный вес
$weight <= 0.5 and $weight = 0.5;
$query['delivery']['deliveryType']['type'] = 'avia';
} else {
// Рассчет для доставки по земле
$query['delivery']['deliveryType']['type'] = 'auto';
}
$query['delivery']['derival']['variant'] = 'terminal';
$query['delivery']['derival']['terminalID'] = $from->id;
$query['delivery']['arrival']['variant'] = 'terminal';
$query['delivery']['arrival']['terminalID'] = $to->id;
}
// Инициализация часового пояса
preg_match_all('/UTC([\+\-0-9:]*)/', $account->zone ?? Settings::searchActive()['timezone_default'] ?? 'UTC+3', $timezone);
$timezone = $timezone[1][0];
// Инициализация
$query = array_merge_recursive(
$query,
[
'appkey' => yii::$app->params['dellin']['key'],
'sessionID' => self::$session,
'delivery' => [
'derival' => [
'produceDate' => (new DateTime())->setTimestamp(time() + 86400 * 3)->setTimezone(new DateTimeZone($timezone))->format('Y-m-d')
]
],
'members' => [
'requester' => [
'role' => 'sender'
]
],
'cargo' => [
'quantity' => 1,
'width' => $width,
'height' => $height,
'length' => $length,
'totalVolume' => $width * $height * $length,
'totalWeight' => $weight,
'oversizedWeight' => $weight,
'oversizedVolume' => $width * $height * $length
]
]
);
// Запрос
$request = self::$browser->post('/v2/calculator.json', [
'json' => $query
]);
if ($request->getStatusCode() === 200) {
// Запрос прошел успешно
// Инициализация
$response = json_decode((string) $request->getBody(), true);
if ($response['metadata']['status'] === 200) {
// Со стороны ДеловыеЛинии ошибок нет
$response['data']['price'] = [
'one' => $response['data']['price'],
'all' => $response['data']['price'] * $amount
];
return $response['data'];
}
throw new Exception('На стороне сервера ДеловыеЛинии какие-то проблемы, либо отправлен неверный запрос', 500);
}
throw new Exception('Не удалось запросить рассчёт доставки у ДеловыеЛинии', 500);
});
}
/**
* Рассчет доставки
*
* @param string $from Идентификатор терминала Dellin
* @param string $to Идентификатор терминала Dellin
*
* @return string
*
* @deprecated
*/
public static function calcDelivery(string $from, string $to): array
{
return self::handle(function () use ($from, $to) {
// Всё готово к работе
// Запрос
$request = self::$browser->post('/v1/micro_calc.json', [
'json' => [
'appkey' => yii::$app->params['dellin']['key'],
'sessionID' => self::$session,
'derival' => [
'city' => $from
],
'arrival' => [
'city' => $to
]
]
]);
if ($request->getStatusCode() === 200) {
// Запрос прошел успешно
// Инициализация
$response = json_decode((string) $request->getBody(), true);
if ($response['metadata']['status'] === 200) {
// Со стороны ДеловыеЛинии ошибок нет
return $response['data'];
}
throw new Exception('На стороне сервера ДеловыеЛинии какие-то проблемы, либо отправлен неверный запрос', 500);
}
throw new Exception('Не удалось запросить рассчёт доставки у ДеловыеЛинии', 500);
});
}
/**
* Импорт терминалов
*
* @return array|null Сохранённые терминалы
*/
public static function importTerminals(Account|int|null $account = null): ?int
{
return self::handle(function () use ($account) {
// Всё готово к работе
if (is_null($account)) {
// Данные аккаунта не переданы
if (isset(yii::$app->user)) {
if (yii::$app->user->isGuest) {
// Аккаунт не аутентифицирован
return 0;
} else {
// Аккаунт аутентифицирован
// Инициализация
$account = yii::$app->user->identity;
}
} else {
return 0;
}
} else {
if (is_int($account)) {
// Передан идентификатор (_key) аккаунта (подразумевается)
// Инициализация (поиск в базе данных)
if (!$account = Account::searchById(Account::collectionName() . "/$account")) {
// Не удалось инициализировать аккаунт
return 0;
}
}
}
// Запрос ссылки на файл с городами, возвращает ['hash' => string, 'url' => string]
$request = self::$browser->post('/v3/public/terminals.json', [
'json' => [
'appkey' => yii::$app->params['dellin']['key'],
]
]);
if ($request->getStatusCode() === 200) {
// Запрос прошел успешно
// Инициализация часового пояса
preg_match_all('/UTC([\+\-0-9:]*)/', $account->zone ?? Settings::searchActive()['timezone_default'] ?? 'UTC+3', $timezone);
$timezone = $timezone[1][0];
// Инициализация параметров
$response = json_decode((string) $request->getBody(), true);
$dir = YII_PATH_PUBLIC . '/../assets/import/' . (new DateTime('now', new DateTimeZone($timezone)))->format('Y-m-d') . '/dellin/terminals/' . (yii::$app->user->identity->_key ?? 'system') . '/';
$amount = 0;
if (!file_exists($dir)) {
// Директории не существует
mkdir($dir, 0775, true);
}
$request = self::$browser->get($response['url'], [
'sink' => $file = $dir . time() . '.json'
]);
// Инициализация
$terminals = json_decode(fread(fopen($file, "r"), filesize($file)), true);
foreach ($terminals['city'] as $terminal) {
// Перебор городов
if ($model = DellinModel::searchByCityId($terminal['id'])) {
// Удалось найти город в базе данных
$after_import_log = function () use ($model): void {
// Запись в журнал
$model->journal('update');
if (yii::$app->getRequest()->isConsoleRequest) {
// Вызов из терминала
echo 'Удалось перезаписать терминалы города: ' . ($model->data['name'] ?? 'Неизвестно') . PHP_EOL;
}
};
} else {
// Не удалось найти город в базе данных
$model = new DellinModel();
$after_import_log = function () use ($model): void {
if (yii::$app->getRequest()->isConsoleRequest) {
// Вызов из терминала
echo 'Удалось записать терминалы города: ' . ($model->data['name'] ?? 'Неизвестно') . PHP_EOL;
}
};
}
// Запись
$model->data = $terminal;
// Отправка в базу данных
if ($model->save()) {
// Удалось сохранить в базе данных
// Запись в журнал
$after_import_log();
// Постинкрементация счётчика
$amount++;
continue;
} else {
// Не удалось сохранить в базе данных
throw new Exception('Не удалось сохранить терминалы города "' . ($model->data['name'] ?? 'Неизвестно') . '" в базу данных', 500);
}
}
return $amount;
}
throw new Exception('Не удалось синхронизировать данные городов с ДеловыеЛинии', 500);
});
}
/**
* Аутентификация и авторизация
*/
protected static function authorization(): bool
{
// Аутентификация и авторизация
$request = self::browser()->post('/v3/auth/login.json', [
'json' => [
'appkey' => yii::$app->params['dellin']['key'],
'login' => yii::$app->params['dellin']['nickname'],
'password' => yii::$app->params['dellin']['password']
]
]);
if ($request->getStatusCode() === 200) {
// Запрос прошел успешно
// Инициализация
$response = json_decode((string) $request->getBody(), true);
if ($response['metadata']['status'] === 200) {
// Аутентификация и авторизация пройдены успешно
// Запись сессии
self::$session = $response['data']['sessionID'];
return true;
}
throw new Exception('Не удалось авторизироваться в ДеловыеЛинии', $response['metadata']['status']);
}
throw new Exception('Не удалось авторизироваться в ДеловыеЛинии', $request->getStatusCode);
}
/**
* Инициализация и выполнение
*
* @param callable|null $function Код к выполнению
* @param [mixed] ...$vars Параметры к нему
*
* @return mixed Возврат из функции
*/
protected static function handle(callable $function = null, mixed ...$vars): mixed
{
try {
if (self::browser() instanceof Guzzle) {
// Браузер инициализирован
if (self::authorization() && isset(self::$session)) {
// Аутентифицирован и авторизован
return $function(...$vars);
} else {
throw new Exception('Аккаунт не авторизирован', 401);
}
} else {
throw new Exception('Браузер не инициализирован', 500);
}
} catch (GuzzleException $e) {
throw new Exception($e->getResponse()->getBody()->getContents() ?? 'Не удалось инициализировать инстанцию для работы с API ДеловыеЛинии', 500, $e->getPrevious());
} catch (Exception $e) {
throw new Exception($e->getMessage() ?? 'Не удалось инициализировать инстанцию для работы с API ДеловыеЛинии', 500, $e->getPrevious());
}
}
/**
* Чтение или инициализация браузера
*
* @return Guzzle Инстанция
*/
protected static function browser(): Guzzle
{
return self::$browser ?? self::$browser = new Guzzle([
'base_uri' => 'https://api.dellin.ru/'
]);
}
}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace app\models\helpers;
use yii;
use yii\base\Model;
use yii\web\View;
class JsManager extends Model
{
/**
* Сгенерировать <script> элементы из данных AssetManager
*
* Создавалось под виджет ActiveForm и только на нём тестировалось
*
* @param View $view Представление
* @param array $targets Названия виджетов (например: "yii\widgets\ActiveFormAsset")
*
* @return string HTML-код
*/
public static function include(View $view, array $targets): string
{
// Инициализация
$buffer = '';
foreach ($targets as $target) {
// Перебор целей для генерации
// Инициализация
$depends = $view->assetBundles[$target]->depends;
if (count($depends) > 0) {
// Найдены зависимости
// Рекурсивный вызов
self::include($view, $depends);
}
// Инициализация
$files = $view->assetBundles[$target]->js;
foreach ($files as $file) {
// Перебор файлов цели для генерации
// Инициализация публичного пути к файлу
$path = $view->assetBundles[$target]->baseUrl . '/' . $file;
// Генерация
$buffer .= <<<HTML
<script src="$path" defer></script>
HTML;
}
}
return $buffer;
}
}

View File

@@ -4,35 +4,36 @@ declare(strict_types=1);
namespace app\models\traits;
use yii;
use exception;
use ArangoDBClient\Document;
trait SearchByEdge
{
/**
* Поиск через связи рёбрами с аккаунтом
*
* @param string $id Идентификатор пользователя
* @param int $limit Количество
* @param int $offset Сдвиг
* @param string $sort Сортировка
* Аргумент $asArray и его реализацию пришлось добавить чтобы не переписывать кучу кода
*/
public static function searchByEdge(
string $from,
string $to,
string|null $edge = null,
string $direction = 'ANY',
int|null $limit = 10,
int|null $offset = 0,
array $sort = ['ASC'],
array|string|null $sort = null,
string|array $subquery_where = [],
string|array $subquery_select = null,
array $foreach = [],
string|array $where = [],
string $direction = 'ANY',
array|null $let = [],
string|array $select = null,
callable|null $handle = null,
array $params = []
array|null $filterStart = null,
array $params = [],
bool $asArray = true,
bool $debug = false,
bool $aql = false,
bool $count = false
): mixed {
$subquery = static::find()
->params($params)
@@ -41,7 +42,8 @@ trait SearchByEdge
->in($edge ?? $from . '_edge_' . $to)
->where($subquery_where);
$subquery = $subquery->select($edge ?? $from . '_edge_' . $to)
$subquery = $subquery
->select($subquery_select ?? $edge ?? $from . '_edge_' . $to)
->createCommand();
$query = static::find()
@@ -57,27 +59,68 @@ trait SearchByEdge
$query->let(...$let);
}
// Запрос
if (!empty($filterStart)) {
// Переданы дополнительные условия фильтрации
$query->filter($filterStart, 'START_SENSETIVE');
}
// Инициализация
$request = $query
->foreach($foreach)
->where($where)
->limit($limit)
->select($select ?? $to);
if (isset($handle)) {
// Режим вывода строки запроса
if ($aql) {
// Запрошена проверка
return (string) $request->createCommand();
}
// Режим проверки
if ($debug) {
// Запрошена проверка
var_dump((string) $request->createCommand());
return null;
}
// Запрос
if ($count) {
// Запрошен подсчет
return $query->count();
} else if (isset($handle)) {
// Передана функция для постобработки
return $handle($request);
} else if (isset($select)) {
// Указан выбор свойств
$response = $request->createCommand()->execute()->getAll();
foreach ($response as &$attribute) {
// Приведение всех свойств в массив и очистка от лишних данных
if ($asArray) {
// Передан параметр указывающий на необходимость возврата как объекта
$attribute = $attribute->getAll();
// Очистка
foreach ($response as &$attribute) {
// Приведение всех свойств в массив и очистка от лишних данных
if ($attribute instanceof Document) {
// Получена инстанция документа ArangoDB
$attribute = $attribute->getAll();
}
}
}
return $response;
} else {
// Иначе просто запросить для ActiveQuery
return $request->all();
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
use yii\helpers\Html;
use yii\bootstrap\ActiveForm;
use yii\widgets\Pjax;
use app\models\AccountForm;
@@ -16,34 +17,107 @@ use app\models\AccountForm;
<div class="row">
<div class="mx-auto">
<?php
// Инициализация идентификатора формы
if ($panel ?? false) {
// Генерация документа во всплывающем меню
// Инициализация параметров
$form_id = 'form_account_panel';
$target = 'panel';
} else {
// Генерация документа в основном блоке страницы
// Инициализация параметров
$form_id = 'form_account';
$target = 'main';
}
$form = ActiveForm::begin([
'id' => 'form_account',
'id' => $form_id,
'action' => false,
'fieldConfig' => [
'template' => '{label}{input}{error}',
'options' => ['class' => '']
'options' => [
'class' => ''
]
],
'options' => [
'class' => '',
'class' => 'form_account',
'onsubmit' => 'return false;'
]
],
'enableClientValidation' => false,
'enableAjaxValidation' => true
]);
$model = $model ?? new AccountForm;
?>
<?= $form->field($model, 'mail', ['enableLabel' => false, 'options' => ['class' => 'mb-2']])->textInput(['autofocus' => true, 'placeholder' => $model->getAttributeLabel('mail')]) ?>
<?= $form->field($model, 'pswd', ['enableLabel' => false])->passwordInput(['placeholder' => $model->getAttributeLabel('pswd')]) ?>
<?php if ($registration ?? false) : ?>
<?php if ($panel ?? false) : ?>
<h5 class="mb-4 text-center">Регистрация</h5>
<?php else : ?>
<h3 class="mb-4 text-center">Регистрация</h3>
<?php endif ?>
<?php else : ?>
<?php if ($panel ?? false) : ?>
<h5 class="mb-4 text-center">Аутентификация</h5>
<?php else : ?>
<h3 class="mb-4 text-center">Аутентификация</h3>
<?php endif ?>
<?php endif ?>
<?= $form->field($model, 'mail', ['enableLabel' => false, 'options' => ['class' => 'mb-2'], 'inputOptions' => ['class' => 'form-control button_clean'], 'errorOptions' => ['class' => 'help-block help-block-error px-2 small']])->textInput(['autofocus' => true, 'placeholder' => $model->getAttributeLabel('mail')]) ?>
<?= $form->field($model, 'pswd', ['enableLabel' => false, 'inputOptions' => ['class' => 'form-control button_clean'], 'errorOptions' => ['class' => 'help-block help-block-error px-2 small']])->passwordInput(['placeholder' => $model->getAttributeLabel('pswd')]) ?>
<div class="d-flex mb-2 mt-3">
<?= Html::submitButton('Войти', ['name' => 'submitAuthentication', 'onclick' => 'authentication(this.parentElement.parentElement);', 'class' => 'flex-grow-1 mr-2 btn btn-primary button_clean']) ?>
<?= Html::submitButton('Войти', ['name' => 'submitAuthentication', 'onclick' => 'authentication(this.parentElement.parentElement, \'' . $target . '\');', 'class' => 'flex-grow-1 mr-2 btn btn-primary button_clean']) ?>
<?= $form->field($model, 'auto', ['checkboxTemplate' => '<div class="checkbox button_clean">{beginLabel}' .
Html::submitButton('{labelTitle}', ['name' => 'submit', 'data-toggle' => 'button', 'class' => 'w-100 btn btn-primary button_clean', 'aria-pressed' => 'false']) .
Html::submitButton('{labelTitle}', ['name' => 'submit', 'data-toggle' => 'button', 'class' => 'w-100 btn btn-primary button_clean', 'aria-pressed' => 'false', 'onclick' => 'return authentication_auto_button_status_switch(this);']) .
'{endLabel}</div>'])->checkbox()->label($model->getAttributeLabel('auto'), ['class' => 'w-100 m-0']) ?>
</div>
<?= Html::submitButton('Регистрация', ['name' => 'submitRegistration', 'onclick' => 'registration(this.parentElement);', 'class' => 'col-12 ml-auto btn btn-success btn-sm button_clean']) ?>
<?php ActiveForm::end(); ?>
<?= Html::submitButton('Регистрация', ['name' => 'submitRegistration', 'onclick' => 'return registration_start(this.parentElement, \'' . $target . '\');', 'class' => 'col-12 ml-auto btn btn-success btn-sm button_clean']) ?>
<small class="d-flex mt-2"><a class="mx-auto text-dark" type="button" onclick="restore(this.parentElement.parentElement)">Восстановить пароль</a></small>
<?php
ActiveForm::end();
?>
</div>
</div>
</div>
</div>
<?php if ($registration ?? false) : ?>
<script async="false">
// Инициализация формы
let form = document.getElementById('<?= $form_id ?>');
// Запуск программы регистрации
registration_start(form, 'panel');
if (document.readyState === "complete") {
// Документ загружен
// Обработчик события инициализации
$(form).on('afterInit', function(e) {
// Запуск программы регистрации
registration_start(form, 'panel');
});
} else {
// Документ не загружен
// Обработчик события загрузки документа
document.addEventListener('DOMContentLoaded', function() {
// Обработчик события инициализации
$(form).on('afterInit', function(e) {
// Запуск программы регистрации
registration_start(form, 'panel');
});
}, false);
}
</script>
<?php endif ?>

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
use app\models\Product;
use app\models\Account;
use app\models\Settings;
// Инициализация счетчика аккаунтов
$i = $amount * ($page - 1);
// Инициализация часового пояса
preg_match_all('/UTC([\+\-0-9:]*)/', $account->zone ?? Settings::searchActive()['timezone_default'] ?? 'UTC+3', $timezone);
$timezone = $timezone[1][0];
?>
<?php if ($page > 1) : ?>
<div class="dropdown-divider mb-3"></div>
<?php endif ?>
<?php foreach ($accounts ?? [] as $account) : ?>
<?php
foreach ($account->jrnl ?? [] as $jrnl) {
// Перебор записей в журнале
if ($jrnl['action'] === 'create') {
// Найдена дата создания
// Инициализация даты
$create = (new DateTime())->setTimestamp($jrnl['date'])->setTimezone(new DateTimeZone($timezone))->format('d.m.Y') ?? 'Неизвестно';
// Выход из цикла
break;
}
}
?>
<div class="mb-3 row">
<div class="pr-0 col-auto"><?= ++$i ?>.</div>
<div class="pr-0 col overflow-hidden" title="ФИО">
<?= $account->name ?? 'Неизвестно' ?>
</div>
<div class="my-auto pr-0 col-auto" title="Псевдоним">
<?= $account->indx ?? '' ?>
</div>
<div class="my-auto pr-0 col-auto" title="Тип аккаунта">
<?= $account->agnt ? 'Поставщик' : 'Покупатель' ?>
</div>
<div class="mr-3 my-auto pr-0 col-2" title="Уровень авторизации">
<?= $account->type() ?>
</div>
<div class="my-auto pr-0 col-auto text-right" title="Дата регистрации">
<?= $create ?? 'Неизвестно' ?>
</div>
<a class="my-auto col-auto fas fa-trash-alt text-dark" type="button" onclick="page_profile_supplies_delete()"></a>
</div>
<?php if ($i < count($accounts) + $amount * ($page - 1)) : ?>
<div class="dropdown-divider mb-3"></div>
<?php endif ?>
<?php endforeach ?>

View File

@@ -4,27 +4,16 @@ declare(strict_types=1);
use yii;
if (!yii::$app->user->isGuest) {
// $popup = yii::$app->controller->renderPartial('/notification/panel');
// echo <<<HTML
// <a id="notification_button" class="text-dark d-flex h-100 mr-2" title="Уведомления" role="button" data-toggle="dropdown" data-offset="-100%p + 100%" onclick="return notification_stream();">
// <i class="fas fa-bell my-auto mx-2"></i>
// </a>
// <div id="notification_button_panel" class="dropdown-menu p-2" aria-labelledby="notification_button">
// $popup
// </div>
// HTML;
}
?>
<?=$notifications_button?>
<?=$notifications_panel?>
<a class="text-dark my-auto mr-2" title="Корзина" href="/cart" role="button" onclick="return page_cart();"><i class="fas fa-shopping-cart mx-2"></i></a>
<?= $notifications_panel ?>
<?= $notifications_button ?>
<?= $cart_button ?>
<a class="text-dark my-auto mr-2" title="Заказы" href="/orders" role="button" onclick="return page_orders();"><i class="fas fa-list mx-2"></i></a>
<div class="btn-group my-auto">
<a class="btn m-0 px-0 text-dark button_clean" title="Личный кабинет" href="/profile" role="button" onclick="return page_profile();">Личный кабинет</a>
<button id="profile_button" class="btn pr-0 dropdown-toggle dropdown-toggle-split button_clean" type="button" data-toggle="dropdown" onmouseover="$('#profile_button').dropdown('show')"></button>
<div id="profile_button_panel" class="dropdown-menu dropdown-menu-right py-1" aria-labelledby="profile_button" onmouseout="$('#profile_button').dropdown('show')">
<a class="dropdown-item button_white text-dark" onclick="deauthentication()">Выход (<?= yii::$app->user->identity->mail ?>)</a>
</div>
</div>
<a class="btn m-0 px-0 text-dark button_clean button_underline" title="Личный кабинет" href="/profile" role="button" onclick="return page_profile();"><b><?= yii::$app->user->identity->mail ?></b></a>
<button id="menu_auth_panel_button" class="pr-0 btn button_clean button_clean_full" type="button" onclick="return deauthentication();" title="Выход"><i class="fas fa-sign-out-alt"></i></button>
<!-- <div id="menu_auth_panel" class="py-1 text-center d-none">
<a class="py-1 px-3 d-block button_white text-dark" onclick="return deauthentication();"><b>Выход</b></a>
</div> -->
</div>

View File

@@ -3,16 +3,18 @@
declare(strict_types=1);
use yii;
use app\models\helpers\JsManager;
?>
<a class="text-dark my-auto mr-2" title="Корзина" href="/cart" role="button" onclick="return page_cart();"><i class="fas fa-shopping-cart mx-2"></i></a>
<a class="text-dark my-auto mr-2" title="Заказы" href="/orders" role="button" onclick="return page_orders();"><i class="fas fa-list mx-2"></i></a>
<div class="btn-group my-auto">
<a class="btn m-0 px-0 text-dark button_clean" title="Личный кабинет" href="/profile" role="button" onclick="return page_profile();">Личный кабинет</a>
<button id="profile_button" class="btn pr-0 dropdown-toggle dropdown-toggle-split button_clean" type="button" data-toggle="dropdown" onmouseover="$('#profile_button').dropdown('show')"></button>
<div id="profile_button_panel" class="dropdown-menu dropdown-menu-long dropdown-menu-right p-3" aria-labelledby="profile_button" onmouseout="$('#profile_button').dropdown('show')">
<h5 class="mb-3 text-center">Аутентификация</h5>
<?= yii::$app->controller->renderPartial('/account/index', compact('model')) ?>
<!-- <a class="dropdown-item-text text-center px-0 py-2" href="#"><small>Восстановление пароля</small></a> -->
<button id="menu_auth_panel_button" class="pr-0 btn button_clean button_clean_full" type="button" onmouseover="return menu_auth_panel_show(document.getElementById('menu_auth_panel'));" onclick="return menu_auth_panel_show(document.getElementById('menu_auth_panel'));"><i class="fas fa-caret-down"></i></button>
<div id="menu_auth_panel" class="p-3 d-none">
<?= yii::$app->controller->renderPartial('/account/index', compact('model') + ['panel' => true]) ?>
</div>
</div>
</div>
<?= JsManager::include($this, ['yii\widgets\ActiveFormAsset']);?>

View File

@@ -0,0 +1,11 @@
<div class="container d-flex flex-column">
<div class="my-auto">
<div class="col-md-10 col-lg-8 mx-auto p-5 alert alert_white d-flex flex-column">
<h4 class="text-center"><b>Подтвердите аккаунт</b></h4>
<p class="text-center mb-5">Мы выслали вам письмо с паролем и ссылкой на активацию</p>
<a class="btn button_blue button_clean mx-auto" type="button" onclick="return verify_resend();">Повторить</a>
</div>
</div>
</div>
<script src="/js/verify.js" defer></script>

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
use yii\helpers\Html;
use app\assets\AppAsset;
AppAsset::register($this);
?>
<?php $this->beginPage() ?>
<!DOCTYPE html>
<html lang="<?= yii::$app->language ?>">
<head>
<meta charset="<?= yii::$app->charset ?>">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/css/pages/buyers.css" rel="stylesheet">
<link rel="apple-touch-icon" sizes="57x57" href="/favicons/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/favicons/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/favicons/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/favicons/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/favicons/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/favicons/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/favicons/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/favicons/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="/favicons/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/favicons/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png">
<link rel="manifest" href="/favicons/manifest.json">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/favicons/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
<?php $this->registerCsrfMetaTags() ?>
<title><?= Html::encode($this->title ?? 'Покупателям | SkillParts') ?></title>
<?php $this->head() ?>
</head>
<body class="d-flex flex-column">
<?php $this->beginBody() ?>
<header class="container pt-2 mt-1 mb-2 mb-sm-4">
<div class="row row h-100">
<div id="logo" class="col-3 col-md-4 h-100">
<a class="py-2 h-100 d-inline-flex" title="SkillParts" href="/">
<img class="img-fluid" src="/img/logos/skillparts.svg" alt="SkillParts">
</a>
</div>
</div>
</header>
<main id="page_buyers" class="container pb-5 flex-grow-1 d-flex justify-content-center">
<article class="col my-auto p-3 px-0 rounded overflow-hidden">
<h1 class="py-3 mt-2 mb-5 text-center"><b>Что в нас ценят?</b></h1>
<div class="row mb-5 pb-3">
<div class="col-4">
<img class="px-5 img-fluid" src="/img/icons/track.png" title="Подробное отслеживание" />
<h5 class="text-center"><b>Подробное отслеживание</b></h5>
</div>
<div class="col-4">
<img class="px-5 img-fluid" src="/img/icons/truck.png" title="Бесплатное экспедирование" />
<h5 class="text-center"><b>Бесплатное экспедирование</b></h5>
</div>
<div class="col-4">
<img class="px-5 img-fluid" src="/img/icons/assortiment.png" title="Отлаженная сеть поставщиков" />
<h5 class="text-center"><b>Отлаженная сеть поставщиков</b></h5>
</div>
</div>
<div class="row px-3 pb-3 justify-content-center">
<a class="mr-4 text-center text-white btn button_blue button_clean" href="/registration">Зарегистрироваться</a>
<a class="text-center text-white btn button_blue button_clean" href="/offer">Оферта</a>
</div>
</article>
</main>
<?php $this->endBody() ?>
</body>
</html>
<?php $this->endPage() ?>

View File

@@ -0,0 +1,18 @@
<a id="cart_button" class="mr-2 h-100 text-dark d-flex" title="Корзина" href="/cart" onclick="return page_cart();">
<?php
if (empty($cart_amount) || $cart_amount < 1) {
// Новые уведомления не найдены
echo <<<HTML
<i class="mx-2 my-auto fas fa-shopping-cart"></i>
HTML;
} else {
// Новые уведомления найдены
echo <<<HTML
<small class="ml-2 mr-1 my-auto"><b>$cart_amount</b></small>
<i class="mr-2 my-auto fas fa-shopping-cart cart_button_active"></i>
HTML;
}
?>
</a>

View File

@@ -1,18 +1,34 @@
<?php
declare(strict_types=1);
use yii;
use yii\bootstrap\ActiveForm;
use app\models\connection\Dellin;
use app\models\Supply;
use DateTime;
?>
<link href="/css/pages/cart.css" rel="stylesheet">
<div id="page_cart" class="container mb-auto py-3">
<article class="py-3 px-4 rounded">
<article class="p-4 rounded">
<h4 class="ml-4 mt-2 mb-4"><i class="fas fa-shopping-cart mr-2"></i>Корзина</h4>
<div class="col mb-4 list rounded overflow-hidden">
<div class="row py-2">
<div class="pl-3 mr-1">
<input id="checkbox_cart_all" type="checkbox" onchange="return cart_list_checkbox(this);"/>
<input id="checkbox_cart_all" type="checkbox" onchange="return cart_list_checkbox(this);" />
</div>
<div class="col-2">
<span>Производитель</span>
</div>
<div class="col-2">
<span>Артикул</span>
</div>
<div class="col-4">
<span>Описание</span>
<div class="col-2">
<span>Поставщик</span>
</div>
<div class="col-1 ml-auto px-0 text-center">
<span>Количество</span>
@@ -25,34 +41,105 @@
</div>
</div>
<?php
if (isset($supplies) && !empty($supplies)) {
foreach ($supplies as $supply) {
echo <<<HTML
<div class="row py-2 cart_list_target">
<div class="pl-3 mr-1">
<input id="cart_list_checkbox_$supply->catn" type="checkbox" onchange="return cart_list_checkbox(this);"/>
</div>
<div class="col-2">
$supply->catn
</div>
<div class="col-4">
$supply->desc
</div>
<div class="col-1 ml-auto">
<input id="cart_list_amnt_$supply->catn" class="form-control text-center" type="text" value="$supply->amnt" onchange="return cart_list_amount_update('$supply->catn', this)" aria-invalid="false">
</div>
<div class="col-2 text-right">
$supply->time
</div>
<div class="col-2 mr-3 text-right">
$supply->cost
</div>
</div>
HTML;
if (!empty($data['supplies'])) {
// Найдены цели для заказа
// Инициализация списка поставок
$targets = [];
foreach ($data['supplies'] as $prod => $list) {
// Перебор поставщиков
foreach ($list as $catn => $deliveries) {
// Перебор поставок
foreach ($deliveries as $delivery => $supply) {
// Перебор типов доставки
// Инициализация комментария
$comment = $supply['edge']['comm'] ?? 'Комментарий к заказу';
// Инициализация доставки
if (empty($supply['delivery'])) {
// Не удалось рассчитать доставку
// Инициализация времени
$days = '?';
} else {
// Удалось рассчитать доставку
// Инициализация даты отправки
try {
// Взять данные из "arrivalToOspSender" (Дата прибытия на терминал-отправитель)
$delivery_send_date = DateTime::createFromFormat('Y-m-d', $supply['delivery']['orderDates']['arrivalToOspSender'])->getTimestamp();
} catch (Throwable $e) {
// Взять данные из "pickup" (Дата передачи груза на адресе отправителя)
$delivery_send_date = DateTime::createFromFormat('Y-m-d', $supply['delivery']['orderDates']['pickup'])->getTimestamp();
}
// Инициализация времени доставки
try {
// Доставка по воздуху (подразумевается), данные из "giveoutFromOspReceiver" (Дата и время, с которого груз готов к выдаче на терминале)
// Оставлено на всякий случай для дальнейших разбирательств
$delivery_converted = DateTime::createFromFormat('Y-m-d H:i:s', $supply['delivery']['orderDates']['giveoutFromOspReceiver'])->getTimestamp();
} catch (Throwable $e) {
// Инициализация даты отправки
// Автоматическая доставка (подразумевается), данные из "arrivalToOspReceiver" (Дата прибытия натерминал-получатель)
$delivery_converted = DateTime::createFromFormat('Y-m-d', $supply['delivery']['orderDates']['arrivalToOspReceiver'])->getTimestamp();
}
$days = ceil(($delivery_converted - ($delivery_send_date ?? 0)) / 60 / 60 / 24) + 1;
}
// Инициализация иконки
$icon = $delivery === 'avia' ? 'fa-plane' : 'fa-truck';
// Генерация HTML
echo <<<HTML
<div class="row py-2 cart_list_target">
<div class="col">
<div class="row">
<div class="pl-3 my-auto mr-1">
<input id="cart_list_checkbox_{$prod}_{$catn}_auto" type="checkbox" onchange="return cart_list_checkbox(this);"/>
</div>
<div class="col-2 my-auto">
$prod
</div>
<div class="col-2 my-auto">
$catn
</div>
<div class="col-2 my-auto">
{$supply['account']['indx']}
</div>
<div class="col-1 my-auto ml-auto">
<input id="cart_list_amnt_{$prod}_{$catn}_auto" class="form-control text-center" type="text" value="{$supply['amount']}" onchange="return cart_list_amount_update('$prod', '$catn', 'auto', this)" aria-invalid="false">
</div>
<div class="col-2 my-auto text-right">
<p title="Ориентировочно"><i class="mr-1 fas $icon"></i> <b>~</b>$days дн</p>
</div>
<div class="col-2 my-auto mr-3 text-right">
{$supply['supply']->cost} {$supply['currency']}
</div>
</div>
<div class="dropdown-divider"></div>
<div class="row mb-1">
<div class="col-12">
<p id="cart_list_comment_{$prod}_{$catn}_auto" class="mt-0 ml-0 text-break pointer-event" role="button" onclick="return cart_list_comment_edit('$prod', '$catn', 'auto', this);">$comment</p>
</div>
</div>
</div>
</div>
HTML;
}
}
}
} else {
echo <<<HTML
<div class="row py-2 cart_list_target">
<div class="row py-2">
<div class="mx-auto py-2">
Корзина пуста
</div>
@@ -61,7 +148,8 @@
}
?>
</div>
<div class="row mb-2 mx-0">
<div class="row mb-3 mx-0">
<select id="cart_list_action" class="form-control mr-3 button_clean w-auto" name="CartListAction">
<option value="none" hidden>Действие с выбранными</option>
<option value="delete" onclick="return cart_list_delete();">Удалить</option>
@@ -69,15 +157,78 @@
<a class="mr-3 btn button_red button_clean" title="Очистить корзину" href="/cart" role="button" onclick="return cart_delete();">
Очистить
</a>
<p class="ml-auto mr-3 cart_field_cost">
<span id="cart_cost">0</span>
руб
</p>
<a class="btn button_clean button_blue" title="Оформить заказ" href="/pay" role="button" onclick="return cart_pay();">
Оформить заказ
</a>
</div>
<div class="dropdown-divider mb-3"></div>
<div class="p-3 mx-0 row">
<div id="cart_registration_menu" class="mr-5 col px-0">
<div class="row mb-4 mx-0">
<label id="cart_registration_individual_button" class="ml-auto btn button_white mb-0 mr-4" for="cart_registration_individual" onclick="cart_registration_choose('cart_registration_individual', <?= $account['_key'] ?>)">Физическое лицо</label>
<label id="cart_registration_entity_button" class="mr-auto btn button_white active mb-0" for="cart_registration_entity" onclick="cart_registration_choose('cart_registration_entity', <?= $account['_key'] ?>); cart_registration_entity_init(<?= $account['_key'] ?>)">Юридическое лицо</label>
</div>
<div class="cart_registration_content d-flex">
<input type="radio" id="cart_registration_individual" name="registration_panel" />
<div id="cart_registration_individual_body" class="col"></div>
<input type="radio" id="cart_registration_entity" name="registration_panel" checked />
<div id="cart_registration_entity_body" class="col"></div>
</div>
</div>
<div class="col-4 px-0 d-flex flex-column">
<div class="mb-3 mx-0 row mt-auto">
<?php $form = ActiveForm::begin([
'id' => 'form_profile_settings',
'action' => false,
'fieldConfig' => [
'template' => '{label}{input}',
],
'options' => [
'onsubmit' => 'return false;',
'class' => 'ml-auto px-0 col'
]
]);
// Инициализация
$model_delivery ?? $model_delivery = yii::$app->user->identity;
$delivery_to_terminal_list ?? $delivery_to_terminal_list = ['Нет данных'];
?>
<small class="mb-2"><b>Терминал для получения</b></small>
<?= $form->field($model_delivery, 'opts[delivery_to_terminal]', ['options' => ['class' => "mb-0"]])
->dropDownList($delivery_to_terminal_list, [
'onChange' => 'page_profile_settings(this.parentElement.parentElement, undefined, \'\'); cart_cost_calculate();',
'disabled' => count($delivery_to_terminal_list) <= 1
])->label(false); ?>
<?php ActiveForm::end(); ?>
</div>
<div class="mb-0 mx-0 row">
<div class="ml-auto px-0 col d-flex">
<b class="ml-auto my-auto mr-3">
<span id="cart_cost">0</span>
руб
</b>
<a class="col-5 btn button_clean button_blue" title="Оформить заказ" href="/orders" role="button" onclick="return cart_request();">Купить</a>
</div>
</div>
</div>
</div>
</article>
</div>
<script src="/js/cart.js" defer></script>
<script src="/js/textarea.js" defer></script>
<script src="/js/cart.js" defer></script>
<script src="/js/profile.js" defer></script>
<script>
document.addEventListener('cart.loaded', function(e) {
cart_cost_calculate();
cart_registration_entity_init(<?= $account['_key'] ?>);
cart_registration_choose('cart_registration_entity', <?= $account['_key'] ?>);
});
</script>

View File

@@ -2,15 +2,15 @@
use yii\helpers\Html;
$this->title = $name;
$this->title = $title;
?>
<div id="page_error" class="container py-3">
<h1><?= Html::encode($this->title) ?></h1>
<div class="alert alert-danger">
<?= nl2br(Html::encode($message)) ?>
<div id="page_error" class="container d-flex flex-column">
<div class="my-auto">
<div class="col-md-10 col-lg-8 mx-auto p-5 alert alert_white d-flex flex-column">
<h2 class="mb-4 text-center gilroy"><b><?= Html::encode($title) ?></b></h4>
<p class="text-center"><?= nl2br(Html::encode($description)) ?></p>
</div>
</div>
</div>

View File

@@ -3,13 +3,12 @@
declare(strict_types=1);
$this->title = 'SkillParts';
?>
<link href="/css/ticker.css" rel="stylesheet">
<link href="/css/hotline.css" rel="stylesheet">
<div id="page_index" class="mb-auto">
<section class="info_panel mb-4">
<section class="info_panel unselectable">
<div class="container h-100 d-flex flex-column justify-content-center">
<h1 class="mb-4 ml-0 gilroy">Проблема с подбором запчастей?</h1>
<p class="ml-0 d-flex">
@@ -22,39 +21,36 @@ $this->title = 'SkillParts';
</div>
</section>
<section class="h-100 d-flex ticker">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/cummins.png" alt="Cummins">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/iveco.png" alt="Iveco">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/komatsu.png" alt="Komatsu">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/case.png" alt="Case">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/isuzu.png" alt="Isuzu">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/new_holland.png" alt="New Holland">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/perkins.png" alt="Perkins">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/john_deere.png" alt="John Deere">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/caterpillar.png" alt="Caterpillar">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/shantui.png" alt="Shantui">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/xcmg.png" alt="XCMG">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/kobelco.png" alt="Kobelco">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/shehwa.png" alt="SHEHWA">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/bomag.png" alt="BOMAG">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/hitachi.png" alt="Hitachi">
<section id="hotline" class="py-4 hotline unselectable" data-hotline="true" data-hotline-step="1">
<article><img src="/img/logos/h32px/compressed/cummins.png" alt="Cummins"></article>
<article><img src="/img/logos/h32px/compressed/iveco.png" alt="Iveco"></article>
<article><img src="/img/logos/h32px/compressed/komatsu.png" alt="Komatsu"></article>
<article><img src="/img/logos/h32px/compressed/case.png" alt="Case"></article>
<article><img src="/img/logos/h32px/compressed/isuzu.png" alt="Isuzu"></article>
<article><img src="/img/logos/h32px/compressed/new_holland.png" alt="New Holland"></article>
<article><img src="/img/logos/h32px/compressed/perkins.png" alt="Perkins"></article>
<article><img src="/img/logos/h32px/compressed/john_deere.png" alt="John Deere"></article>
<article><img src="/img/logos/h32px/compressed/caterpillar.png" alt="Caterpillar"></article>
<article><img src="/img/logos/h32px/compressed/shantui.png" alt="Shantui"></article>
<article><img src="/img/logos/h32px/compressed/xcmg.png" alt="XCMG"></article>
<article><img src="/img/logos/h32px/compressed/kobelco.png" alt="Kobelco"></article>
<article><img src="/img/logos/h32px/compressed/shehwa.png" alt="SHEHWA"></article>
<article><img src="/img/logos/h32px/compressed/bomag.png" alt="BOMAG"></article>
<article><img src="/img/logos/h32px/compressed/hitachi.png" alt="Hitachi"></article>
</section>
<section class="container mb-4">
<!-- <div class="row mb-3">
<h4 class="col gilroy categories_blocks_panel_title">Сопутствующие товары</h4>
</div> -->
<div class="row mb-5 mb-md-0 px-3 px-md-0">
<div class="row mb-5 mb-md-0 px-3 px-md-0 unselectable">
<div class="col-12 col-md-6 col-lg-4 mb-4 mb-lg-0 py-0 d-flex flex-column">
<div class="px-3 px-xl-4 pt-3 d-inline-block category_block_title">
<h4 class="m-0">Масла, смазки</h4>
</div>
<div class="p-3 px-md-4 category_block">
<dl class="mb-0">
<dd>Масла моторные</dd>
<dd>Масла трансмиссионные</dd>
<dd>Масла гидравлические</dd>
<dd>Смазки</dd>
<dd><a onclick="return writeInDevelopment(this);" type="button">Масла моторные</a></dd>
<dd><a onclick="return writeInDevelopment(this);" type="button">Масла трансмиссионные</a></dd>
<dd><a onclick="return writeInDevelopment(this);" type="button">Масла гидравлические</a></dd>
<dd><a onclick="return writeInDevelopment(this);" type="button">Смазки</a></dd>
</dl>
</div>
</div>
@@ -64,7 +60,7 @@ $this->title = 'SkillParts';
</div>
<div class="p-3 px-md-4 category_block">
<dl class="mb-0">
<dd>Фары и свет</dd>
<dd><a onclick="return writeInDevelopment(this);" type="button">Фары и свет</a></dd>
</dl>
</div>
</div>
@@ -74,9 +70,9 @@ $this->title = 'SkillParts';
</div>
<div class="p-3 px-md-4 category_block">
<dl class="mb-0">
<dd>Шприцы для смазки </dd>
<dd>Ключи, съёмники</dd>
<dd>Наборы инструментов</dd>
<dd><a onclick="return writeInDevelopment(this);" type="button">Шприцы для смазки</a></dd>
<dd><a onclick="return writeInDevelopment(this);" type="button">Ключи, съёмники</a></dd>
<dd><a onclick="return writeInDevelopment(this);" type="button">Наборы инструментов</a></dd>
</dl>
</div>
</div>
@@ -84,4 +80,13 @@ $this->title = 'SkillParts';
</section>
</div>
<script src="/js/ticker.js" defer></script>
<script src="/js/hotline.js" defer></script>
<script src="/js/text.js" defer></script>
<script>
document.addEventListener('hotline.loaded', function(e) {
// Загружена программа: "бегущая строка"
// Обработка HTML-документа и генерация бегущих строк
e.detail.hotline.preprocessing();
});
</script>

View File

@@ -0,0 +1,210 @@
<?php
use app\models\Settings;
use app\models\Product;
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Счёт №<?= $data['order']->_key ?></title>
</head>
<body>
<table style="height: 100vh;" cols="13" rows="50">
<tr>
<td style="text-align: center; outline: 2px solid;" colspan="13" rowspan="3" valign="center"><b>ВНИМАНИЕ! Уважаемые покупатели, просим вас указывать в назначении платежа: Аванс за запчасти по договору №<?= $buyer['id'] ?></b></td>
</tr>
<tr>
<td colspan="13"></td>
</tr>
<tr>
<td colspan="13"></td>
</tr>
<tr>
<td colspan="13"></td>
</tr>
<tr>
<td style="text-align: center;" colspan="13"><b>Образец заполнения платежного поручения</b></td>
</tr>
<tr>
<td style="border: solid;" colspan="6" rowspan="2" valign="top">АО "ТИНЬКОФФ БАНК" г. Москва</td>
<td style="border: solid;" valign="top">БИК</td>
<td style="border: solid;" colspan="6" valign="top">044525974</td>
</tr>
<tr>
<td style="border: solid;" rowspan="2" valign="top">Сч. №</td>
<td style="border: solid;" colspan="6" rowspan="2" valign="top">30101810145250000974</td>
</tr>
<tr>
<td style="border: solid;" colspan="6" valign="top">Банк получателя</td>
</tr>
<tr>
<td style="border: solid;" valign="top">ИНН</td>
<td style="text-align: left; border: solid;" colspan="2" valign="top">2724241607</td>
<td style="border: solid;" valign="top">КПП</td>
<td style="text-align: left; border: solid;" colspan="2" valign="top">272401001</td>
<td style="border: solid;" valign="top">Сч. №</td>
<td style="text-align: left; border: solid;" colspan="6" valign="top">40702810610000696279</td>
</tr>
<tr>
<td style="border: solid;" colspan="6" rowspan="3" valign="top">ООО "СтандартМашинери"</td>
<td style="border: solid;" valign="top">Вид оп.</td>
<td style="text-align: left; border: solid;" valign="top">01</td>
<td style="border: solid;" colspan="2" valign="top">Срок плат.</td>
<td style="text-align: left; border: solid;" colspan="3" valign="top"></td>
</tr>
<tr>
<td style="border: solid;" valign="top">Наз. пл.</td>
<td style="border: solid;"></td>
<td style="border: solid;" colspan="2" valign="top">Очер. плат.</td>
<td style="text-align: left; border: solid;" colspan="3" valign="top">5</td>
</tr>
<tr>
<td style="border: solid;" valign="top">Код</td>
<td style="border: solid;"></td>
<td style="border: solid;" colspan="2" valign="top">Рез. поле</td>
<td style="text-align: left; border: solid;" colspan="3" valign="top"></td>
</tr>
<tr>
<td colspan="13"></td>
</tr>
<tr>
<td style="text-indent: 2px; text-align: left; border-bottom: medium; font-size: 18rem;" colspan="13" rowspan="3" valign="center">
<?php
// Инициализация часового пояса
preg_match_all('/UTC([\+\-0-9:]*)/', $account->zone ?? Settings::searchActive()['timezone_default'] ?? 'UTC+3', $timezone);
$timezone = $timezone[1][0];
?>
<b>Счет на оплату №<?= $data['order']->_key ?> от <?= (new DateTime())->setTimestamp($date)->setTimezone(new DateTimeZone($timezone))->format('d.m.Y') ?></b>
</td>
</tr>
<tr>
<td colspan="13"></td>
</tr>
<tr>
<td colspan="13"></td>
</tr>
<tr>
<td colspan="13"></td>
</tr>
<tr>
<td colspan="2" rowspan="2" valign="center">Поставщик</td>
<td style="word-wrap: break-word;" colspan="11" rowspan="2" valign="top"><b>ООО "СтандартМашинери", ИНН 2724241607, КПП 272401001, 680014, Хабаровский край, Хабаровск г, Промышленная ул, дом 3, офис 105, тел.: 89242128879</b></td>
</tr>
<tr>
<td colspan="13"></td>
</tr>
<tr>
<td colspan="13"></td>
</tr>
<tr>
<td colspan="2" rowspan="2" valign="center">Покупатель</td>
<td style="word-wrap: break-word;" colspan="11" rowspan="2" valign="top"><b><?= $buyer['info'] ?></b></td>
</tr>
<tr>
<td colspan="13"></td>
</tr>
<tr>
<td colspan="13"></td>
</tr>
<tr>
<td style="text-align: center; border: solid; border-top: thick; border-left: thick;" colspan="1" rowspan="2" valign="center"><b>№</b></td>
<td style="text-align: center; border: solid; border-top: thick;" colspan="6" rowspan="2" valign="center"><b>Товар</b></td>
<td style="text-align: center; border: solid; border-top: thick;" colspan="2" rowspan="2" valign="center"><b>Количество</b></td>
<td style="text-align: center; border: solid; border-top: thick;" colspan="2" rowspan="2" valign="center"><b>Цена</b></td>
<td style="text-align: center; border: solid; border-top: thick; border-right: thick;" colspan="2" rowspan="2" valign="center"><b>Сумма</b></td>
</tr>
<tr>
<td colspan="13"></td>
</tr>
<?php
// Инициализация счётчика строк
$row = 1;
// Инициализация итоговой цены
$cost = 0;
?>
<?php foreach ($data['supplies'] as $prod => $supplies) : ?>
<?php foreach ($supplies as $catn => $deliveries) : ?>
<?php foreach ($deliveries as $delivery => $supply) : ?>
<?php
// Инициализация названия
$name = Product::searchByCatn($catn)->name ?? 'Без названия';
?>
<tr>
<td style="text-align: center; border: solid; border-left: thick;" colspan="1" valign="center"><?= $row++ ?></td>
<td style="text-align: left; border: solid;" colspan="6" valign="center"><?= $prod ?> <?= $catn ?> <?= $name ?></td>
<td style="text-align: center; border: solid;" colspan="2" valign="center"><?= $supply['amount'] ?></td>
<td style="text-align: center; border: solid;" valign="center"><?= $supply['cost'] * $supply['amount'] ?></td>
<td style="text-align: center; border: solid;" valign="center"><?= $supply['currency'] ?></td>
<td style="text-align: center; border: solid; border-right: thick;" colspan="2" valign="center"><?= $cost += $supply['cost'] * $supply['amount'] ?></td>
</tr>
<?php endforeach ?>
<?php endforeach ?>
<?php endforeach ?>
<tr>
<td style="border-top: thick;" colspan="13"></td>
</tr>
<tr>
<td colspan="9"></td>
<td style="text-align: right;" colspan="2" valign="center"><b>Итого:</b></td>
<td colspan="2" valign="center"><b><?= $cost - $tax = $cost * 0.2 ?></b></td>
</tr>
<tr>
<td colspan="9"></td>
<td style="text-align: right;" colspan="2" valign="center"><b>В т.ч. НДС (20%):</b></td>
<td colspan="2" valign="center"><b><?= $tax ?></b></td>
</tr>
<tr>
<td colspan="9"></td>
<td style="text-align: right;" colspan="2" valign="center"><b>Итого с НДС:</b></td>
<td colspan="2" valign="center"><b><?= $cost ?></b></td>
</tr>
<tr>
<td colspan="13" valign="center">Всего наименований <?= $row - 1 ?>, на сумму <?= $cost ?> руб.</td>
</tr>
<tr>
<td colspan="13" valign="center"><b><?= mb_strtoupper(mb_substr($text = mb_strtolower($text = (new NumberFormatter("ru", NumberFormatter::SPELLOUT))->format($cost), 'UTF-8'), 0, 1, 'UTF-8'), 'UTF-8') . mb_substr($text, 1, null, 'UTF-8') ?> рублей</b></td>
</tr>
<tr>
<td style="width: 8pt;" colspan="13"><img src="<?= YII_PATH_PUBLIC . '/img/invoices/signature.png' ?>" /></td>
</tr>
</table>
</body>
</html>

View File

@@ -13,30 +13,15 @@ AppAsset::register($this);
<?php $this->beginPage() ?>
<!DOCTYPE html>
<html lang="<?= Yii::$app->language ?>">
<html lang="<?= yii::$app->language ?>">
<head>
<meta charset="<?= Yii::$app->charset ?>">
<meta charset="<?= yii::$app->charset ?>">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="apple-touch-icon" sizes="57x57" href="/favicons/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/favicons/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/favicons/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/favicons/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/favicons/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/favicons/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/favicons/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/favicons/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="/favicons/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/favicons/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png">
<link rel="manifest" href="/favicons/manifest.json">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/favicons/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
<?php $this->registerCsrfMetaTags() ?>
<title><?= Html::encode($this->title ?? 'SkillParts') ?></title>
@@ -46,9 +31,9 @@ AppAsset::register($this);
<body>
<?php $this->beginBody() ?>
<div id="notifications_popup_wrap" class="col-3 m-4"></div>
<div id="notifications_popup_wrap" class="m-4 col-6 col-sm-5 col-md-4 col-lg-4 col-xl-3"></div>
<header class="container pt-2 mt-1 mb-2 mb-sm-4">
<header class="container pt-2 mt-1 mb-2 mb-sm-4 unselectable">
<div class="row h-100">
<div id="logo" class="col-3 col-md-4 h-100">
<a class="py-2 h-100 d-inline-flex" title="SkillParts" href="/" role="button" onclick="return page_main();">
@@ -65,47 +50,34 @@ AppAsset::register($this);
<menu class="col-auto col-lg-4 mb-0 d-flex justify-content-end"></menu>
</div>
<div class="h-divider"></div>
<script src="/js/js.cookie.min.js" defer></script>
</header>
<aside class="container mb-4">
<div class="row">
<div class="col-lg-3 d-flex flex-column align-center justify-content-end dropdownMenuButton_column">
<button id="catalog" class="btn form-control d-flex align-items-center button_clean catalog_button" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<button id="catalog" class="btn button_clean form-control d-flex align-items-center button_clean catalog_button" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-bars col-auto text-left p-0 mr-auto h-100 d-flex flex-column justify-content-center"></i>
<p class="col-10 m-0 p-0">Каталог товаров</p>
</button>
<div class="dropdown-menu" aria-labelledby="catalog">
<a class="dropdown-item" href="#">Action</a>
<a class="dropdown-item" href="#">Another action</a>
<a class="dropdown-item" href="#">Something else here</a>
<a class="dropdown-item" type="button">В разработке</a>
</div>
</div>
<div id="searchPanel" class="col">
<!-- <input id="catalog_search_panel_button_1" class="btn btn-sm button_clean" type="radio" name="catalog_search_panel_buttons" value="catalog_search_panel_button_1" checked>
<label class="mb-0 px-3 px-md-4 py-1" for="catalog_search_panel_button_1">Номер детали</label>
<input id="catalog_search_panel_button_2" class="btn btn-sm text-white button_clean" type="radio" name="catalog_search_panel_buttons" value="catalog_search_panel_button_2">
<label class="mb-0 px-3 px-md-4 py-1" for="catalog_search_panel_button_2">Вторая кнопка</label>
<input id="catalog_search_panel_button_3" class="btn btn-sm5 text-white button_clean" type="radio" name="catalog_search_panel_buttons" value="catalog_search_panel_button_3">
<label class="mb-0 px-3 px-md-4 py-1" for="catalog_search_panel_button_3">Третья кнопка</label> -->
<form class="d-flex catalog_search" onsubmit="return false;">
<div class="position-relative col-sm-8 col-lg-10 px-0">
<input id="search_line" type="text" class="form-control col-12 catalog_search_line button_clean" placeholder="Введите номер запчасти, например: 45223503481" oninput="$('#search_line').dropdown('hide'); product_search(this.value);" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" autocomplete="off">
<input id="search_line" type="text" class="form-control button_clean col-12 catalog_search_line" placeholder="Введите номер запчасти, например: 45223503481" oninput="$('#search_line').dropdown('hide'); return product_search(this.value);" data-toggle="dropdown" aria-expanded="false" autocomplete="off">
<?php
// Сделать системные настройки и по ним работать
$search_panel = yii::$app->controller->renderPartial('/search/panel', ['history' => true]);
// if (!yii::$app->user->isGuest && $search_panel = $search_panel ?? yii::$app->controller->renderPartial('/search/panel', ['history' => true])) {
echo <<<HTML
<div id="search_line_window" class="dropdown-menu w-100" aria-labelledby="search_line">
$search_panel
</div>
HTML;
// } else {
// echo <<<HTML
// <div id="search_line_window" class="dropdown-menu w-100" style="display: none" aria-labelledby="search_line"></div>
// HTML;
// }
<div id="search_line_window" class="dropdown-menu w-100" aria-labelledby="search_line">
$search_panel
</div>
HTML;
?>
</div>
<button type="submit" class="col btn button_clean catalog_search_button" onclick="product_search(this.parentElement.getElementsByTagName('input')[0].value, 1)">ПОИСК</button>
@@ -121,24 +93,28 @@ AppAsset::register($this);
<footer class="container py-4">
<div class="row px-3">
<div class="col-12 col-md-auto mr-md-5">
<h5 class="row mb-2"><b>Контакты</b></h5>
<h5 class="row mb-2 unselectable"><b>Контакты</b></h5>
<small class="row mb-1"><b>Адрес:&nbsp;</b>Хабаровск, Промышленная 3, 105</small>
<small class="row mb-1"><b>Время работы:&nbsp;</b>пн-пт 09:00-18:00</small>
<small class="row mb-1"><b>Телефон:&nbsp;</b>+7 (4212) 35-85-34</small>
<small class="row mb-1"><b>Почта:&nbsp;</b>info@skillparts.ru</small>
<small class="row mb-1"><b>Телефон:&nbsp;</b><a href="tel:+74212358534">+7 (4212) 35-85-34</a></small>
<small class="row"><b>Почта:&nbsp;</b><a href="mailto:info@skillparts.ru">info@skillparts.ru</a></small>
</div>
<div class="col-md-auto mr-md-5 partnership">
<div class="col-md-auto mr-md-5 partnership unselectable">
<h5 class="row mb-2"><b>Партнёрство</b></h5>
<small class="row mb-1"><a>Оптовым покупателям</a></small>
<small class="row mb-1"><a>Поставщикам</a></small>
<small class="row mb-1"><a>Партнерская сеть</a></small>
<small class="row mb-1"><a href="/buyers">Покупателям</a></small>
<small class="row mb-1"><a href="/suppliers">Поставщикам</a></small>
<small class="row"><a href="/partners">Сеть филиалов</a></small>
</div>
<div class="mt-auto ml-auto col-auto unselectable">
<small class="row"><a href="mailto:info@skillparts.ru?subject=Ошибка на сайте">Сообщить об ошибке</a></small>
</div>
</div>
</footer>
<script src="/js/loading.js" defer></script>
<?php $this->endBody() ?>
</body>
</html>
<?php $this->endPage() ?>
<?php $this->endPage() ?>

View File

@@ -0,0 +1,14 @@
<div style="padding: 0 14%;">
<div style="background: #fff;">
<a title="SkillParts" href="https://skillparts.ru">
<img style="width: 150px;" src="https://skillparts.ru/img/logos/skillparts.png" alt="SkillParts">
</a>
</div>
<div style="background: #f0eefb; padding: 40px; margin: 30px 0;">
<h3 style="text-align: center; margin-bottom: 30px;"><b>Новый пароль</b></h3>
<p style="margin: 0 40px; margin-bottom: 8px;">По вашему запросу сгенерирован новый пароль: <b><?= $pswd ?? 'ОШИБКА' ?></b></p>
</div>
<div style="background: #fff;">
<small>Если это были не вы свяжитесь с администрацией</small>
</div>
</div>

View File

@@ -0,0 +1,15 @@
<div style="padding: 0 14%;">
<div style="background: #fff;">
<a title="SkillParts" href="https://skillparts.ru">
<img style="width: 150px;" src="https://skillparts.ru/img/logos/skillparts.png" alt="SkillParts">
</a>
</div>
<div style="background: #f0eefb; padding: 40px; margin: 30px 0;">
<h3 style="text-align: center; margin-bottom: 30px;"><b>Генерация нового пароля</b></h3>
<p style="margin: 0 40px; margin-bottom: 8px;">Только что был получен запрос на генерацию нового пароля для вашего аккаунта</p>
<a style="display: block; text-align: center;" href="https://skillparts.ru/restore/<?= $id ?? '' ?>/<?= $chpk ?? '' ?>">Подтвердить</a>
</div>
<div style="background: #fff;">
<small>Если это были не вы, проигнорируйте это письмо</small>
</div>
</div>

View File

@@ -0,0 +1,9 @@
<html>
<body>
<p>ФИО: <span style="margin-left: auto;"><?= $name ?></span></p>
<p>Телефон: <span style="margin-left: auto;"><?= $phon ?></span></p>
<p>Почта: <span style="margin-left: auto;"><?= $mail ?></span></p>
</body>
</html>

View File

@@ -0,0 +1,15 @@
<div style="padding: 0 14%;">
<div style="background: #fff;">
<a title="SkillParts" href="https://skillparts.ru">
<img style="width: 150px;" src="https://skillparts.ru/img/logos/skillparts.png" alt="SkillParts">
</a>
</div>
<div style="background: #f0eefb; padding: 40px; margin: 30px 0;">
<h3 style="text-align: center; margin-bottom: 30px;"><b>Отказано в регистрации</b></h3>
<p style="margin: 0 40px; margin-bottom: 8px;"><b>Причина: </b><?= $reason ?? 'Ошибка' ?></p>
<a style="display: block; text-align: center;" href="https://skillparts.loc/suppliers/request">Повторная заявка</a>
</div>
<div style="background: #fff;">
<small>Если это были не вы свяжитесь с администрацией</small>
</div>
</div>

View File

@@ -0,0 +1,18 @@
<div style="padding: 0 14%;">
<div style="background: #fff;">
<a title="SkillParts" href="https://skillparts.ru">
<img style="width: 150px;" src="https://skillparts.ru/img/logos/skillparts.png" alt="SkillParts">
</a>
</div>
<div style="background: #f0eefb; padding: 40px; margin: 30px 0;">
<h3 style="text-align: center; margin-bottom: 30px;"><b>Подтвердите регистрацию</b></h3>
<p style="margin: 0 40px; margin-bottom: 8px;"><b>Ваш пароль: </b><?= $password ?? 'Ошибка' ?></p>
<small style="display: block; margin: 0 40px; margin-bottom: 40px;">Нажимая на кнопку ниже вы соглашаетесь с <a href="https://skillparts.ru/policy">политикой конфиденциальности</a></small>
<a style="display: block; text-align: center;" href="https://skillparts.ru/verify/<?= $vrfy ?? '' ?>">Принять и подтвердить</a>
</div>
<div style="background: #fff;">
<small>Вы получили это сообщение потому, что на ваш почтовый адрес была совершена регистрация</small>
</br>
<small>Если это были не вы свяжитесь с администрацией</small>
</div>
</div>

View File

@@ -1,18 +1,18 @@
<a id="notification_button" class="text-dark d-flex h-100 mr-2" title="Уведомления" role="button" data-toggle="dropdown" data-offset="-100%p + 100%" onclick="return notification_stream();">
<a id="notification_button" class="mr-2 h-100 text-dark d-flex" title="Уведомления" role="button" data-toggle="dropdown" data-offset="-100%p + 100%" onclick="return notification_stream();">
<?php
if (empty($notifications_new_amount) || $notifications_new_amount < 1) {
// Новые уведомления не найдены
echo <<<HTML
<i class="fas fa-bell my-auto mx-2"></i>
<i class="mx-2 fas fa-bell my-auto"></i>
HTML;
} else {
// Новые уведомления найдены
echo <<<HTML
<small class="my-auto ml-2 mr-1"><b>$notifications_new_amount</b></small>
<i class="fas fa-bell my-auto mr-2 notification_button_active"></i>
<small class="ml-2 mr-1 my-auto"><b>$notifications_new_amount</b></small>
<i class="mr-2 my-auto fas fa-bell notification_button_active"></i>
HTML;
}
?>
</a>
</a>

Some files were not shown because too many files have changed in this diff Show More