26 Commits
0.4.1 ... 0.6.x

Author SHA1 Message Date
Arsen Mirzaev Tatyano-Muradovich
4c61ccbb65 Исправления 2021-03-29 11:23:00 +10:00
Arsen Mirzaev Tatyano-Muradovich
a9259eb7dc Исправления 2021-03-29 10:26:55 +10:00
Arsen Mirzaev Tatyano-Muradovich
6dc1b081f9 Докидываю 2021-03-29 10:20:35 +10:00
Arsen Mirzaev Tatyano-Muradovich
3fea918255 Добавлены сессии, разделение по правам доступа, редактирование товаров 2021-03-29 10:19:03 +10:00
Arsen Mirzaev Tatyano-Muradovich
722bf6c378 Разработка сессий, переработка под браузеры, уведомления приходят только получателю 2021-03-22 05:03:37 +10:00
Arsen Mirzaev Tatyano-Muradovich
8cbe77e354 Исправление багов 2021-03-16 03:10:50 +10:00
Arsen Mirzaev Tatyano-Muradovich
b71f6e8efc Большое исправление ВСЕГО 2021-03-16 02:50:45 +10:00
Arsen Mirzaev Tatyano-Muradovich
1fac5cdfc1 Доработка настроек 2021-03-10 12:08:25 +10:00
Arsen Mirzaev Tatyano-Muradovich
081a34a8f4 Доработка корзины, фронтенд изменений 2021-03-09 08:11:55 +10:00
Arsen Mirzaev Tatyano-Muradovich
53d7e2a048 Создание корзины 2021-03-07 20:29:19 +10:00
Arsen Mirzaev Tatyano-Muradovich
b8c1f7cef0 Исправление ошибок с тегами 2021-02-25 03:10:29 +10:00
Arsen Mirzaev Tatyano-Muradovich
b91ef68091 Исправление ошибок 2021-02-25 01:10:26 +10:00
Arsen Mirzaev Tatyano-Muradovich
301b2945bc Исправление багов 2021-02-24 22:07:12 +10:00
Arsen Mirzaev Tatyano-Muradovich
78ae20cdac Исправление багов 2021-02-24 22:06:59 +10:00
Arsen Mirzaev Tatyano-Muradovich
65c2e19e55 Исправление тегов вставки кода 2021-02-24 20:54:00 +10:00
Arsen Mirzaev Tatyano-Muradovich
c9e3b542b7 Очистка мусора 2021-02-24 08:37:57 +10:00
Arsen Mirzaev Tatyano-Muradovich
8a3897ad8f Создан поиск, уведомления, настройки, мониторгин, админ-панель 2021-02-24 08:24:57 +10:00
RedHood
0f8e6a7a6a Исправления наследовавших классов 2021-01-31 20:39:02 +10:00
RedHood
6a7f8897d5 Исправление представлений 2021-01-31 17:23:17 +10:00
RedHood
706c9b0743 Обновление composer.json 2021-01-31 11:36:59 +10:00
RedHood
25709ee380 Доработка поиска, поиск аттрибутов 2021-01-31 11:31:22 +10:00
RedHood
5d18c95dc4 Починка представления главной страницы 2021-01-24 17:00:53 +10:00
RedHood
aaec25f64c composer 2021-01-24 16:49:39 +10:00
RedHood
9887be35f9 Обновление composer.json 2021-01-24 16:27:45 +10:00
RedHood
c2afcfcc5a Поиск по товарам 2021-01-24 16:25:08 +10:00
root
212694c917 Создан импорт из 1C, без поддержки групп 2021-01-19 07:42:10 +00:00
252 changed files with 8410 additions and 1677 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/vendor
/cache
/cache
/.vscode

View File

@@ -8,22 +8,25 @@
{
"name": "Arsen Mirzaev Tatyano-Muradovich",
"email": "red@hood.su",
"homepage": "https://hood.su/mirzaev",
"role": "Developer"
}
],
"require": {
"php": ">=8.0.0",
"php": "^8.0.0",
"twbs/bootstrap": ">=4.5",
"yiisoft/yii2": ">=2.0.14",
"yiisoft/yii2": "2.*",
"yiisoft/yii2-bootstrap": ">=2.0.0",
"yiisoft/yii2-swiftmailer": ">=2.0.0",
"bower-asset/bootstrap": "*",
"npm-asset/jquery": "^3.5",
"bower-asset/jquery": "^3.5",
"explosivebit/arangodb": "^2.0",
"triagens/arangodb": "^3.6",
"moonlandsoft/yii2-phpexcel": "^2.0",
"carono/yii2-1c-exchange": "^0.3.1"
"moonlandsoft/yii2-phpexcel": ">=2.0",
"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"
},
"require-dev": {
"codeception/codeception": ">=4.1",
@@ -41,10 +44,7 @@
"autoload": {
"psr-4": {
"mirzaev\\skillparts\\": "mirzaev/skillparts/system"
},
"classmap": [
"vendor/explosivebit"
]
}
},
"autoload-dev": {
"psr-4": {
@@ -87,18 +87,6 @@
{
"type": "composer",
"url": "https://asset-packagist.org"
},
{
"type": "package",
"package": {
"name": "explosivebit/arangodb",
"version": "2.0",
"source": {
"type": "git",
"url": "https://github.com/pBazsi/yii2-arangodb.git",
"reference": "master"
}
}
}
]
}

1193
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@
namespace app\assets;
use yii\web\AssetBundle;
use yii\web\View;
/**
* Main application asset bundle.
@@ -25,21 +26,26 @@ class AppAsset extends AssetBundle
'css/bootstrap/bootstrap.min.css',
'css/main.css',
'css/header.css',
'css/notification.css',
'css/info_panel.css',
'css/categories_blocks_panel.css',
'css/footer.css'
];
public $js = [
'https://kit.fontawesome.com/d7e922c226.js',
'https://code.jquery.com/jquery-3.5.1.min.js',
'js/bootstrap/popper.min.js',
'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',
'js/menu.js',
'js/account.js',
'js/search.js',
'js/notification.js',
'js/reinitialization.js'
];
public $jsOptions = [
// 'position' => View::POS_HEAD
];
public $depends = [
'yii\web\YiiAsset',
// 'yii\bootstrap\BootstrapAsset'

View File

@@ -1,6 +1,8 @@
<?php
$config = [
declare(strict_types=1);
return [
'id' => 'skillparts-console',
'basePath' => dirname(__DIR__),
'bootstrap' => ['log'],
@@ -9,7 +11,6 @@ $config = [
'@vendor' => dirname(__DIR__) . '/../../../vendor',
'@bower' => '@vendor/bower-asset',
'@npm' => '@vendor/npm-asset',
'@explosivebit' => '@vendor/explosivebit',
'@tests' => '@app/tests',
],
'components' => [
@@ -28,19 +29,9 @@ $config = [
],
'params' => require __DIR__ . '/params.php',
'controllerMap' => [
'arangodb-migrate' => 'explosivebit\arangodb\console\controllers\MigrateController',
'arangodb-migrate' => 'mirzaev\yii2\arangodb\console\controllers\MigrateController',
'fixture' => [
'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

@@ -3,7 +3,7 @@
use ArangoDBClient\ConnectionOptions;
return [
'class' => '\explosivebit\arangodb\Connection',
'class' => '\mirzaev\yii2\arangodb\Connection',
'connectionOptions' => [
ConnectionOptions::OPTION_DATABASE => '',
ConnectionOptions::OPTION_ENDPOINT => 'tcp://127.0.0.1:8529',

View File

@@ -7,8 +7,7 @@ $config = [
'aliases' => [
'@vendor' => dirname(__DIR__) . '/../../../vendor',
'@bower' => '@vendor/bower-asset',
'@npm' => '@vendor/npm-asset',
'@explosivebit' => '@vendor/explosivebit',
'@npm' => '@vendor/npm-asset'
],
'components' => [
'request' => [
@@ -20,16 +19,34 @@ $config = [
'class' => 'yii\caching\FileCache',
],
'user' => [
'identityClass' => 'app\models\Account',
'identityClass' => 'app\models\Account',
'loginUrl' => ['/authentication'],
'enableAutoLogin' => true
// 'enableAutoLogin' => true,
'enableSession' => true
],
// 'session' => [
// 'class' => 'yii\web\Session',
// 'cookieParams' => ['lifetime' => 3600 * 24 * 30 * 12],
// 'timeout' => 3600 * 24 * 30 * 12,
// 'useCookies' => true,
// 'authManager' => [
// 'class' => 'mirzaev\yii2\arangodb\rbac\DbManager',
// ],
'session' => [
'class' => 'mirzaev\yii2\arangodb\sessions\ArangoDbSession',
'document' => 'session',
'cookieParams' => [
// 'lifetime' => 3600 * 24 * 30 * 12
'lifetime' => 3600 * 24 * 3
],
'writeCallback' => function($session): array {
// Инициализация
$data = [];
yii::$app->request->userIP and $data['ip'] = yii::$app->request->userIP;
Yii::$app->user->id and $data['account'] = Yii::$app->user->id;
return $data ?? [];
},
'timeout' => 3600 * 24 * 3,
'useStrictMode' => false,
'useCookies' => true
],
'errorHandler' => [
'errorAction' => 'error',
],
@@ -58,7 +75,9 @@ $config = [
'class' => 'yii\rest\UrlRule',
'controller' => 'main'
],
'product/<id:\d+>' => 'product/index'
'product/<catn:[^/]+>' => 'product/index',
'product/<catn:[^/]+>/<action:(write|edit|delete)>/<target:(title|catn|desc|image)>' => 'product/<action>-<target>',
'orders' => 'order/index'
],
],
@@ -68,12 +87,19 @@ $config = [
'class' => 'carono\exchange1c\ExchangeModule',
'groupClass' => 'app\models\SupplyGroup',
'productClass' => 'app\models\Supply',
'offerClass' => 'app\models\Product',
'offerClass' => 'app\models\SupplyEdgeProduct',
'partnerClass' => 'app\models\Account',
'documentClass' => 'app\models\Purchase',
'auth' => function ($mail, $pswd) {
// Необходимо уничтожить AccountForm
return (new \app\models\AccountForm())->authentication($mail, $pswd);
// return (new \app\models\AccountForm())->authentication($mail, $pswd);
if ($user = \app\models\Account::findByMail($mail)) {
if ($user->validatePassword($pswd)) {
return $user;
}
}
return false;
}
]
],
@@ -86,7 +112,7 @@ if (YII_ENV_DEV) {
'class' => 'yii\debug\Module',
'panels' => [
'ArangoDB' => [
'class' => 'explosivebit\arangodb\panels\arangodb\ArangoDbPanel'
'class' => 'mirzaev\yii2\arangodb\panels\arangodb\ArangoDbPanel'
]
]
];

View File

@@ -2,11 +2,15 @@
namespace app\controllers;
use Yii;
use app\models\AccountForm;
use yii;
use yii\web\Controller;
use yii\web\Response;
use yii\filters\AccessControl;
use app\models\AccountForm;
use Throwable;
use Exception;
class AuthenticationController extends Controller
{
@@ -31,37 +35,61 @@ class AuthenticationController extends Controller
public function actionIndex()
{
if (Yii::$app->request->isAjax) {
if (yii::$app->request->isAjax) {
// AJAX-POST-запрос
// Инициализация
$model = new AccountForm(Yii::$app->request->post('AccountForm'));
$model = new AccountForm(yii::$app->request->post('AccountForm'));
$model->scenario = $model::SCENARIO_AUTHENTICATION;
Yii::$app->response->format = Response::FORMAT_JSON;
yii::$app->response->format = Response::FORMAT_JSON;
if (!Yii::$app->user->isGuest || $model->authentication()) {
if (!yii::$app->user->isGuest || $model->authentication()) {
// Аккаунт аутентифицирован
// Создание сессии
// yii::$app->session->open();
// Инициализация
$notifications_button = $this->renderPartial('/notification/button');
$notifications_panel_full = true;
$notifications_panel = $this->renderPartial('/notification/panel', compact('notifications_panel_full'));
// Запись ответа
$return = [
'nav' => (new AccountForm())->deauthenticationGenHtml(),
'_csrf' => Yii::$app->request->getCsrfToken()
'menu' => $this->renderPartial('/account/panel/authenticated', compact(
'notifications_button',
'notifications_panel',
'notifications_panel_full'
)),
'_csrf' => yii::$app->request->getCsrfToken()
];
if (($cookies = Yii::$app->request->cookies)->has('redirect')) {
if (($cookies = yii::$app->request->cookies)->has('redirect')) {
// Найдено cookie с переадресацией
// Запись ответа
$return['redirect'] = '/' . $cookies['redirect'];
$return['main'] = $this->renderPartial($return['redirect'] . '/index');
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']);
unset(yii::$app->response->cookies['redirect']);
} else {
// Не найдено cookie с переадресацией
if (Yii::$app->request->pathInfo === 'authentication' || Yii::$app->request->pathInfo === 'registration') {
if (yii::$app->request->pathInfo === 'authentication' || yii::$app->request->pathInfo === 'registration') {
// Если клиент на промежуточном URI
// Запись ответа
@@ -74,20 +102,20 @@ class AuthenticationController extends Controller
} else {
// Аккаунт не аутентифицирован
Yii::$app->response->statusCode = 400;
yii::$app->response->statusCode = 400;
return [
'main' => $this->renderPartial('/account', compact('model')),
'main' => $this->renderPartial('/account/index', compact('model')),
'redirect' => '/authentication',
'_csrf' => Yii::$app->request->getCsrfToken()
'_csrf' => yii::$app->request->getCsrfToken()
];
}
}
if (!Yii::$app->user->isGuest) {
Yii::$app->response->redirect('/');
if (!yii::$app->user->isGuest) {
yii::$app->response->redirect('/');
} else {
return $this->render('/account');
return $this->render('/account/index');
}
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace app\controllers;
use yii;
use yii\filters\AccessControl;
use yii\web\Controller;
use yii\web\Response;
use app\models\Order;
use Exception;
class CartController extends Controller
{
/**
* Страница: "Корзина"
*
* @see $this->behaviors Доступ только аутентифицированным
*/
public function actionIndex(): string|array|null
{
// Инициализация
$page = yii::$app->request->get('page') ?? yii::$app->request->post('page') ?? 1;
$account = yii::$app->user;
// Поиск корзины (текущего заказа)
$model = Order::search();
if (empty($model)) {
// Корзина не инициализирована
// Инициализация
$model = new Order();
$model->save() or throw new Exception('Не удалось инициализировать заказ');
// Подключение
$model->connect($account);
}
// Инициализация содержимого корзины
$supplies = $model->content(10, $page);
if (yii::$app->request->isPost) {
// POST-запрос
yii::$app->response->format = Response::FORMAT_JSON;
return [
'main' => $this->renderPartial('index', compact('model', 'supplies')),
'title' => 'Корзина',
'redirect' => '/cart',
'_csrf' => yii::$app->request->getCsrfToken()
];
}
return $this->render('index', compact('model', 'supplies'));
}
}

View File

@@ -2,7 +2,7 @@
namespace app\controllers;
use Yii;
use yii;
use yii\web\Controller;
use yii\web\Response;
use yii\filters\AccessControl;
@@ -34,20 +34,26 @@ class DeauthenticationController extends Controller
public function actionIndex()
{
Yii::$app->response->format = Response::FORMAT_JSON;
// Инициализация
yii::$app->response->format = Response::FORMAT_JSON;
// Удаление сессии
yii::$app->session['status'] = 'inactive';
// yii::$app->session->close();
// Выход из аккаунта
Yii::$app->user->logout();
yii::$app->user->logout();
// Инициализация
$model = new AccountForm(Yii::$app->request->post('AccountForm') ?? Yii::$app->request->get('AccountForm') ?? null);
$model = new AccountForm(yii::$app->request->post('AccountForm') ?? yii::$app->request->get('AccountForm') ?? null);
// Ответа
return [
'nav' => $model->authenticationGenHtml($this->renderPartial('/account', compact('model'))),
'menu' => $this->renderPartial('/account/panel/deauthenticated', compact('model')),
'main' => $this->renderPartial('/index'),
'redirect' => '/',
'_csrf' => Yii::$app->request->getCsrfToken()
'_csrf' => yii::$app->request->getCsrfToken()
];
}
}

View File

@@ -4,8 +4,6 @@ namespace app\controllers;
use Yii;
use yii\web\Controller;
use yii\web\ErrorAction;
use yii\web\Response;
class ErrorController extends Controller
{

View File

@@ -1,53 +1,64 @@
<?php
declare(strict_types=1);
namespace app\controllers;
use Yii;
use yii;
use yii\web\Controller;
use yii\web\Response;
use app\models\AccountForm;
class IdentificationController extends Controller
{
public function actionIndex()
{
if (Yii::$app->request->isAjax) {
// AJAX-POST-запрос
if (yii::$app->request->isPost) {
// POST-запрос
Yii::$app->response->format = Response::FORMAT_JSON;
yii::$app->response->format = Response::FORMAT_JSON;
if (Yii::$app->user->isGuest) {
if (yii::$app->user->isGuest) {
// Аккаунт не аутентифицирован
// Инициализация
$model = new AccountForm(Yii::$app->request->post('AccountForm') ?? Yii::$app->request->get('AccountForm') ?? null);
$model = new AccountForm(yii::$app->request->post('AccountForm') ?? yii::$app->request->get('AccountForm') ?? null);
// Запись ответа
$return = [
'nav' => $model->authenticationGenHtml($this->renderPartial('/account', compact('model'))),
'_csrf' => Yii::$app->request->getCsrfToken()
'menu' => $this->renderPartial('/account/panel/deauthenticated', compact('model')),
'_csrf' => yii::$app->request->getCsrfToken()
];
} else {
// Аккаунт аутентифицирован
// Инициализация
$model = Yii::$app->user;
$model = yii::$app->user;
// Инициализация
$notifications_button = $this->renderPartial('/notification/button');
$notifications_panel_full = true;
$notifications_panel = $this->renderPartial('/notification/panel', compact('notifications_panel_full'));
// Запись ответа
$return = [
'nav' => (new AccountForm())->deauthenticationGenHtml(),
'_csrf' => Yii::$app->request->getCsrfToken()
'menu' => $this->renderPartial('/account/panel/authenticated', compact(
'notifications_button',
'notifications_panel'
)),
'_csrf' => yii::$app->request->getCsrfToken()
];
}
if (($cookies = Yii::$app->request->cookies)->has('redirect')) {
if (($cookies = yii::$app->request->cookies)->has('redirect')) {
// Найдено cookie с переадресацией
// Запись ответа
$return['redirect'] = '/' . $cookies['redirect'];
// Очистка cookie
unset(Yii::$app->response->cookies['redirect']);
unset(yii::$app->response->cookies['redirect']);
}
return $return;

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace app\controllers;
use Yii;
@@ -22,11 +24,9 @@ class MainController extends Controller
}
/**
* Displays homepage.
*
* @return string
* Главная страница
*/
public function actionIndex()
public function actionIndex(): string|array
{
if (Yii::$app->request->isAjax) {
// AJAX-POST-запрос

View File

@@ -0,0 +1,212 @@
<?php
declare(strict_types=1);
namespace app\controllers;
use app\models\AccountEdgeNotification;
use yii;
use yii\filters\AccessControl;
use yii\web\Controller;
use yii\web\Response;
use app\models\Notification;
class NotificationController extends Controller
{
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::class,
'rules' => [
[
'allow' => true,
'roles' => ['@'],
'actions' => ['index']
]
]
]
];
}
/**
* @todo Перенести логику в модель
*/
public function actionIndex()
{
if (yii::$app->request->isPost) {
// POST-запрос
// Инициализация
$model = new Notification(yii::$app->request->post('Notification'));
$preload = (bool) (int) yii::$app->request->post('preload');
$account = yii::$app->user->identity;
yii::$app->response->format = Response::FORMAT_JSON;
$return = [
'_csrf' => yii::$app->request->getCsrfToken()
];
if (yii::$app->request->post('last')) {
// Запрос последнего уведомлений (всплывающее окно)
// Инициализация
$limit = 1;
} else if (yii::$app->request->post('stream')) {
// Запрос последних уведомлений (панель)
$limit = 10;
}
if (isset($limit)) {
// Обработка для всплывающего окна или панель
// Подзапрос для проверки статуса уведомления относительно пользователя
// Поиск рёбер: ПОЛЬЗОВАТЕЛЬ -> УВЕДОМЛЕНИЕ
$let = $model::find()
->for(['account', $model::collectionName() . '_edge_account'])
->traversal($model::collectionName(), 'OUTBOUND')
->in('account_edge_' . $model::collectionName())
->where(['account._id == "' . $account->readId() . '" && notification_edge_account._from != account_edge_notification[0]._to'])
->select($model::collectionName() . '_edge_account');
if (yii::$app->request->post('last')) {
// Запрос последнего уведомлений (всплывающее окно)
// Уведомление которое не выводилось на мониторе пользователя
$type = 'received';
} else if (yii::$app->request->post('stream')) {
// Запрос последних уведомлений (панель)
// Уведомление которое не было прочитано в окне уведомний
$type = 'checked';
}
// Генерация
$let = $let->createCommand();
/**
* Поиск рёбер: (УВЕДОМЛЕНИЕ)? -> ПОЛЬЗОВАТЕЛЬ
*
* @param bool $new Активация проверки на то, что уведомление не получено
* @param bool $count Посчитать
*/
$search = function (bool $new = false, bool $count = false) use ($model, $account, $type, $let, $limit): array|int {
if ($count) {
// Запрошен подсчёт непрочитанных уведомлений
// Реинициализация
$type = 'checked';
$limit = null;
}
return $model::searchByEdge(
from: 'account',
to: 'notification',
params: $new ? $let->getBindVars() : [],
subquery_where: [
[
'account._id' => $account->readId()
],
[
[
'notification.html' => null
],
'operator' => '!='
],
[
'account_edge_notification.type' => $type
]
],
let: [
'notification_edge_account',
'(' . (string) $let . ')'
],
foreach: [
'edge' => 'notification_edge_account'
],
where: $new ? [
'edge != null'
] : [],
limit: $limit,
sort: ['DESC'],
direction: 'INBOUND',
handle: $count ? function ($request) {
// Посчитать рёбра (информация о количестве непрочитанных уведомлений)
return $request->count();
} : null
);
};
// Поиск новых (непрочитанных) уведомлений пользователя
// Если заккоментировать, то вместо только новых отправит все последние уведомления
$notifications = $search(true);
// Подсчёт новых (непрочитанных) уведомлений
$return['button'] = $this->renderPartial('button', ['notifications_new_amount' => $search(new: true, count: true)]);
if (!yii::$app->request->post('last') && empty($notifications)) {
// Запрошены НЕ ВСПЛЫВАЮЩИЕ уведомления и новые уведомления не найдены
// Поиск уведомлений пользователя
$notifications = $search();
}
if (empty($notifications)) {
// Уведомления не найдены
/**
* @todo Определить какой код лучше использовать (404 не подходит)
*/
yii::$app->response->statusCode = 200;
goto end;
}
foreach ($notifications as $notification) {
// Перебор найденных уведомлений
if ($preload) {
// Запрошены уведомления для предзагрузки (их ещё не увидели)
break;
}
// Запись ребра: ПОЛЬЗОВАТЕЛЬ -> УВЕДОМЛЕНИЕ (о том, что уведомление прочитано)
AccountEdgeNotification::write(yii::$app->user->id, $notification->readId(), $type);
}
if (yii::$app->request->post('last')) {
// Запрос последнего уведомлений (всплывающее окно)
// Реинициализация
$notification = $notifications[0];
$return['popup'] = [
'html' => $this->renderPartial('popup', compact('model', 'notification')),
'id' => 'popup/' . $notification->readId()
];
} else if (yii::$app->request->post('stream')) {
// Запрос последних уведомлений (панель)
$return['panel'] = $this->renderPartial('panel', compact('model', 'notifications'));
}
} else {
// Иначе обрабатывается как запрос страницы уведомлений
$return['main'] = $this->renderPartial('index', compact('model'));
$return['redirect'] = '/notification';
}
/**
* Конец алгоритма
*/
end:
return $return;
}
return $this->render('index', ['model' => new Notification(yii::$app->request->get('Notification'))]);
}
}

View File

@@ -0,0 +1,338 @@
<?php
declare(strict_types=1);
namespace app\controllers;
use yii;
use yii\filters\AccessControl;
use yii\web\Controller;
use yii\web\Response;
use app\models\Order;
use app\models\AccountEdgeOrder;
use Exception;
class OrderController extends Controller
{
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::class,
'rules' => [
[
'allow' => true,
'roles' => ['@'],
'actions' => ['index', 'write', 'delete', 'amount-update', 'pay']
],
[
'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('/orders/index'),
'redirect' => '/authentication',
'_csrf' => Yii::$app->request->getCsrfToken()
]);
} else if (Yii::$app->request->isGet) {
// GET-запрос
$this->redirect('/authentication');
}
}
public function actionIndex()
{
// Инициализация
$orders = Order::search(type: 'all', limit: 10, page: 1, select: '{account_edge_order, order}');
if (yii::$app->request->isPost) {
// POST-запрос
yii::$app->response->format = Response::FORMAT_JSON;
return [
'main' => $this->renderPartial('/orders/index', compact('orders')),
'title' => 'Заказы',
'redirect' => '/orders',
'_csrf' => yii::$app->request->getCsrfToken()
];
}
return $this->render('/orders/index', compact('orders'));
}
/**
* Запись
*/
public function actionWrite(): string|array|null
{
if (yii::$app->request->isPost) {
// POST-запрос
// Инициализация входных данных
$account = yii::$app->user;
$supplies = yii::$app->request->post('supplies');
yii::$app->response->format = Response::FORMAT_JSON;
// Инициализация возврата по умолчанию
$return = [
'_csrf' => yii::$app->request->getCsrfToken()
];
if (is_null($supplies)) {
// 501 Not Implemented
yii::$app->response->statusCode = 501;
}
if (!is_null($supplies)) {
// Переданы поставки для записи
if (!$model = Order::search($account)) {
// Корзина не найдена (текущий заказ)
// Инициализация
$model = new Order();
$model->save() or throw new Exception('Не удалось инициализировать заказ');
// Запись ребра: АККАУНТ -> ЗАКАЗ
AccountEdgeOrder::write($account->id, $model->readId(), 'current') or $model->addError('errors', 'Не удалось инициализировать ребро: АККАУНТ -> ЗАКАЗ');
}
// Проверка входных данных
if (!is_array($supplies)) {
// Неверные входные данные
// Запись ошибки
$model->addError('errors', 'Переменная должна быть массивом');
}
// Если запись не удалась, то вернуть код: 500 Internal Server Error
$model->writeSupply($supplies) or yii::$app->response->statusCode = 500;
}
return $return;
}
yii::$app->response->statusCode = 500;
return null;
}
/**
* Удаление
*
* @todo Разделить логику (для удобства чтения)
*/
public function actionDelete(): string|array|null
{
// Инициализация
$targets = yii::$app->request->post('targets') ?? yii::$app->request->get('targets');
$page = yii::$app->request->get('page') ?? yii::$app->request->post('page') ?? 1;
$account = yii::$app->user;
$order = Order::search();
if ($targets) {
// Удаление выбранных целей (удаление из заказа)
foreach (isset($targets[0]) && is_array($targets[0]) ? $targets : [$targets] as $target) {
// Унификация входных параметров
foreach ($target as $catn => $amount) {
// Перебор целей
// Удаление
$order->deleteSupply([$catn => (int) $amount]);
}
}
} else {
// Целью подразумевается сам заказ (удаление заказа)
// Инициализация
$order->stts = 'reserved';
if ($order->update()) {
// Запись в журнал
$order->journal('reserved');
// Поиск
$edge = AccountEdgeOrder::searchByVertex($account->id, $order->readId(), 'current');
if (count($edge) > 1) {
// Найден более чем 1 заказ
return null;
}
// Инициализация
$edge = $edge[0];
$edge->type = 'reserved';
// Запись
$edge->update();
// Реинициализация
$order = Order::search();
if (empty($order)) {
// Корзина не инициализирована
// Инициализация
$order = new Order();
$order->save() or throw new Exception('Не удалось инициализировать заказ');
// Подключение
$order->connect($account);
}
}
}
// Инициализация содержимого корзины
$supplies = $order->content(10, $page);
if (yii::$app->request->isPost) {
// POST-запрос
yii::$app->response->format = Response::FORMAT_JSON;
return [
'main' => $this->renderPartial('/cart/index', compact('order', 'supplies')),
'title' => 'Корзина',
'redirect' => '/cart',
'_csrf' => yii::$app->request->getCsrfToken()
];
}
return $this->render('/cart/index', compact('order', 'supplies'));
}
/**
* Обновление количества товара
*/
public function actionAmountUpdate(): string|array|null
{
// Инициализация
$targets = yii::$app->request->post('targets') ?? yii::$app->request->get('targets');
$page = yii::$app->request->get('page') ?? yii::$app->request->post('page') ?? 1;
$account = yii::$app->user;
$order = Order::search();
$supplies = $order->content(10, $page);
foreach (isset($targets[0]) && is_array($targets[0]) ? $targets : [$targets] as $target) {
// Унификация входных параметров
foreach ($target as $catn => $amount) {
// Перебор целей (переданных объектов в корзине)
foreach ($supplies as $supply) {
// Перебор объектов в корзине
if ($supply->catn === $catn) {
// Цель найдена
if ($supply->amnt > $amount) {
// Запрошено уменьшение количества
// Удаление
$order->deleteSupply([$catn => $supply->amnt - $amount]);
} else if ($supply->amnt < $amount) {
// Запрошено увеличение количества
// Запись
$order->writeSupply([$supply->catn => $amount - $supply->amnt]);
}
}
}
}
}
// Ренициализация
$supplies = $order->content(10, $page);
if (yii::$app->request->isPost) {
// POST-запрос
yii::$app->response->format = Response::FORMAT_JSON;
return [
'main' => $this->renderPartial('/cart/index', compact('order', 'supplies')),
'title' => 'Корзина',
'redirect' => '/cart',
'_csrf' => yii::$app->request->getCsrfToken()
];
}
return $this->render('/cart/index', compact('order', 'supplies'));
}
/**
* Оплата
*/
public function actionPay(): string|array|null
{
// Инициализация
$model = Order::search();
// Поиск ребра
$edge = AccountEdgeOrder::searchByVertex(yii::$app->user->id, $model->readId(), 'current');
if (count($edge) > 1) {
// Найден более чем 1 заказ
return null;
}
// Инициализация
$edge = $edge[0];
// Запись
$edge->type = 'accepted';
// Отправка изменений
$edge->update();
// Инициализация
$orders = Order::search(type: 'all', limit: 10, page: 1, select: '{account_edge_order, order}');
if (yii::$app->request->isPost) {
// POST-запрос
yii::$app->response->format = Response::FORMAT_JSON;
return [
'main' => $this->renderPartial('/orders/index', compact('orders')),
'title' => 'Заказы',
'redirect' => '/orders',
'_csrf' => yii::$app->request->getCsrfToken()
];
}
return $this->render('/orders/index', compact('orders'));
}
}

View File

@@ -1,52 +1,34 @@
<?php
declare(strict_types=1);
namespace app\controllers;
use Yii;
use yii;
use yii\filters\AccessControl;
use yii\web\Controller;
use yii\web\Response;
use app\models\Product;
use yii\web\HttpException;
use yii\web\UploadedFile;
use app\models\Product;
class ProductController extends Controller
{
/**
* {@inheritdoc}
*/
public function behaviors()
public function actionIndex(string $catn): array|string|null
{
return [
'access' => [
'class' => AccessControl::class,
'only' => ['add'],
'rules' => [
[
'allow' => false,
'roles' => ['?']
]
]
]
];
}
public function actionIndex(int $id)
{
if ($model = Product::readById($id)) {
if ($model = Product::searchByCatn($catn)) {
// Товар найден
// Инициализация
$model = $model->getAttributes();
if (Yii::$app->request->isAjax) {
if (yii::$app->request->isAjax) {
// AJAX-POST-запрос
Yii::$app->response->format = Response::FORMAT_JSON;
yii::$app->response->format = Response::FORMAT_JSON;
return [
'main' => $this->renderPartial('index', compact('model')),
'redirect' => '/product/' . $id,
'_csrf' => Yii::$app->request->getCsrfToken()
'redirect' => '/product/' . $catn,
'_csrf' => yii::$app->request->getCsrfToken()
];
}
@@ -55,4 +37,203 @@ class ProductController extends Controller
throw new HttpException(404);
}
}
public function actionEditTitle(string $catn): array|string|null
{
// Инициализация
$return = [
'_csrf' => yii::$app->request->getCsrfToken()
];
if (is_null($catn)) {
// Не получен артикул
yii::$app->response->statusCode = 500;
goto end;
}
if ($model = Product::searchByCatn($catn)) {
// Товар найден
// Инициализация
$text = yii::$app->request->get('text') ?? yii::$app->request->post('text') ?? 'Без названия';
$model->name = $text;
if ($model->save()) {
// Товар обновлён
$return['name'] = $text;
}
}
/**
* Конец алгоритма
*/
end:
if (yii::$app->request->isPost) {
// POST-запрос
yii::$app->response->format = Response::FORMAT_JSON;
return $return;
}
if ($model = Product::searchByCatn($catn)) {
return $this->render('index', compact('model'));
} else {
return $this->redirect('/');
}
}
public function actionEditCatn(string $catn): array|string|null
{
// Инициализация
$return = [
'_csrf' => yii::$app->request->getCsrfToken()
];
if (is_null($catn)) {
// Не получен артикул
yii::$app->response->statusCode = 500;
goto end;
}
if ($model = Product::searchByCatn($catn)) {
// Товар найден
// Инициализация
$text = yii::$app->request->get('text') ?? yii::$app->request->post('text') ?? 'Без названия';
$model->catn = $text;
if ($model->save()) {
// Товар обновлён
$return['main'] = $this->renderPartial('index', compact('model'));
}
}
/**
* Конец алгоритма
*/
end:
if (yii::$app->request->isPost) {
// POST-запрос
yii::$app->response->format = Response::FORMAT_JSON;
return $return;
}
if ($model = Product::searchByCatn($catn)) {
return $this->render('index', compact('model'));
} else {
return $this->redirect('/');
}
}
public function actionEditDesc(string $catn): array|string|null
{
// Инициализация
$return = [
'_csrf' => yii::$app->request->getCsrfToken()
];
if (is_null($catn)) {
// Не получен артикул
yii::$app->response->statusCode = 500;
goto end;
}
if ($product = Product::searchByCatn($catn)) {
// Товар найден
// Инициализация
$text = yii::$app->request->get('text') ?? yii::$app->request->post('text') ?? 'Без названия';
$product->desc = $text;
if ($product->save()) {
// Товар обновлён
$return['description'] = $text;
}
}
/**
* Конец алгоритма
*/
end:
if (yii::$app->request->isPost) {
// POST-запрос
yii::$app->response->format = Response::FORMAT_JSON;
return $return;
}
if ($model = Product::searchByCatn($catn)) {
return $this->render('index', compact('model'));
} else {
return $this->redirect('/');
}
}
public function actionWriteImage(string $catn): array|string|null
{
// Инициализация
$return = [
'_csrf' => yii::$app->request->getCsrfToken()
];
if (is_null($catn)) {
// Не получен артикул
yii::$app->response->statusCode = 500;
goto end;
}
if ($model = Product::searchByCatn($catn)) {
// Товар найден
// Инициализация
$model->file_image = UploadedFile::getInstancesByName('images');
$model->scenario = $model::SCENARIO_IMPORT_IMAGE;
if ($model->importImages() > 0) {
// Товар обновлён
$return['main'] = $this->renderPartial('index', compact('model'));
}
}
/**
* Конец алгоритма
*/
end:
if (yii::$app->request->isPost) {
// POST-запрос
yii::$app->response->format = Response::FORMAT_JSON;
return $return;
}
if ($model = Product::searchByCatn($catn)) {
return $this->render('index', compact('model'));
} else {
return $this->redirect('/');
}
}
}

View File

@@ -1,21 +1,25 @@
<?php
declare(strict_types=1);
namespace app\controllers;
use Yii;
use yii;
use yii\filters\AccessControl;
use yii\web\Controller;
use yii\web\Response;
use yii\web\Cookie;
use yii\web\UploadedFile;
use app\models\Supply;
use app\models\SupplyGroup;
use app\models\Search;
use app\models\Notification;
use app\models\Settings;
use app\models\SettingsEdgeSettings;
class ProfileController extends Controller
{
/**
* {@inheritdoc}
*/
public function behaviors()
{
return [
@@ -24,13 +28,27 @@ class ProfileController extends Controller
'rules' => [
[
'allow' => true,
'roles' => ['@']
'roles' => ['@'],
'actions' => ['index', 'supplies', 'import', 'monitoring', 'readGroups']
],
[
'allow' => false,
'roles' => ['?'],
'verbs' => ['POST'],
'denyCallback' => [$this, 'accessDenied']
],
[
'allow' => true,
'actions' => ['panel', 'panel-notification-write'],
'matchCallback' => function ($rule, $action): bool {
if (
yii::$app->user->identity->type === 'administrator'
|| yii::$app->user->identity->type === 'moderator'
) {
return true;
}
return false;
}
]
]
]
@@ -39,67 +57,340 @@ class ProfileController extends Controller
public function accessDenied()
{
$cookies = Yii::$app->response->cookies;
// Инициализация
$cookies = yii::$app->response->cookies;
// Запись cookie с редиректом, который выполнится после авторизации
$cookies->add(new Cookie([
'name' => 'redirect',
'value' => Yii::$app->request->pathInfo
'value' => yii::$app->request->pathInfo
]));
Yii::$app->response->format = Response::FORMAT_JSON;
// Проверить переадресацию на уровне сервера
Yii::$app->response->content = json_encode([
'main' => $this->renderPartial('/account'),
'redirect' => '/authentication',
'_csrf' => Yii::$app->request->getCsrfToken()
]);
}
public function actionIndex()
{
// Инициализация
$model = new Supply(Yii::$app->request->post('Supply') ?? Yii::$app->request->get('Supply'));
if (Yii::$app->request->isAjax) {
// AJAX-POST-запрос
if (Yii::$app->request->isPost) {
// POST-запрос
// Настройка
Yii::$app->response->format = Response::FORMAT_JSON;
return [
'main' => $this->renderPartial('index'),
'redirect' => '/profile',
// Генерация ответа
Yii::$app->response->content = json_encode([
'main' => $this->renderPartial('/account/index'),
'redirect' => '/authentication',
'_csrf' => Yii::$app->request->getCsrfToken()
]);
} else if (Yii::$app->request->isGet) {
// GET-запрос
$this->redirect('/authentication');
}
}
/**
* Страница с настройками аккаунта
*/
public function actionIndex(): string|array
{
// Инициализация
$model = yii::$app->user->identity;
$panel = yii::$app->request->post('panel') ?? yii::$app->request->get('panel');
$sidebar = $this->renderPartial('sidebar');
// Обработка настроек аккаунта
if ($vars = yii::$app->request->post('Account') ?? yii::$app->request->get('Account')) {
// Обнаружены входные параметры
if (isset($vars['opts'])) {
// Переданы параметры
// Инициализация
is_array($model->opts) || $model->opts = [];
// Запись
$model->opts = array_merge($model->opts, $vars['opts']);
$model->update();
} else {
/**
* @todo Написать обработчик ошибок
*/
}
}
// Инициализация
$list = $model->genListOem(Supply::searchByAccount(select: 'supply.onec["ЗначенияСвойств"]', limit: null));
if (yii::$app->request->isPost) {
// POST-запрос
yii::$app->response->format = Response::FORMAT_JSON;
return [
'main' => $this->renderPartial('index', compact(
'model',
'sidebar',
'list',
'panel'
)),
'redirect' => '/profile',
'_csrf' => yii::$app->request->getCsrfToken()
];
}
return $this->render('index', compact('model'));
return $this->render('index', compact(
'model',
'sidebar',
'list',
'panel'
));
}
/**
* Страница панели управления для доверенных пользователей
*/
public function actionPanel(): string|array
{
// Инициализация
$model_notifications = null;
$model_settings = Settings::readLast();
$panel = yii::$app->request->post('panel') ?? yii::$app->request->get('panel');
$sidebar = $this->renderPartial('sidebar');
if (!is_null($vars = yii::$app->request->post('Notification') ?? yii::$app->request->get('Notification'))) {
// Обнаружены входные параметры из раздела "Уведомления"
// Реинициализация с новыми параметрами
$model_notifications = new Notification($vars);
// Запись уведомления и отправка (запись ребра до аккаунта)
$model_notifications->write();
} else if (!is_null($vars = yii::$app->request->post('Settings') ?? yii::$app->request->get('Settings'))) {
// Обнаружены входные параметры из раздела "Настройки"
if ($to = new Settings($vars)) {
// Настройки инициализированы
// Отправка
if ($to->save()) {
// Сохранено в базе данных
// Буфер
$from = $model_settings;
// Реинициализация (для представления)
$model_settings = $to;
if ($from) {
// Найдена старая версия настроек
// Запись ребра: НАСТРОЙКИ (старые) -> НАСТРОЙКИ (новые)
SettingsEdgeSettings::write($from->readId(), $to->readId(), 'update');
}
} else {
// Не сохранено в базе данных
// Запись ошибок
$model_settings->addErrors($to->getErrors());
}
}
}
// Деинициализация
unset($vars);
if (yii::$app->request->isPost) {
// AJAX-POST-запрос
yii::$app->response->format = Response::FORMAT_JSON;
return [
'main' => $this->renderPartial('panel', compact(
'model_notifications',
'model_settings',
'sidebar',
'panel'
)),
'redirect' => '/profile/panel',
'_csrf' => yii::$app->request->getCsrfToken()
];
}
return $this->render('panel', compact(
'model_notifications',
'model_settings',
'sidebar',
'panel'
));
}
/**
* Страница мониторинга
*/
public function actionMonitoring(): string|array
{
// Инициализация
$panel = yii::$app->request->post('panel') ?? yii::$app->request->get('panel');
$sidebar = $this->renderPartial('sidebar');
// Инициализация номера страницы
$page_search_history = (yii::$app->request->post('search') ?? yii::$app->request->get('search')) - 1;
if ($page_search_history <= 0) {
// Вышли за границу поиска перед первой страницей
// Инициализация
$page_search_history = 0;
}
// Инициализация количества строк на одной странице
$rows_amount = 10;
$search_history = Search::SearchByEdge(
from: 'account',
to: 'search',
subquery_where: [
[
'account._id' => yii::$app->user->id
]
],
foreach: ['edge' => 'account_edge_search'],
where: 'edge._to == search._id',
limit: $rows_amount,
offset: $offset = ((int) $page_search_history ?? 0) * $rows_amount
);
// Проверка результатов
monitoring_result_check:
if (count($search_history) === 0 && $offset !== 0) {
// Вышли за границу поиска после последней страницы
// Реинициализация (вычитание для идентичного конечного результата)
--$page_search_history;
$search_history = Search::SearchByEdge(
from: 'account',
to: 'search',
subquery_where: [
[
'account._id' => yii::$app->user->id
]
],
foreach: ['edge' => 'account_edge_search'],
where: 'edge._to == search._id',
limit: $rows_amount,
offset: $offset = ((int) $page_search_history ?? 0) * $rows_amount
);
// Повторная проверка
goto monitoring_result_check;
}
if (yii::$app->request->isPost) {
// AJAX-POST-запрос
yii::$app->response->format = Response::FORMAT_JSON;
return [
'main' => $this->renderPartial('monitoring', compact(
'sidebar',
'search_history',
'page_search_history',
'panel'
)),
'search' => $page_search_history + 1,
'redirect' => '/profile/monitoring',
'_csrf' => yii::$app->request->getCsrfToken()
];
}
return $this->render('monitoring', compact(
'sidebar',
'search_history',
'page_search_history',
'panel'
));
}
/**
* Страница поставок
*/
public function actionSupplies(): string|array
{
// Инициализация
$model = new Supply(yii::$app->request->post('Supply') ?? yii::$app->request->get('Supply'));
$panel = yii::$app->request->post('panel') ?? yii::$app->request->get('panel');
$sidebar = $this->renderPartial('sidebar');
$groups = self::readGroups();
if (yii::$app->request->isPost) {
// POST-запрос
yii::$app->response->format = Response::FORMAT_JSON;
return [
'main' => $this->renderPartial('supplies', compact(
'model',
'groups',
'sidebar',
'panel'
)),
'redirect' => '/profile/supplies',
'_csrf' => yii::$app->request->getCsrfToken()
];
}
return $this->render('supplies', compact(
'model',
'groups',
'sidebar',
'panel'
));
}
/**
* Импорт поставок
*
* На данный момент только из Excel-таблицы
*/
public function actionImport()
{
var_dump($_FILES);
// Инициализация
$model = new Supply(Yii::$app->request->post('Supply') ?? Yii::$app->request->get('Supply'));
$model->scenario = $model::SCENARIO_IMPORT;
$model = new Supply(yii::$app->request->post('Supply') ?? yii::$app->request->get('Supply'));
$model->scenario = $model::SCENARIO_IMPORT_EXCEL;
$panel = yii::$app->request->post('panel') ?? yii::$app->request->get('panel');
$sidebar = $this->renderPartial('sidebar');
$groups = self::readGroups();
if (Yii::$app->request->isAjax) {
if (yii::$app->request->isPost) {
// AJAX-POST-запрос
Yii::$app->response->format = Response::FORMAT_JSON;
yii::$app->response->format = Response::FORMAT_JSON;
$model->file = UploadedFile::getInstances($model, 'file');
$model->file_excel = UploadedFile::getInstances($model, 'file_excel');
if (!$test = $model->import()) {
Yii::$app->response->statusCode = 409;
if ($model->importExcel()) {
return [
'main' => $this->renderPartial('supplies', compact(
'model',
'groups',
'sidebar',
'panel'
)),
'_csrf' => yii::$app->request->getCsrfToken()
];
}
return [
'main' => $this->renderPartial('index', compact('model')),
'_csrf' => Yii::$app->request->getCsrfToken()
];
yii::$app->response->statusCode = 409;
}
return $this->render('index', compact('model'));
return $this->render('supplies', compact(
'model',
'groups',
'sidebar',
'panel'
));
}
public static function readGroups()

View File

@@ -1,59 +1,66 @@
<?php
declare(strict_types=1);
namespace app\controllers;
use Yii;
use yii;
use yii\filters\AccessControl;
use yii\web\Controller;
use yii\web\Response;
use app\models\AccountForm;
class RegistrationController extends Controller
{
/**
* {@inheritdoc}
*/
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::class,
'rules' => [
[
'allow' => true,
'roles' => ['?'],
]
],
]
];
}
// public function behaviors()
// {
// return [
// 'access' => [
// 'class' => AccessControl::class,
// 'rules' => [
// [
// 'allow' => true,
// 'roles' => ['?'],
// ]
// ],
// ]
// ];
// }
public function actionIndex()
{
if (Yii::$app->request->isAjax) {
// AJAX-POST-запрос
// Инициализация
$model = new AccountForm(yii::$app->request->post('AccountForm') ?? yii::$app->request->get('AccountForm'));
$model->scenario = $model::SCENARIO_REGISTRATION;
// Инициализация
$model = new AccountForm(Yii::$app->request->post('AccountForm'));
$model->scenario = $model::SCENARIO_REGISTRATION;
if (yii::$app->request->isPost) {
// POST-запрос
Yii::$app->response->format = Response::FORMAT_JSON;
yii::$app->response->format = Response::FORMAT_JSON;
if (!Yii::$app->user->isGuest || $model->registration()) {
if (!yii::$app->user->isGuest || $model->registration()) {
// Данные прошли проверку и аккаунт был создан
// Аутентификация
$model->scenario = $model::SCENARIO_AUTHENTICATION;
$model->authentication();
// Инициализация
$notifications_button = $this->renderPartial('/notification/button');
$notifications_panel_full = true;
$notifications_panel = $this->renderPartial('/notification/panel', compact('notifications_panel_full'));
// Запись ответа
$return = [
'nav' => (new AccountForm())->deauthenticationGenHtml(),
'_csrf' => Yii::$app->request->getCsrfToken()
'menu' => $this->renderPartial('/account/panel/authenticated', compact(
'notifications_button',
'notifications_panel'
)),
'_csrf' => yii::$app->request->getCsrfToken()
];
if (($cookies = Yii::$app->response->cookies)->has('redirect')) {
if (($cookies = yii::$app->response->cookies)->has('redirect')) {
// Найдено cookie с переадресацией
// Запись ответа
@@ -61,11 +68,11 @@ class RegistrationController extends Controller
$return['main'] = $this->renderPartial($return['redirect'] . '/index');
// Очистка cookie
unset(Yii::$app->response->cookies['redirect']);
unset(yii::$app->response->cookies['redirect']);
} else {
// Не найдено cookie с переадресацией
if (Yii::$app->request->pathInfo === 'authentication' || Yii::$app->request->pathInfo === 'registration') {
if (yii::$app->request->pathInfo === 'authentication' || yii::$app->request->pathInfo === 'registration') {
// Если клиент на промежуточном URI
// Запись ответа
@@ -78,20 +85,20 @@ class RegistrationController extends Controller
} else {
// Данные не прошли проверку
Yii::$app->response->statusCode = 400;
yii::$app->response->statusCode = 400;
return [
'main' => $this->renderPartial('/account', compact('model')),
'main' => $this->renderPartial('/account/index', compact('model')),
'redirect' => '/registration',
'_csrf' => Yii::$app->request->getCsrfToken()
'_csrf' => yii::$app->request->getCsrfToken()
];
}
}
if (!Yii::$app->user->isGuest) {
Yii::$app->response->redirect('/');
if (!yii::$app->user->isGuest) {
yii::$app->response->redirect('/');
} else {
return $this->render('/account', compact('model'));
return $this->render('/account/index', compact('model'));
}
}
}

View File

@@ -0,0 +1,231 @@
<?php
declare(strict_types=1);
namespace app\controllers;
use yii;
use yii\web\Controller;
use yii\web\Response;
use app\models\Product;
use app\models\Supply;
use app\models\Search;
class SearchController extends Controller
{
/**
* @todo Сессию привязать к аккаунту и проверку по нему делать, иначе её можно просто сбрасывать
*/
public function actionIndex(): array|string
{
// Инициализация параметров
$auth_only = false;
if ($auth_only && yii::$app->user->isGuest) {
// Если активирован режим "Поиск только аутентифицированным" и запрос пришел не от аутентифицированного
// Запись кода ответа: 401 (необходима авторизация)
yii::$app->response->statusCode = 401;
// Переход к концу обработки
goto skip_search;
}
// Инициализация
$query = yii::$app->request->post('request') ?? yii::$app->request->get('q');
if (yii::$app->request->post('type') === 'product' || yii::$app->request->get('type') === 'product') {
// Поиск по продуктам
if (yii::$app->request->post('history')) {
// Запрошена история
yii::$app->response->format = Response::FORMAT_JSON;
return [
'panel' => $this->renderPartial('/search/panel', ['history' => true]),
'_csrf' => yii::$app->request->getCsrfToken()
];
}
// Инициализация сессии
$session = yii::$app->session;
// Инициализация ответа
$response = null;
// Инициализация параметров
$timer = 0;
// Период пропуска запросов (в секундах)
$period = 5;
$keep_connect = false;
$sanction = false;
$sanction_condition = ($session['last_request'] + $period - time()) < $period;
$sanction_time = 2;
$query_min = 2;
$query_max = 20;
if (isset($session['last_request'])) {
// Данные о времени последнего запроса не найдены
if ($sanction && $sanction_condition) {
// Наказание за повторный запрос при условии задержки
$session['last_request'] += $sanction_time;
}
} else {
// Это первый запрос
// Инициализация
$session['last_request'] = time();
// Пропуск проверок
goto first_request;
}
// Метка: "Повтор обработки поиска" (для создания непрерывного соединения)
keep_connect_wait:
// Запись времени последнего запроса и вычисление об истечении таймера
$timer = $session['last_request'] + $period - time();
if ($timer > 0) {
// Время блокировки не истекло
// Запись кода ответа: 202 (запрос не обработан)
yii::$app->response->statusCode = 202;
$return = [
'timer' => $timer,
'panel' => $this->renderPartial('/search/loading'),
'_csrf' => yii::$app->request->getCsrfToken()
];
} else {
// Запрос
// Очистка времени последнего запроса
unset($session['last_request']);
// Метка: "Первый запрос"
first_request:
if (strlen($query) < $query_min) {
// Выход за ограничения длины с недостатком
// Пропуск поиска
goto skip_query;
} else if (strlen($query) > $query_max) {
// Выход за ограничения длины с превышением
// Пропуск поиска
goto skip_query;
}
// Запись в историю
Search::write($query);
$limit = yii::$app->request->isPost ? 10 : 20;
if ($response = Product::searchByPartialCatn($query, $limit, ['catn' => 'catn', '_key' => '_key'])) {
// Данные найдены по поиску в полях Каталожного номера
foreach ($response as &$row) {
// Перебор продуктов
// Поиск поставок привязанных к продуктам
$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')) {
// Полноценный поиск
// Запись ответа
$return['main'] = $this->renderPartial('/search/index', compact('response'));
$return['hide'] = 1;
$return['redirect'] = '/search?type=product&q=' . $query;
}
} else {
// Данные не найдены
// Запись кода ответа: 404 (запрашиваемые данные не найдены)
yii::$app->response->statusCode = 404;
}
}
// Метка: "Пропуск запроса" (для пропуска самого поиска и возврата ответа)
skip_query:
if (yii::$app->request->isPost) {
// POST-запрос
yii::$app->response->format = Response::FORMAT_JSON;
return $return ?? [
'panel' => $this->renderPartial('/search/panel'),
'_csrf' => yii::$app->request->getCsrfToken()
];
} else {
// GET-запрос
if (empty($return['main']) && $keep_connect && $timer > 0) {
// Режим непрерывного соединения
// Ожидание
sleep($timer);
// Повтор обработки
goto keep_connect_wait;
}
return $this->render('/search/index', compact('response', 'timer'));
}
}
// Метка: "Пропуск обработки"
skip_search:
if (yii::$app->request->isPost) {
// POST-запрос
yii::$app->response->format = Response::FORMAT_JSON;
return [
'panel' => $this->renderPartial('/search/panel'),
'hide' => (int) $auth_only,
'_csrf' => yii::$app->request->getCsrfToken()
];
} else {
// GET-запрос
return $this->render('/search/index');
}
}
}

View File

@@ -1,6 +1,6 @@
<?php
use explosivebit\arangodb\Migration;
use mirzaev\yii2\arangodb\Migration;
class m201219_074926_create_account_collection extends Migration
{

View File

@@ -1,6 +1,6 @@
<?php
use explosivebit\arangodb\Migration;
use mirzaev\yii2\arangodb\Migration;
class m210101_092505_create_product_collection extends Migration
{

View File

@@ -1,6 +1,6 @@
<?php
use explosivebit\arangodb\Migration;
use mirzaev\yii2\arangodb\Migration;
class m210107_163448_create_supply_collection extends Migration
{

View File

@@ -1,6 +1,6 @@
<?php
use explosivebit\arangodb\Migration;
use mirzaev\yii2\arangodb\Migration;
class m210108_014505_create_account_edge_supply_collection extends Migration
{

View File

@@ -1,6 +1,6 @@
<?php
use explosivebit\arangodb\Migration;
use mirzaev\yii2\arangodb\Migration;
class m210108_212826_create_product_group_collection extends Migration
{

View File

@@ -1,6 +1,6 @@
<?php
use explosivebit\arangodb\Migration;
use mirzaev\yii2\arangodb\Migration;
class m210108_221446_create_product_edge_product_group_collection extends Migration
{

View File

@@ -1,16 +1,16 @@
<?php
use explosivebit\arangodb\Migration;
use mirzaev\yii2\arangodb\Migration;
class m210108_222132_create_supply_edge_product_collection extends Migration
{
public function up()
{
$this->createCollection('supply_edge_product', ['Type' => 3]);
}
public function up()
{
$this->createCollection('supply_edge_product', ['Type' => 3]);
}
public function down()
{
$this->dropCollection('supply_edge_product');
}
public function down()
{
$this->dropCollection('supply_edge_product');
}
}

View File

@@ -1,6 +1,6 @@
<?php
use explosivebit\arangodb\Migration;
use mirzaev\yii2\arangodb\Migration;
class m210108_222740_create_product_edge_product_collection extends Migration
{

View File

@@ -1,6 +1,6 @@
<?php
use explosivebit\arangodb\Migration;
use mirzaev\yii2\arangodb\Migration;
class m210109_214817_create_supply_group_collection extends Migration
{

View File

@@ -1,6 +1,6 @@
<?php
use explosivebit\arangodb\Migration;
use mirzaev\yii2\arangodb\Migration;
class m210109_214833_create_supply_edge_supply_group_collection extends Migration
{

View File

@@ -1,6 +1,6 @@
<?php
use explosivebit\arangodb\Migration;
use mirzaev\yii2\arangodb\Migration;
class m210111_044635_create_supply_edge_supply_collection extends Migration
{

View File

@@ -1,6 +1,6 @@
<?php
use explosivebit\arangodb\Migration;
use mirzaev\yii2\arangodb\Migration;
class m210112_010347_create_product_group_edge_product_group_collection extends Migration
{

View File

@@ -1,6 +1,6 @@
<?php
use explosivebit\arangodb\Migration;
use mirzaev\yii2\arangodb\Migration;
class m210112_010411_create_supply_group_edge_supply_group_collection extends Migration
{

View File

@@ -1,6 +1,6 @@
<?php
use explosivebit\arangodb\Migration;
use mirzaev\yii2\arangodb\Migration;
class m210112_034135_create_requisite_collection extends Migration
{

View File

@@ -1,12 +1,12 @@
<?php
use explosivebit\arangodb\Migration;
use mirzaev\yii2\arangodb\Migration;
class m210112_034232_create_supply_edge_requisite_collection extends Migration
{
public function up()
{
$this->createCollection('supply_edge_requisite', ['Type' => 3]);
$this->createCollection('supply_edge_requisite', ['Type' => 3]);
}
public function down()

View File

@@ -1,6 +1,6 @@
<?php
use explosivebit\arangodb\Migration;
use mirzaev\yii2\arangodb\Migration;
class m210113_021800_create_purchase_collection extends Migration
{

View File

@@ -1,6 +1,6 @@
<?php
use explosivebit\arangodb\Migration;
use mirzaev\yii2\arangodb\Migration;
class m210113_021905_create_account_edge_purchase_collection extends Migration
{

View File

@@ -1,6 +1,6 @@
<?php
use explosivebit\arangodb\Migration;
use mirzaev\yii2\arangodb\Migration;
class m210113_021917_create_purchase_edge_supply_collection extends Migration
{

View File

@@ -0,0 +1,16 @@
<?php
use mirzaev\yii2\arangodb\Migration;
class m210206_154140_create_search_collection extends Migration
{
public function up()
{
$this->createCollection('search', []);
}
public function down()
{
$this->dropCollection('search');
}
}

View File

@@ -0,0 +1,16 @@
<?php
use mirzaev\yii2\arangodb\Migration;
class m210206_154210_create_account_edge_search_collection extends Migration
{
public function up()
{
$this->createCollection('account_edge_search', ['Type' => 3]);
}
public function down()
{
$this->dropCollection('account_edge_search');
}
}

View File

@@ -0,0 +1,16 @@
<?php
use mirzaev\yii2\arangodb\Migration;
class m210209_185314_create_notification_collection extends Migration
{
public function up()
{
$this->createCollection('notification', []);
}
public function down()
{
$this->dropCollection('notification');
}
}

View File

@@ -0,0 +1,16 @@
<?php
use mirzaev\yii2\arangodb\Migration;
class m210209_185328_create_account_edge_notification_collection extends Migration
{
public function up()
{
$this->createCollection('account_edge_notification', ['type' => 3]);
}
public function down()
{
$this->dropCollection('account_edge_notification');
}
}

View File

@@ -0,0 +1,16 @@
<?php
use mirzaev\yii2\arangodb\Migration;
class m210223_121142_create_settings_collection extends Migration
{
public function up()
{
$this->createCollection('settings', []);
}
public function down()
{
$this->dropCollection('settings');
}
}

View File

@@ -0,0 +1,16 @@
<?php
use mirzaev\yii2\arangodb\Migration;
class m210223_145042_create_settings_edge_settings_collection extends Migration
{
public function up()
{
$this->createCollection('settings_edge_settings', ['type' => 3]);
}
public function down()
{
$this->dropCollection('settings_edge_settings');
}
}

View File

@@ -0,0 +1,16 @@
<?php
use mirzaev\yii2\arangodb\Migration;
class m210303_192326_create_order_collection extends Migration
{
public function up()
{
$this->createCollection('order', []);
}
public function down()
{
$this->dropCollection('order');
}
}

View File

@@ -0,0 +1,16 @@
<?php
use mirzaev\yii2\arangodb\Migration;
class m210303_192451_create_order_edge_supply_collection extends Migration
{
public function up()
{
$this->createCollection('order_edge_supply', ['type' => 3]);
}
public function down()
{
$this->dropCollection('order_edge_supply');
}
}

View File

@@ -0,0 +1,16 @@
<?php
use mirzaev\yii2\arangodb\Migration;
class m210307_000210_create_account_edge_order_collection extends Migration
{
public function up()
{
$this->createCollection('account_edge_order', ['type' => 3]);
}
public function down()
{
$this->dropCollection('account_edge_order');
}
}

View File

@@ -0,0 +1,16 @@
<?php
use mirzaev\yii2\arangodb\Migration;
class m210314_133722_create_session_collection extends Migration
{
public function up()
{
$this->createCollection('session', []);
}
public function down()
{
$this->dropCollection('session');
}
}

View File

@@ -0,0 +1,16 @@
<?php
use mirzaev\yii2\arangodb\Migration;
class m210314_133926_create_account_edge_session_collection extends Migration
{
public function up()
{
$this->createCollection('account_edge_session', ['type' => 3]);
}
public function down()
{
$this->dropCollection('account_edge_session');
}
}

View File

@@ -1,27 +1,81 @@
<?php
declare(strict_types=1);
namespace app\models;
use Yii;
use yii\web\IdentityInterface;
use carono\exchange1c\interfaces\PartnerInterface;
/**
* Аккаунт
*
* Реализует аккаунты пользователей и поставщиков
*/
class Account extends Document implements IdentityInterface, PartnerInterface
{
public static function collectionName()
/**
* Имя коллекции
*/
public static function collectionName(): string
{
return 'account';
}
public function attributes()
/**
* Свойства
*/
public function attributes(): array
{
return array_merge(
parent::attributes(),
['mail', 'pswd', 'name', 'simc', 'sity', 'comp', 'taxn', 'auth']
[
'auth',
'mail',
'pswd',
'name',
'simc',
'sity',
'comp',
'taxn',
'onec',
'opts',
'agnt',
'type'
]
);
}
public function rules()
/**
* Метки свойств
*/
public function attributeLabels(): array
{
return array_merge(
parent::attributeLabels(),
[
'auth' => 'Аутентификационный хеш',
'mail' => 'Почта',
'pswd' => 'Пароль',
'name' => 'Имя',
'simc' => 'Номер',
'sity' => 'Город',
'comp' => 'Компания',
'taxn' => 'ИНН',
'onec' => 'Данные 1C',
'opts' => 'Параметры',
'agnt' => 'Агент (поставщик)',
'type' => 'Тип аккаунта'
]
);
}
/**
* Правила
*/
public function rules(): array
{
return array_merge(
parent::rules(),
@@ -33,24 +87,13 @@ class Account extends Document implements IdentityInterface, PartnerInterface
);
}
public function attributeLabels()
{
return array_merge(
parent::attributeLabels(),
[
'mail' => 'Почта',
'pswd' => 'Пароль',
'name' => 'Имя',
'simc' => 'Номер',
'sity' => 'Город',
'comp' => 'Компания',
'taxn' => 'ИНН',
'auth' => 'Аутентификационный хеш'
]
);
}
public function beforeSave($data)
/**
* Перед сохранением
*
* @todo Подождать обновление от ебаного Yii2 и добавить
* проверку типов передаваемых параметров
*/
public function beforeSave($data): bool
{
if (parent::beforeSave($data)) {
if ($this->isNewRecord) {
@@ -62,65 +105,201 @@ class Account extends Document implements IdentityInterface, PartnerInterface
return false;
}
public function getExportFields1c($context = null)
/**
* Чтение полей для экспорта из 1С
*/
public function getExportFields1c($context = null): array
{
return [
'Ид' => 'id',
'Наименование' => 'username',
'ПолноеНаименование' => 'full_name',
'Фамилия' => 'surname',
'Имя' => 'name',
];
return [];
}
public function getId()
/**
* Чтение идентификатора
*
* @see IdentityInterface
*/
public function getId(): string
{
return $this->_key;
return self::collectionName() . '/' . $this->_key;
}
public function getAuthKey()
/**
* Чтение идентификатора
*/
public function readId(): string
{
return $this->getId();
}
/**
* Чтение аутентификационного ключа
*/
public function getAuthKey(): string
{
return $this->auth;
}
public static function findIdentity($_key)
/**
* Идентификация
*
* @todo Подождать обновление от ебаного Yii2 и добавить
* проверку типов передаваемых параметров
*/
public static function findIdentity($_id): self
{
return static::findByKey($_key);
return static::findById($_id);
}
public static function findIdentityByAccessToken($pass, $type = null)
/**
* Поиск по ключу
*
* @todo Подождать обновление от ебаного Yii2 и добавить
* проверку типов передаваемых параметров
*/
public static function findIdentityByAccessToken($pass, $type = null): self
{
return static::findOne(['pass' => $pass]);
}
public static function findByMail($mail)
/**
* Поиск по почте
*
* @todo Подождать обновление Yii2 и добавить
* проверку типов передаваемых параметров
*/
public static function findByMail($mail): ?self
{
return static::findOne(['mail' => $mail]);
}
public static function findByKey($_key)
/**
* Поиск по идентификатору
*
* @todo Подождать обновление Yii2 и добавить
* проверку типов передаваемых параметров
*/
public static function findById($_id): self
{
return static::findOne(['_key' => $_key]);
return static::searchById($_id);
}
public static function validateMail($mail)
/**
* Проверка почты
*/
public static function validateMail(string $mail): bool
{
if (static::findByMail($mail)) {
// Почта найдена в базе данных
return false;
return true;
}
return true;
return false;
}
public function validatePassword($pswd)
/**
* Проверка пароля
*/
public function validatePassword(string $pswd): bool
{
return Yii::$app->security->validatePassword($pswd, $this->pswd);
}
public function validateAuthKey($auth)
/**
* Проверка аутентификационного ключа
*
* @todo Подождать обновление Yii2 и добавить
* проверку типов передаваемых параметров
*/
public function validateAuthKey($auth): bool
{
return $this->getAuthKey() === $auth;
}
/**
* Записать параметр
*
* @param string $name Название параметра
* @param mixed $value Значение параметра
*/
public function writeOption(string $name, mixed $value = null): bool
{
// Запись
$this->opts[$name] = $value;
// Отправка
return $this->save();
}
/**
* Удалить параметр
*
* @param string $name Название параметра
*/
public function deleteOption(string $name): bool
{
// Удаление
unset($this->opts[$name]);
// Отправка
return $this->save();
}
/**
* Генерация списка OEM-номеров
*
* Актуальное (выбранное, активное) значение записывается первым
*
* @param array $supplies Необработанный список поставок
*/
public function genListOem(array $supplies): array
{
// Инициализация
$list = [];
// Перебор свойств поставок
foreach ($supplies as $supply) {
// Инициализация
$id = $supply['ЗначенияСвойства']['Ид'];
if (in_array($id, $list, true)) {
// Если встретился дубликат (исполняется очень часто)
continue;
}
// Генерация
!isset($supply['ЗначенияСвойства']['Наименование']) or $list[$id] = $supply['ЗначенияСвойства']['Наименование'];
}
// Инициализация текущего значения параметра в начале массива
if (isset($this->opts['import_sections_oem'])) {
// Параметр 'import_sections_oem' найден в настройках аккаунта
if (isset($list[$this->opts['import_sections_oem']])) {
// Найдено совпадение сохранённого параметра с полученным списком из поставок
// Буфер для сохранения параметра
$buffer = $list[$this->opts['import_sections_oem']];
// Удаление параметра
unset($list[$this->opts['import_sections_oem']]);
// Сохранение параметра в начале массива
$list = array_merge([$this->opts['import_sections_oem'] => $buffer], $list);
} else {
// Совпадение не найдено
// Сохранение параметра из данных аккаунта в начале массива
$list = array_merge([$this->opts['import_sections_oem'] => $this->opts['import_sections_oem']], $list);
}
} else {
// Параметр 'import_sections_oem' не найден в настройках аккаунта
// Сохранение параметра из данных аккаунта в начале массива
$list = array_merge(['Выберите'], $list);
}
return $list;
}
}

View File

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

View File

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

View File

@@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
namespace app\models;
class AccountEdgePurchase extends Edge
{
public static function collectionName()
public static function collectionName(): string
{
return 'account_edge_purchase';
}

View File

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

View File

@@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
namespace app\models;
class AccountEdgeSupply extends Edge
{
public static function collectionName()
public static function collectionName(): string
{
return 'account_edge_supply';
}

View File

@@ -1,9 +1,12 @@
<?php
declare(strict_types=1);
namespace app\models;
use Yii;
use yii;
use yii\base\Model;
use app\models\Account;
/**
@@ -20,7 +23,7 @@ class AccountForm extends Model
public $mail;
public $pswd;
public $auto = true;
public $auto = false;
private $account = false;
@@ -56,7 +59,7 @@ class AccountForm extends Model
$account = $this->getAccount();
if (!$account || !$account->validateMail($this->mail)) {
if (!$account || $account->validateMail($this->mail)) {
// Проверка не пройдена
$this->addError($attribute, 'Почта уже привязана');
@@ -94,7 +97,7 @@ class AccountForm extends Model
// Проверка пройдена
// Аутентификация
return Yii::$app->user->login($this->getAccount(), $this->auto ? 3600 * 24 * 30 : 0);
return yii::$app->user->login($this->getAccount(), $this->auto ? 3600 * 24 * 30 : 0);
}
return false;
@@ -113,7 +116,7 @@ class AccountForm extends Model
// Запись параметров
$this->account->mail = $this->mail;
$this->account->pswd = Yii::$app->security->generatePasswordHash($this->pswd);
$this->account->pswd = yii::$app->security->generatePasswordHash($this->pswd);
// Регистрация
return $this->account->save();
@@ -135,38 +138,4 @@ class AccountForm extends Model
return $this->account;
}
public function authenticationGenHtml(string $dropdown): string
{
return <<<HTML
<a class="text-dark my-auto mr-2" href="/cart"><i class="fas fa-shopping-cart mx-2"></i></a>
<a class="text-dark my-auto mr-2" href="/orders"><i class="fas fa-list mx-2"></i></a>
<div class="btn-group">
<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 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>
$dropdown
<!-- <a class="dropdown-item-text text-center px-0 py-2" href="#"><small>Восстановление пароля</small></a> -->
</div>
</div>
HTML;
}
public function deauthenticationGenHtml(): string
{
$mail = Yii::$app->user->identity->mail;
return <<<HTML
<a class="text-dark my-auto mr-2" href="/cart"><i class="fas fa-shopping-cart mx-2"></i></a>
<a class="text-dark my-auto mr-2" href="/orders"><i class="fas fa-list mx-2"></i></a>
<div class="btn-group">
<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 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()">Выход ($mail)</a>
</div>
</div>
HTML;
}
}

View File

@@ -1,39 +1,80 @@
<?php
declare(strict_types=1);
namespace app\models;
use explosivebit\arangodb\ActiveRecord;
use yii;
use mirzaev\yii2\arangodb\ActiveRecord;
use Exception;
/**
* Документ
*/
abstract class Document extends ActiveRecord
{
public function attributes()
/**
* Имя коллекции
*/
public static function collectionName(): string
{
return ['_key', 'date'];
return throw new Exception('Не инициализировано название коллекции');
}
public function rules()
/**
* Свойства
*/
public function attributes(): array
{
return [
// [
// 'date',
// 'required',
// 'message' => 'Заполните поле: {attribute}'
// ]
'_key',
'jrnl'
];
}
public function attributeLabels()
/**
* Метки свойств
*/
public function attributeLabels(): array
{
return [
'date' => 'Дата'
'_key' => 'Ключ',
'jrnl' => 'Журнал'
];
}
public function beforeSave($data)
/**
* Правила
*
* @todo Добавить проверку существования аккаунта
*/
public function rules(): array
{
return [];
}
/**
* Перед сохранением
*
* @todo Подождать обновление от ебаного Yii2 и добавить
* проверку типов передаваемых параметров
*/
public function beforeSave($data): bool
{
if (parent::beforeSave($data)) {
if ($this->isNewRecord) {
$this->date = time();
// Запись в журнал
$this->jrnl = array_merge(
[[
'date' => time(),
'account' => yii::$app->user->id,
'action' => 'create'
]],
$this->jrnl ?? []
);
}
return true;
@@ -42,8 +83,86 @@ abstract class Document extends ActiveRecord
return false;
}
public static function readAmount()
/**
* Журнал
*
* Записывает данные в журнал
*
* @param string $action
*
* @return int|bool Время записанное в журнале или false, если не удалось записать
*/
public function journal(string $action = 'update', array ...$data): int|bool
{
// Инициализация
if (isset($this->jrnl) && is_array($this->jrnl)) {
} else {
$this->jrnl = [];
}
// Генерация
$this->jrnl = array_merge(
$this->jrnl,
[array_merge(
[
'date' => $time = time(),
'account' => yii::$app->user->id,
'action' => $action
],
...$data
)]
);
// Запись и возврат
return $this->update() ? $time : false;
}
/**
* Чтение идентификатора
*/
public function readId(): ?string
{
return isset($this->_key) && static::collectionName() ? static::collectionName() . '/' . $this->_key : null;
}
/**
* Поиск по идентификатору
*/
public static function searchById(string $_id): ?static
{
return static::findOne(['_id' => $_id]);
}
public static function readLast(): ?static
{
return static::find()->orderBy(['DESC'])->one();
}
/**
* Чтение всех записей
*/
public static function readAll(): array
{
return static::find()->all();
}
/**
* Чтение количества записей
*/
public static function readAmount(): int
{
return static::find()->count();
}
/**
* Проверка на то, что в свойство передан массив
*/
public function arrayValidator(string $attribute, array $params = null): bool
{
if (is_array($this->$attribute)) {
return true;
}
return false;
}
}

View File

@@ -1,54 +1,74 @@
<?php
declare(strict_types=1);
namespace app\models;
use explosivebit\arangodb\ActiveRecord;
abstract class Edge extends ActiveRecord
/**
* Ребро
*/
abstract class Edge extends Document
{
public function attributes()
/**
* Свойства
*/
public function attributes(): array
{
return ['_key', '_from', '_to', 'date', 'type', 'account'];
}
public function rules()
{
return [
return array_merge(
parent::attributes(),
[
['_from', '_to', 'date', 'type', 'account'],
'required',
'message' => 'Заполните поле: {attribute}'
],
[
'date',
'integer'
],
[
'type',
'string'
],
[
'account',
'string'
// Надо добавить проверку существования аккаунта
'_from',
'_to',
'type'
]
];
);
}
public function attributeLabels()
/**
* Метки свойств
*/
public function attributeLabels(): array
{
return [
'date' => 'Дата',
'type' => 'Тип',
'account' => 'Аккаунт'
];
return array_merge(
parent::attributeLabels(),
[
'_from' => 'От кого',
'_to' => 'К кому',
]
);
}
public function beforeSave($data)
/**
* Правила
*/
public function rules(): array
{
return array_merge(
parent::rules(),
[
[
['_from', '_to'],
'required',
'message' => 'Заполните поле: {attribute}'
],
[
'type',
'string'
]
]
);
}
/**
* Перед сохранением
*
* @todo Подождать обновление от ебаного Yii2 и добавить
* проверку типов передаваемых параметров
*/
public function beforeSave($data): bool
{
if (parent::beforeSave($data)) {
if ($this->isNewRecord) {
$this->date = time();
}
return true;
@@ -56,4 +76,83 @@ abstract class Edge extends ActiveRecord
return false;
}
/**
* Записать (с проверкой на существование)
*
* Создаст ребро только в том случае, если его аналога не существует
*/
public static function writeSafe(string $_from, string $_to, string $type = '', array $data = []): ?static
{
if ($edge = self::searchByVertex($_from, $_to, limit: 1)) {
// Найдено в базе данных
return $edge;
}
return self::write($_from, $_to, $type, $data);
}
/**
* Записать
*/
public static function write(string $_from, string $_to, string $type, array $data = []): ?static
{
// Инициализация
$edge = new static;
// Настройка
$edge->_from = $_from;
$edge->_to = $_to;
$edge->type = $type;
foreach ($data as $key => $value) {
if (is_int($key)) {
// Обычная запись
$edge->{$value} = true;
} else {
// Ассоциативная запись
$edge->{$key} = $value;
}
}
return $edge->save() ? $edge : null;
}
/**
* Поиск ребра по его вершинам
*/
public static function searchByVertex(string $_from, string $_to, string|null $type = null, int $limit = 1): array|null
{
$query = self::find()->where([
'_from' => $_from,
'_to' => $_to
]);
if (isset($type)) {
$query->where(['type' => $type]);
}
return $query->limit($limit)->all();
}
/**
* Поиск рёбер
*/
public static function search(string $target, string $direction = 'OUTBOUND', string $type = '', int $limit = 1): static|array|null
{
if ($direction === 'OUTBOUND') {
$query = self::find()->where(['_from' => $target, 'type' => $type]);
} else if ($direction === 'INBOUND') {
$query = self::find()->where(['_to' => $target, 'type' => $type]);
}
if ($limit < 2) {
return $query->one();
} else {
return $query->limit($limit)->all();
}
}
}

View File

@@ -0,0 +1,219 @@
<?php
declare(strict_types=1);
namespace app\models;
use yii;
use app\models\traits\SearchByEdge;
use app\models\Account;
use Exception;
/**
* Поиск
*
* @see Product Поиск по товарам
*
* @todo Нет смысла каждый раз возвращать кнопку, надо написать проверки
*/
class Notification extends Document
{
use SearchByEdge;
/**
* Сценарий для доверенного пользователя с созданием уведомления
*/
const SCENARIO_TRUSTED_CREATE = 'create';
/**
* Тип уведомления: памятка
*/
const TYPE_NOTICE = 'notice';
/**
* Тип уведомления: предупреждение
*/
const TYPE_WARNING = 'warning';
/**
* Цель для отправки уведомления
*
* Расшифровывается как $target
*
* @see SCENARIO_TRUSTED_CREATE
*/
public string|null $account;
/**
* Текст уведомления
*/
public string $text;
/**
* Разделитель получателей уведомлений
*/
public static string $delimiter = ',';
/**
* Типы уведомлений
*/
public array $typs = [
'notice' => 'Уведомление',
'warning' => 'Предупреждение'
];
/**
* Имя коллекции
*/
public static function collectionName(): string
{
return 'notification';
}
/**
* Свойства
*/
public function attributes(): array
{
return array_merge(
parent::attributes(),
[
'html',
'type'
]
);
}
/**
* Метки свойств
*/
public function attributeLabels(): array
{
return array_merge(
parent::attributeLabels(),
[
'html' => 'HTML',
'type' => 'Тип',
'trgt' => 'Получатели',
'text' => 'Текст',
]
);
}
/**
* Правила
*/
public function rules(): array
{
return array_merge(
parent::rules(),
[
[
'html',
'required'
],
[
'type',
'default',
'value' => 'notice'
]
]
);
}
/**
* Запись
*/
public function write(): self|array|null
{
return $this::_write($this->text, $this->html, $this->account, $this->type);
}
/**
* Запись
*
* @param string $html Содержимое уведомления (HTML или текст)
* @param bool|string|null $html Содержимое уведомления (HTML или текст)
* @param string $account Получатель уведомления
* @param string $type Тип уведомления
*/
public static function _write(string $text, bool|string|null $html = false, string $account = null, string $type = self::TYPE_NOTICE): self|array|null
{
// Инициализация
$model = new self;
$account or $account = yii::$app->user->identity->_key ?? throw new Exception('Не удалось инициализировать получателя');
if ((bool) (int) $html) {
// Получен текст в формете HTML-кода
$model->html = $text ?? null;
} else {
// Получен необработанный текст
$text = htmlspecialchars(strip_tags($text ?? null));
$model->html = <<<HTML
<p class="my-2 mx-3">$text</p>
HTML;
}
if ($model->save()) {
// Уведомление записано
// Инициализация получателей и создание ребра
self::searchReceiverAndConnect($model, $account, $type);
}
return null;
}
/**
* Поиск получателя
*
* @param self $model Уведомление
* @param string $text Необработанный текст
* @param string $type Тип уведомления
*/
protected static function searchReceiverAndConnect(self $model, string $text, string $type = self::TYPE_NOTICE): AccountEdgeNotification|array|null
{
// Инициализация
$return = [];
// Конвертация
$accounts = array_map('trim', explode(self::$delimiter, $text));
foreach ($accounts as $account) {
if (in_array('@all', $accounts, true)) {
// Найден флаг обозначающий отправку всем пользователям
// Инициализация
$return = [];
foreach (Account::readAll() 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 {
// Найден идентификатор (подразумевается)
if ($account = Account::searchById(Account::collectionName() . '/' . $account)) {
// Аккаунт найден
// Запись ребра: УВЕДОМЛЕНИЕ -> АККАУНТ
$return[] = AccountEdgeNotification::writeSafe($model->readId(), $account->readId(), $type);
}
}
}
return $return ? $return : null;
}
}

View File

@@ -0,0 +1,369 @@
<?php
declare(strict_types=1);
namespace app\models;
use yii;
use yii\web\User as Account;
use app\models\traits\SearchByEdge;
use Exception;
/**
* Заказ
*
* @see Account Заказчик
* @see Supply Поставки для заказа
*/
class Order extends Document
{
use SearchByEdge;
/**
* Поставки для записи
*/
public array $supplies;
/**
* Имя коллекции
*/
public static function collectionName(): string
{
return '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',
'message' => '{attribute} должен быть строкой'
],
[
'stts',
'default',
'value' => 'preparing'
]
]
);
}
/**
* Подключение к аккаунту
*/
public function connect(Account $account): ?AccountEdgeOrder
{
// Запись ребра: АККАУНТ -> ЗАКАЗ
return AccountEdgeOrder::write($account->id, $this->readId(), 'current') ?? throw new Exception('Не удалось инициализировать ребро: АККАУНТ -> ЗАКАЗ');
}
/**
* Запись товара
*
* $supply = [ Supply $supply, int $amount = 1 ]
*
* @param Supply|array $supply Поставка
* @param Account $trgt Заказчик
*
* @return int Количество записанных поставок
*
* @todo Создать параметр разделителя для администрации
*/
public function writeSupply(Supply|string|array $supply, Account $trgt = null): int
{
// Инициализация
$trgt ?? $trgt = yii::$app->user ?? throw new Exception('Не удалось инициализировать заказчика');
if ($supply instanceof Supply) {
// Передана инстанция класса поставки или второй элемент массива не является числом
// Унификация входных данных
$supply = [$supply->catn => 1];
}
if (is_null($this->_key)) {
// Корзина не инициализирована
// Инициализация
if (!$this->save()) {
// Инициализация заказа не удалась
throw new Exception('Ошибка при записи заказа в базу данных');
}
// Инициализация ребра: АККАУНТ -> ЗАКАЗ
if (!AccountEdgeOrder::write($trgt->readId(), $this->readId(), 'create')) {
// Инициализация не удалась
throw new Exception('Ошибка при записи ребра от аккаунта до заказа в базу данных');
}
}
// Инициализация
$amount = 0;
foreach (is_array($supply) ? $supply : [$supply => 1] as $supply_raw => $amount_raw) {
// Перебор поставок
for ($i = 0; $i < $amount_raw; $i++) {
// Создание рёбер соразмерно запросу (добавление нескольких продуктов в корзину)
// Запись ребра: ЗАКАЗ -> ПОСТАВКА
if (!$supply_model = Supply::searchByCatn($supply_raw) or !OrderEdgeSupply::write($this->readId(), $supply_model->readId(), 'write')) {
// Поставка не найдена или запись ребра не удалась
continue;
} else {
// Ребро создано (товар подключен к заказу)
// Постинкрементация счётчика добавленных товаров
$amount++;
// Запись в журнал
$this->journal('write', ['target' => $supply_model->readId()]);
}
}
}
if ($amount === 0) {
// Отправка уведомления
self::notification('Неудачная попытка добавить товар в корзину');
} else if ($amount === 1) {
// Отправка уведомления
self::notification('Товар ' . $supply_model->catn . ' добавлен в корзину');
} else {
// Отправка уведомления
self::notification('Добавлено ' . $amount . ' товаров в корзину');
}
return $amount;
}
/**
* Удаление поставки
*
* @param Supply|string|array $supply Товары
*
* @return int Количество удалённых рёбер
*/
public function deleteSupply(Supply|string|array $supply): int
{
// Инициализация
$amount = 0;
if ($supply instanceof Supply) {
// Передана инстанция класса поставки или второй элемент массива не является числом
// Унификация входных данных
$supply = [$supply->catn => 1];
}
foreach (is_array($supply) ? $supply : [$supply => 1] as $catn => $amount_raw) {
// Перебор товаров
if ($supply = Supply::searchByCatn($catn)) {
foreach (OrderEdgeSupply::searchByVertex($this->readId(), $supply->readId(), limit: $amount_raw) as $edge) {
// Перебор рёбер до продукта (если товаров в заказе несколько)
// Удаление
$edge->delete();
// Запись в журнал
$this->journal('delete', ['target' => $supply->readId()]);
// Постинкрементация счётчика удалённых рёбер
$amount++;
}
}
}
if ($amount === 0) {
// Отправка уведомления
self::notification('Неудачная попытка удалить товар из корзины');
} else if ($amount === 1) {
// Отправка уведомления
self::notification('Товар ' . $supply->catn . ' удалён из корзины');
} else {
// Отправка уведомления
self::notification('Удалено ' . $amount . ' товаров из корзины');
}
return $amount;
}
/**
* Поиск заказа
*/
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('Не удалось инициализировать пользователя');
// Генерация сдвига по запрашиваемым данным (пагинация)
$offset = $limit * ($page - 1);
if (strcasecmp($type, 'all') !== 0) {
// Если не указан параметр поиска всех заказов
$where_type = [
'account_edge_order.type' => $type
];
} else {
$where_type = [];
}
$return = self::searchByEdge(
from: 'account',
to: 'order',
subquery_where: [
[
'account._id' => $account->id
],
$where_type
],
foreach: ['edge' => 'account_edge_order'],
where: 'edge._to == order._id',
limit: $limit,
offset: $offset,
sort: ['DESC'],
select: $select,
direction: 'INBOUND'
);
return $limit === 1 ? $return[0] ?? null : $return;
}
/**
* Поиск содержимого заказа
*
* @todo В будущем возможно заказ не только поставок реализовать
*/
public function content(int $limit = 1, int $page = 1): Supply|array|null
{
// Генерация сдвига по запрашиваемым данным (пагинация)
$offset = $limit * ($page - 1);
// Поиск рёбер: ЗАКАЗ -> ПОСТАВКА
$supplies = Supply::searchByEdge(
from: 'order',
to: 'supply',
edge: 'order_edge_supply',
subquery_where: [
[
'order._id' => $this->readId()
]
],
foreach: ['edge' => 'order_edge_supply'],
where: 'edge._to == supply._id',
limit: $limit,
offset: $offset,
direction: 'INBOUND'
);
// Инициализация реестра дубликатов
$registry = [];
// Подсчёт и перестройка массива для очистки от дубликатов
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;
}
// Поиск стоимости для каждой поставки
foreach ($supplies as $key => &$supply) {
// Перебор поставок
// Чтение стоимости
$cost = $supply->readCost();
if ($cost < 1) {
// Если стоимость равна нулю (явная ошибка)
// Удаление из базы данных
$this->deleteSupply($supply->readId());
// Удаление из списка
unset($supplies[$key]);
// Пропуск итерации
continue;
}
// Запись цены
$supply->cost = $cost['ЦенаЗаЕдиницу'] . ' ' . $cost['Валюта'];
}
return $supplies;
}
/**
* Отправка уведомления
*/
public static function notification(string $text, string|null $type = Notification::TYPE_NOTICE): Notification|array|null
{
// Отправка
return Notification::_write($text, type: $type);
}
}

View File

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

View File

@@ -1,52 +1,149 @@
<?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 carono\exchange1c\interfaces\ProductInterface;
use Zenwalker\CommerceML\Model\Product as Product1c;
class Product extends Document implements ProductInterface
/**
* Продукт (в ассортименте магазина)
*
* Представляет собой лот состоящий из предложений от поставщиков
*
* @see Supply Поставки для продуктов
*/
class Product extends Document
{
const SCENARIO_IMPORT = 'import';
use SearchByEdge;
public $file;
public $group;
/**
* Сценарий импорта .excel документа
*
* Использовать для обхода правил при загрузке файла
*/
const SCENARIO_IMPORT_EXCEL = 'import_excel';
public static function collectionName()
/**
* Сценарий импорта изображений
*
* Использовать для обхода правил при загрузке файла
*/
const SCENARIO_IMPORT_IMAGE = 'import_image';
/**
* Сценарий записи товара
*/
const SCENARIO_WRITE = 'write';
/**
* Файл .excel для импорта товаров
*/
public Excel|string|array|null $file_excel = null;
/**
* Изображение для импорта
*/
public UploadedFile|string|array|null $file_image = null;
/**
* Группа в которой состоит товар
*/
public ProductGroup|null $group = null;
/**
* Имя коллекции
*/
public static function collectionName(): string
{
return 'product';
}
public function attributes()
/**
* Свойства
*/
public function attributes(): array
{
return array_merge(
parent::attributes(),
['name', 'catn', 'oemn', 'data', 'cost', 'time']
[
'catn',
'name',
'desc',
'ocid',
'imgs',
'time',
'oemn',
'cost'
]
);
}
public function rules()
/**
* Метки свойств
*/
public function attributeLabels(): array
{
return array_merge(
parent::attributeLabels(),
[
'catn' => 'Каталожный номер (catn)',
'name' => 'Название (name)',
'desc' => 'Описание (desc)',
'ocid' => 'Идентификатор 1C (ocid)',
'imgs' => 'Изображения (imgs)',
'time' => 'Срок доставки (time)',
'oemn' => 'OEM номера (oemn)',
'cost' => 'Стоимость (cost)',
'file_excel' => 'Документ (file_excel)',
'file_image' => 'Изображение (file_image)',
'group' => 'Группа (group)'
]
);
}
/**
* Правила
*/
public function rules(): array
{
return array_merge(
parent::rules(),
[
[
['name', 'catn'],
'catn',
'required',
'message' => 'Заполните поля: {attribute}',
'except' => self::SCENARIO_IMPORT
'on' => self::SCENARIO_WRITE,
'except' => [self::SCENARIO_IMPORT_EXCEL, self::SCENARIO_IMPORT_IMAGE]
],
[
'file',
'catn',
'string',
'message' => '{attribute} должен быть строкой'
],
[
[
'oemn',
'imgs'
],
'arrayValidator',
'message' => '{attribute} должен быть массивом.'
],
[
'file_excel',
'required',
'message' => 'Заполните поля: {attribute}',
'on' => self::SCENARIO_IMPORT
'on' => self::SCENARIO_IMPORT_EXCEL
],
['catn', 'integer', 'message' => '{attribute} должен быть числом'],
// ['oemn', 'integer'], Нужна своя проверка на массив
[
'file',
'file_excel',
'file',
'skipOnEmpty' => false,
'extensions' => 'xlsx',
@@ -55,59 +152,209 @@ class Product extends Document implements ProductInterface
'maxSize' => 1024 * 1024 * 30,
'wrongExtension' => 'Разрешены только документы в формате: ".xlsx"',
'message' => 'Проблема при чтении документа',
'on' => self::SCENARIO_IMPORT
'on' => self::SCENARIO_IMPORT_EXCEL
],
[
'file_image',
'required',
'message' => 'Загрузите изображение',
'on' => self::SCENARIO_IMPORT_IMAGE
],
[
'file_image',
'file',
'skipOnEmpty' => false,
'extensions' => ['jpg', 'jpeg', 'png', 'gif', 'webp'],
'checkExtensionByMimeType' => true,
'maxFiles' => 10,
'maxSize' => 1024 * 1024 * 30,
'wrongExtension' => 'Разрешены только изображения в формате: ".jpg", ".jpeg", ".png", ".gif", ".webp"',
'message' => 'Проблема при загрузке изображения',
'on' => self::SCENARIO_IMPORT_IMAGE
]
]
);
}
public function attributeLabels()
/**
* Инициализация продукта
*
* @param string $catn Артикул, каталожный номер
*/
public static function initEmpty(string $catn): self|array
{
return array_merge(
parent::attributeLabels(),
[
'name' => 'Название (name)',
'catn' => 'Каталожный номер (catn)',
'oemn' => 'OEM номера (oemn)',
'data' => 'Данные товара (data)',
'cost' => 'Цены (cost)',
'time' => 'Сроки доставки (time)',
'file' => 'Документ',
'group' => 'Группа',
]
);
}
$oemn = self::searchOemn($catn);
public function beforeSave($data)
{
if (parent::beforeSave($data)) {
if ($this->isNewRecord) {
// Надо избавиться от unset();
unset($this->_key);
if (count($oemn) === 1) {
// Передан только один артикул
if ($model = self::searchByCatn($catn)) {
// Продукт уже существует
return $model;
}
return true;
// Запись пустого продукта
return self::writeEmpty($catn);
}
return false;
// Инициализация
$models = [];
foreach ($oemn as $catn) {
// Перебор всех найденных артикулов
if ($model = self::searchByCatn($catn)) {
// Продукт уже существует
continue;
}
// Запись
if ($model = self::writeEmpty($catn)) {
// Записано
// Запись в массив сохранённых моделей
$models[] = $model;
}
}
return $models;
}
public function import()
/**
* Запись пустого продукта
*/
public static function writeEmpty(string $catn): ?self
{
// Инициализация массива данных
// Инициализация
$model = new self;
// Настройки
$model->catn = $catn;
// Запись
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];
}
/**
* Импорт изображений
*
* @return int Количество сохранённых изображений
*/
public function importImages(): int
{
if ($this->validate()) {
// Проверка пройдена
// Инициализация
$amount = 0;
foreach ($this->file_image as $file) {
// Перебор обрабатываемых изображений
if (!file_exists(YII_PATH_PUBLIC . $catalog = '/img/products/' . $this->_key)) {
// Директория для изображений продукта не найдена
if (!mkdir(YII_PATH_PUBLIC . $catalog, 0775, true)) {
// Не удалось записать директорию
return false;
};
}
if (!file_exists(YII_PATH_PUBLIC . $catalog_h150 = '/img/products/' . $this->_key . '/h150')) {
// Директория для обложек изображений продукта не найдена
if (!mkdir(YII_PATH_PUBLIC . $catalog_h150, 0775, true)) {
// Не удалось записать директорию
return false;
};
}
// Запись на сервер
$file->saveAs(YII_PATH_PUBLIC . $catalog . '/' . $file->baseName . '.' . $file->extension . '.original');
// Конвертация изображения для сохранения полного изображения
Image::resize(YII_PATH_PUBLIC . $catalog . '/' . $file->baseName . '.' . $file->extension . '.original', 800, 800)
->save(YII_PATH_PUBLIC . $catalog . '/' . $file->baseName . '.' . $file->extension, ['quality' => 80]);
// Конвертация изображения для сохранения обложки (150px)
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 = array_merge(
$this->imgs ?? [],
[[
'orig' => $catalog . '/' . $file->baseName . '.' . $file->extension,
'h150' => $catalog_h150 . '/' . $file->baseName . '.' . $file->extension
]]
);
$this->scenario = self::SCENARIO_WRITE;
if ($this->save()) {
// Изменения сохранены в базе данных
// Постинкрементация счётчика
$amount++;
}
}
}
return $amount;
}
/**
* Импорт товаров
*
* На данный момент обрабатывает только импорт из
* файлов с расширением .excel
*/
public function importExcel(): bool
{
// Инициализация
$data = [];
$amount = 0;
if ($this->validate()) {
foreach ($this->file as $file) {
foreach ($this->file_excel as $file) {
// Перебор файлов
// Сохранение на диск
if (!file_exists('../assets/import/excel/')) {
mkdir('../assets/import/excel/', 0775, true);
}
$file->saveAs($path = '../assets/import/excel/' . $file->baseName . '.' . $file->extension);
// Инициализация
$dir = '../assets/import/' . date('Y_m_d#H-i', time()) . '/excel/';
// Проверка файла пройдена
// Сохранение на диск
if (!file_exists($dir)) {
mkdir($dir, 0775, true);
}
$file->saveAs($path = $dir . $file->baseName . '.' . $file->extension);
$data[] = Excel::import($path, [
'setFirstRecordAsKeys' => true,
@@ -115,238 +362,157 @@ class Product extends Document implements ProductInterface
]);
}
foreach ($data[0] as $doc) {
// Перебор полученных документов
// Сохранение в базе данных
$product = new static($doc);
foreach ($data as $data) {
// Перебор конвертированных файлов
if ($product->validate()) {
// Проверка пройдена
if (count($data) < 1) {
// Не найдены строки с товарами
// Запись документа
$product->save();
// Запись группы
$group = static::class . 'Group';
(new $group())->writeMember($product, $this->group);
$this->addError('erros', 'Не удалось найти данные товаров');
} else {
// Проверка не пройдена
foreach ($product->errors as $attribute => $error) {
$this->addError($attribute, $error);
// Перебор найденных товаров
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);
}
}
}
}
}
// Деинициализация
$this->file_excel = '';
static::afterImportExcel($amount);
return true;
}
return false;
}
$this->addError('erros', 'Неизвестная ошибка');
/**
* Установка реквизитов для продукта
*/
// public function setRequisite1c(string $name, string $value): bool
public function setRequisite1c($name, $value): bool
{
if (!$requisite = Requisite::readByName($name)) {
// Реквизиты не найдены
// Инициализация
$requisite = new Requisite();
$requisite->name = $name;
$requisite->value = $value;
// Запись
return $requisite->save();
}
static::afterImportExcel($amount);
return false;
}
/**
* Установка группы, где находится продукт
*/
// public function setGroup1c(ProductGroup $group): bool
public function setGroup1c($group): bool
{
// Чтение группы
$group = SupplyGroup::readByOnecName($group->id)[0];
// Запись ребра: ПОСТАВКА => ГРУППА ПОСТАВОК
return static::writeEdgeBetweenGroup(static::collectionName() . '/' . $this->_key, $group->collectionName() . '/' . $group->_key);
}
/**
* Запись всех параметров. Вызывается 1 раз при импорте
*/
public static function createProperties1c($properties): void
{
// Это нам не нужно, кажется, надо будет тестить
/**
* @var \Zenwalker\CommerceML\Model\Property $property
*/
// foreach ($properties as $property) {
// $propertyModel = Property::createByMl($property);
// foreach ($property->getAvailableValues() as $value) {
// if (!$propertyValue = PropertyValue::findOne(['onec_name' => $value->id])) {
// $propertyValue = new PropertyValue();
// $propertyValue->name = (string)$value->Значение;
// $propertyValue->property_id = $propertyModel->id;
// $propertyValue->onec_name = (string)$value->ИдЗначения;
// $propertyValue->save();
// unset($propertyValue);
// }
// }
// }
}
/**
* $property - Свойство товара (import.xml > Классификатор > Свойства > Свойство)
* $property->value - Разыменованное значение (string) (import.xml > Классификатор > Свойства > Свойство > Значение)
* $property->getValueModel() - Данные по значению, Ид значения, и т.д (import.xml > Классификатор > Свойства > Свойство > ВариантыЗначений > Справочник)
* Поиск по каталожному номеру
*
* @param MlProperty $property
* @return void
*/
public function setProperty1c($property): void
{
// Это тоже нам не нужно
// $propertyModel = Property::findOne(['onec_name' => $property->id]);
// $propertyValue = $property->getValueModel();
// if ($propertyAccountingId = (string)$propertyValue->ИдЗначения) {
// $value = PropertyValue::findOne(['onec_name' => $propertyAccountingId]);
// $attributes = ['property_value_id' => $value->id];
// } else {
// $attributes = ['value' => $propertyValue->value];
// }
// $this->addPivot($propertyModel, PvProductProperty::class, $attributes);
}
/**
* В этой фукнции мы получаем абсолютный путь до картинки и название изрбражения (для alt аттрибута)
* Ищет продукт и возвращает его,
* либо выполняет поиск через представление
*
* @param string $path
* @param string $caption
* @return mixed
* @todo Переделать нормально
*/
public function addImage1c($path, $caption): bool
public static function searchByCatn(string $catn, int $limit = 1, array $select = []): static|array|null
{
// if (!$this->getImages()->andWhere(['md5' => md5_file($path)])->exists()) {
// $this->addPivot(FileUpload::startUpload($path)->process(), PvProductImage::class, ['caption' => $caption]);
// }
return false;
}
/**
* В эту фукнцию отправляется xml данные предложения из файла
*/
// public function getOffer1c(Supply $offer): Supply
public function getOffer1c($offer): Supply
{
$supply = Supply::createByMl($offer);
$supply->product_id = $this->id;
if ($supply->getDirtyAttributes()) {
$supply->save();
if ($limit <= 1) {
return static::findOne(['catn' => $catn]);
}
return $supply;
}
$query = self::find()
->where(['catn' => $catn])
->limit($limit)
->select($select)
->createCommand()
->execute()
->getAll();
/**
* @param MlOffer $offer
*/
public static function createByMl($offer): Supply
{
if (!$model = static::readByOnecName($offer->id)) {
// Нет записей в базе данных
foreach ($query as &$attribute) {
// Приведение всех свойств в массив и очистка от лишних данных
// Инициализация
$model = new static;
$model->name = (string) $offer->name;
$model->onec_name = (string) $offer->id;
$attribute = $attribute->getAll();
}
$model->remnant = (string) $offer->Количество;
return $model;
return $query;
}
/**
* @param $product
* @return self
* Поиск по каталожному номеру (через представления)
*
* Ищет продукт и возвращает его,
* либо выполняет поиск через представление
*
* @todo Переделать нормально
*/
public static function createModel1c($product): static
public static function searchByPartialCatn(string $catn, int $limit = 1, array $select = []): static|array|null
{
if (!$model = static::findOne(['onec_name' => $product->id])) {
$model = new static();
$model->onec_name = $product->id;
$query = self::find()
->for('product')
->in('product_search')
->search(['catn' => $catn])
->limit($limit)
->select($select)
->createCommand()
->execute()
->getAll();
foreach ($query as &$attribute) {
// Приведение всех свойств в массив и очистка от лишних данных
$attribute = $attribute->getAll();
}
$model->name = $product->name;
$model->description = (string) $product->Описание;
$model->article = (string) $product->Артикул;
$model->save();
return $model;
}
public function setRaw1cData($cml, $object)
{
return $query;
}
/**
* Название поля в котором хранится ID из 1C
* Вызывается после загрузки поставок из excel-документа
*
* @param int $amount Количество
*/
public static function getIdFieldName1c(): string
{
return 'onec_name';
}
private static function writeEdgeBetweenGroup(string $from, string $to): bool
public static function afterImportExcel(int $amount = 0): bool
{
// Инициализация
$edge = new SupplyEdgeSupplyGroup();
$model = new Notification;
$date = date('H:i d.m.Y', time());
// Настройка
$edge->_from = $from;
$edge->_to = $to;
$model->text = yii::$app->controller->renderPartial('/notification/system/afterImportExcel', compact('amount', 'date'));
$model->type = $model::TYPE_NOTICE;
// Запись
return $edge->save();
// Отправка
return (bool) $model->write();
}
private static function writeEdgeBetweenRequisite(string $from, string $to): bool
/**
* Вызывается после загрузки поставок из 1С
*
* @param int $amount Количество
*/
public static function afterImportOnec(): bool
{
// Инициализация
$edge = new SupplyEdgeRequisite();
$model = new Notification;
$date = date('H:i d.m.Y', time());
// Настройка
$edge->_from = $from;
$edge->_to = $to;
$model->text = yii::$app->controller->renderPartial('/notification/system/afterImportOnec', compact('amount', 'date'));
$model->type = $model::TYPE_NOTICE;
// Запись
return $edge->save();
}
public static function readById(string $_key): ?Product
{
return Product::findOne(['_key' => $_key]);
}
public function getGroup1c(): ProductGroup
{
return $this->group;
}
public static function readByOnecName(string $name): ?Product
{
return static::findOne([static::getIdFieldName1c() => $name]);
// Отправка
return (bool) $model->write();
}
}

View File

@@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
namespace app\models;
class ProductEdgeProduct extends Edge
{
public static function collectionName()
public static function collectionName(): string
{
return 'product_edge_product';
}

View File

@@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
namespace app\models;
class ProductEdgeProductGroup extends Edge
{
public static function collectionName()
public static function collectionName(): string
{
return 'product_edge_product_group';
}

View File

@@ -1,26 +1,56 @@
<?php
declare(strict_types=1);
namespace app\models;
use carono\exchange1c\interfaces\GroupInterface;
use Zenwalker\CommerceML\Model\Group;
/**
* Группировка продуктов
*/
class ProductGroup extends Document implements GroupInterface
{
public static function collectionName()
/**
* Имя коллекции
*/
public static function collectionName(): string
{
return 'product_group';
}
public function attributes()
/**
* Свойства
*/
public function attributes(): array
{
return array_merge(
parent::attributes(),
['name', 'onec_name', 'onec_prnt_name']
[
'name'
]
);
}
public function rules()
/**
* Метки свойств
*/
public function attributeLabels(): array
{
return array_merge(
parent::attributeLabels(),
[
'name' => 'Название (name)'
]
);
}
/**
* Правила
*/
public function rules(): array
{
return array_merge(
parent::rules(),
@@ -34,34 +64,18 @@ class ProductGroup extends Document implements GroupInterface
);
}
public function attributeLabels()
/**
* Запись члена группы
*/
public function writeMember(Product $member): ProductEdgeProductGroup
{
return array_merge(
parent::attributeLabels(),
[
'name' => 'Название (name)',
'onec_name' => 'Название 1C (onec_name)',
'onec_prnt_name' => 'Название родителя 1C (onec_prnt_name)',
]
);
}
public function writeMember(Document $member, string $group)
{
if (isset($member->_key)) {
return static::writeEdgeBetweenMember($member->collectionName() . '/' . $member->_key, $this->collectionName() . '/' . $group);
}
return false;
return ProductEdgeProductGroup::write($member->readId(), $this->readId(), 'member');
}
/**
* Создание дерева групп
* в параметр передаётся массив всех групп (import.xml > Классификатор > Группы)
* $groups[0]->parent - родительская группа
* $groups[0]->children - дочерние группы
* Запись рёбер групп
*
* @param Group[] $groups
* Создание взаимоотношений между группами по типу древовидной системы
*/
public static function createTree1c($groups): Document|null
{
@@ -88,53 +102,43 @@ class ProductGroup extends Document implements GroupInterface
}
/**
* Создаём группу по модели группы CommerceML
* проверяем все дерево родителей группы, если родителя нет в базе - создаём
* Запись группы
*
* @param Group $group
* Создаём группу по модели группы CommerceML
* проверяем все дерево родителей группы,
* если родителя нет в базе - создаём
*/
public static function createByML(Group $group): static|array|null
{
if (!$model = static::readByOnecName($group->id)) {
// Группа не найдена
// if (!$model = static::readByOnecId($group->id)) {
// // Группа не найдена
// Инициализация
$model = new static;
// // Инициализация
// $model = new static;
$model->onec_name = $group->id;
}
// $model->onec_id = $group->id;
// }
$model->name = $group->name;
// $model->name = $group->name;
if ($parent = $group->getParent()) {
// Найден родитель
// if ($parent = $group->getParent()) {
// // Найден родитель
// Инициализация (рекурсия)
$parentModel = static::createByML($parent);
// // Инициализация (рекурсия)
// $parentModel = static::createByML($parent);
$model->onec_prnt_name = $parentModel->id;
// $model->onec_prnt_id = $parentModel->id;
unset($parentModel);
} else {
$model->onec_prnt_name = null;
}
// unset($parentModel);
// } else {
// $model->onec_prnt_id = null;
// }
$model->save();
// $model->save();
return $model;
}
// return $model;
private static function writeEdgeBetweenMember(string $from, string $to): bool
{
// Инициализация
$edge = new ProductEdgeProductGroup();
// Настройка
$edge->_from = $from;
$edge->_to = $to;
// Запись
return $edge->save();
return null;
}
private static function writeEdgeBetweenGroup(string $from, string $to): bool
@@ -150,11 +154,6 @@ class ProductGroup extends Document implements GroupInterface
return $edge->save();
}
public static function readAll()
{
return static::find()->all();
}
public static function readByName(string $name)
{
return static::findOne(['name' => $name]);
@@ -165,11 +164,11 @@ class ProductGroup extends Document implements GroupInterface
*/
public static function getIdFieldName1c(): string
{
return 'onec_name';
return 'onec_id';
}
public static function readByOnecName(string $name): ?Product
public static function readByOnecId(string $onec_id): ?ProductGroup
{
return static::findOne([static::getIdFieldName1c() => $name]);
return static::findOne(['onec_id' => $onec_id]);
}
}

View File

@@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
namespace app\models;
class ProductGroupEdgeProductGroup extends Edge
{
public static function collectionName()
public static function collectionName(): string
{
return 'product_group_edge_product_group';
}

View File

@@ -1,20 +1,22 @@
<?php
declare(strict_types=1);
namespace app\models;
use carono\exchange1c\interfaces\DocumentInterface;
class Purchase extends Document implements DocumentInterface
{
public static function collectionName()
public static function collectionName(): string
{
return 'requisite';
return 'purchase';
}
/**
* @return DocumentInterface[]
*/
public static function findDocuments1c()
public static function findDocuments1c(): ?array
{
return self::find()->andWhere(['status_id' => 2])->all();
}
@@ -22,13 +24,14 @@ class Purchase extends Document implements DocumentInterface
/**
* @return OfferInterface[]
*/
public function getOffers1c()
public function getOffers1c(): mixed
{
return $this->offers;
return true;
}
public function getRequisites1c()
public function getRequisites1c(): mixed
{
return true;
}
/**
@@ -36,32 +39,15 @@ class Purchase extends Document implements DocumentInterface
*
* @return PartnerInterface
*/
public function getPartner1c()
public function getPartner1c(): Account
{
return $this->user;
// !!!!!!!!!!!!!!!!!!!
return $this->user ?? new Account;
}
public function getExportFields1c($context = null)
{
return [
'Ид' => 'id',
'Наименование' => 'login',
'ПолноеНаименование' => 'full_name',
'Фамилия' => 'surname',
'Имя' => 'name',
'Контакты' => [
[
'@name' => 'Контакт',
'Тип' => 'Почта',
'Значение' => $this->email,
],
[
'@name' => 'Контакт',
'Тип' => 'ТелефонРабочий',
'Значение' => $this->phone,
],
],
];
return [];
}
/**
@@ -71,10 +57,10 @@ class Purchase extends Document implements DocumentInterface
*/
public static function getIdFieldName1c()
{
return 'accounting_id';
return 'onec["Ид"]';
}
public function setRaw1cData($cml, $object)
public function setRaw1cData($cml, $object): void
{
}
}

View File

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

View File

@@ -1,15 +1,17 @@
<?php
declare(strict_types=1);
namespace app\models;
class Requisite extends Document
{
public static function collectionName()
public static function collectionName(): string
{
return 'requisite';
}
public function attributes()
public function attributes(): array
{
return array_merge(
parent::attributes(),
@@ -17,7 +19,7 @@ class Requisite extends Document
);
}
public function rules()
public function rules(): array
{
return array_merge(
parent::rules(),
@@ -31,7 +33,7 @@ class Requisite extends Document
);
}
public function attributeLabels()
public function attributeLabels(): array
{
return array_merge(
parent::attributeLabels(),

View File

@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace app\models;
use yii;
use yii\web\IdentityInterface;
use app\models\traits\SearchByEdge;
/**
* Поиск
*
* @see Product Поиск по товарам
*/
class Search extends Document
{
use SearchByEdge;
/**
* Имя коллекции
*/
public static function collectionName(): string
{
return 'search';
}
/**
* Свойства
*/
public function attributes(): array
{
return array_merge(
parent::attributes(),
[
'text',
'ipv4',
'head'
]
);
}
/**
* Метки свойств
*/
public function attributeLabels(): array
{
return array_merge(
parent::attributeLabels(),
[
'text' => 'Текст',
'ipv4' => 'IPv4',
'head' => 'Заголовки'
]
);
}
/**
* Правила
*/
public function rules(): array
{
return array_merge(
parent::rules(),
[
[
'text',
'required'
],
[
'ipv4',
'default',
'value' => yii::$app->request->userIP
],
[
'head',
'default',
'value' => yii::$app->request->getHeaders()
]
]
);
}
/**
* Запись
*
* @param string $text Текст запроса
* @param Account|null $account Пользователь совершивший запрос
*/
public static function write(string $text, Account|null $account = null): ?self
{
// Инициализация
$vertex = new self;
isset($account) && yii::$app->user->isGuest ?: $account = yii::$app->user->identity;
// Настройки
$vertex->text = $text;
if ($vertex->save()) {
// Поиск записан
// Запись ребра: АККАУНТ -> ПОИСК
return $account && AccountEdgeSearch::writeSafe($account->id, $vertex->readId(), 'request') ? $vertex : null;
}
return null;
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace app\models;
/**
* Настройки
*/
class Settings extends Document
{
/**
* Имя коллекции
*/
public static function collectionName(): string
{
return 'settings';
}
/**
* Свойства
*/
public function attributes(): array
{
return array_merge(
parent::attributes(),
[
'search_period',
'search_connect_keep'
]
);
}
/**
* Метки свойств
*/
public function attributeLabels(): array
{
return array_merge(
parent::attributeLabels(),
[
'search_period' => 'Поисковый период',
'search_connect_keep' => 'Режим удержания'
]
);
}
/**
* Правила
*/
public function rules(): array
{
return array_merge(
parent::rules(),
[
[
[
'search_period'
],
'integer',
'message' => '{attribute} должен хранить цифровое значение'
],
[
[
'search_connect_keep'
],
'string',
'message' => '{attribute} должен хранить строковый тип'
]
]
);
}
}

View File

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

View File

@@ -1,158 +1,448 @@
<?php
declare(strict_types=1);
namespace app\models;
use Yii;
use yii;
use yii\web\User;
use app\models\Account;
use app\models\Product;
use app\models\SupplyEdgeProduct;
use carono\exchange1c\interfaces\OfferInterface;
use app\models\traits\Xml2Array;
class Supply extends Product implements OfferInterface
use carono\exchange1c\interfaces\ProductInterface;
use carono\exchange1c\controllers\ApiController;
use exception;
/**
* Поставка (выгрузка товаров от поставщиков)
*
* Представляет собой предложения от поставщиков которые добавляются
* в универсальные лоты товаров в асспортименте магазина
*
* @see Product Продукт (туда добавляются поставки)
*/
class Supply extends Product implements ProductInterface
{
public static function collectionName()
use Xml2Array;
/**
* Количество
*
* Используется при выводе в корзине
*/
public int $amnt = 0;
/**
* Имя коллекции
*/
public static function collectionName(): string
{
return 'supply';
}
public function afterSave($data, $vars)
/**
* Свойства
*/
public function attributes(): array
{
if (is_null($product = self::readByCatn($this->catn))) {
// Товар не найден
return array_merge(
parent::attributes(),
[
'onec'
]
);
}
/**
* Метки свойств
*/
public function attributeLabels(): array
{
return array_merge(
parent::attributeLabels(),
[
'onec' => 'Данные 1С'
]
);
}
/**
* После сохранения
*/
public function afterSave($data, $vars): void
{
if (AccountEdgeSupply::searchByVertex(yii::$app->user->id, $this->readId())) {
// Ребро: "АККАУНТ -> ПОСТАВКА" уже существует
} else {
// Ребра не существует
// Запись ребра: АККАУНТ -> ПОСТАВКА
(new AccountEdgeSupply)->write(yii::$app->user->id, $this->readId(), 'import');
}
}
/**
* Запись реквизитов из 1С
*/
public function setRequisite1c($name, $value): mixed
{
return true;
}
/**
* Запись группы из 1С
*/
public function setGroup1c($group): mixed
{
// Чтение группы
// if ($group = SupplyGroup::readByOcid($group->id)) {
// // Запись ребра: ПОСТАВКА => ГРУППА ПОСТАВОК
// return static::writeEdgeBetweenGroup(static::collectionName() . '/' . $this->_key, $group->collectionName() . '/' . $group->_key);
// }
return true;
}
/**
* Поиск через связь с аккаунтом
*
* @param string|null $id Идентификатор пользователя
* @param string|array|null $select Запрашиваемые значения
*/
public static function searchByAccount(string|null $id = null, string|array|null $select = null, int|null $limit = 10): array
{
isset($id) ?: $id = yii::$app->user->id ?? throw new Exception('Не найден идентификатор');
return self::searchByEdge(
from: 'account',
to: 'supply',
subquery_where: [
[
'account._id' => $id
]
],
where: 'supply._id == account_edge_supply[0]._to AND supply.onec["ЗначенияСвойств"] != null',
select: $select,
limit: $limit
);
}
/**
* Запись данных свойств по UUID 1C
*
* Ищет записанные свойства из 1C по их идентификатору и добавляет к ним
* недостающие данные. Это костыль оставшийся от реляционных баз данных
*
* @todo Понять что может храниться внутри "$model->onec['ЗначенияСвойств']['ЗначенияСвойства']" и переписать
*/
public static function createProperties1c($properties, Account|null $account = null): void
{
// Инициализация
$account ?? $account = yii::$app->user->identity;
$models = self::searchByAccount($account->readId());
$properties = self::xml2array($properties->xml);
$account->on(ApiController::EVENT_AFTER_OFFER_SYNC, self::afterImport());
foreach ($models as $model) {
// Перебор записей
// Инициализация
$product = (new Product(array_intersect_key($this->getAttributes(), (new Product)->getAttributes())));
$changes = false;
$transit = $model->onec;
foreach ($model->onec['ЗначенияСвойств'] as $attribute_name => $attribute_value) {
// Перебор аттрибутов
foreach ($properties as $property) {
// Перебор свойств
if (is_array($attribute_value) && is_array($property) && $attribute_value['Ид'] === $property['Ид']) {
// Совпадение идентификаторов
// Объединение данных
$transit['ЗначенияСвойств'][$attribute_name] = array_merge($attribute_value, $property, $transit['ЗначенияСвойств'][$attribute_name]);
// Запись индикатора наличия изменений
$changes = true;
} else {
// Объединение данных
$transit['ЗначенияСвойств'][$attribute_name] = $property;
}
}
}
if ($changes) {
// Если указано, что записаны изменения
// Настройка ($transit нужен из-за особенностей __set())
$model->onec = $transit;
foreach ($model->onec['ЗначенияСвойств'] as $property) {
// Перебор всех свойств
if (is_array($property)) {
// if ($property['Ид'] === $account->opts['import_sections_oem']) {
// // Если идентификатор свойства совпадает с указанным в настройках свойства хранящего OEM номера
// Настройка
$model->catn = $property['Значение'];
// }
}
}
// Запись
$model->save();
}
}
}
/**
* Запись параметров из 1С
*/
public function setProperty1c($property): mixed
{
return true;
}
/**
* Запись изображений из 1С
*
* @todo Добавить параметры в админ-панель
* Запретить доступ к изображениям
*/
public function addImage1c($path, $caption): bool
{
// Инициализация
$i = 0;
if (!file_exists(YII_PATH_PUBLIC . $catalog = '/img/supplies/' . $this->_key)) {
// Директория для изображений продукта не найдена
if (!mkdir(YII_PATH_PUBLIC . $catalog, 0775, true)) {
// не удалось записать директорию
// Запись
if (!$product->save()) {
return false;
};
}
foreach ($this->imgs ?? [] as $image) {
// Перебор имеющихся изображений
if ($path === $image['sorc']) {
// Изображение уже записано на сервер
return true;
}
}
// Запись рёбер: АККАУНТ => ПОСТАВКА => ТОВАР, и проверка на то, что оба созданы
static::writeEdgeBetweenAccount(Account::collectionName() . '/' . Yii::$app->user->identity->_key, static::collectionName() . '/' . $this->_key);
static::writeEdgeBetweenProduct(static::collectionName() . '/' . $this->_key, Product::collectionName() . '/' . $product->_key);
// Инициализация
$urn = basename($path);
// Запись
copy($path, $path_local = YII_PATH_PUBLIC . $catalog . '/' . $urn);
// Запись свойства
$this->imgs = array_merge(
$this->imgs ?? [],
[
[
'desc' => $caption,
'path' => $path_local,
'sorc' => $path
]
]
);
// Отправка в базу данных
return $this->update();
}
/**
* В этом методе необходимо создать все типы цен, фукнция вызывается один раз
* Запись ребра (предложения от поставок к продуктам) из 1С
*
* @todo Разобраться зачем нужно возвращать SupplyEdgeProduct
* Вернуть создание карточек, но только по условиям (загрузка от админа, например)
*/
public static function createPriceTypes1c($types): void
public function getOffer1c($offer): SupplyEdgeProduct
{
foreach ($types as $type) {
// PriceType::createByMl($type);
if (empty($this->catn)) {
// Не передан каталожный номер
// Разработчику библеотеки надо дать по жопе
return new SupplyEdgeProduct;
}
// Создание продукта (временно заблокировано)
// // Инициализация п̸̨͇͑͋͠р̷̬̂́̀̊о̸̜̯̹̅͒͘͝д̴̨̨̨̟̈́̆у̴̨̭̮̠́͋̈́к̴̭͊̋̎т̵̛̣͈̔̐͆а̵̨͖͑
// $product = Product::initEmpty($this->catn);
// if (!is_array($product)) {
// // Создался только один товар и вернулся в виде модели
// $product = [$product];
// }
// if (is_array($this->oemn)) {
// // Значение OEM было инициализировано
// foreach ($this->oemn as $oem) {
// // Перебор артикулов из массива ОЕМ-номеров
// // Инициализация и запись
// $product[] = Product::initEmpty($oem);
// }
// }
// foreach ($product as $product) {
// // Перебор всех инициализированных продуктов
// if ($this->catn !== $product->catn) {
// // Каталожные номера не соответствуют друг другу
// continue;
// }
// // Код ниже скорее всего устарел
// if (SupplyEdgeProduct::searchByVertex($this->readId(), $product->readId())) {
// // Ребро уже существует
// continue;
// }
// // Запись ребра: ПОСТАВКА -> ПРОДУКТ
// $return = (new SupplyEdgeProduct)->write(
// $this->readId(),
// $product->readId(),
// 'connect',
// [
// 'onec' => self::xml2array($offer->xml)
// ]
// );
// }
// Возвращает последнее сохранённое ребро
// Надо будет с этим разобраться
return $return ?? new SupplyEdgeProduct();
}
/**
* offers.xml > ПакетПредложений > Предложения > Предложение > Цены
* Запись продукта из 1С (поставка)
*
* Цена товара,
* К $price можно обратиться как к массиву, чтобы получить список цен (Цены > Цена)
* $price->type - тип цены (offers.xml > ПакетПредложений > ТипыЦен > ТипЦены)
* @see Supply
*
* @param \Zenwalker\CommerceML\Model\Price $price
* @todo Понять что может храниться внутри "$model->onec['ЗначенияСвойств']['ЗначенияСвойства']" и переписать
* Разобраться и создать возможность загрузки от лица другого аккаунта
*/
public function setPrice1c($price): void
public static function createModel1c($product): ?self
{
// $priceType = PriceType::findOne(['accounting_id' => $price->getType()->id]);
// $priceModel = Price::createByMl($price, $this, $priceType);
// $this->addPivot($priceModel, PvOfferPrice::class);
// Инициализация
$model = self::searchByOcid($id = (string) $product->Ид) ?? new self;
$account ?? $account = yii::$app->user->identity;
// Настройка
$model->ocid = $id ?? null;
$model->catn = (string) $product->Артикул;
$model->desc = (string) $product->Описание;
$model->onec = self::xml2array($product->xml);
if (isset($model->onec['ЗначенияСвойств'])) {
// Свойства инициализированы
foreach ($model->onec['ЗначенияСвойств'] as $property) {
// Перебор всех свойств
if (is_array($property)) {
if (!empty($account->opts['import_sections_oem']) && $property['Ид'] === $account->opts['import_sections_oem']) {
// Если идентификатор свойства совпадает с указанным в настройках свойства хранящего OEM номера
// Настройка
$model->oemn = array_merge(self::searchOemn($property['Значение']), self::searchOemn((string) $product->Артикул));
}
}
}
}
// Запись
if ($model->save()) {
// Поставка успешно сохранена
return $model;
}
return null;
}
/**
* offers.xml > ПакетПредложений > Предложения > Предложение > ХарактеристикиТовара > ХарактеристикаТовара
*
* Характеристики товара
* $name - Наименование
* $value - Значение
*
* @param \Zenwalker\CommerceML\Model\Simple $specification
* @return void
* Запись цены из 1С
*/
public function setSpecification1c($specification)
public function setPrice1c($price): mixed
{
// $specificationModel = Specification::createByMl($specification);
// $this->addPivot($specificationModel, PvOfferSpecification::class, ['value' => (string)$specification->Значение]);
return true;
}
public function getExportFields1c($context = null)
/**
* Запись данных на случай ошибки при экспорте из 1С
*/
public function setRaw1cData($cml, $object): bool
{
return [
'Ид' => 'id',
'Наименование' => 'login',
'ПолноеНаименование' => 'full_name',
'Фамилия' => 'surname',
'Имя' => 'name',
'Контакты' => [
[
'@name' => 'Контакт',
'Тип' => 'Почта',
'Значение' => $this->email,
],
[
'@name' => 'Контакт',
'Тип' => 'ТелефонРабочий',
'Значение' => $this->phone,
],
],
];
return true;
}
public static function writeEdgeBetweenAccount(string $from, string $to): bool
/**
* Поиск по идентификатору из 1С
*
* @param string $ocid Идентификатор из 1С
*
* @return Supply|null
*/
public static function searchByOcid(string $ocid): ?Supply
{
// Инициализация
$edge = new AccountEdgeSupply();
// Настройка
$edge->_from = $from;
$edge->_to = $to;
// Запись
return $edge->save();
return static::findOne([static::getIdFieldName1c() => $ocid]);
}
private static function writeEdgeBetweenProduct(string $from, string $to): bool
/**
* Чтение группы из 1С
*/
public function getGroup1c(): ?SupplyGroup
{
// Инициализация
$edge = new SupplyEdgeProduct();
// Настройка
$edge->_from = $from;
$edge->_to = $to;
// Запись
return $edge->save();
return $this->group;
}
private static function writeEdgeBetweenGroup(string $from, string $to): bool
/**
* Чтение названия поля в котором хранится идентификатор из 1С
*/
public static function getIdFieldName1c(): string
{
// Инициализация
$edge = new SupplyEdgeSupplyGroup();
// Настройка
$edge->_from = $from;
$edge->_to = $to;
// Запись
return $edge->save();
return 'ocid';
}
private static function writeEdgeBetweenRequisite(string $from, string $to): bool
/**
* Поиск по OEM-номерам
*
* @todo Реализовать с помощью LIKE
*/
public static function searchByOemn(): array
{
// Инициализация
$edge = new SupplyEdgeRequisite();
// Настройка
$edge->_from = $from;
$edge->_to = $to;
// Запись
return $edge->save();
return [];
}
public static function readByCatn(string $catn): ?Product
/**
* Прочитать стоимость
*/
public function readCost(Product $product = null): array
{
return Product::findOne(['catn' => $catn]);
if (isset($product)) {
return SupplyEdgeProduct::searchByVertex($this->readId(), $product->readId(), type: 'connect', limit: 1)['onec']['Цены']['Цена'];
}
return SupplyEdgeProduct::search($this->readId(), type: 'connect', limit: 1)['onec']['Цены']['Цена'];
}
}

View File

@@ -1,11 +1,79 @@
<?php
declare(strict_types=1);
namespace app\models;
class SupplyEdgeProduct extends Edge
use app\models\traits\Xml2Array;
use carono\exchange1c\interfaces\OfferInterface;
use Zenwalker\CommerceML\Model\Offer;
class SupplyEdgeProduct extends Edge implements OfferInterface
{
public static function collectionName()
use Xml2Array;
public static function collectionName(): string
{
return 'supply_edge_product';
}
public function attributes(): array
{
return array_merge(
parent::attributes(),
[
'ocid',
'onec'
]
);
}
public function attributeLabels(): array
{
return array_merge(
parent::attributeLabels(),
[
'ocid' => 'Идентификатор 1C (ocid)',
'onec' => 'Данные 1C'
]
);
}
public static function createPriceTypes1c($types): mixed
{
return true;
}
public function setPrice1c($price): mixed
{
return true;
}
public function setSpecification1c($specification): mixed
{
return true;
}
public function getExportFields1c($context = null): array
{
return [];
}
public function getGroup1c(): ProductGroup
{
return $this->group ?? new ProductGroup;
}
public static function readByOnecId(string $ocid): ?Supply
{
return self::findOne([self::getIdFieldName1c() => $ocid]);
}
/**
* Название поля в котором хранится ID из 1C
*/
public static function getIdFieldName1c(): string
{
return 'ocid';
}
}

View File

@@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
namespace app\models;
class SupplyEdgeRequisite extends Edge
{
public static function collectionName()
public static function collectionName(): string
{
return 'supply_edge_requisite';
}

View File

@@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
namespace app\models;
class SupplyEdgeSupply extends ProductEdgeProduct
{
public static function collectionName()
public static function collectionName(): string
{
return 'supply_edge_supply';
}

View File

@@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
namespace app\models;
class SupplyEdgeSupplyGroup extends ProductEdgeProductGroup
{
public static function collectionName()
public static function collectionName(): string
{
return 'supply_edge_supply_group';
}

View File

@@ -1,42 +1,19 @@
<?php
declare(strict_types=1);
namespace app\models;
/**
* Группировка поставок
*/
class SupplyGroup extends ProductGroup
{
public static function collectionName()
/**
* Имя коллекции
*/
public static function collectionName(): string
{
return 'supply_group';
}
protected static function writeEdgeBetweenMember(string $from, string $to): bool
{
// Инициализация
$edge = new SupplyEdgeSupplyGroup();
// Настройка
$edge->_from = $from;
$edge->_to = $to;
// Запись
return $edge->save();
}
protected static function writeEdgeBetweenGroup(string $from, string $to): bool
{
// Инициализация
$edge = new SupplyGroupEdgeSupplyGroup();
// Настройка
$edge->_from = $from;
$edge->_to = $to;
// Запись
return $edge->save();
}
public static function readByOnecName(string $onec_name): ?Product
{
return static::findOne(['onec_name' => $onec_name]);
}
}

View File

@@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
namespace app\models;
class SupplyGroupEdgeSupplyGroup extends Edge
{
public static function collectionName()
public static function collectionName(): string
{
return 'supply_group_edge_supply_group';
}

View File

@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace app\models\traits;
use yii;
use exception;
trait SearchByEdge
{
/**
* Поиск через связи рёбрами с аккаунтом
*
* @param string $id Идентификатор пользователя
* @param int $limit Количество
* @param int $offset Сдвиг
* @param string $sort Сортировка
*/
public static function searchByEdge(
string $from,
string $to,
string|null $edge = null,
int|null $limit = 10,
int|null $offset = 0,
array $sort = ['ASC'],
string|array $subquery_where = [],
array $foreach = [],
string|array $where = [],
string $direction = 'ANY',
array|null $let = [],
string|array $select = null,
callable|null $handle = null,
array $params = []
): mixed {
$subquery = static::find()
->params($params)
->for([$from, $edge ?? $from . '_edge_' . $to])
->traversal($to, $direction)
->in($edge ?? $from . '_edge_' . $to)
->where($subquery_where);
$subquery = $subquery->select($edge ?? $from . '_edge_' . $to)
->createCommand();
$query = static::find()
->params($subquery->getBindVars())
->let($edge ?? $from . '_edge_' . $to, '(' . (string) $subquery . ')')
->limit($limit)
->offset($offset)
->orderBy($sort);
if (!empty($let)) {
// Переданы дополнительные условия фильтрации
$query->let(...$let);
}
// Запрос
$request = $query
->foreach($foreach)
->where($where)
->select($select ?? $to);
if (isset($handle)) {
// Передана функция для постобработки
return $handle($request);
} else if (isset($select)) {
$response = $request->createCommand()->execute()->getAll();
foreach ($response as &$attribute) {
// Приведение всех свойств в массив и очистка от лишних данных
$attribute = $attribute->getAll();
}
return $response;
} else {
return $request->all();
}
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace app\models\traits;
trait Xml2Array {
protected static function xml2array($xmlObject, $out = [])
{
foreach ((array) $xmlObject as $index => $node)
$out[$index] = (is_object($node) || is_array($node)) ? self::xml2array($node) : $node;
return $out;
}
}

View File

@@ -1,13 +1,19 @@
<?php
declare(strict_types=1);
use yii\helpers\Html;
use yii\bootstrap\ActiveForm;
use app\models\AccountForm;
/**
* @todo Восстановить сохранение сессии
*/
?>
<div class="container d-flex flex-column h-100">
<div class="row my-auto">
<div class="container">
<div class="row">
<div class="mx-auto">
<?php
$form = ActiveForm::begin([
@@ -26,7 +32,7 @@ use app\models\AccountForm;
$model = $model ?? new AccountForm;
?>
<?= $form->field($model, 'mail', ['enableLabel' => false])->textInput(['autofocus' => true, 'placeholder' => $model->getAttributeLabel('mail')]) ?>
<?= $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')]) ?>
<div class="d-flex mb-2 mt-3">

View File

@@ -0,0 +1,30 @@
<?php
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>
<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>

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
use yii;
?>
<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> -->
</div>
</div>

View File

@@ -0,0 +1,83 @@
<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">
<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);"/>
</div>
<div class="col-2">
<span>Артикул</span>
</div>
<div class="col-4">
<span>Описание</span>
</div>
<div class="col-1 ml-auto px-0 text-center">
<span>Количество</span>
</div>
<div class="col-2 text-right">
<span>Доставка</span>
</div>
<div class="col-2 mr-3 text-right">
<span>Стоимость</span>
</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;
}
} else {
echo <<<HTML
<div class="row py-2 cart_list_target">
<div class="mx-auto py-2">
Корзина пуста
</div>
</div>
HTML;
}
?>
</div>
<div class="row mb-2 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>
</select>
<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>
</article>
</div>
<script src="/js/cart.js" defer></script>

View File

@@ -1,45 +1,46 @@
<?php
/* @var $this yii\web\View */
declare(strict_types=1);
$this->title = 'SkillParts';
?>
<link href="/css/ticker.css" rel="stylesheet">
<div id="page_index">
<div class="info_panel mb-4">
<div id="page_index" class="mb-auto">
<section class="info_panel mb-4">
<div class="container h-100 d-flex flex-column justify-content-center">
<p class="mb-4 gilroy">Проблема с подбором запчастей?</p>
<p class="d-flex">
<span class="p-2 px-3 button_call_icon"><i class="fas fa-phone-alt text-white"></i></span>
<h1 class="mb-4 ml-0 gilroy">Проблема с подбором запчастей?</h1>
<p class="ml-0 d-flex">
<span class="p-2 px-3 button_call_icon d-flex"><i class="fas fa-phone-alt my-auto text-white"></i></span>
<a class="btn text-white button_clean button_blue button_call" href="/call" role="button">Связаться с менеджером</a>
</p>
</div>
<div class="h-100 d-flex flex-column justify-content-end">
<img class="img-fluid" src="/img/photos/compressed/963K_cutted.webp" alt="Связаться с менеджером">
</div>
</div>
</section>
<div class="h-100 d-flex ticker">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h100px/compressed/cummins.png" alt="Cummins">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h100px/compressed/iveco.png" alt="Iveco">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h100px/compressed/komatsu.png" alt="Komatsu">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h100px/compressed/case.png" alt="Case">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h100px/compressed/isuzu.png" alt="Isuzu">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/new_holland.svg" alt="New Holland">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h100px/compressed/perkins.png" alt="Perkins">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h100px/compressed/john_deere.png" alt="John Deere">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h100px/compressed/caterpillar.png" alt="Caterpillar">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h100px/compressed/shantui.png" alt="Shantui">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h100px/compressed/xcmg.png" alt="XCMG">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h100px/compressed/kobelco.png" alt="Kobelco">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h100px/compressed/shehwa.png" alt="SHEHWA">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h100px/compressed/bomag.png" alt="BOMAG">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h100px/compressed/hitachi.png" alt="Hitachi">
</div>
<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>
<div class="container mb-4">
<section class="container mb-4">
<!-- <div class="row mb-3">
<h4 class="col gilroy categories_blocks_panel_title">Сопутствующие товары</h4>
</div> -->
@@ -80,7 +81,7 @@ $this->title = 'SkillParts';
</div>
</div>
</div>
</div>
</section>
</div>
<script src="/js/ticker.js" defer></script>

View File

@@ -1,9 +1,9 @@
<?php
/* @var $this \yii\web\View */
/* @var $content string */
declare(strict_types=1);
use yii\helpers\Html;
use app\assets\AppAsset;
AppAsset::register($this);
@@ -19,6 +19,25 @@ AppAsset::register($this);
<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">
<?php $this->registerCsrfMetaTags() ?>
<title><?= Html::encode($this->title ?? 'SkillParts') ?></title>
<?php $this->head() ?>
@@ -27,19 +46,23 @@ AppAsset::register($this);
<body>
<?php $this->beginBody() ?>
<header class="container pt-2 mt-1 mb-4">
<div id="notifications_popup_wrap" class="col-3 m-4"></div>
<header class="container pt-2 mt-1 mb-2 mb-sm-4">
<div class="row h-100">
<a class="col-3 h-100 py-2" title="SkillParts" href="/" role="button" onclick="return page_main();">
<img class="h-100" src="/img/logos/skillparts.svg" alt="SkillParts">
</a>
<div class="col-6 px-0 mt-auto d-flex">
<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();">
<img class="h-100" src="/img/logos/skillparts.svg" alt="SkillParts">
</a>
</div>
<div class="col px-0 mt-auto d-flex h-title">
<div class="ml-auto p-0 h-divider-title-left"></div>
<div class="px-4 d-flex flex-column justify-content-center h-divider-title">
<h6 class="text-center text-white my-0"><b>Запчасти для спецтехники</b></h6>
</div>
<div class="mr-auto p-0 h-divider-title-right"></div>
</div>
<nav class="col-3 mt-auto d-flex justify-content-end"></nav>
<menu class="col-auto col-lg-4 mb-0 d-flex justify-content-end"></menu>
</div>
<div class="h-divider"></div>
</header>
@@ -47,11 +70,11 @@ AppAsset::register($this);
<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="dropdownMenuButton" 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 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="dropdownMenuButton">
<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>
@@ -65,15 +88,33 @@ AppAsset::register($this);
<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">
<input type="text" class="form-control col-8 col-lg-10 catalog_search_line" id="productNumber" placeholder="Введите номер запчасти, например: 45223503481">
<button type="submit" class="col btn button_clean catalog_search_button">ПОИСК</button>
<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">
<?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>
<button type="submit" class="col btn button_clean catalog_search_button" onclick="product_search(this.parentElement.getElementsByTagName('input')[0].value, 1)">ПОИСК</button>
</form>
</div>
</div>
</aside>
<main class="col p-0">
<main class="col p-0 d-flex flex-column justify-content-center">
<?= $content ?>
</main>

View File

@@ -0,0 +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();">
<?php
if (empty($notifications_new_amount) || $notifications_new_amount < 1) {
// Новые уведомления не найдены
echo <<<HTML
<i class="fas fa-bell my-auto mx-2"></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>
HTML;
}
?>
</a>

View File

@@ -0,0 +1,30 @@
<?php
if (empty($notifications)) {
echo <<<HTML
<p class="px-2 py-4 text-center">Уведомлений нет</p>
HTML;
return;
}
foreach($notifications as $notification) {
// Перебор уведомлений
// Инициализация
$notification = $notification->getAttributes();
if ($notification['type'] === 'notice') {
// Уведомление
echo $notification['html'];
} else if ($notification['type'] === 'warning') {
// Предупреждение
echo $notification['html'];
} else {
// Неизвестно
echo $notification['html'] ?? '<p>ОШИБКА</p>';
}
}
?>

View File

@@ -0,0 +1,45 @@
<?php
if (empty($notifications)) {
// Уведомления не найдены
$content = <<<HTML
<p class="px-2 py-4 text-center">Уведомлений нет</p>
HTML;
} else {
// Уведомления найдены
// Инициализация
$content = '';
foreach ($notifications as $notification) {
// Перебор уведомлений
// Инициализация
$notification = $notification->getAttributes();
if ($notification['type'] === 'notice') {
// Уведомление
$content .= $notification['html'];
} else if ($notification['type'] === 'warning') {
// Предупреждение
$content .= $notification['html'];
} else {
// Неизвестно
$content .= $notification['html'] ?? '<p>ОШИБКА</p>';
}
}
}
if($notifications_panel_full ?? false) {
echo <<<HTML
<div id="notification_button_panel" class="dropdown-menu p-2" aria-labelledby="notification_button">
$content
</div>
HTML;
} else {
echo $content;
}
?>

View File

@@ -0,0 +1,10 @@
<?php
$id = 'popup/' . $notification->readId();
$html = $notification->html;
echo <<<HTML
<div id="$id" class="mt-3 p-0 notification rounded" onmouseleave="return notification_popup_delete(this);">
$html
</div>
HTML;
?>

View File

@@ -0,0 +1,13 @@
<?php
if($amount > 0) {
echo <<<HTML
<p>Импортировано $amount товаров из excel-документа <span>($date)</span></p>
HTML;
} else {
echo <<<HTML
<p>Неудачная попытка импорта товаров из excel-документа <span>($date)</span></p>
HTML;
}
?>

View File

@@ -0,0 +1,2 @@
<p>Импортированы товары из 1C <span>(<?= $date ?>)</span></p>

View File

@@ -0,0 +1,64 @@
<link href="/css/pages/orders.css" rel="stylesheet">
<div id="page_orders" class="container mb-auto py-3">
<article class="py-3 px-4 rounded">
<h4 class="ml-4 mt-2 mb-4"><i class="fas fa-list 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);" />
</div>
<div class="col-2 ml-auto">
<span>Статус</span>
</div>
<div class="col-2 mr-3 text-right">
<span>Время</span>
</div>
</div>
<?php
if (isset($orders) && !empty($orders)) {
foreach ($orders as $order) {
// Перебор заказов
// Инициализация
extract($order);
// Инициализация
$date = empty($jrnl) ? '' : date('H:i d.m.Y', end($jrnl)['date']);
echo <<<HTML
<div class="row py-2 cart_list_target">
<div class="pl-3 mr-1">
<input type="checkbox" onchange="return cart_list_checkbox(this);"/>
</div>
<div class="col-2 ml-auto">
{$account_edge_order[0]['type']}
</div>
<div class="col-2 mr-3 text-right">
$date
</div>
</div>
HTML;
}
} else {
echo <<<HTML
<div class="row py-2 cart_list_target">
<div class="mx-auto py-2">
Заказов нет
</div>
</div>
HTML;
}
?>
</div>
<div class="row mb-2 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>
</select>
</div>
</article>
</div>
<script src="/js/orders.js" defer></script>

View File

@@ -1,18 +1,186 @@
<?php
use yii\bootstrap\ActiveForm;
use yii\helpers\Html;
use app\models\Product;
?>
<div id="page_product" class="h-100">
<div class="container h-100">
<div class="row h-100 py-3">
<div class="col-12">
<div class="block_main h-100 p-3 rounded">
<h4 class="ml-4"><?php echo $model['name'] ?></h4>
<div class="dropdown-divider"></div>
<p class="ml-4">Дата создания: <?php echo date('d.m.Y H:i', $model['date']) ?></p>
<pre class="ml-4"><?php var_dump($model) ?></pre>
<link href="/css/pages/product.css" rel="stylesheet">
<div id="page_product" class="container mb-auto">
<div class="row py-3">
<article class="col-12">
<div class="p-3 d-flex flex-column rounded">
<div id="product_slider" class="row px-3 profile_panel">
<div class="col-1 product_slider_preview p-0 pr-3 mb-3">
<?php
foreach ($model['imgs'] ?? [null] as $key => $image) {
// Перебор изображений
// Инициализация
$h150 = $image['h150'] ?? '/img/covers/h150/product.png';
// Генерация предпросмотра изображения
echo <<<HTML
<label class="p-0 mb-2" for="product_slider_image_$key">
<img class="img-fluid rounded" src="$h150"/>
</label>
HTML;
}
?>
<?php if (
!yii::$app->user->isGuest
&& (yii::$app->user->identity->type === 'administrator'
|| yii::$app->user->identity->type === 'moderator')
) : ?>
<input id="product_slider_image_new" type="file" class="d-none" onchange="return product_panel_images_write('<?= $model['catn'] ?>', this);" multiple />
<label class="p-0 mb-2" for="product_slider_image_new" role="button">
<img class="img-fluid rounded" src="/img/covers/h150/product_new.png" />
</label>
<?php endif ?>
</div>
<div class="product_slider_image">
<?php
// Инициализация
$imgs = $model['imgs'] ?? [null];
$checked = '';
foreach ($model['imgs'] ?? [null] as $key => $image) {
// Перебор изображений
// Инициализация
$name = $image['name'] ?? 'Без названия';
$orig = $image['orig'] ?? '/img/covers/product.png';
$covr = $image['covr'] ?? false;
if ($covr || count($imgs) < 2) {
// Если это изображение является обложкой
// Реинициализация
$checked = 'checked';
}
// Генерация изображения
echo <<<HTML
<input type="radio" id="product_slider_image_$key" name="slider" $checked/>
<div class="col p-0">
<img class="img-fluid rounded" src="$orig"/>
</div>
HTML;
// Деинициализация
$checked = '';
}
?>
</div>
<div class="col ml-4 d-flex flex-column">
<div class="row mb-1">
<?php if (
!yii::$app->user->isGuest
&& (yii::$app->user->identity->type === 'administrator'
|| yii::$app->user->identity->type === 'moderator')
) : ?>
<h3 id="title_<?= $model['catn'] ?>" class="my-auto pointer-event" role="button" onclick="return product_panel_title_edit('<?= $model['catn'] ?>', this);"><?= $model['name'] ?? 'Без названия' ?></h3>
<?php else : ?>
<h3 id="title_<?= $model['catn'] ?>" class="my-auto"><?= $model['name'] ?? 'Без названия' ?></h3>
<?php endif ?>
<?php if (
!yii::$app->user->isGuest
&& (yii::$app->user->identity->type === 'administrator'
|| yii::$app->user->identity->type === 'moderator')
) : ?>
<h5 id="catn_<?= $model['catn'] ?>" class="ml-auto my-auto d-flex pointer-event" role="button" onclick="return product_panel_catn_edit('<?= $model['catn'] ?>', this, true);">
<?= $model['catn'] ?>
<!-- <small class="d-flex">
<a class="text-dark my-auto ml-3" title="Редактировать" role="button" onclick="return product_panel_edit('<?= $model['catn'] ?>');">
<i class="fas fa-edit"></i>
</a>
<a class="text-dark my-auto ml-2" title="Удалить" role="button" onclick="return product_panel_delete('<?= $model['catn'] ?>');">
<i class="fas fa-trash-alt"></i>
</a>
</small> -->
</h5>
<?php else : ?>
<h5 id="catn_<?= $model['catn'] ?>" class="ml-auto my-auto d-flex">
<?= $model['catn'] ?>
</h5>
<?php endif ?>
</div>
<div class="dropdown-divider px-0 mb-3"></div>
<div class="row mb-3 h-100 product_panel d-flex flex-column">
<?php if (
!yii::$app->user->isGuest
&& (yii::$app->user->identity->type === 'administrator'
|| yii::$app->user->identity->type === 'moderator')
) : ?>
<p id="description_<?= $model['catn'] ?>" class="mt-0 ml-0 text-break pointer-event product_description" role="button" onclick="return product_panel_description_edit('<?= $model['catn'] ?>', this);"><?= $model['desc'] ?? 'Без описания' ?></p>
<?php else : ?>
<p id="description_<?= $model['catn'] ?>" class="mt-0 ml-0 text-break product_description"><?= $model['name'] ?? 'Без описания' ?></p>
<?php endif ?>
<p class="mt-0">
<?php
// foreach ($model['catn'] ?? [] as $catn) {
// echo <<<HTML
// $catn
// HTML;
// }
?>
</p>
</div>
<?php
$form = ActiveForm::begin([
'id' => 'form_product_cart',
'action' => false,
'fieldConfig' => [
'template' => '{input}',
],
'options' => [
'onsubmit' => 'return false;',
'class' => 'row mt-auto'
]
]);
// Просто для теста
$model = new Product();
?>
<div class="col-6 px-0 mr-4 btn-group">
<?= Html::submitButton('В корзину', ['name' => 'cartWrite', 'onclick' => 'product_cart_write(this.parentElement);', 'class' => 'col-10 btn button_blue button_clean py-2 px-5']) ?>
<?= $form->field($model, 'amount', ['options' => ['class' => 'col h-100 m-0 form-group']])->textInput(['value' => '1', 'class' => 'form-control h-100 rounded-0 text-center button_clean']); ?>
</div>
<div>
<p class="mt-0">
<strong>Хабаровск: </strong>
в наличии
</p>
<p class="mb-0">
Доставим: завтра
</p>
</div>
<?php ActiveForm::end(); ?>
</div>
</div>
<!-- <div class="row mt-3 mx-0">
<p class="ml-0">Время для повышения релевантности в поисковиках</p>
<time class="ml-auto"></time>
</div> -->
</div>
</div>
</article>
</div>
</div>
</div>
<script src="/js/product.js" defer></script>
<?php if (
!yii::$app->user->isGuest
&& (yii::$app->user->identity->type === 'administrator'
|| yii::$app->user->identity->type === 'moderator')
) : ?>
<script src="/js/product_panel.js" defer></script>
<?php endif ?>

View File

@@ -1,61 +1,105 @@
<?php
declare(strict_types=1);
use yii;
use yii\bootstrap\ActiveForm;
use app\controllers\ProfileController;
use app\models\Product;
use app\models\Supply;
// Инициализация
if (
!yii::$app->user->isGuest
&& yii::$app->user->identity->agnt
) {
$panel ?? $panel = 'profile_panel_settings_import';
} else {
$panel ?? $panel = 'profile_panel_settings_account';
}
?>
<link href="/css/pages/profile.css" rel="stylesheet">
<div id="page_profile" class="h-100">
<div class="container h-100">
<div class="row h-100 py-3">
<div class="col-3">
<div class="block_sidebar h-100 p-3 rounded">
<div class="d-flex">
<p>Почта: </p>
<p class="ml-auto"><?php echo Yii::$app->user->identity->mail ?></p>
<div id="page_profile" class="container mb-auto">
<div class="row h-100 py-3">
<nav class="col-3">
<?= $sidebar ?? yii::$app->controller->renderPartial('/profile/sidebar') ?>
</nav>
<article class="col-9">
<div class="p-4 rounded">
<h4 class="ml-4 mb-4"><i class="fas fa-sliders-h my-auto mr-2"></i>Настройки</h4>
<div id="profile_panel_settings" class="profile_panel">
<div class="profile_panel_menu mb-3">
<label class="btn button_white mb-0 mr-2" for="profile_panel_settings_account">Аккаунт</label>
<?php
if (
!yii::$app->user->isGuest
&& yii::$app->user->identity->agnt
) {
echo <<<HTML
<label class="btn button_white mb-0 mr-2" for="profile_panel_settings_company">Компания</label>
<label class="btn button_white mb-0" for="profile_panel_settings_import">Импорт</label>
HTML;
}
?>
</div>
<div class="profile_panel_content d-flex">
<input type="radio" id="profile_panel_settings_account" name="main_panel" <?= $panel === 'profile_panel_settings_account' ? 'checked' : null ?> />
<div class="col">
1
</div>
<?php if (
!yii::$app->user->isGuest
&& yii::$app->user->identity->agnt
) : ?>
<input type="radio" id="profile_panel_settings_company" name="main_panel" <?= $panel === 'profile_panel_settings_company' ? 'checked' : null ?> />
<div class="col">
2
</div>
<input type="radio" id="profile_panel_settings_import" name="main_panel" <?= $panel === 'profile_panel_settings_import' ? 'checked' : null ?> />
<div class="col">
<h5>Параметры 1C</h5>
<div class="dropdown-divider mb-3"></div>
<?php $form = ActiveForm::begin([
'id' => 'form_profile_settings',
'action' => false,
'fieldConfig' => [
'template' => '{label}{input}',
],
'options' => [
'onsubmit' => 'return false;'
]
]);
// Инициализация
$model ?? $model = yii::$app->user->identity;
$list or $list = ['Нет данных'];
?>
<?= $form->field($model, 'opts[import_sections_oem]', ['options' => ['class' => "mb-1"]])
->dropDownList($list, [
'onChange' => 'page_profile_settings(this.parentElement.parentElement, \'profile_panel_settings_import\')',
'disabled' => count($list) <= 1
])->label('OEM-номера'); ?>
<small class="d-block mb-1">Выберите поле в котором хранятся <b>ОЕМ-номера</b> и повторите импорт</small>
<small class="d-block">Значения взяты из импортированных товаров</small>
<?php ActiveForm::end(); ?>
</div>
<?php endif ?>
</div>
</div>
</div>
<div class="col-9">
<div class="block_main h-100 p-3 rounded">
<h4 class="ml-4">Личный кабинет</h4>
<div class="dropdown-divider"></div>
<p>Не знаю что сюда пока добавить</p>
<?php
$form = ActiveForm::begin([
'id' => 'form_product_import_excel',
'action' => false,
'fieldConfig' => [
'template' => '{label}{input}',
'options' => ['class' => '']
],
'options' => [
'class' => 'mb-3',
'onsubmit' => 'return false;'
]
]);
$model = $model ?? new Supply;
$groups = ProfileController::readGroups();
?>
<?= $form->field($model, 'group', ['options' => ['class' => "mb-3"]])->dropDownList($groups ?? ['Нет данных']); ?>
<?= $form->field($model, 'file', ['enableLabel' => false])->fileInput(['multiple' => true, 'onChange' => 'supply_import(this.parentElement.parentElement)']) ?>
<?= $form->errorSummary($model, ['header' => 'В документе были допущены ошибки:' /*, 'footer' => 'Исправьте их и попробуйте снова'*/]); ?>
<?php ActiveForm::end(); ?>
<p>Всего товаров: <?php echo Product::readAmount() ?></p>
<p>Всего поставок: <?php echo Supply::readAmount() ?></p>
</div>
</div>
</div>
</article>
</div>
</div>
<script src="/js/profile.js" defer></script>
<script src="/js/profile.js" defer></script>
<?php if (
!yii::$app->user->isGuest
&& yii::$app->user->identity->type === 'administrator'
|| yii::$app->user->identity->type === 'moderator'
) : ?>
<script src="/js/profile_panel.js" defer></script>
<?php endif ?>

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
// Инициализация
$panel ?? $panel = 'profile_panel_monitoring_input_search_history';
?>
<link href="/css/pages/profile.css" rel="stylesheet">
<div id="page_profile" class="container mb-auto">
<div class="row h-100 py-3">
<nav class="col-3">
<?= $sidebar ?? Yii::$app->controller->renderPartial('/profile/sidebar') ?>
</nav>
<article class="col-9">
<div class="p-4 rounded">
<h4 class="ml-3 mb-4"><i class="fas fa-eye my-auto mr-2"></i>Мониторинг</h4>
<div id="profile_panel_monitoring" class="profile_panel">
<div class="profile_panel_menu mb-3">
<label class="btn button_white mb-0 mr-2" for="profile_panel_monitoring_input_orders_history">Заказы</label>
<label class="btn button_white mb-0 mr-2" for="profile_panel_monitoring_input_search_history">Поисковые запросы</label>
<label class="btn button_white mb-0" for="profile_panel_monitoring_input_log">Журнал действий</label>
</div>
<div class="profile_panel_content">
<input type="radio" id="profile_panel_monitoring_input_orders_history" name="main_panel" <?= $panel === 'profile_panel_monitoring_input_orders_history' ? 'checked' : null ?> />
<div class="col p-3">
История заказов в разработке
</div>
<input type="radio" id="profile_panel_monitoring_input_search_history" name="main_panel" <?= $panel === 'profile_panel_monitoring_input_search_history' ? 'checked' : null ?> />
<div class="col rounded overflow-hidden">
<div class="row header_blue py-2">
<div class="col-sm-4 col-md-3">IPv4</div>
<div class="col-7 col-sm-3 col-md-4 col-lg-6 pr-0 px-sm-0 px-lg-3">Поисковый запрос</div>
<div class="col">Время</div>
</div>
<?php
foreach ($search_history ?? [] as $row) {
// Инициализация
$time = $row->jrnl;
$date = empty($row->jrnl) ? '' : date('H:i d.m.Y', end($time)['date']);
echo <<<HTML
<div class="row py-1">
<div class="col-sm-4 col-md-3">$row->ipv4</div>
<div class="col-7 col-sm-3 col-md-4 col-lg-6 pr-0 px-sm-0 px-lg-3">$row->text</div>
<div class="col">$date</div>
</div>
HTML;
}
?>
<div class="mt-3">
<?php
$page = ($page_search_history ?? 0) + 1;
echo <<<HTML
<div class="row">
<button class="btn button_clean button_blue" onclick="return page_profile_monitoring(-1, 'profile_panel_monitoring_input_search_history');">Назад</button>
<p>$page</p>
<button class="btn button_clean button_blue" onclick="return page_profile_monitoring(1, 'profile_panel_monitoring_input_search_history');">Вперёд</button>
<div class="col-8"></div>
</div>
HTML;
?>
</div>
</div>
<input type="radio" id="profile_panel_monitoring_input_log" name="main_panel" <?= $panel === 'profile_panel_monitoring_input_log' ? 'checked' : null ?> />
<div class="col p-3">
Журнал в разработке
</div>
</div>
</div>
</div>
</article>
</div>
</div>
<script src="/js/profile.js" defer></script>
<?php if (
!yii::$app->user->isGuest
&& (yii::$app->user->identity->type === 'administrator'
|| yii::$app->user->identity->type === 'moderator')
) : ?>
<script src="/js/profile_panel.js" defer></script>
<?php endif ?>

View File

@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
use yii;
use yii\bootstrap\ActiveForm;
use yii\helpers\Html;
use app\models\Notification;
// Инициализация
$panel ?? $panel = 'profile_panel_panel_input_notifications';
?>
<link href="/css/pages/profile.css" rel="stylesheet">
<div id="page_profile" class="container mb-auto">
<div class="row h-100 py-3">
<nav class="col-3">
<?= $sidebar ?? yii::$app->controller->renderPartial('/profile/sidebar') ?>
</nav>
<article class="col-9">
<div class="p-4 rounded">
<h4 class="ml-4 mb-4"><i class="fas fa-user-shield my-auto mr-2"></i>Панель управления</h4>
<div id="profile_panel_panel" class="profile_panel">
<div class="profile_panel_menu mb-3">
<label class="btn button_white mb-0 mr-2" for="profile_panel_panel_input_notifications">Уведомления</label>
<label class="btn button_white mb-0 mr-2" for="profile_panel_panel_input_settings">Настройки</label>
</div>
<div class="profile_panel_content">
<input type="radio" id="profile_panel_panel_input_notifications" name="main_panel" <?= $panel === 'profile_panel_panel_input_notifications' ? 'checked' : null ?>/>
<div class="col">
<h5>Отправка уведомления</h5>
<div class="dropdown-divider mb-3"></div>
<?php
$form = ActiveForm::begin([
'id' => 'form_profile_panel_notifications',
'action' => false,
'fieldConfig' => [
'template' => '{label}{input}'
],
'options' => [
'onsubmit' => 'return false;'
]
]);
// Значения по умолчанию
$model_notifications ?? $model_notifications = new Notification;
$model_notifications->account ?? $model_notifications->account = null;
$model_notifications->text ?? $model_notifications->text = '';
?>
<?= $form->errorSummary($model_notifications) ?>
<div class="row">
<?= $form->field($model_notifications, 'account', ['options' => ['class' => "mb-1 col-9"]])->input('text', ['placeholder' => yii::$app->user->identity->_key]); ?>
<?= $form->field($model_notifications, 'type', ['options' => ['class' => "col pl-0"]])->dropDownList($model_notifications->typs); ?>
</div>
<small class="d-block mb-1"><b>Множественная отправка:</b> @all или перечислить через запятую</small>
<small class="d-block mb-3"><b>Тестирование:</b> @test или оставить поле пустым</small>
<?= $form->field($model_notifications, 'text', ['options' => ['class' => "mb-3"]])->textarea(); ?>
<?= Html::submitButton('Отправить', ['name' => 'submitNotification', 'onclick' => 'page_profile_panel_notification_create(this.parentElement);', 'class' => 'flex-grow-1 mr-2 btn button_white button_clean']) ?>
<?= Html::submitButton('Отправить как HTML', ['name' => 'submitNotification', 'onclick' => 'page_profile_panel_notification_create(this.parentElement, 1);', 'class' => 'flex-grow-1 mr-2 btn button_white button_clean']) ?>
<?php ActiveForm::end(); ?>
</div>
<input type="radio" id="profile_panel_panel_input_settings" name="main_panel" <?= $panel === 'profile_panel_panel_input_settings' ? 'checked' : null ?>/>
<div class="col">
<?php
$form = ActiveForm::begin([
'id' => 'form_profile_panel_settings_search_period',
'action' => false,
'fieldConfig' => [
'template' => '{label}{input}',
],
'options' => [
'onsubmit' => 'return false;',
'class' => 'mb-4'
]
]);
?>
<?= $form->errorSummary($model_settings, ['header' => 'Получены ошибки:']) ?>
<?= $form->field($model_settings, 'search_period', ['options' => ['class' => "mb-1"]])->textInput(['value' => $model_settings['search_period'], 'onChange' => 'page_profile_panel_settings(this.parentElement.parentElement, \'profile_panel_panel_input_settings\')']); ?>
<small class="d-block mb-1">Время которое надо ждать для повторного поиска в секундах</small>
<?php ActiveForm::end(); ?>
<?php
$form = ActiveForm::begin([
'id' => 'form_profile_panel_settings_search_connect_keep',
'action' => false,
'fieldConfig' => [
'template' => '{label}{input}',
],
'options' => [
'onsubmit' => 'return false;'
]
]);
$list = ['false' => 'Нет', 'true' => 'Да'];
if (isset($model_settings->search_connect_keep)) {
// Найден ранее записанный параметр
if (isset($list[$model_settings->search_connect_keep])) {
// Совпадение параметра с параметрами списка найдено
// Буфер для сохранения параметра
$buffer = $list[$model_settings->search_connect_keep];
// Удаление параметра
unset($list[$model_settings->search_connect_keep]);
// Сохранение параметра в начале массива
$list = array_merge([$model_settings->search_connect_keep => $buffer], $list);
} else {
// Совпадение параметра с параметрами списка не найдено
// Сохранение параметра из данных аккаунта в начале массива
$list = array_merge([$model_settings->search_connect_keep => $model_settings->search_connect_keep], $list);
}
}
?>
<?= $form->field($model_settings, 'search_connect_keep', ['options' => ['class' => "mb-1"]])->dropDownList($list, ['onChange' => 'page_profile_panel_settings(this.parentElement.parentElement, \'profile_panel_panel_input_settings\')']); ?>
<small class="d-block mb-1">Удерживать открытое соединение до истечения срока блокировки поиска?</small>
<small class="d-block mb-1">При малой задержке позволяет снизить время загрузки страницы, но при большой будет казаться, что сайт завис</small>
<?php ActiveForm::end(); ?>
</div>
</div>
</div>
</div>
</article>
</div>
</div>
<script src="/js/profile.js" defer></script>
<script src="/js/profile_panel.js" defer></script>

View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
use yii;
use app\models\Product;
use app\models\Supply;
use app\models\AccountEdgeSupply;
use app\models\SupplyEdgeProduct;
?>
<div class="col py-3 rounded">
<div class="row px-3">
<p class="ml-0">Почта: </p>
<p class="mr-0"><?= yii::$app->user->identity->mail ?></p>
</div>
<div class="dropdown-divider my-3"></div>
<dl class="m-0">
<?php
if (
!yii::$app->user->isGuest
&& (yii::$app->user->identity->type === 'administrator'
|| yii::$app->user->identity->type === 'moderator')
) {
// Пользователь является доверенным
// Инициализация
$targetUrl = '/profile/panel';
if ('/' . yii::$app->request->pathInfo === $targetUrl) {
// Запрошена та же страница от которой послан запрос (текущая)
echo <<<HTML
<dt>
<a class="row text-dark button_white button_white_hover px-3 py-3 py-lg-2 font-weight-normal" title="Панель управления сайтом" href="$targetUrl" role="button" onclick="return false;"><i class="fas fa-user-shield my-auto mx-auto mx-lg-0 col-1 pl-0 mr-lg-2"></i><span>Панель управления</span></a>
</dt>
HTML;
} else {
echo <<<HTML
<dt>
<a class="row text-dark button_white px-3 py-3 py-lg-2 font-weight-normal" title="Панель управления сайтом" href="$targetUrl" role="button" onclick="return page_profile_panel_settings();"><i class="fas fa-user-shield my-auto mx-auto mx-lg-0 col-1 pl-0 mr-lg-2"></i><span>Панель управления</span></a>
</dt>
HTML;
}
}
?>
<dt>
<?php
if (yii::$app->user->identity->agnt) {
if (
'/' . yii::$app->request->pathInfo === $targetUrl = '/profile/supplies'
|| '/' . yii::$app->request->pathInfo === $targetUrl = '/profile/import'
) {
// Запрошена та же страница от которой послан запрос (текущая)
echo <<<HTML
<a class="row text-dark button_white button_white_hover px-3 py-3 py-lg-2 font-weight-normal" title="Управление поставками" href="$targetUrl" role="button" onclick="return false;"><i class="fas fa-truck my-auto mx-auto mx-lg-0 col-1 pl-0 mr-lg-2"></i><span>Поставки</span></a>
HTML;
} else {
echo <<<HTML
<a class="row text-dark button_white px-3 py-3 py-lg-2 font-weight-normal" title="Управление поставками" href="$targetUrl" role="button" onclick="return page_profile_supplies();"><i class="fas fa-truck my-auto mx-auto mx-lg-0 col-1 pl-0 mr-lg-2"></i><span>Поставки</span></a>
HTML;
}
}
?>
</dt>
<dt>
<?php
// Инициализация
$targetUrl = '/profile/monitoring';
if ('/' . yii::$app->request->pathInfo === $targetUrl) {
// Запрошена та же страница от которой послан запрос (текущая)
echo <<<HTML
<a class="row text-dark button_white button_white_hover px-3 py-3 py-lg-2 font-weight-normal" title="Мониторинг и журналирование" href="$targetUrl" role="button" onclick="return false;"><i class="fas fa-eye my-auto mx-auto mx-lg-0 col-1 pl-0 mr-lg-2"></i><span>Мониторинг</span></a>
HTML;
} else {
echo <<<HTML
<a class="row text-dark button_white px-3 py-3 py-lg-2 font-weight-normal" title="Мониторинг и журналирование" href="$targetUrl" role="button" onclick="return page_profile_monitoring();"><i class="fas fa-eye my-auto mx-auto mx-lg-0 col-1 pl-0 mr-lg-2"></i><span>Мониторинг</span></a>
HTML;
}
?>
</dt>
<dt>
<?php
// Инициализация
$targetUrl = '/profile';
if ('/' . yii::$app->request->pathInfo === $targetUrl) {
// Запрошена та же страница от которой послан запрос (текущая)
echo <<<HTML
<a class="row text-dark button_white button_white_hover px-3 py-3 py-lg-2 font-weight-normal" title="Настройки аккаунта" href="$targetUrl" role="button" onclick="return false;"><i class="fas fa-sliders-h my-auto mx-auto mx-lg-0 col-1 pl-0 mr-lg-2"></i><span>Настройки</span></a>
HTML;
} else {
echo <<<HTML
<a class="row text-dark button_white px-3 py-3 py-lg-2 font-weight-normal" title="Настройки аккаунта" href="$targetUrl" role="button" onclick="return page_profile_settings();"><i class="fas fa-sliders-h my-auto mx-auto mx-lg-0 col-1 pl-0 mr-lg-2"></i><span>Настройки</span></a>
HTML;
}
?>
</dt>
</dl>
<?php if (yii::$app->user->identity->agnt) : ?>
<div class="dropdown-divider my-3"></div>
<div class="row px-3">
<p class="ml-0">Загрузок с аккаунта:</p>
<p class="mr-0"><?= AccountEdgeSupply::readAmount() ?>
</p>
</div>
<div class="row px-3">
<p class="ml-0">Товары:</p>
<p class="mr-0"><?= Product::readAmount() ?></p>
</div>
<div class="row px-3">
<p class="ml-0">Поставки:</p>
<p class="mr-0"><?= Supply::readAmount() ?></p>
</div>
<div class="row px-3">
<p class="ml-0">Связано поставок:</p>
<p class="mr-0"><?= SupplyEdgeProduct::readAmount() ?></p>
</div>
<?php endif ?>
</div>

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
use yii;
use yii\bootstrap\ActiveForm;
// Инициализация
$panel ?? $panel = 'profile_panel_supplies_input_import';
?>
<link href="/css/pages/profile.css" rel="stylesheet">
<div id="page_profile" class="container mb-auto">
<div class="row h-100 py-3">
<nav class="col-3">
<?= $sidebar ?? yii::$app->controller->renderPartial('/profile/sidebar') ?>
</nav>
<article class="col-9">
<div class="h-100 p-4 rounded">
<h4 class="ml-4 mb-4"><i class="fas fa-truck my-auto mr-2"></i>Управление поставками</h4>
<div id="profile_panel_supplies" class="profile_panel">
<div class="profile_panel_menu mb-3">
<label class="btn button_white mb-0 mr-2" for="profile_panel_supplies_input_settings">Настройки</label>
<label class="btn button_white mb-0 mr-2" for="profile_panel_supplies_input_import">Импорт</label>
</div>
<div class="profile_panel_content">
<input type="radio" id="profile_panel_supplies_input_settings" name="main_panel" <?= $panel === 'profile_panel_supplies_input_settings' ? 'checked' : null ?> />
<div class="col">
<p>В разработке</p>
</div>
<input type="radio" id="profile_panel_supplies_input_import" name="main_panel" <?= $panel === 'profile_panel_supplies_input_import' ? 'checked' : null ?> />
<div class="col">
<h5>Импорт из Excel-документа</h5>
<div class="dropdown-divider mb-3"></div>
<?php
$form = ActiveForm::begin([
'id' => 'form_product_import_excel',
'action' => false,
'fieldConfig' => [
'template' => '{label}{input}',
'options' => ['class' => '']
],
'options' => [
'class' => 'mb-3',
'onsubmit' => 'return false;'
]
]);
?>
<?= $form->field($model, 'file_excel', ['enableLabel' => false])->fileInput(['multiple' => true, 'class' => 'mb-2', 'onChange' => 'page_profile_supplies_import_excel(this.parentElement.parentElement, \'profile_panel_supplies_input_import\')']) ?>
<?= $form->errorSummary($model, ['header' => 'В документе были допущены ошибки:' /*, 'footer' => 'Исправьте их и попробуйте снова'*/]); ?>
<?php ActiveForm::end(); ?>
</div>
</div>
</div>
</article>
</div>
</div>
<script src="/js/profile.js" defer></script>
<?php if (
!yii::$app->user->isGuest
&& (yii::$app->user->identity->type === 'administrator'
|| yii::$app->user->identity->type === 'moderator')
) : ?>
<script src="/js/profile_panel.js" defer></script>
<?php endif ?>

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