32 Commits
0.1.0 ... 0.4.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
RedHood
94b719b67e Лень соединять коммиты, это туда же 2021-01-13 14:46:40 +10:00
RedHood
ac48e8e064 Добавлена точка с запятой как вариант решения проблемы со сжатием кода 2021-01-13 14:46:30 +10:00
RedHood
d8a1284cc5 Небольшие доработки 2021-01-13 14:15:30 +10:00
RedHood
fbd64ecb96 Разработка архитектуры, профиля, загрузки из Excel и 1C, много мелких изменений 2021-01-13 14:04:59 +10:00
RedHood
7d27736a02 Переработка регистрации и аутентификации в единую модель 2020-12-25 11:14:41 +10:00
RedHood
e0a6ae88e4 Создание аутентификации и регистрации на ArangoDB через Ajax + фронтенд правки 2020-12-24 09:57:19 +10:00
272 changed files with 11059 additions and 1456 deletions

5
.gitignore vendored
View File

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

View File

@@ -8,20 +8,25 @@
{
"name": "Arsen Mirzaev Tatyano-Muradovich",
"email": "red@hood.su",
"homepage": "https://hood.su/mirzaev",
"role": "Developer"
}
],
"require": {
"php": ">=7.4.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",
"pbazsi/yii2-arangodb": "2.0",
"triagens/arangodb": "^3.6"
"triagens/arangodb": "^3.6",
"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",
@@ -82,18 +87,6 @@
{
"type": "composer",
"url": "https://asset-packagist.org"
},
{
"type": "package",
"package": {
"name": "pbazsi/yii2-arangodb",
"version": "2.0",
"source": {
"type" : "git",
"url" : "https://github.com/pBazsi/yii2-arangodb.git",
"reference" : "master"
}
}
}
}
]
}

1922
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
/import

View File

@@ -9,6 +9,7 @@
namespace app\assets;
use yii\web\AssetBundle;
use yii\web\View;
/**
* Main application asset bundle.
@@ -25,18 +26,25 @@ 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/ticker.css',
'css/footer.css'
];
public $js = [
'https://code.jquery.com/jquery-3.5.1.min.js',
'https://kit.fontawesome.com/d7e922c226.js',
'js/bootstrap/popper.min.js',
'js/bootstrap/bootstrap.min.js',
'https://cdn.jsdelivr.net/bxslider/4.1.1/jquery.bxslider.min.js',
'js/ticker.js',
'https://kit.fontawesome.com/d7e922c226.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',

View File

@@ -1,17 +1,17 @@
<?php
$params = require __DIR__ . '/params.php';
$db = require __DIR__ . '/db.php';
declare(strict_types=1);
$config = [
'id' => 'basic-console',
return [
'id' => 'skillparts-console',
'basePath' => dirname(__DIR__),
'bootstrap' => ['log'],
'controllerNamespace' => 'app\commands',
'aliases' => [
'@bower' => '@vendor/bower-asset',
'@npm' => '@vendor/npm-asset',
'@tests' => '@app/tests',
'@vendor' => dirname(__DIR__) . '/../../../vendor',
'@bower' => '@vendor/bower-asset',
'@npm' => '@vendor/npm-asset',
'@tests' => '@app/tests',
],
'components' => [
'cache' => [
@@ -25,24 +25,13 @@ $config = [
],
],
],
'db' => $db,
'arangodb' => require __DIR__ . '/db.php'
],
'params' => $params,
/*
'params' => require __DIR__ . '/params.php',
'controllerMap' => [
'fixture' => [ // Fixture generation command line.
'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

@@ -1,14 +1,14 @@
<?php
return [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=;dbname=',
'username' => '',
'password' => '',
'charset' => 'utf8',
use ArangoDBClient\ConnectionOptions;
// Schema cache options (for production environment)
//'enableSchemaCache' => true,
//'schemaCacheDuration' => 60,
//'schemaCache' => 'cache',
return [
'class' => '\mirzaev\yii2\arangodb\Connection',
'connectionOptions' => [
ConnectionOptions::OPTION_DATABASE => '',
ConnectionOptions::OPTION_ENDPOINT => 'tcp://127.0.0.1:8529',
ConnectionOptions::OPTION_AUTH_TYPE => 'Basic',
ConnectionOptions::OPTION_AUTH_USER => '',
ConnectionOptions::OPTION_AUTH_PASSWD => '',
],
];

View File

@@ -1,12 +1,10 @@
<?php
$params = require __DIR__ . '/params.php';
$db = require __DIR__ . '/test_db.php';
/**
* Application configuration shared by all test types
*/
return [
'id' => 'basic-tests',
'id' => 'skillparts-tests',
'basePath' => dirname(__DIR__),
'aliases' => [
'@bower' => '@vendor/bower-asset',
@@ -14,7 +12,7 @@ return [
],
'language' => 'en-US',
'components' => [
'db' => $db,
'db' => require __DIR__ . '/test_db.php',
'mailer' => [
'useFileTransport' => true,
],
@@ -25,7 +23,7 @@ return [
'showScriptName' => true,
],
'user' => [
'identityClass' => 'app\models\User',
'identityClass' => 'app\models\Account',
],
'request' => [
'cookieValidationKey' => 'test',
@@ -38,5 +36,5 @@ return [
*/
],
],
'params' => $params,
'params' => require __DIR__ . '/params.php',
];

View File

@@ -1,33 +1,54 @@
<?php
use ArangoDBClient\ConnectionOptions;
$params = require __DIR__ . '/params.php';
$db = require __DIR__ . '/db.php';
$config = [
'id' => 'basic',
'id' => 'skillparts',
'basePath' => dirname(__DIR__),
'bootstrap' => ['log'],
'aliases' => [
'@vendor' => '../../../../vendor',
'@bower' => '@vendor/bower-asset',
'@npm' => '@vendor/npm-asset',
'@vendor' => dirname(__DIR__) . '/../../../vendor',
'@bower' => '@vendor/bower-asset',
'@npm' => '@vendor/npm-asset'
],
'components' => [
'request' => [
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
'cookieValidationKey' => '',
'baseUrl' => ''
],
'cache' => [
'class' => 'yii\caching\FileCache',
],
// 'cache' => [
// 'class' => 'yii\caching\FileCache',
// ],
'user' => [
'identityClass' => 'app\models\User',
'enableAutoLogin' => true,
'identityClass' => 'app\models\Account',
'loginUrl' => ['/authentication'],
// 'enableAutoLogin' => true,
'enableSession' => 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' => 'site/error',
'errorAction' => 'error',
],
'mailer' => [
'class' => 'yii\swiftmailer\Mailer',
@@ -45,42 +66,56 @@ $config = [
],
],
],
'db' => $db,
'arangodb' => [
'class' => '\explosivebit\arangodb\Connection',
'connectionOptions' => [
ConnectionOptions::OPTION_DATABASE => '',
ConnectionOptions::OPTION_ENDPOINT => '',
ConnectionOptions::OPTION_AUTH_TYPE => 'Basic',
ConnectionOptions::OPTION_AUTH_USER => '',
ConnectionOptions::OPTION_AUTH_PASSWD => '',
],
],
'arangodb' => require __DIR__ . '/db.php',
'urlManager' => [
'enablePrettyUrl' => true,
'showScriptName' => false,
'rules' => [],
'enablePrettyUrl' => true,
'showScriptName' => false,
'rules' => [
[
'class' => 'yii\rest\UrlRule',
'controller' => 'main'
],
'product/<catn:[^/]+>' => 'product/index',
'product/<catn:[^/]+>/<action:(write|edit|delete)>/<target:(title|catn|desc|image)>' => 'product/<action>-<target>',
'orders' => 'order/index'
],
],
],
'params' => $params,
'modules' => [
'exchange' => [
'class' => 'carono\exchange1c\ExchangeModule',
'groupClass' => 'app\models\SupplyGroup',
'productClass' => 'app\models\Supply',
'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);
if ($user = \app\models\Account::findByMail($mail)) {
if ($user->validatePassword($pswd)) {
return $user;
}
}
return false;
}
]
],
'params' => require __DIR__ . '/params.php',
];
if (YII_ENV_DEV) {
// configuration adjustments for 'dev' environment
$config['bootstrap'][] = 'debug';
$config['modules']['debug'] = [
'class' => 'yii\debug\Module',
// uncomment the following to add your IP if you are not connecting from localhost.
//'allowedIPs' => ['127.0.0.1', '::1'],
];
$config['bootstrap'][] = 'gii';
$config['modules']['gii'] = [
'class' => 'yii\gii\Module',
// uncomment the following to add your IP if you are not connecting from localhost.
//'allowedIPs' => ['127.0.0.1', '::1'],
'panels' => [
'ArangoDB' => [
'class' => 'mirzaev\yii2\arangodb\panels\arangodb\ArangoDbPanel'
]
]
];
}
return $config;
return $config;

View File

@@ -0,0 +1,121 @@
<?php
namespace app\controllers;
use app\models\AccountForm;
use yii;
use yii\web\Controller;
use yii\web\Response;
use yii\filters\AccessControl;
use Throwable;
use Exception;
class AuthenticationController extends Controller
{
/**
* {@inheritdoc}
*/
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'));
$model->scenario = $model::SCENARIO_AUTHENTICATION;
yii::$app->response->format = Response::FORMAT_JSON;
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 = [
'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')) {
// Найдено cookie с переадресацией
// Запись ответа
$return['redirect'] = '/' . $cookies['redirect'];
try {
if (empty($return['main'] = $this->renderPartial($return['redirect']))) {
throw new Exception('Представление найдено, но вернуло пустой результат');
}
} catch (Throwable $t) {
$return['main'] = $this->renderPartial($return['redirect'] . '/index');
}
// Генерация и запись
// $controller = 'app\\controllers\\' . ucfirst($cookies['redirect']) . 'Controller';
// $action = 'action' . ucfirst($cookies['redirect_action']);
// $return['main'] = (new $controller())->$action();
// Очистка cookie
unset(yii::$app->response->cookies['redirect']);
} else {
// Не найдено cookie с переадресацией
if (yii::$app->request->pathInfo === 'authentication' || yii::$app->request->pathInfo === 'registration') {
// Если клиент на промежуточном URI
// Запись ответа
$return['redirect'] = '/';
$return['main'] = $this->renderPartial('/index');
}
}
return $return;
} else {
// Аккаунт не аутентифицирован
yii::$app->response->statusCode = 400;
return [
'main' => $this->renderPartial('/account/index', compact('model')),
'redirect' => '/authentication',
'_csrf' => yii::$app->request->getCsrfToken()
];
}
}
if (!yii::$app->user->isGuest) {
yii::$app->response->redirect('/');
} else {
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

@@ -0,0 +1,59 @@
<?php
namespace app\controllers;
use yii;
use yii\web\Controller;
use yii\web\Response;
use yii\filters\AccessControl;
use yii\filters\VerbFilter;
use app\models\AccountForm;
class DeauthenticationController extends Controller
{
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::class,
'rules' => [
[
'allow' => true,
'roles' => ['@'],
]
],
],
'verbs' => [
'class' => VerbFilter::class,
'actions' => [
'index' => ['post'],
],
]
];
}
public function actionIndex()
{
// Инициализация
yii::$app->response->format = Response::FORMAT_JSON;
// Удаление сессии
yii::$app->session['status'] = 'inactive';
// yii::$app->session->close();
// Выход из аккаунта
yii::$app->user->logout();
// Инициализация
$model = new AccountForm(yii::$app->request->post('AccountForm') ?? yii::$app->request->get('AccountForm') ?? null);
// Ответа
return [
'menu' => $this->renderPartial('/account/panel/deauthenticated', compact('model')),
'main' => $this->renderPartial('/index'),
'redirect' => '/',
'_csrf' => yii::$app->request->getCsrfToken()
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace app\controllers;
use Yii;
use yii\web\Controller;
class ErrorController extends Controller
{
public function actionIndex()
{
$exception = Yii::$app->errorHandler->exception;
if ($exception !== null) {
// Исключение не выброшено
// Запись кода ошибки
$statusCode = $exception->statusCode;
// Запись названия ошибки
$name = match ($exception->statusCode) {
404 => '404 (Не найдено)',
default => $exception->getName()
};
// Запись сообщения об ошибке
$message = match ($exception->statusCode) {
404 => 'Страница не найдена',
default => $exception->getMessage()
};
return $this->render('/error', compact('exception', 'statusCode', 'name', 'message'));
}
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace app\controllers;
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->isPost) {
// POST-запрос
yii::$app->response->format = Response::FORMAT_JSON;
if (yii::$app->user->isGuest) {
// Аккаунт не аутентифицирован
// Инициализация
$model = new AccountForm(yii::$app->request->post('AccountForm') ?? yii::$app->request->get('AccountForm') ?? null);
// Запись ответа
$return = [
'menu' => $this->renderPartial('/account/panel/deauthenticated', compact('model')),
'_csrf' => yii::$app->request->getCsrfToken()
];
} else {
// Аккаунт аутентифицирован
// Инициализация
$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 = [
'menu' => $this->renderPartial('/account/panel/authenticated', compact(
'notifications_button',
'notifications_panel'
)),
'_csrf' => yii::$app->request->getCsrfToken()
];
}
if (($cookies = yii::$app->request->cookies)->has('redirect')) {
// Найдено cookie с переадресацией
// Запись ответа
$return['redirect'] = '/' . $cookies['redirect'];
// Очистка cookie
unset(yii::$app->response->cookies['redirect']);
}
return $return;
}
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace app\controllers;
use Yii;
use yii\web\Controller;
use yii\web\Response;
class MainController extends Controller
{
/**
* {@inheritdoc}
*/
public function actions()
{
return [
'captcha' => [
'class' => 'yii\captcha\CaptchaAction',
'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
],
];
}
/**
* Главная страница
*/
public function actionIndex(): string|array
{
if (Yii::$app->request->isAjax) {
// AJAX-POST-запрос
Yii::$app->response->format = Response::FORMAT_JSON;
return [
'main' => $this->renderPartial('/index'),
'redirect' => '/',
'_csrf' => Yii::$app->request->getCsrfToken()
];
}
return $this->render('/index');
}
}

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

@@ -0,0 +1,239 @@
<?php
declare(strict_types=1);
namespace app\controllers;
use yii;
use yii\filters\AccessControl;
use yii\web\Controller;
use yii\web\Response;
use yii\web\HttpException;
use yii\web\UploadedFile;
use app\models\Product;
class ProductController extends Controller
{
public function actionIndex(string $catn): array|string|null
{
if ($model = Product::searchByCatn($catn)) {
// Товар найден
if (yii::$app->request->isAjax) {
// AJAX-POST-запрос
yii::$app->response->format = Response::FORMAT_JSON;
return [
'main' => $this->renderPartial('index', compact('model')),
'redirect' => '/product/' . $catn,
'_csrf' => yii::$app->request->getCsrfToken()
];
}
return $this->render('index', compact('model'));
} else {
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

@@ -0,0 +1,410 @@
<?php
declare(strict_types=1);
namespace app\controllers;
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
{
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::class,
'rules' => [
[
'allow' => true,
'roles' => ['@'],
'actions' => ['index', 'supplies', 'import', 'monitoring', 'readGroups']
],
[
'allow' => false,
'roles' => ['?'],
'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;
}
]
]
]
];
}
public function accessDenied()
{
// Инициализация
$cookies = yii::$app->response->cookies;
// Запись cookie с редиректом, который выполнится после авторизации
$cookies->add(new Cookie([
'name' => 'redirect',
'value' => yii::$app->request->pathInfo
]));
if (Yii::$app->request->isPost) {
// POST-запрос
// Настройка
Yii::$app->response->format = Response::FORMAT_JSON;
// Генерация ответа
Yii::$app->response->content = json_encode([
'main' => $this->renderPartial('/account/index'),
'redirect' => '/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',
'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_EXCEL;
$panel = yii::$app->request->post('panel') ?? yii::$app->request->get('panel');
$sidebar = $this->renderPartial('sidebar');
$groups = self::readGroups();
if (yii::$app->request->isPost) {
// AJAX-POST-запрос
yii::$app->response->format = Response::FORMAT_JSON;
$model->file_excel = UploadedFile::getInstances($model, 'file_excel');
if ($model->importExcel()) {
return [
'main' => $this->renderPartial('supplies', compact(
'model',
'groups',
'sidebar',
'panel'
)),
'_csrf' => yii::$app->request->getCsrfToken()
];
}
yii::$app->response->statusCode = 409;
}
return $this->render('supplies', compact(
'model',
'groups',
'sidebar',
'panel'
));
}
public static function readGroups()
{
// Инициализация
$groups = [];
foreach (SupplyGroup::readAll() as $group) {
// Перебор всех групп
// Генерация [КЛЮЧ => ИМЯ]
$groups[$group->_key] = $group->name;
}
return $groups;
}
}

View File

@@ -0,0 +1,104 @@
<?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\AccountForm;
class RegistrationController extends Controller
{
// public function behaviors()
// {
// return [
// 'access' => [
// 'class' => AccessControl::class,
// 'rules' => [
// [
// 'allow' => true,
// 'roles' => ['?'],
// ]
// ],
// ]
// ];
// }
public function actionIndex()
{
// Инициализация
$model = new AccountForm(yii::$app->request->post('AccountForm') ?? yii::$app->request->get('AccountForm'));
$model->scenario = $model::SCENARIO_REGISTRATION;
if (yii::$app->request->isPost) {
// POST-запрос
yii::$app->response->format = Response::FORMAT_JSON;
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 = [
'menu' => $this->renderPartial('/account/panel/authenticated', compact(
'notifications_button',
'notifications_panel'
)),
'_csrf' => yii::$app->request->getCsrfToken()
];
if (($cookies = yii::$app->response->cookies)->has('redirect')) {
// Найдено cookie с переадресацией
// Запись ответа
$return['redirect'] = '/' . $cookies['redirect'];
$return['main'] = $this->renderPartial($return['redirect'] . '/index');
// Очистка cookie
unset(yii::$app->response->cookies['redirect']);
} else {
// Не найдено cookie с переадресацией
if (yii::$app->request->pathInfo === 'authentication' || yii::$app->request->pathInfo === 'registration') {
// Если клиент на промежуточном URI
// Запись ответа
$return['redirect'] = '/';
$return['main'] = $this->renderPartial('/index');
}
}
return $return;
} else {
// Данные не прошли проверку
yii::$app->response->statusCode = 400;
return [
'main' => $this->renderPartial('/account/index', compact('model')),
'redirect' => '/registration',
'_csrf' => yii::$app->request->getCsrfToken()
];
}
}
if (!yii::$app->user->isGuest) {
yii::$app->response->redirect('/');
} else {
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,128 +0,0 @@
<?php
namespace app\controllers;
use Yii;
use yii\filters\AccessControl;
use yii\web\Controller;
use yii\web\Response;
use yii\filters\VerbFilter;
use app\models\LoginForm;
use app\models\ContactForm;
class SiteController extends Controller
{
/**
* {@inheritdoc}
*/
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'only' => ['logout'],
'rules' => [
[
'actions' => ['logout'],
'allow' => true,
'roles' => ['@'],
],
],
],
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'logout' => ['post'],
],
],
];
}
/**
* {@inheritdoc}
*/
public function actions()
{
return [
'error' => [
'class' => 'yii\web\ErrorAction',
],
'captcha' => [
'class' => 'yii\captcha\CaptchaAction',
'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
],
];
}
/**
* Displays homepage.
*
* @return string
*/
public function actionIndex()
{
return $this->render('index');
}
/**
* Login action.
*
* @return Response|string
*/
public function actionLogin()
{
if (!Yii::$app->user->isGuest) {
return $this->goHome();
}
$model = new LoginForm();
if ($model->load(Yii::$app->request->post()) && $model->login()) {
return $this->goBack();
}
$model->password = '';
return $this->render('login', [
'model' => $model,
]);
}
/**
* Logout action.
*
* @return Response
*/
public function actionLogout()
{
Yii::$app->user->logout();
return $this->goHome();
}
/**
* Displays contact page.
*
* @return Response|string
*/
public function actionContact()
{
$model = new ContactForm();
if ($model->load(Yii::$app->request->post()) && $model->contact(Yii::$app->params['adminEmail'])) {
Yii::$app->session->setFlash('contactFormSubmitted');
return $this->refresh();
}
return $this->render('contact', [
'model' => $model,
]);
}
/**
* Displays about page.
*
* @return string
*/
public function actionAbout()
{
return $this->render('about');
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
<?php
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 down()
{
$this->dropCollection('supply_edge_product');
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
<?php
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]);
}
public function down()
{
$this->dropCollection('supply_edge_requisite');
}
}

View File

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

View File

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

View File

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

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

@@ -0,0 +1,305 @@
<?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(): string
{
return 'account';
}
/**
* Свойства
*/
public function attributes(): array
{
return array_merge(
parent::attributes(),
[
'auth',
'mail',
'pswd',
'name',
'simc',
'sity',
'comp',
'taxn',
'onec',
'opts',
'agnt',
'type'
]
);
}
/**
* Метки свойств
*/
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(),
[
[['mail', 'pswd'], 'required', 'message' => 'Заполните поле'],
['mail', 'email'],
[['mail', 'comp', 'simc'], 'unique', 'message' => '2']
]
);
}
/**
* Перед сохранением
*
* @todo Подождать обновление от ебаного Yii2 и добавить
* проверку типов передаваемых параметров
*/
public function beforeSave($data): bool
{
if (parent::beforeSave($data)) {
if ($this->isNewRecord) {
$this->auth = Yii::$app->security->generateRandomString();
}
return true;
}
return false;
}
/**
* Чтение полей для экспорта из 1С
*/
public function getExportFields1c($context = null): array
{
return [];
}
/**
* Чтение идентификатора
*
* @see IdentityInterface
*/
public function getId(): string
{
return self::collectionName() . '/' . $this->_key;
}
/**
* Чтение идентификатора
*/
public function readId(): string
{
return $this->getId();
}
/**
* Чтение аутентификационного ключа
*/
public function getAuthKey(): string
{
return $this->auth;
}
/**
* Идентификация
*
* @todo Подождать обновление от ебаного Yii2 и добавить
* проверку типов передаваемых параметров
*/
public static function findIdentity($_id): self
{
return static::findById($_id);
}
/**
* Поиск по ключу
*
* @todo Подождать обновление от ебаного Yii2 и добавить
* проверку типов передаваемых параметров
*/
public static function findIdentityByAccessToken($pass, $type = null): self
{
return static::findOne(['pass' => $pass]);
}
/**
* Поиск по почте
*
* @todo Подождать обновление Yii2 и добавить
* проверку типов передаваемых параметров
*/
public static function findByMail($mail): ?self
{
return static::findOne(['mail' => $mail]);
}
/**
* Поиск по идентификатору
*
* @todo Подождать обновление Yii2 и добавить
* проверку типов передаваемых параметров
*/
public static function findById($_id): self
{
return static::searchById($_id);
}
/**
* Проверка почты
*/
public static function validateMail(string $mail): bool
{
if (static::findByMail($mail)) {
// Почта найдена в базе данных
return true;
}
return false;
}
/**
* Проверка пароля
*/
public function validatePassword(string $pswd): bool
{
return Yii::$app->security->validatePassword($pswd, $this->pswd);
}
/**
* Проверка аутентификационного ключа
*
* @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

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace app\models;
class AccountEdgePurchase extends Edge
{
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

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

View File

@@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace app\models;
use yii;
use yii\base\Model;
use app\models\Account;
/**
* AccountForm is the model behind the login form.
*
* @property-read Account|null $account This property is read-only.
*
*/
class AccountForm extends Model
{
const SCENARIO_REGISTRATION = 'registration';
const SCENARIO_REGISTRATION_END = 'registration_end';
const SCENARIO_AUTHENTICATION = 'authentication';
public $mail;
public $pswd;
public $auto = false;
private $account = false;
public function rules()
{
return [
// Обязательные поля
[['mail', 'pswd'], 'required', 'message' => 'Заполните поле'],
// Функция "Запомнить меня"
['auto', 'boolean', 'on' => self::SCENARIO_AUTHENTICATION],
// Проверка почты,
['mail', 'email', 'message' => 'Проверьте почту'],
['mail', 'validateMail', 'on' => self::SCENARIO_REGISTRATION],
// Проверка пароля
['pswd', 'validatePassword', 'on' => self::SCENARIO_AUTHENTICATION]
];
}
public function attributeLabels()
{
return [
'mail' => 'Почта',
'pswd' => 'Пароль',
'auto' => '<i class="fas fa-lock"></i>'
];
}
public function validateMail($attribute, $params)
{
if (!$this->hasErrors()) {
// Проблем нет, обрабатывается событие регистрации
$account = $this->getAccount();
if (!$account || $account->validateMail($this->mail)) {
// Проверка не пройдена
$this->addError($attribute, 'Почта уже привязана');
}
}
}
public function validatePassword($attribute, $params)
{
if (!$this->hasErrors()) {
// Проблем нет, обрабатывается событие аутентификации
$account = $this->getAccount();
if (!$account || !$account->validatePassword($this->pswd)) {
// Проверка не пройдена
$this->addError($attribute, 'Проверьте пароль');
}
}
}
/**
* Logs in a account using the provided accountname and password.
* @return bool whether the account is logged in successfully
*/
public function authentication(string $mail = null, string $pswd = null)
{
if (isset($mail, $pswd)) {
$this->mail = $mail;
$this->pswd = $pswd;
}
if (isset($this->mail, $this->pswd) && $this->validate()) {
// Проверка пройдена
// Аутентификация
return yii::$app->user->login($this->getAccount(), $this->auto ? 3600 * 24 * 30 : 0);
}
return false;
}
/**
* @return bool
*/
public function registration()
{
// Инициализация нового аккаунта
$this->account = new Account();
if (isset($this->mail, $this->pswd) && $this->validate()) {
// Проверка пройдена
// Запись параметров
$this->account->mail = $this->mail;
$this->account->pswd = yii::$app->security->generatePasswordHash($this->pswd);
// Регистрация
return $this->account->save();
}
return false;
}
/**
* Finds account by [[accountname]]
*
* @return Account|null
*/
public function getAccount()
{
if ($this->account === false) {
$this->account = Account::findByMail($this->mail);
}
return $this->account;
}
}

View File

@@ -1,65 +0,0 @@
<?php
namespace app\models;
use Yii;
use yii\base\Model;
/**
* ContactForm is the model behind the contact form.
*/
class ContactForm extends Model
{
public $name;
public $email;
public $subject;
public $body;
public $verifyCode;
/**
* @return array the validation rules.
*/
public function rules()
{
return [
// name, email, subject and body are required
[['name', 'email', 'subject', 'body'], 'required'],
// email has to be a valid email address
['email', 'email'],
// verifyCode needs to be entered correctly
['verifyCode', 'captcha'],
];
}
/**
* @return array customized attribute labels
*/
public function attributeLabels()
{
return [
'verifyCode' => 'Verification Code',
];
}
/**
* Sends an email to the specified email address using the information collected by this model.
* @param string $email the target email address
* @return bool whether the model passes validation
*/
public function contact($email)
{
if ($this->validate()) {
Yii::$app->mailer->compose()
->setTo($email)
->setFrom([Yii::$app->params['senderEmail'] => Yii::$app->params['senderName']])
->setReplyTo([$this->email => $this->name])
->setSubject($this->subject)
->setTextBody($this->body)
->send();
return true;
}
return false;
}
}

View File

@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace app\models;
use yii;
use mirzaev\yii2\arangodb\ActiveRecord;
use Exception;
/**
* Документ
*/
abstract class Document extends ActiveRecord
{
/**
* Имя коллекции
*/
public static function collectionName(): string
{
return throw new Exception('Не инициализировано название коллекции');
}
/**
* Свойства
*/
public function attributes(): array
{
return [
'_key',
'jrnl'
];
}
/**
* Метки свойств
*/
public function attributeLabels(): array
{
return [
'_key' => 'Ключ',
'jrnl' => 'Журнал'
];
}
/**
* Правила
*
* @todo Добавить проверку существования аккаунта
*/
public function rules(): array
{
return [];
}
/**
* Перед сохранением
*
* @todo Подождать обновление от ебаного Yii2 и добавить
* проверку типов передаваемых параметров
*/
public function beforeSave($data): bool
{
if (parent::beforeSave($data)) {
if ($this->isNewRecord) {
// Запись в журнал
$this->jrnl = array_merge(
[[
'date' => time(),
'account' => yii::$app->user->id,
'action' => 'create'
]],
$this->jrnl ?? []
);
}
return true;
}
return false;
}
/**
* Журнал
*
* Записывает данные в журнал
*
* @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

@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
namespace app\models;
/**
* Ребро
*/
abstract class Edge extends Document
{
/**
* Свойства
*/
public function attributes(): array
{
return array_merge(
parent::attributes(),
[
'_from',
'_to',
'type'
]
);
}
/**
* Метки свойств
*/
public function attributeLabels(): array
{
return array_merge(
parent::attributeLabels(),
[
'_from' => 'От кого',
'_to' => 'К кому',
]
);
}
/**
* Правила
*/
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) {
}
return true;
}
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

@@ -1,81 +0,0 @@
<?php
namespace app\models;
use Yii;
use yii\base\Model;
/**
* LoginForm is the model behind the login form.
*
* @property-read User|null $user This property is read-only.
*
*/
class LoginForm extends Model
{
public $username;
public $password;
public $rememberMe = true;
private $_user = false;
/**
* @return array the validation rules.
*/
public function rules()
{
return [
// username and password are both required
[['username', 'password'], 'required'],
// rememberMe must be a boolean value
['rememberMe', 'boolean'],
// password is validated by validatePassword()
['password', 'validatePassword'],
];
}
/**
* Validates the password.
* This method serves as the inline validation for password.
*
* @param string $attribute the attribute currently being validated
* @param array $params the additional name-value pairs given in the rule
*/
public function validatePassword($attribute, $params)
{
if (!$this->hasErrors()) {
$user = $this->getUser();
if (!$user || !$user->validatePassword($this->password)) {
$this->addError($attribute, 'Incorrect username or password.');
}
}
}
/**
* Logs in a user using the provided username and password.
* @return bool whether the user is logged in successfully
*/
public function login()
{
if ($this->validate()) {
return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600*24*30 : 0);
}
return false;
}
/**
* Finds user by [[username]]
*
* @return User|null
*/
public function getUser()
{
if ($this->_user === false) {
$this->_user = User::findByUsername($this->username);
}
return $this->_user;
}
}

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

@@ -0,0 +1,518 @@
<?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;
/**
* Продукт (в ассортименте магазина)
*
* Представляет собой лот состоящий из предложений от поставщиков
*
* @see Supply Поставки для продуктов
*/
class Product extends Document
{
use SearchByEdge;
/**
* Сценарий импорта .excel документа
*
* Использовать для обхода правил при загрузке файла
*/
const SCENARIO_IMPORT_EXCEL = 'import_excel';
/**
* Сценарий импорта изображений
*
* Использовать для обхода правил при загрузке файла
*/
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(): array
{
return array_merge(
parent::attributes(),
[
'catn',
'name',
'desc',
'ocid',
'imgs',
'time',
'oemn',
'cost'
]
);
}
/**
* Метки свойств
*/
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(),
[
[
'catn',
'required',
'message' => 'Заполните поля: {attribute}',
'on' => self::SCENARIO_WRITE,
'except' => [self::SCENARIO_IMPORT_EXCEL, self::SCENARIO_IMPORT_IMAGE]
],
[
'catn',
'string',
'message' => '{attribute} должен быть строкой'
],
[
[
'oemn',
'imgs'
],
'arrayValidator',
'message' => '{attribute} должен быть массивом.'
],
[
'file_excel',
'required',
'message' => 'Заполните поля: {attribute}',
'on' => self::SCENARIO_IMPORT_EXCEL
],
[
'file_excel',
'file',
'skipOnEmpty' => false,
'extensions' => 'xlsx',
'checkExtensionByMimeType' => false,
'maxFiles' => 5,
'maxSize' => 1024 * 1024 * 30,
'wrongExtension' => 'Разрешены только документы в формате: ".xlsx"',
'message' => 'Проблема при чтении документа',
'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
]
]
);
}
/**
* Инициализация продукта
*
* @param string $catn Артикул, каталожный номер
*/
public static function initEmpty(string $catn): self|array
{
$oemn = self::searchOemn($catn);
if (count($oemn) === 1) {
// Передан только один артикул
if ($model = self::searchByCatn($catn)) {
// Продукт уже существует
return $model;
}
// Запись пустого продукта
return self::writeEmpty($catn);
}
// Инициализация
$models = [];
foreach ($oemn as $catn) {
// Перебор всех найденных артикулов
if ($model = self::searchByCatn($catn)) {
// Продукт уже существует
continue;
}
// Запись
if ($model = self::writeEmpty($catn)) {
// Записано
// Запись в массив сохранённых моделей
$models[] = $model;
}
}
return $models;
}
/**
* Запись пустого продукта
*/
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_excel as $file) {
// Перебор файлов
// Инициализация
$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,
'setIndexSheetByName' => true,
]);
}
foreach ($data as $data) {
// Перебор конвертированных файлов
if (count($data) < 1) {
// Не найдены строки с товарами
$this->addError('erros', 'Не удалось найти данные товаров');
} else {
// Перебор найденных товаров
foreach ($data as $doc) {
// Перебор полученных документов
// Сохранение в базе данных
$product = new static($doc);
$product->scenario = $product::SCENARIO_WRITE;
if ($product->validate()) {
// Проверка пройдена
// Запись документа
$product->save();
// Постинкрементация счётчика
$amount++;
// Запись группы
// $group = static::class . 'Group';
// (new $group())->writeMember($product, $this->group);
} else {
// Проверка не пройдена
foreach ($product->errors as $attribute => $error) {
$this->addError($attribute, $error);
}
}
}
}
}
// Деинициализация
$this->file_excel = '';
static::afterImportExcel($amount);
return true;
}
$this->addError('erros', 'Неизвестная ошибка');
static::afterImportExcel($amount);
return false;
}
/**
* Поиск по каталожному номеру
*
* Ищет продукт и возвращает его,
* либо выполняет поиск через представление
*
* @todo Переделать нормально
*/
public static function searchByCatn(string $catn, int $limit = 1, array $select = []): static|array|null
{
if ($limit <= 1) {
return static::findOne(['catn' => $catn]);
}
$query = self::find()
->where(['catn' => $catn])
->limit($limit)
->select($select)
->createCommand()
->execute()
->getAll();
foreach ($query as &$attribute) {
// Приведение всех свойств в массив и очистка от лишних данных
$attribute = $attribute->getAll();
}
return $query;
}
/**
* Поиск по каталожному номеру (через представления)
*
* Ищет продукт и возвращает его,
* либо выполняет поиск через представление
*
* @todo Переделать нормально
*/
public static function searchByPartialCatn(string $catn, int $limit = 1, array $select = []): static|array|null
{
$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();
}
return $query;
}
/**
* Вызывается после загрузки поставок из excel-документа
*
* @param int $amount Количество
*/
public static function afterImportExcel(int $amount = 0): bool
{
// Инициализация
$model = new Notification;
$date = date('H:i d.m.Y', time());
// Настройка
$model->text = yii::$app->controller->renderPartial('/notification/system/afterImportExcel', compact('amount', 'date'));
$model->type = $model::TYPE_NOTICE;
// Отправка
return (bool) $model->write();
}
/**
* Вызывается после загрузки поставок из 1С
*
* @param int $amount Количество
*/
public static function afterImportOnec(): bool
{
// Инициализация
$model = new Notification;
$date = date('H:i d.m.Y', time());
// Настройка
$model->text = yii::$app->controller->renderPartial('/notification/system/afterImportOnec', compact('amount', 'date'));
$model->type = $model::TYPE_NOTICE;
// Отправка
return (bool) $model->write();
}
}

View File

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

View File

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

View File

@@ -0,0 +1,174 @@
<?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(): string
{
return 'product_group';
}
/**
* Свойства
*/
public function attributes(): array
{
return array_merge(
parent::attributes(),
[
'name'
]
);
}
/**
* Метки свойств
*/
public function attributeLabels(): array
{
return array_merge(
parent::attributeLabels(),
[
'name' => 'Название (name)'
]
);
}
/**
* Правила
*/
public function rules(): array
{
return array_merge(
parent::rules(),
[
// [
// 'name',
// 'required',
// 'message' => 'Заполните поле: {attribute}'
// ]
]
);
}
/**
* Запись члена группы
*/
public function writeMember(Product $member): ProductEdgeProductGroup
{
return ProductEdgeProductGroup::write($member->readId(), $this->readId(), 'member');
}
/**
* Запись рёбер групп
*
* Создание взаимоотношений между группами по типу древовидной системы
*/
public static function createTree1c($groups): Document|null
{
foreach ($groups as $group) {
// Перебор групп
// Создание
$parent = static::createByML($group);
if ($children = $group->getChildren()) {
// Найден потомок
// Вход в рекурсию
$children = static::createTree1c($children);
// Запись рёбер с родителем
static::writeEdgeBetweenGroup($parent::collectionName() . '/' . $parent->_key, $children::collectionName() . '/' . $children->_key);
return $children;
}
return $parent;
}
}
/**
* Запись группы
*
* Создаём группу по модели группы CommerceML
* проверяем все дерево родителей группы,
* если родителя нет в базе - создаём
*/
public static function createByML(Group $group): static|array|null
{
// if (!$model = static::readByOnecId($group->id)) {
// // Группа не найдена
// // Инициализация
// $model = new static;
// $model->onec_id = $group->id;
// }
// $model->name = $group->name;
// if ($parent = $group->getParent()) {
// // Найден родитель
// // Инициализация (рекурсия)
// $parentModel = static::createByML($parent);
// $model->onec_prnt_id = $parentModel->id;
// unset($parentModel);
// } else {
// $model->onec_prnt_id = null;
// }
// $model->save();
// return $model;
return null;
}
private static function writeEdgeBetweenGroup(string $from, string $to): bool
{
// Инициализация
$edge = new ProductGroupEdgeProductGroup();
// Настройка
$edge->_from = $from;
$edge->_to = $to;
// Запись
return $edge->save();
}
public static function readByName(string $name)
{
return static::findOne(['name' => $name]);
}
/**
* Название поля в котором хранится ID из 1C
*/
public static function getIdFieldName1c(): string
{
return 'onec_id';
}
public static function readByOnecId(string $onec_id): ?ProductGroup
{
return static::findOne(['onec_id' => $onec_id]);
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace app\models;
class Requisite extends Document
{
public static function collectionName(): string
{
return 'requisite';
}
public function attributes(): array
{
return array_merge(
parent::attributes(),
['name', 'value']
);
}
public function rules(): array
{
return array_merge(
parent::rules(),
[
[
['name', 'value'],
'required',
'message' => 'Заполните поле: {attribute}'
]
]
);
}
public function attributeLabels(): array
{
return array_merge(
parent::attributeLabels(),
[
'name' => 'Название',
'value' => 'Значение'
]
);
}
public static function readByName(string $name)
{
return static::findOne(['name' => $name]);
}
}

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

@@ -0,0 +1,448 @@
<?php
declare(strict_types=1);
namespace app\models;
use yii;
use yii\web\User;
use app\models\Account;
use app\models\Product;
use app\models\SupplyEdgeProduct;
use app\models\traits\Xml2Array;
use carono\exchange1c\interfaces\ProductInterface;
use carono\exchange1c\controllers\ApiController;
use exception;
/**
* Поставка (выгрузка товаров от поставщиков)
*
* Представляет собой предложения от поставщиков которые добавляются
* в универсальные лоты товаров в асспортименте магазина
*
* @see Product Продукт (туда добавляются поставки)
*/
class Supply extends Product implements ProductInterface
{
use Xml2Array;
/**
* Количество
*
* Используется при выводе в корзине
*/
public int $amnt = 0;
/**
* Имя коллекции
*/
public static function collectionName(): string
{
return 'supply';
}
/**
* Свойства
*/
public function attributes(): array
{
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) {
// Перебор записей
// Инициализация
$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)) {
// не удалось записать директорию
return false;
};
}
foreach ($this->imgs ?? [] as $image) {
// Перебор имеющихся изображений
if ($path === $image['sorc']) {
// Изображение уже записано на сервер
return true;
}
}
// Инициализация
$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 function getOffer1c($offer): SupplyEdgeProduct
{
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();
}
/**
* Запись продукта из 1С (поставка)
*
* @see Supply
*
* @todo Понять что может храниться внутри "$model->onec['ЗначенияСвойств']['ЗначенияСвойства']" и переписать
* Разобраться и создать возможность загрузки от лица другого аккаунта
*/
public static function createModel1c($product): ?self
{
// Инициализация
$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;
}
/**
* Запись цены из 1С
*/
public function setPrice1c($price): mixed
{
return true;
}
/**
* Запись данных на случай ошибки при экспорте из 1С
*/
public function setRaw1cData($cml, $object): bool
{
return true;
}
/**
* Поиск по идентификатору из 1С
*
* @param string $ocid Идентификатор из 1С
*
* @return Supply|null
*/
public static function searchByOcid(string $ocid): ?Supply
{
return static::findOne([static::getIdFieldName1c() => $ocid]);
}
/**
* Чтение группы из 1С
*/
public function getGroup1c(): ?SupplyGroup
{
return $this->group;
}
/**
* Чтение названия поля в котором хранится идентификатор из 1С
*/
public static function getIdFieldName1c(): string
{
return 'ocid';
}
/**
* Поиск по OEM-номерам
*
* @todo Реализовать с помощью LIKE
*/
public static function searchByOemn(): array
{
return [];
}
/**
* Прочитать стоимость
*/
public function readCost(Product $product = null): array
{
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

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace app\models;
use app\models\traits\Xml2Array;
use carono\exchange1c\interfaces\OfferInterface;
use Zenwalker\CommerceML\Model\Offer;
class SupplyEdgeProduct extends Edge implements OfferInterface
{
use 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

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

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace app\models;
/**
* Группировка поставок
*/
class SupplyGroup extends ProductGroup
{
/**
* Имя коллекции
*/
public static function collectionName(): string
{
return 'supply_group';
}
}

View File

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

View File

@@ -1,104 +0,0 @@
<?php
namespace app\models;
class User extends \yii\base\BaseObject implements \yii\web\IdentityInterface
{
public $id;
public $username;
public $password;
public $authKey;
public $accessToken;
private static $users = [
'100' => [
'id' => '100',
'username' => 'admin',
'password' => 'admin',
'authKey' => 'test100key',
'accessToken' => '100-token',
],
'101' => [
'id' => '101',
'username' => 'demo',
'password' => 'demo',
'authKey' => 'test101key',
'accessToken' => '101-token',
],
];
/**
* {@inheritdoc}
*/
public static function findIdentity($id)
{
return isset(self::$users[$id]) ? new static(self::$users[$id]) : null;
}
/**
* {@inheritdoc}
*/
public static function findIdentityByAccessToken($token, $type = null)
{
foreach (self::$users as $user) {
if ($user['accessToken'] === $token) {
return new static($user);
}
}
return null;
}
/**
* Finds user by username
*
* @param string $username
* @return static|null
*/
public static function findByUsername($username)
{
foreach (self::$users as $user) {
if (strcasecmp($user['username'], $username) === 0) {
return new static($user);
}
}
return null;
}
/**
* {@inheritdoc}
*/
public function getId()
{
return $this->id;
}
/**
* {@inheritdoc}
*/
public function getAuthKey()
{
return $this->authKey;
}
/**
* {@inheritdoc}
*/
public function validateAuthKey($authKey)
{
return $this->authKey === $authKey;
}
/**
* Validates password
*
* @param string $password password to validate
* @return bool if password provided is valid for current user
*/
public function validatePassword($password)
{
return $this->password === $password;
}
}

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

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
use yii\helpers\Html;
use yii\bootstrap\ActiveForm;
use app\models\AccountForm;
/**
* @todo Восстановить сохранение сессии
*/
?>
<div class="container">
<div class="row">
<div class="mx-auto">
<?php
$form = ActiveForm::begin([
'id' => 'form_account',
'action' => false,
'fieldConfig' => [
'template' => '{label}{input}{error}',
'options' => ['class' => '']
],
'options' => [
'class' => '',
'onsubmit' => 'return false;'
]
]);
$model = $model ?? new AccountForm;
?>
<?= $form->field($model, 'mail', ['enableLabel' => false, 'options' => ['class' => 'mb-2']])->textInput(['autofocus' => true, 'placeholder' => $model->getAttributeLabel('mail')]) ?>
<?= $form->field($model, 'pswd', ['enableLabel' => false])->passwordInput(['placeholder' => $model->getAttributeLabel('pswd')]) ?>
<div class="d-flex mb-2 mt-3">
<?= Html::submitButton('Войти', ['name' => 'submitAuthentication', 'onclick' => 'authentication(this.parentElement.parentElement);', 'class' => 'flex-grow-1 mr-2 btn btn-primary button_clean']) ?>
<?= $form->field($model, 'auto', ['checkboxTemplate' => '<div class="checkbox button_clean">{beginLabel}' .
Html::submitButton('{labelTitle}', ['name' => 'submit', 'data-toggle' => 'button', 'class' => 'w-100 btn btn-primary button_clean', 'aria-pressed' => 'false']) .
'{endLabel}</div>'])->checkbox()->label($model->getAttributeLabel('auto'), ['class' => 'w-100 m-0']) ?>
</div>
<?= Html::submitButton('Регистрация', ['name' => 'submitRegistration', 'onclick' => 'registration(this.parentElement);', 'class' => 'col-12 ml-auto btn btn-success btn-sm button_clean']) ?>
<?php ActiveForm::end(); ?>
</div>
</div>
</div>

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

@@ -0,0 +1,16 @@
<?php
use yii\helpers\Html;
$this->title = $name;
?>
<div id="page_error" class="container py-3">
<h1><?= Html::encode($this->title) ?></h1>
<div class="alert alert-danger">
<?= nl2br(Html::encode($message)) ?>
</div>
</div>

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
$this->title = 'SkillParts';
?>
<link href="/css/ticker.css" rel="stylesheet">
<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">
<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>
</section>
<section class="h-100 d-flex ticker">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/cummins.png" alt="Cummins">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/iveco.png" alt="Iveco">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/komatsu.png" alt="Komatsu">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/case.png" alt="Case">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/isuzu.png" alt="Isuzu">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/new_holland.png" alt="New Holland">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/perkins.png" alt="Perkins">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/john_deere.png" alt="John Deere">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/caterpillar.png" alt="Caterpillar">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/shantui.png" alt="Shantui">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/xcmg.png" alt="XCMG">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/kobelco.png" alt="Kobelco">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/shehwa.png" alt="SHEHWA">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/bomag.png" alt="BOMAG">
<img class="w-auto h-100 mr-3 my-auto" src="/img/logos/h32px/compressed/hitachi.png" alt="Hitachi">
</section>
<section class="container mb-4">
<!-- <div class="row mb-3">
<h4 class="col gilroy categories_blocks_panel_title">Сопутствующие товары</h4>
</div> -->
<div class="row mb-5 mb-md-0 px-3 px-md-0">
<div class="col-12 col-md-6 col-lg-4 mb-4 mb-lg-0 py-0 d-flex flex-column">
<div class="px-3 px-xl-4 pt-3 d-inline-block category_block_title">
<h4 class="m-0">Масла, смазки</h4>
</div>
<div class="p-3 px-md-4 category_block">
<dl class="mb-0">
<dd>Масла моторные</dd>
<dd>Масла трансмиссионные</dd>
<dd>Масла гидравлические</dd>
<dd>Смазки</dd>
</dl>
</div>
</div>
<div class="col-12 col-md-6 col-lg-4 mb-4 mb-lg-0 py-0 d-flex flex-column">
<div class="px-3 px-xl-4 pt-3 d-inline-block category_block_title">
<h4 class="m-0">Электрооборудование</h4>
</div>
<div class="p-3 px-md-4 category_block">
<dl class="mb-0">
<dd>Фары и свет</dd>
</dl>
</div>
</div>
<div class="col-12 col-md-6 col-lg-4 mb-4 mb-lg-0 py-0 d-flex flex-column">
<div class="px-3 px-xl-4 pt-3 d-inline-block category_block_title">
<h4 class="m-0">Инструмент</h4>
</div>
<div class="p-3 px-md-4 category_block">
<dl class="mb-0">
<dd>Шприцы для смазки </dd>
<dd>Ключи, съёмники</dd>
<dd>Наборы инструментов</dd>
</dl>
</div>
</div>
</div>
</section>
</div>
<script src="/js/ticker.js" defer></script>

View File

@@ -1,14 +1,17 @@
<?php
/* @var $this \yii\web\View */
/* @var $content string */
declare(strict_types=1);
use yii\helpers\Html;
use app\assets\AppAsset;
AppAsset::register($this);
?>
<?php $this->beginPage() ?>
<!DOCTYPE html>
<html lang="<?= Yii::$app->language ?>">
@@ -16,93 +19,119 @@ 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) ?></title>
<title><?= Html::encode($this->title ?? 'SkillParts') ?></title>
<?php $this->head() ?>
</head>
<body>
<?php $this->beginBody() ?>
<header>
<div class="container">
<div class="row mt-3 mt-sm-0 pt-3">
<div class="col-3 col-sm-4 col-md-2 d-flex flex-column justify-content-end logotype">
<img class="img-fluid" src="/img/logos/skillparts.png" alt="SkillParts">
</div>
<div class="col ml-auto text-right d-flex flex-column justify-content-end">
<div class="row">
<div class="col">
<p class="m-0"><i class="fas fa-shopping-cart mr-4"></i><i class="fas fa-list mr-4"></i><a>Личный кабинет</a></p>
</div>
</div>
</div>
</div>
<div id="notifications_popup_wrap" class="col-3 m-4"></div>
<div class="row mb-4">
<div class="col">
<div class="h-divider d-flex">
<div class="col-1 ml-auto p-0 h-divider-title-left"></div>
<div class="col-5 col-lg-4 h-divider-title d-flex flex-column justify-content-center">
<h6 class="text-center text-white my-0"><b>Запчасти для спецтехники</b></h6>
</div>
<div class="col-1 mr-auto p-0 h-divider-title-right"></div>
</div>
</div>
<header class="container pt-2 mt-1 mb-2 mb-sm-4">
<div class="row h-100">
<div id="logo" class="col-3 col-md-4 h-100">
<a class="py-2 h-100 d-inline-flex" title="SkillParts" href="/" role="button" onclick="return page_main();">
<img class="h-100" src="/img/logos/skillparts.svg" alt="SkillParts">
</a>
</div>
<div class="row mb-4">
<div class="col-lg-3 pr-0 pr-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">
<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">
<a class="dropdown-item" href="#">Action</a>
<a class="dropdown-item" href="#">Another action</a>
<a class="dropdown-item" href="#">Something else here</a>
</div>
</div>
<div id="searchPanel" class="col">
<!-- <input id="catalog_search_panel_button_1" class="btn btn-sm button_clean" type="radio" name="catalog_search_panel_buttons" value="catalog_search_panel_button_1" checked>
<label class="mb-0 px-3 px-md-4 py-1" for="catalog_search_panel_button_1">Номер детали</label>
<input id="catalog_search_panel_button_2" class="btn btn-sm text-white button_clean" type="radio" name="catalog_search_panel_buttons" value="catalog_search_panel_button_2">
<label class="mb-0 px-3 px-md-4 py-1" for="catalog_search_panel_button_2">Вторая кнопка</label>
<input id="catalog_search_panel_button_3" class="btn btn-sm5 text-white button_clean" type="radio" name="catalog_search_panel_buttons" value="catalog_search_panel_button_3">
<label class="mb-0 px-3 px-md-4 py-1" for="catalog_search_panel_button_3">Третья кнопка</label> -->
<form class="d-flex catalog_search">
<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>
<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>
<menu class="col-auto col-lg-4 mb-0 d-flex justify-content-end"></menu>
</div>
<div class="h-divider"></div>
</header>
<main>
<aside class="container mb-4">
<div class="row">
<div class="col-lg-3 d-flex flex-column align-center justify-content-end dropdownMenuButton_column">
<button id="catalog" class="btn form-control d-flex align-items-center button_clean catalog_button" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-bars col-auto text-left p-0 mr-auto h-100 d-flex flex-column justify-content-center"></i>
<p class="col-10 m-0 p-0">Каталог товаров</p>
</button>
<div class="dropdown-menu" aria-labelledby="catalog">
<a class="dropdown-item" href="#">Action</a>
<a class="dropdown-item" href="#">Another action</a>
<a class="dropdown-item" href="#">Something else here</a>
</div>
</div>
<div id="searchPanel" class="col">
<!-- <input id="catalog_search_panel_button_1" class="btn btn-sm button_clean" type="radio" name="catalog_search_panel_buttons" value="catalog_search_panel_button_1" checked>
<label class="mb-0 px-3 px-md-4 py-1" for="catalog_search_panel_button_1">Номер детали</label>
<input id="catalog_search_panel_button_2" class="btn btn-sm text-white button_clean" type="radio" name="catalog_search_panel_buttons" value="catalog_search_panel_button_2">
<label class="mb-0 px-3 px-md-4 py-1" for="catalog_search_panel_button_2">Вторая кнопка</label>
<input id="catalog_search_panel_button_3" class="btn btn-sm5 text-white button_clean" type="radio" name="catalog_search_panel_buttons" value="catalog_search_panel_button_3">
<label class="mb-0 px-3 px-md-4 py-1" for="catalog_search_panel_button_3">Третья кнопка</label> -->
<form class="d-flex catalog_search" onsubmit="return false;">
<div class="position-relative col-sm-8 col-lg-10 px-0">
<input id="search_line" type="text" class="form-control col-12 catalog_search_line button_clean" placeholder="Введите номер запчасти, например: 45223503481" oninput="$('#search_line').dropdown('hide'); product_search(this.value);" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" autocomplete="off">
<?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 d-flex flex-column justify-content-center">
<?= $content ?>
</main>
<footer class="py-4">
<div class="container">
<div class="row px-3">
<div class="col-12 col-md-auto mr-md-5">
<h5 class="row mb-2"><b>Контакты</b></h5>
<small class="row mb-1"><b>Адрес:&nbsp;</b>Хабаровск, Промышленная 3, 105</small>
<small class="row mb-1"><b>Время работы:&nbsp;</b>пн-пт 09:00-18:00</small>
<small class="row mb-1"><b>Телефон:&nbsp;</b>+7 (4212) 35-85-34</small>
<small class="row mb-1"><b>Почта:&nbsp;</b>info@skillparts.ru</small>
</div>
<div class="col-md-auto mr-md-5 partnership">
<h5 class="row mb-2"><b>Партнёрство</b></h5>
<small class="row mb-1"><a>Оптовым покупателям</a></small>
<small class="row mb-1"><a>Поставщикам</a></small>
<small class="row mb-1"><a>Партнерская сеть</a></small>
</div>
<!-- <p class="pull-left">&copy; My Company <?= date('Y') ?></p>
<p class="pull-right"><?= Yii::powered() ?></p> -->
<footer class="container py-4">
<div class="row px-3">
<div class="col-12 col-md-auto mr-md-5">
<h5 class="row mb-2"><b>Контакты</b></h5>
<small class="row mb-1"><b>Адрес:&nbsp;</b>Хабаровск, Промышленная 3, 105</small>
<small class="row mb-1"><b>Время работы:&nbsp;</b>пн-пт 09:00-18:00</small>
<small class="row mb-1"><b>Телефон:&nbsp;</b>+7 (4212) 35-85-34</small>
<small class="row mb-1"><b>Почта:&nbsp;</b>info@skillparts.ru</small>
</div>
<div class="col-md-auto mr-md-5 partnership">
<h5 class="row mb-2"><b>Партнёрство</b></h5>
<small class="row mb-1"><a>Оптовым покупателям</a></small>
<small class="row mb-1"><a>Поставщикам</a></small>
<small class="row mb-1"><a>Партнерская сеть</a></small>
</div>
</div>
</footer>
@@ -111,4 +140,5 @@ AppAsset::register($this);
</body>
</html>
<?php $this->endPage() ?>

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>

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