diff --git a/mirzaev/skillparts/system/commands/ImportController.php b/mirzaev/skillparts/system/commands/ImportController.php new file mode 100644 index 0000000..8290142 --- /dev/null +++ b/mirzaev/skillparts/system/commands/ImportController.php @@ -0,0 +1,63 @@ + 0) { + // Найдены файлы + + foreach ($files as $file) { + // Перебор файлов для загрузки + + // Загрузка в базу данных + Supply::loadExcel($file); + } + } + } catch (exception $e) { + return ExitCode::UNSPECIFIED_ERROR; + } + + return ExitCode::OK; + } +} diff --git a/mirzaev/skillparts/system/commands/SuppliesController.php b/mirzaev/skillparts/system/commands/SuppliesController.php new file mode 100644 index 0000000..eceb163 --- /dev/null +++ b/mirzaev/skillparts/system/commands/SuppliesController.php @@ -0,0 +1,24 @@ + true, 'actions' => [ 'file', - 'data' + 'data', + 'restore', + 'generate-password' ] ], [ @@ -38,7 +41,10 @@ class AccountController extends Controller ], [ 'allow' => true, - 'actions' => ['accept', 'decline'], + 'actions' => [ + 'read', + 'accept', + 'decline'], 'matchCallback' => function ($rule, $action): bool { if ( !yii::$app->user->isGuest @@ -352,4 +358,127 @@ class AccountController extends Controller ]; } } + + /** + * Восстановление пароля + * + * @return string + */ + public function actionRestore() + { + // Инициализация + $model = new AccountForm(yii::$app->request->post('AccountForm')); + $type = yii::$app->request->post('type') ?? yii::$app->request->get('type'); + $target = yii::$app->request->post('target') ?? yii::$app->request->get('target'); + + // Фильтрация + $target = match ($target) { + 'panel' => 'panel', + 'main' => 'main', + default => 'main' + }; + + // Рендер для всплывающей панели + $panel = $target === 'panel'; + + if (yii::$app->request->isPost) { + // AJAX-POST-запрос + + // Настройка кода ответа + yii::$app->response->format = Response::FORMAT_JSON; + + if (yii::$app->user->isGuest || $model->validate()) { + // Аккаунт не аутентифицирован и проверка пройдена + + // Отправка запроса на генерацию пароля и запись ответа + $return = [ + 'status' => Account::restoreSend($model->mail), + '_csrf' => yii::$app->request->getCsrfToken() + ]; + + return $return; + } else { + // Аккаунт аутентифицирован + + // Настройка кода ответа + yii::$app->response->statusCode = 400; + + return [ + 'redirect' => '/', + '_csrf' => yii::$app->request->getCsrfToken() + ]; + } + } + } + + /** + * Генерация нового пароля + * + * @return string + */ + public function actionGeneratePassword(string $id, string $key) + { + if ($account = Account::searchById(Account::collectionName() . "/$id")) { + // Найден аккаунт + + if ($account->chpk === $key) { + // Ключи совпадают + + // Инициализация буфера пароля + $old = $account->pswd; + + // Генерация пароля + $account->restoreGenerate(); + + if ($account->pswd !== $old) { + // Успешно сгенерирован новый пароль + + // Инициализация формы аутентификации + $form = new AccountForm; + + // Запись параметров + $form->mail = $account->mail; + $form->pswd = $account->pswd; + + // Аутентификация + $form->authentication(); + } + } + } + + // Перенаправление на главную страницу + $this->redirect('/'); + } + + /** + * Генерация нового пароля + * + * @return string + */ + public function actionRead(int $page = 1): string|array|null + { + if (yii::$app->request->isPost) { + // POST-запрос + + // Инициализация входных параметров + $amount = yii::$app->request->post('amount') ?? yii::$app->request->get('amount') ?? 20; + $order = yii::$app->request->post('order') ?? yii::$app->request->get('order') ?? ['DESC']; + + // Инициализация cookie + $cookies = yii::$app->response->cookies; + + // Чтение аккаунтов + $accounts = Account::read(limit: $amount, page: $page, order: $order); + + // Запись формата ответа + yii::$app->response->format = Response::FORMAT_JSON; + + return [ + 'accounts' => $this->renderPartial('/account/list', compact('accounts', 'amount', 'page')), + '_csrf' => yii::$app->request->getCsrfToken() + ]; + } + + return false; + } } diff --git a/mirzaev/skillparts/system/controllers/AuthenticationController.php b/mirzaev/skillparts/system/controllers/AuthenticationController.php index e60dbb9..8e0a56f 100644 --- a/mirzaev/skillparts/system/controllers/AuthenticationController.php +++ b/mirzaev/skillparts/system/controllers/AuthenticationController.php @@ -4,6 +4,7 @@ namespace app\controllers; use app\models\AccountForm; use app\models\Order; +use app\models\Notification; use yii; use yii\web\Controller; use yii\web\Response; @@ -61,6 +62,9 @@ class AuthenticationController extends Controller if (!yii::$app->user->isGuest || $model->authentication()) { // Аккаунт аутентифицирован + // Отправка уведомления + Notification::_write('Вы аутентифицированы с устройства ' . $_SERVER['HTTP_USER_AGENT'] . ' ' . $_SERVER['REMOTE_ADDR'], true, yii::$app->user->identity->_key, Notification::TYPE_NOTICE); + // Инициализация $notifications_button = $this->renderPartial('/notification/button'); $notifications_panel = $this->renderPartial('/notification/panel', ['notifications_panel_full' => true]); diff --git a/mirzaev/skillparts/system/controllers/CartController.php b/mirzaev/skillparts/system/controllers/CartController.php index ef82ecb..5f76c12 100644 --- a/mirzaev/skillparts/system/controllers/CartController.php +++ b/mirzaev/skillparts/system/controllers/CartController.php @@ -88,7 +88,6 @@ class CartController extends Controller // Поиск корзины (текущего заказа) $data = Order::searchSmart()[0] ?? null; - if (empty($data['order'])) { // Корзина не инициализирована diff --git a/mirzaev/skillparts/system/controllers/OrderController.php b/mirzaev/skillparts/system/controllers/OrderController.php index 728ae1f..40c8c2a 100644 --- a/mirzaev/skillparts/system/controllers/OrderController.php +++ b/mirzaev/skillparts/system/controllers/OrderController.php @@ -56,6 +56,21 @@ class OrderController extends Controller 'supply-edit-comm' ] ], + [ + 'allow' => true, + 'actions' => [ + 'list' + ], + 'matchCallback' => function ($rule, $action): bool { + if ( + !yii::$app->user->isGuest + && (yii::$app->user->identity->type === 'administrator' + || yii::$app->user->identity->type === 'moderator') + ) return true; + + return false; + } + ], [ 'allow' => false, 'roles' => ['?'], @@ -283,51 +298,20 @@ class OrderController extends Controller to: $to, search: $search ); - - // Инициализация панели модератора заказов - if (!yii::$app->user->isGuest && Account::isMinimalAuthorized($account)) { - // Имеет доступ пользователь - - // Инициализация заказов для модератора - $moderator_data = Order::searchSmart(account: '@all', stts: '@all', limit: 10, page: 1, supplies: true); - } else { - // Не имеет доступ пользователь - - // Инициализация заглушки - $moderator_data = null; - } } else if ($window === 'orders_panel_moderation') { // Обработка панели модерации заказов // Инициализация заказов $data = Order::searchSmart( - stts: '@all', + stts: $stts, limit: 10, page: 1, - supplies: true + supplies: true, + from: $from, + to: $to, + search: $search ); - // Инициализация панели модератора заказов - if (!yii::$app->user->isGuest && Account::isMinimalAuthorized($account)) { - // Имеет доступ пользователь - - // Инициализация заказов для модератора - $moderator_data = Order::searchSmart( - account: '@all', - stts: $stts, - limit: 10, - page: 1, - supplies: true, - from: $from, - to: $to, - search: $search - ); - } else { - // Не имеет доступ пользователь - - // Инициализация заглушки - $moderator_data = null; - } } else { // Запрошено неизвестное окно } @@ -343,7 +327,7 @@ class OrderController extends Controller yii::$app->response->format = Response::FORMAT_JSON; return [ - 'main' => $this->renderPartial('/orders/index', compact('data', 'moderator_data', 'account', 'search', 'from', 'to', 'window') + 'main' => $this->renderPartial('/orders/index', compact('data', 'account', 'search', 'from', 'to', 'window') + ['panel' => $this->renderPartial('/orders/search/panel', compact('account') + ['data' => $data] ?? null)]), 'title' => 'Заказы', 'redirect' => '/orders', @@ -351,7 +335,7 @@ class OrderController extends Controller ]; } - return $this->render('/orders/index', compact('data', 'moderator_data', 'account')); + return $this->render('/orders/index', compact('data', 'account')); } /** @@ -822,6 +806,60 @@ class OrderController extends Controller } } + /** + * Список заказов для модерации + * + * @param string $catn Артикул + */ + public function actionList(int $page): array|string|null + { + if (!yii::$app->user->isGuest && Account::isMinimalAuthorized()) { + // Авторизован пользователь + + // Инициализация буфера ответа + $return = [ + '_csrf' => yii::$app->request->getCsrfToken() + ]; + + // Инициализация количества + $amount = yii::$app->request->post('amount') ?? yii::$app->request->get('amount') ?? 8; + + if ($moderator_data = Order::searchSmart( + account: '@all', + stts: '@all', + limit: (int) $amount, + page: $page, + supplies: true + )) { + // Найдены заказы + + // Генерация списка + $list = trim($this->renderPartial('/orders/moderation', compact('moderator_data'))); + + if (!empty($list)) $return['list'] = $list; + } else { + // Не найдены заказы + + // Запись кода ответа + yii::$app->response->statusCode = 500; + + // Запись в буфер возврата + // $return['alert'] = "Не удалось найти заказы для генерации списка"; + } + + if (yii::$app->request->isPost) { + // POST-запрос + + yii::$app->response->format = Response::FORMAT_JSON; + + return $return; + } + } + + // Переадресация на главную страницу + return $this->redirect("/"); + } + /** * Чтение инстанции поставки в заказе (order_edge_supply) * diff --git a/mirzaev/skillparts/system/controllers/ProductController.php b/mirzaev/skillparts/system/controllers/ProductController.php index 2db8ed4..b4fc80d 100644 --- a/mirzaev/skillparts/system/controllers/ProductController.php +++ b/mirzaev/skillparts/system/controllers/ProductController.php @@ -33,6 +33,7 @@ class ProductController extends Controller 'allow' => true, 'actions' => [ 'index', + 'analogs' ] ], [ @@ -81,6 +82,36 @@ class ProductController extends Controller ]; } + public function accessDenied() + { + // Инициализация + $cookies = yii::$app->response->cookies; + + // Запись cookie с редиректом, который выполнится после авторизации + $cookies->add(new Cookie([ + 'name' => 'redirect', + 'value' => yii::$app->request->pathInfo + ])); + + if (Yii::$app->request->isPost) { + // POST-запрос + + // Настройка + Yii::$app->response->format = Response::FORMAT_JSON; + + // Генерация ответа + Yii::$app->response->content = json_encode([ + 'main' => $this->renderPartial('/account/index'), + 'redirect' => yii::$app->request->pathInfo, + '_csrf' => Yii::$app->request->getCsrfToken() + ]); + } else if (Yii::$app->request->isGet) { + // GET-запрос + + $this->redirect('/authentication'); + } + } + public function actionIndex(string $prod, string $catn): array|string|null { if ($model = Product::searchByCatnAndProd($catn, $prod)) { @@ -210,34 +241,34 @@ class ProductController extends Controller /** * Чтение товаров * - * @param string $stts Статус + * @param string $type Тип + * @param int $page Страница * * @return string|array|null */ - public function actionRead(string $stts = 'all'): string|array|null + public function actionRead(string $type = 'all', int $page = 1): string|array|null { if (yii::$app->request->isPost) { // POST-запрос // Инициализация входных параметров - $amount = yii::$app->request->post('amount') ?? yii::$app->request->get('amount') ?? 50; + $amount = yii::$app->request->post('amount') ?? yii::$app->request->get('amount') ?? 20; $order = yii::$app->request->post('order') ?? yii::$app->request->get('order') ?? ['DESC']; // Инициализация cookie $cookies = yii::$app->response->cookies; - // Инициализация аккаунта $account ?? $account = Account::initAccount(); // Чтение товаров - $products = Product::read(where: $stts === 'all' || $stts === 'inactive' ? [] : ['stts' => $stts], limit: $amount, order: $order); + $products = Product::read(where: $type === 'all' || $type === 'inactive' ? [] : ['stts' => $type], limit: $amount, page: $page, order: $order); // Запись формата ответа yii::$app->response->format = Response::FORMAT_JSON; return [ - 'products' => $this->renderPartial('/product/list', compact('account', 'products')), + 'products' => $this->renderPartial('/product/list', compact('account', 'products', 'amount', 'page')), '_csrf' => yii::$app->request->getCsrfToken() ]; } @@ -250,15 +281,15 @@ class ProductController extends Controller * * @param string $catn Артикул */ - public function actionDelete(string $catn, string $prod): array|string|null + public function actionDelete(string $prod, string $catn): array|string|null { // Инициализация буфера ответа $return = [ '_csrf' => yii::$app->request->getCsrfToken() ]; - if (empty($catn)) { - // Не получен артикул + if (empty($catn) || empty($prod)) { + // Не получен артикул или производитель // Запись кода ответа yii::$app->response->statusCode = 500; @@ -355,8 +386,8 @@ class ProductController extends Controller '_csrf' => yii::$app->request->getCsrfToken() ]; - if (empty($catn)) { - // Не получен артикул + if (empty($catn) || empty($prod)) { + // Не получен артикул или производитель // Запись кода ответа yii::$app->response->statusCode = 500; @@ -368,49 +399,52 @@ class ProductController extends Controller if ($from = Product::searchByCatnAndProd($catn, $prod)) { // Найден товар - if ($target = yii::$app->request->post('catn') ?? yii::$app->request->get('catn')) { - // Инициализирован артикул товара для связи + if (($target_catn = yii::$app->request->post('catn') ?? yii::$app->request->get('catn')) && + $target_prod = yii::$app->request->post('prod') ?? yii::$app->request->get('prod')) { + // Инициализирован артикул и производитель товара для связи - if ($to = Product::searchByCatn($target)) { - // Существуют товары к которым планируется соединение - } else { - // Не существуют товары к которым планируется соединение + foreach (explode(',', $target_catn, 50) as $analog) { + // Перебор аналогов - // Инициализация товара - if ($to = [Product::writeEmpty((string) $target, $from->prod)]) { - // Удалось записать товар + foreach (explode('/', $analog, 50) as $value) { + // Перебор аналогов (дополнительная фильтрация) - // Запись в буфер возврата - $return['alert'] = "Записан новый товар: $target ($from->prod)"; - } else { - // Не удалось записать товар + if ($to = Product::searchByCatnAndProd($value, $target_prod)) { + // Существуют товары к которым планируется соединение + } else { + // Не существуют товары к которым планируется соединение - // Запись кода ответа - yii::$app->response->statusCode = 500; + // Инициализация товара + if ($to = [Product::writeEmpty($value, $target_prod, true)]) { + // Удалось записать товар + } else { + // Не удалось записать товар - // Запись в буфер возврата - $return['alert'] = "Не удалось записать новый товар: $target ($from->prod)"; + // Запись кода ответа + yii::$app->response->statusCode = 500; - // Переход в конец алгоритма - goto end; + // Запись в буфер возврата + $return['alert'] = "Не удалось записать новый товар: $value ($target_prod)"; + + // Переход в конец алгоритма + goto end; + } + } + + // Инициализация количества созданных рёбер + $writed = 0; + + foreach (is_array($to) ? $to : [$to] as $product) { + // Перебор товаров для записи связи: ТОВАР -> ТОВАР + + // Универсализация данных (приведение к объекту) + if (is_array($product) && !$product = Product::searchByCatnAndProd($product['catn'], $product['prod'])) continue; + + // Запись в группу + if ($from->connect($product)) $writed++; + } } } - - // Инициализация количества созданных рёбер - $writed = 0; - - foreach ($to as $product) { - // Перебор товаров для записи связи: ТОВАР -> ТОВАР - - // Универсализация данных (приведение к объекту) - if (is_array($product) && !$product = Product::searchByCatnAndProd($product['catn'], $product['prod'])) continue; - - // Запись ребра и синхронизация (добавление в группу к остальным аналогам) - if (count($from->synchronization($product)) > 0) $writed++; - } - - // Запись в буфер возврата - $return['alert'] = "Создано $writed связей"; } } else { // Не найден товар @@ -433,6 +467,9 @@ class ProductController extends Controller yii::$app->response->format = Response::FORMAT_JSON; + // Запись в буфер возврата + $return['list'] = $this->renderPartial('analogs', ['model' => $from]); + return $return; } @@ -462,7 +499,7 @@ class ProductController extends Controller ]; if (empty($catn) || empty($prod)) { - // Не получен артикул + // Не получен артикул или производитель // Запись кода ответа yii::$app->response->statusCode = 500; @@ -471,35 +508,32 @@ class ProductController extends Controller goto end; } - if ($from = Product::searchByCatnAndProd($catn, $prod)) { + if ($target = Product::searchByCatnAndProd($catn, $prod)) { // Товар найден - // Инициализация цели - $target = yii::$app->request->post('catn') ?? yii::$app->request->get('catn'); + if ($target->disconnect()) { + // Удалено ребро (связь) - if ($from->disconnect($target)) { - // Удалено ребро (связь) + // Запись в буфер возврата + $return['disconnected'] = 1; + } else { + // Не удалено ребро (связь) - // Запись в буфер возврата - $return['alert'] = "Продукты успешно отсоединены"; - } else { - // Не удалено ребро (связь) + // Запись кода ответа + yii::$app->response->statusCode = 500; - // Запись кода ответа - yii::$app->response->statusCode = 500; + // Запись в буфер возврата + $return['alert'] = "Не удалось удалить $catn ($prod) из группы"; - // Запись в буфер возврата - $return['alert'] = "Не удалось отсоединить $target от $catn"; - - // Переход в конец алгоритма - goto end; - } + // Переход в конец алгоритма + goto end; + } } else { // Запись кода ответа yii::$app->response->statusCode = 500; // Запись в буфер возврата - $return['alert'] = "Не удалось найти товар от когорого требуется отсоединение: $catn"; + $return['alert'] = "Не удалось найти товар $catn ($prod)"; // Переход в конец алгоритма goto end; @@ -529,6 +563,64 @@ class ProductController extends Controller } } + /** + * Отключение аналога + * + * @param string $catn Артикул + */ + public function actionAnalogs(string $catn, string $prod, int $page): array|string|null + { + // Инициализация буфера ответа + $return = [ + '_csrf' => yii::$app->request->getCsrfToken() + ]; + + if (empty($catn) || empty($prod)) { + // Не получен артикул или производитель + + // Запись кода ответа + yii::$app->response->statusCode = 500; + + // Переход в конец алгоритма + goto end; + } + + // Инициализация количества + $amount = yii::$app->request->post('amount') ?? yii::$app->request->get('amount') ?? 30; + + if ($model = Product::searchByCatnAndProd($catn, $prod)) { + // Товар найден + + // Генерация списка + $list = trim($this->renderPartial('analogs', compact('model', 'page', 'amount'))); + + if (!empty($list)) $return['list'] = $list; + } else { + // Запись кода ответа + yii::$app->response->statusCode = 500; + + // Запись в буфер возврата + $return['alert'] = "Не удалось найти товар $catn ($prod) для инициализации аналогов"; + + // Переход в конец алгоритма + goto end; + } + + // Конец алгоритма + end: + + if (yii::$app->request->isPost) { + // POST-запрос + + yii::$app->response->format = Response::FORMAT_JSON; + + return $return; + } + + // Переадресация на главную страницу + return $this->redirect("/"); + } + public function actionEditTitle(string $catn, string $prod): array|string|null { // Инициализация diff --git a/mirzaev/skillparts/system/controllers/ProfileController.php b/mirzaev/skillparts/system/controllers/ProfileController.php index f566f53..2980f80 100644 --- a/mirzaev/skillparts/system/controllers/ProfileController.php +++ b/mirzaev/skillparts/system/controllers/ProfileController.php @@ -884,7 +884,7 @@ class ProfileController extends Controller /** * Поиск заявок на регистрацию */ - public function actionPanelSuppliersRequestsSearch() + public function actionPanelSuppliersRequestsSearch(int $page = 1) { if (Yii::$app->request->isPost) { // POST-запрос @@ -892,11 +892,15 @@ class ProfileController extends Controller if (Account::isAdmin() || Account::isModer()) { // Доступ разрешен + // Инициализация входных параметров + $amount = yii::$app->request->post('amount') ?? yii::$app->request->get('amount') ?? 3; + $order = yii::$app->request->post('order') ?? yii::$app->request->get('order') ?? ['DESC']; + // Инициализация буфера ответа $response = []; // Поиск заявок на регистрацию - $suppliers = Account::searchSuppliersRequests(); + $suppliers = Account::searchSuppliersRequests($amount, $page, $order); foreach ($suppliers as $account) { // Перебор заявок @@ -1059,6 +1063,8 @@ class ProfileController extends Controller foreach (Supply::searchByImport($import->readId(), limit: 9999) as $supply) { // Перебор найденных поставок + if (empty($supply)) continue; + if (ImportEdgeSupply::searchBySupply($supply)?->delete()) { // Удалено ребро: ИНСТАНЦИЯ ПОСТАВКИ -> ПОСТАВКА @@ -1068,7 +1074,7 @@ class ProfileController extends Controller } // Отправка уведомления - Notification::_write("Удалено $deleted поставок из инстанции поставки $_key", account: $account->_key); + Notification::_write("Удалено $deleted поставок из инстанции поставки $_key", account: $account->readId()); if ($edge->delete()) { // Удалено ребро: СКЛАД -> ИНСТАНЦИЯ ПОСТАВКИ @@ -1077,7 +1083,7 @@ class ProfileController extends Controller // Удалена инстанция поставки // Отправка уведомления - Notification::_write("Инстанция поставки $_key была удалена", account: $account->_key); + Notification::_write("Инстанция поставки $_key была удалена", account: $account->readId()); } } } @@ -1085,7 +1091,7 @@ class ProfileController extends Controller // Не найдена инстанция поставки // Отправка уведомления - Notification::_write("Не найдена инстанция поставки $_key", account: $account->_key); + Notification::_write("Не найдена инстанция поставки $_key", account: $account->readId()); } // Запись в буфер вывода реинициализированного элемента diff --git a/mirzaev/skillparts/system/controllers/SearchController.php b/mirzaev/skillparts/system/controllers/SearchController.php index 5c13ba3..5114828 100644 --- a/mirzaev/skillparts/system/controllers/SearchController.php +++ b/mirzaev/skillparts/system/controllers/SearchController.php @@ -106,7 +106,7 @@ class SearchController extends Controller $return = [ 'timer' => $timer, - 'panel' => $this->renderPartial('/search/loading'), + 'search_line_window_show' => 1, '_csrf' => yii::$app->request->getCsrfToken() ]; } else { @@ -159,6 +159,7 @@ class SearchController extends Controller // Запись ответа $return = [ 'panel' => $this->renderPartial('/search/panel', compact('response', 'query')), + 'search_line_window_show' => 1, '_csrf' => yii::$app->request->getCsrfToken() ]; @@ -167,7 +168,7 @@ class SearchController extends Controller // Запись ответа $return['main'] = $this->renderPartial('/search/index', compact('response', 'query')); - $return['hide'] = 1; + $return['search_line_window_show'] = 1; $return['redirect'] = '/search?type=product&q=' . $query; } } @@ -189,6 +190,7 @@ class SearchController extends Controller return $return ?? [ 'panel' => $this->renderPartial('/search/panel'), + 'search_line_window_show' => 1, '_csrf' => yii::$app->request->getCsrfToken() ]; } else { diff --git a/mirzaev/skillparts/system/controllers/SupplyController.php b/mirzaev/skillparts/system/controllers/SupplyController.php new file mode 100644 index 0000000..9b63e79 --- /dev/null +++ b/mirzaev/skillparts/system/controllers/SupplyController.php @@ -0,0 +1,101 @@ + [ + 'class' => AccessControl::class, + 'rules' => [ + [ + 'allow' => true, + 'actions' => [ + 'index', + ] + ], + [ + 'allow' => true, + 'roles' => ['@'], + 'actions' => [] + ], + [ + 'allow' => true, + 'actions' => [ + 'read' + ], + 'matchCallback' => function ($rule, $action): bool { + if ( + !yii::$app->user->isGuest + && (yii::$app->user->identity->type === 'administrator' + || yii::$app->user->identity->type === 'moderator') + ) return true; + + return false; + } + ], + [ + 'allow' => false, + 'roles' => ['?'], + 'denyCallback' => [$this, 'accessDenied'] + ] + ] + ] + ]; + } + + /** + * Чтение поставок + * + * @param int $page Страница + * + * @return string|array|null + */ + public function actionRead(int $page = 1): string|array|null + { + if (yii::$app->request->isPost) { + // POST-запрос + + // Инициализация входных параметров + $amount = yii::$app->request->post('amount') ?? yii::$app->request->get('amount') ?? 20; + $order = yii::$app->request->post('order') ?? yii::$app->request->get('order') ?? ['DESC']; + + // Инициализация cookie + $cookies = yii::$app->response->cookies; + + // Чтение поставок + $supplies = Supply::read(limit: $amount, page: $page, order: $order); + + // Запись формата ответа + yii::$app->response->format = Response::FORMAT_JSON; + + return [ + 'supplies' => $this->renderPartial('/supply/list', compact('supplies', 'amount', 'page')), + '_csrf' => yii::$app->request->getCsrfToken() + ]; + } + + return false; + } +} diff --git a/mirzaev/skillparts/system/migrations/arangodb/m220808_185553_create_file_collection.php b/mirzaev/skillparts/system/migrations/arangodb/m220808_185553_create_file_collection.php new file mode 100644 index 0000000..04199f3 --- /dev/null +++ b/mirzaev/skillparts/system/migrations/arangodb/m220808_185553_create_file_collection.php @@ -0,0 +1,20 @@ +createCollection('file', ['type' => 2]); + } + + public function down() + { + $this->dropCollection('file'); + } +} diff --git a/mirzaev/skillparts/system/migrations/arangodb/m220817_210350_create_import_edge_file_collection.php b/mirzaev/skillparts/system/migrations/arangodb/m220817_210350_create_import_edge_file_collection.php new file mode 100644 index 0000000..f2ab761 --- /dev/null +++ b/mirzaev/skillparts/system/migrations/arangodb/m220817_210350_create_import_edge_file_collection.php @@ -0,0 +1,20 @@ +createCollection('import_edge_file', ['type' => 3]); + } + + public function down() + { + $this->dropCollection('import_edge_file'); + } +} diff --git a/mirzaev/skillparts/system/models/Account.php b/mirzaev/skillparts/system/models/Account.php index 23c093c..f0f8f91 100644 --- a/mirzaev/skillparts/system/models/Account.php +++ b/mirzaev/skillparts/system/models/Account.php @@ -62,7 +62,8 @@ class Account extends Document implements IdentityInterface, PartnerInterface 'vrfy', 'geol', 'auth', - 'acpt' + 'acpt', + 'chpk' ] ); } @@ -98,7 +99,8 @@ class Account extends Document implements IdentityInterface, PartnerInterface 'vrfy' => 'Статус подтверждения владением почты', 'geol' => 'Геолокация', 'auth' => 'Аутентификационный хеш', - 'acpt' => 'Согласие с офертой' + 'acpt' => 'Согласие с офертой', + 'chpk' => 'Ключ для смены пароля' ] ); } @@ -212,6 +214,59 @@ class Account extends Document implements IdentityInterface, PartnerInterface return false; } + /** + * Отправка сообщения для генерации нового пароля + */ + public static function restoreSend(string $mail): bool + { + if (($account = self::findByMail($mail)) instanceof self) { + // Найден аккаунт + + // Запись ключа для аутентификации + $account->chpk = yii::$app->security->generateRandomString(); + + if ($account->update()) { + // Удалось обновить аккаунт + + // Отправка письма + yii::$app->mail_system->compose() + ->setFrom(yii::$app->params['mail']['system']) + ->setTo($account->mail) + ->setSubject('Подтвердите сброс пароля') + ->setHtmlBody(yii::$app->controller->renderPartial('/mails/restore', ['id' => $account->_key, 'chpk' => $account->chpk])) + ->send(); + + return true; + } + } + + return false; + } + + /** + * Генерация нового пароля + */ + public function restoreGenerate(): void + { + // Удаление ключа + $this->chpk = null; + + // Генерация пароля + $this->pswd = self::passwordGenerate(); + + if ($this->update()) { + // Удалось обновить аккаунт + + // Отправка письма + yii::$app->mail_system->compose() + ->setFrom(yii::$app->params['mail']['system']) + ->setTo($this->mail) + ->setSubject('Генерация пароля') + ->setHtmlBody(yii::$app->controller->renderPartial('/mails/password', ['pswd' => $this->pswd])) + ->send(); + } + } + /** * Чтение полей для экспорта из 1С */ @@ -669,18 +724,15 @@ class Account extends Document implements IdentityInterface, PartnerInterface edge: 'account_edge_supply', direction: 'OUTBOUND', subquery_where: [ - [ - 'account_edge_supply._from == account._id' - ], [ 'account_edge_supply._to == "' . $_id . '"' ] ], subquery_select: 'account', where: 'account_edge_supply[0]._id != null', - limit: 1, - select: 'account_edge_supply[0]' - )[0]; + select: 'account_edge_supply[0]', + limit: 1 + )[0] ?? null; } /** @@ -918,9 +970,9 @@ class Account extends Document implements IdentityInterface, PartnerInterface * * @return array */ - public static function searchSuppliersRequests(): array + public static function searchSuppliersRequests(int $amount = 3, int $page, array $order = ['DESC']): array { - return self::find()->where(['agnt' => true, 'type' => 'requested'])->orderBy(['DESC'])->all(); + return self::find()->where(['agnt' => true, 'type' => 'requested'])->limit($amount)->offset($amount * ($page - 1))->orderBy($order)->all(); } /** @@ -928,12 +980,12 @@ class Account extends Document implements IdentityInterface, PartnerInterface * * @param static|null $account Аккаунт */ - public static function initAccount(Account|int $account = null): ?static + public static function initAccount(Account|string|int $account = null): ?static { if (is_null($account)) { // Данные аккаунта не переданы - if (yii::$app->user->isGuest) { + if (empty(yii::$app->user) || yii::$app->user->isGuest) { // Аккаунт не аутентифицирован } else { // Аккаунт аутентифицирован @@ -955,6 +1007,15 @@ class Account extends Document implements IdentityInterface, PartnerInterface if ($account = Account::searchById(Account::collectionName() . "/$account")) { // Удалось инициализировать аккаунт + return $account; + } + } else if (is_string($account)) { + // Передан идентификатор документа (_id) (подразумевается) + + // Инициализация (поиск в базе данных) + if ($account = Account::searchById($account)) { + // Удалось инициализировать аккаунт + return $account; } } diff --git a/mirzaev/skillparts/system/models/Document.php b/mirzaev/skillparts/system/models/Document.php index 8ae593a..b47ad2b 100644 --- a/mirzaev/skillparts/system/models/Document.php +++ b/mirzaev/skillparts/system/models/Document.php @@ -168,9 +168,9 @@ abstract class Document extends ActiveRecord /** * Чтение записей по максимальному ограничению */ - public static function read(?array $where = [], int $limit = 100, ?array $order = null): array + public static function read(?array $where = [], int $limit = 100, int $page = 1, ?array $order = null): array { - return static::find()->where($where)->orderby($order)->limit($limit)->all(); + return static::find()->where($where)->orderby($order)->limit($limit)->offset(($page - 1) * $limit)->all(); } /** diff --git a/mirzaev/skillparts/system/models/Edge.php b/mirzaev/skillparts/system/models/Edge.php index 3d06e84..761fe65 100644 --- a/mirzaev/skillparts/system/models/Edge.php +++ b/mirzaev/skillparts/system/models/Edge.php @@ -167,7 +167,7 @@ abstract class Edge extends Document /** * Поиск рёбер по направлению */ - public static function searchByDirection(string $_from, string $direction = 'OUTBOUND', string $type = '', array $where = [], int $limit = 1): static|array|null + public static function searchByDirection(string $_from, string $direction = 'OUTBOUND', string $type = '', array $where = [], int $limit = 1, int $page = 1): static|array|null { if (str_contains($direction, 'OUTBOUND')) { // Исходящие рёбра @@ -204,7 +204,7 @@ abstract class Edge extends Document } else if (str_contains($direction, 'ANY')) { // Исходящие и входящие рёбра - return static::searchByDirection(_from: $_from, direction: 'OUTBOUND', type: $type, where: $where, limit: $limit) + static::searchByDirection(_from: $_from, direction: 'INBOUND', type: $type, where: $where, limit: $limit); + return static::searchByDirection(_from: $_from, direction: 'OUTBOUND', type: $type, where: $where, limit: $limit, page: $page) + static::searchByDirection(_from: $_from, direction: 'INBOUND', type: $type, where: $where, limit: $limit, page: $page); } if ($limit < 2) { @@ -214,7 +214,7 @@ abstract class Edge extends Document } else { // Несколько рёбер - return $query->limit($limit)->all(); + return $query->limit($limit)->offset($limit * ($page - 1))->all(); } } } diff --git a/mirzaev/skillparts/system/models/File.php b/mirzaev/skillparts/system/models/File.php new file mode 100644 index 0000000..191b057 --- /dev/null +++ b/mirzaev/skillparts/system/models/File.php @@ -0,0 +1,100 @@ + 'Тип файла', + 'path' => 'Относительный от хранилища путь до файла', + 'name' => 'Название файла', + 'user' => 'Пользователь управляющий файлом', + 'stts' => 'Статус', + 'meta' => 'Метаданные' + ] + ); + } + + /** + * Перед сохранением + * + * @todo Подождать обновление от ебаного Yii2 и добавить + * проверку типов передаваемых параметров + */ + public function beforeSave($data): bool + { + if (parent::beforeSave($data)) { + if ($this->isNewRecord) { + if ($this->type = 'supplies excel') { + // Список поставок + + $this->stts = 'needed to load'; + } + } + + return true; + } + + return false; + } + + public static function searchSuppliesNeededToLoad(int $amount = 3): array + { + return static::find()->where(['stts' => 'needed to load'])->limit($amount)->all(); + } + + + /** + * Поиск по инстанции импорта + * + * @param Import $import Инстанция импорта + */ + public static function searchByImport(Import $import): ?File + { + return new File(self::searchByEdge( + from: 'import', + to: 'file', + subquery_where: [ + [ + 'import._id' => $import->readId() + ] + ], + where: 'import_edge_file[0]._id != null', + select: 'file', + limit: 1 + )[0]) ?? null; + } +} diff --git a/mirzaev/skillparts/system/models/Import.php b/mirzaev/skillparts/system/models/Import.php index 5386640..70f6022 100644 --- a/mirzaev/skillparts/system/models/Import.php +++ b/mirzaev/skillparts/system/models/Import.php @@ -79,9 +79,9 @@ class Import extends Document * @param Warehouse $warehouse Инстанция склада * @param int $limit Ограничение по максимальному количеству * - * @return array Инстанции испортов + * @return array Инстанции импортов */ - public static function searchByWarehouse(Warehouse $warehouse, int $limit = 10): array + public static function searchByWarehouse(Warehouse $warehouse, int $limit = 10): ?array { return self::searchByEdge( from: 'warehouse', @@ -103,9 +103,9 @@ class Import extends Document * @param Supply $supply Поставка * @param int $limit Ограничение по максимальному количеству * - * @return array Инстанции испортов + * @return array Инстанции импортов */ - public static function searchBySupply(Supply $supply, int $limit = 10): array + public static function searchBySupply(Supply $supply, int $limit = 10): ?array { return self::searchByEdge( from: 'supply', @@ -120,4 +120,27 @@ class Import extends Document limit: $limit ); } + + /** + * Поиск по файлу + * + * @param File $file Файл + * + * @return Import|null Инстанция импорта + */ + public static function searchByFile(File $file): ?Import + { + return self::searchByEdge( + from: 'file', + to: 'import', + edge: 'import_edge_file', + direction: 'OUTBOUND', + subquery_where: [ + ['import_edge_file._to' => $file->readId()], + ['import_edge_supply.type' => 'connected'] + ], + where: 'import_edge_supply[0] != null', + limit: 1 + )[0] ?? null; + } } diff --git a/mirzaev/skillparts/system/models/ImportEdgeFile.php b/mirzaev/skillparts/system/models/ImportEdgeFile.php new file mode 100644 index 0000000..9126e78 --- /dev/null +++ b/mirzaev/skillparts/system/models/ImportEdgeFile.php @@ -0,0 +1,80 @@ +where(['_to' => $file->readId(), 'type' => 'connected'])->limit($limit)->all(); + } + + /** + * Поиск по инстанции импорта + * + * @param Import $import Инстанция импорта + * @param int $limit Ограничение по максимальному количеству + */ + public static function searchByImport(Import $import, int $limit = 1): array + { + return static::find()->where(['_from' => $import->readId(), 'type' => 'connected'])->limit($limit)->all(); + } +} diff --git a/mirzaev/skillparts/system/models/ImportEdgeSupply.php b/mirzaev/skillparts/system/models/ImportEdgeSupply.php index a4ef76d..52dca78 100644 --- a/mirzaev/skillparts/system/models/ImportEdgeSupply.php +++ b/mirzaev/skillparts/system/models/ImportEdgeSupply.php @@ -110,6 +110,6 @@ class ImportEdgeSupply extends Edge */ public static function searchBySupply(Supply $supply): ?static { - return static::find()->where(['_to' => $supply->readId()])->one()[0] ?? null; + return static::find()->where(['_to' => $supply->readId()])->one() ?? null; } } diff --git a/mirzaev/skillparts/system/models/Notification.php b/mirzaev/skillparts/system/models/Notification.php index 1358912..e2ad113 100644 --- a/mirzaev/skillparts/system/models/Notification.php +++ b/mirzaev/skillparts/system/models/Notification.php @@ -147,16 +147,16 @@ class Notification extends Document * * @param string $html Содержимое уведомления (HTML или текст) * @param bool|string|null $html Содержимое уведомления (HTML или текст) - * @param string $account Получатель уведомления (_key) + * @param string $account Получатель уведомления (_key или "@...") * @param string $type Тип уведомления * * @todo Намного удобнее будет заменить _key на _id, чтобы из рёбер сразу получать аккаунт без лишних операций */ - public static function _write(string $text, bool|string|null $html = false, string $account = null, string $type = self::TYPE_NOTICE): self|array|null + public static function _write(string $text, bool|string|null $html = false, string $account = '', string $type = self::TYPE_NOTICE): self|array|null { // Инициализация $model = new self; - $account ?? $account = yii::$app->user->identity->_key ?? throw new Exception('Не удалось инициализировать получателя'); + $receiver = Account::initAccount($account)->_key ?? $account ?? throw new Exception('Не удалось инициализировать получателя'); if ((bool) (int) $html) { // Получен текст в формете HTML-кода @@ -176,7 +176,7 @@ class Notification extends Document // Уведомление записано // Инициализация получателей и создание ребра - self::searchReceiverAndConnect($model, $account, $type); + self::searchReceiverAndConnect($model, $receiver, $type); } return null; diff --git a/mirzaev/skillparts/system/models/Order.php b/mirzaev/skillparts/system/models/Order.php index 7f8126c..982cb44 100644 --- a/mirzaev/skillparts/system/models/Order.php +++ b/mirzaev/skillparts/system/models/Order.php @@ -273,7 +273,8 @@ class Order extends Document implements DocumentInterface bool $supplies = false, int|null $from = null, int|null $to = null, - bool $count = false + bool $count = false, + bool $debug = false ): int|array|null { // Инициализация аккаунта if (empty($account)) { @@ -350,9 +351,15 @@ class Order extends Document implements DocumentInterface sort: ['DESC'], select: $select, direction: 'INBOUND', - count: !$supplies && $count + count: !$supplies && $count, + debug: $debug ); + if ($debug) { + var_dump($orders); + die; + } + if (!$supplies && $count) { // Запрошен подсчет заказов @@ -363,7 +370,7 @@ class Order extends Document implements DocumentInterface $return = []; // Инициализация архитектуры буфера вывода - foreach ($orders as $key => $order) { + foreach ($orders ?? [null] as $key => $order) { // Перебор заказов // Запись в буфер возврата @@ -526,6 +533,7 @@ class Order extends Document implements DocumentInterface $buffer['delivery'] = $buffer_connection['data']; } else { // Инициализация инстанции продукта в базе данных (реинициализация под ActiveRecord) + $product = Product::searchByCatnAndProd($buffer['product']['catn'], $buffer['product']['prod']); // Инициализация доставки Dellin (автоматическая) @@ -548,6 +556,7 @@ class Order extends Document implements DocumentInterface $product->update(); } + // Запись цены (цена поставки + цена доставки + наша наценка) $buffer['cost'] = ($supply->cost ?? $supply->onec['Цены']['Цена']['ЦенаЗаЕдиницу'] ?? throw new exception('Не найдена цена товара')) + ($buffer['delivery']['price']['all'] ?? $buffer['delivery']['price']['one'] ?? 0) + ($settings['increase'] ?? 0) ?? 0; } catch (Exception $e) { diff --git a/mirzaev/skillparts/system/models/Product.php b/mirzaev/skillparts/system/models/Product.php index c9ffce3..67f3a13 100644 --- a/mirzaev/skillparts/system/models/Product.php +++ b/mirzaev/skillparts/system/models/Product.php @@ -457,7 +457,7 @@ class Product extends Document where: 'supply_edge_product[0]._id != null', limit: 1, select: 'supply_edge_product[0]' - )[0]; + )[0] ?? null; } /** @@ -606,20 +606,6 @@ class Product extends Document return false; } - /** - * Деактивация - * - * @return bool Статус выполнения - */ - public function deactivate(): bool - { - $this->stts = 'inactive'; - - if ($this->update() > 0) return true; - - return false; - } - /** * Найти товары по группе * @@ -645,4 +631,31 @@ class Product extends Document select: 'product_edge_product_group[0]' )[0]; } + + /** + * Инициализация обложки для товара + * + * Ищет логотип в нужной категории (размере) для выбранного производителя + * + * @param string $prod Производитель + * @param int $size Размерная группа + * + * @return string Относительный путь до изображения от публичной корневой папки + */ + public static function cover(string $prod, int $size = 150): string + { + if ($size === 0) $size = ''; + else $size = "h$size/"; + + // Инициализация пути + $path = "/img/covers/$size" . strtolower($prod); + + // Поиск файла и возврат + if (file_exists(YII_PATH_PUBLIC . DIRECTORY_SEPARATOR . $path . '.jpg')) return $path . '.jpg'; + else if (file_exists(YII_PATH_PUBLIC . DIRECTORY_SEPARATOR . $path . '.jpeg')) return $path . '.jpeg'; + else if (file_exists(YII_PATH_PUBLIC . DIRECTORY_SEPARATOR . $path . '.png')) return $path . '.png'; + + // Возврат изображения по умолчанию + return "/img/covers/$size" . 'product.png'; + } } diff --git a/mirzaev/skillparts/system/models/ProductGroup.php b/mirzaev/skillparts/system/models/ProductGroup.php index 238421d..4d85dbe 100644 --- a/mirzaev/skillparts/system/models/ProductGroup.php +++ b/mirzaev/skillparts/system/models/ProductGroup.php @@ -125,7 +125,7 @@ class ProductGroup extends Document implements GroupInterface public function deleteProduct(Product $product): void { // Удаление товара из группы (подразумевается, что будет только одно) - foreach (ProductEdgeProductGroup::searchByVertex($product->readId(), $this->readId(), filter: ['type' => 'member']) as $edge) $edge->delete; + foreach (ProductEdgeProductGroup::searchByVertex($product->readId(), $this->readId(), filter: ['type' => 'member']) as $edge) $edge->delete(); // Запись в журнал $this->journal('delete member', [ @@ -138,9 +138,9 @@ class ProductGroup extends Document implements GroupInterface * * @param int $limit Ограничение по максимальному количеству */ - public function searchEdges(int $limit = 999): ?array + public function searchEdges(int $limit = 100, int $page = 1): ?array { - return ProductEdgeProductGroup::searchByDirection($this->readId(), 'INBOUND', where: ['type' => 'member'], limit: $limit); + return ProductEdgeProductGroup::searchByDirection($this->readId(), 'INBOUND', where: ['type' => 'member'], limit: $limit, page: $page); } /** @@ -148,12 +148,12 @@ class ProductGroup extends Document implements GroupInterface * * @param int $limit Ограничение по максимальному количеству */ - public function searchProducts(int $limit = 999): ?array + public function searchProducts(int $limit = 100, int $page = 1): ?array { // Инициализация буфера товаров $products = []; - foreach ($this->searchEdges($limit) as $edge) { + foreach ($this->searchEdges($limit, $page) as $edge) { // Перебор рёбер $products[] = Product::searchById($edge->_from); @@ -191,6 +191,20 @@ class ProductGroup extends Document implements GroupInterface return $transfered; } + /** + * Деактивация + * + * @return bool Статус выполнения + */ + public function deactivate(): bool + { + $this->stts = 'inactive'; + + if ($this->update() > 0) return true; + + return false; + } + /** * Запись рёбер групп * diff --git a/mirzaev/skillparts/system/models/Search.php b/mirzaev/skillparts/system/models/Search.php index 0e21f65..da844ec 100644 --- a/mirzaev/skillparts/system/models/Search.php +++ b/mirzaev/skillparts/system/models/Search.php @@ -129,7 +129,7 @@ class Search extends Document // Инициализация буфера вывода $response = $products; - // Генерация сдвига по запрашиваемым данным (пагинация) + // Генерация сдвига по запрашиваемым данным (система страниц) $offset = $limit * ($page - 1); foreach ($response as &$row) { @@ -167,7 +167,7 @@ class Search extends Document // Инициализация буфера $buffer_connections = []; - if (count($connections) === 11) { + if (count($connections) === 100) { // Если в базе данных хранится много поставок // Инициализация @@ -418,7 +418,7 @@ class Search extends Document // Не удалось использовать первое изображение как обложку // Запись обложки по умолчанию - $cover = '/img/covers/h150/product.png'; + $cover = Product::cover($row['prod'], 150); } } diff --git a/mirzaev/skillparts/system/models/Supply.php b/mirzaev/skillparts/system/models/Supply.php index dc86ce1..fe38ea5 100644 --- a/mirzaev/skillparts/system/models/Supply.php +++ b/mirzaev/skillparts/system/models/Supply.php @@ -12,8 +12,10 @@ use app\models\Product; use app\models\SupplyEdgeProduct; use app\models\Settings; use app\models\Import; +use app\models\File; use app\models\ImportEdgeSupply; use app\models\WarehouseEdgeImport; +use app\models\ImportEdgeFile; use carono\exchange1c\interfaces\OfferInterface; use carono\exchange1c\interfaces\ProductInterface; @@ -23,6 +25,8 @@ use moonland\phpexcel\Excel; use DateTime; use DateTimeZone; +use DatePeriod; +use DateInterval; use Exception; @@ -38,13 +42,6 @@ class Supply extends Product implements ProductInterface, OfferInterface { use Xml2Array; - /** - * Количество - * - * Используется при выводе в корзине - */ - public int $amnt = 0; - /** * Имя коллекции */ @@ -101,14 +98,21 @@ class Supply extends Product implements ProductInterface, OfferInterface */ public function afterSave($data, $vars): void { - if (AccountEdgeSupply::searchByVertex(yii::$app->user->id, $this->readId())) { - // Ребро: "АККАУНТ -> ПОСТАВКА" уже существует + // Инициализация + $account = Account::initAccount(); - } else { - // Ребра не существует + if (isset($account)) { + // Инициализирован аккаунт - // Запись ребра: АККАУНТ -> ПОСТАВКА - (new AccountEdgeSupply)->write(yii::$app->user->id, $this->readId(), 'import'); + if (AccountEdgeSupply::searchByVertex($account->readId(), $this->readId())) { + // Ребро: "АККАУНТ -> ПОСТАВКА" уже существует + + } else { + // Ребра не существует + + // Запись ребра: АККАУНТ -> ПОСТАВКА + (new AccountEdgeSupply)->write($account->readId(), $this->readId(), 'import'); + } } } @@ -171,8 +175,13 @@ class Supply extends Product implements ProductInterface, OfferInterface foreach (ImportEdgeSupply::find()->where(['_from' => $id])->limit($limit)->all() as $edge) { // Перебор найденных рёбер - // Поиск поставки и запись в буфер вывода - $supplies[] = static::searchById($edge->_to); + // Поиск поставки + $supply = static::searchById($edge->_to); + + if (empty($supply)) continue; + + // Запись поставки в буфер вывода + $supplies[] = $supply; } return $supplies; @@ -387,20 +396,15 @@ class Supply extends Product implements ProductInterface, OfferInterface } /** - * Запись поставок из excel - * - * На данный момент обрабатывает только импорт из - * файлов с расширением .excel + * Импорт Excel-файла * * @param int $warehouse Идентификатор склада (_key) - * @param Account|int|null $account Аккаунт + * @param Account|int|null $account Аккаунт управляющий файлом и его данными + * @param bool $load Загрузить в базу данных */ public function importExcel(int $warehouse, Account|int|null $account = null): bool { // Инициализация - $data = []; - $created = 0; - $updated = 0; $account = Account::initAccount($account); if ($this->validate()) { @@ -425,9 +429,85 @@ class Supply extends Product implements ProductInterface, OfferInterface if (!mkdir($path, 0775, true)) throw new Exception('Не удалось создать директорию', 500); - $this->file_excel->saveAs($path = "$path/" . $filename = $this->file_excel->baseName . '.' . $this->file_excel->extension); + $this->file_excel->saveAs($path = "$path/" . $this->file_excel->baseName . '.' . $this->file_excel->extension); - $data[] = Excel::import($path, [ + // Инициализация инстанции файла + $file = new File(); + + // Запись настроек файла + $file->type = 'supplies excel'; + $file->path = $path; + $file->name = $this->file_excel->baseName . '.' . $this->file_excel->extension; + $file->user = (int) $account->_key; + $file->meta = [ + 'warehouse' => $warehouse + ]; + + // Запись в базу данных + $file->save(); + + // Инициализация инстанции импорта + $import = new Import; + + if ($import->save()) { + // Записано в базу данных + + if (ImportEdgeFile::write($import->readId(), $file->readId(), data: ['type' => 'connected'])) { + // Записано ребро: "ИНСТАНЦИЯ ИМПОРТА" -> ФАЙЛ + + // Запись в журнал инстанции импорта + $import->journal('connect_with_file', [ + 'target' => $file->readId() + ]); + } + + if (WarehouseEdgeImport::write(Warehouse::collectionName() . '/' . $file->meta['warehouse'], $import->readId(), data: ['type' => 'loaded'])) { + // Записано ребро: СКЛАД -> ИНСТАНЦИЯ ПОСТАВОК + + // Запись в журнал инстанции импорта + $import->journal('connect_with_warehouse', [ + 'target' => Warehouse::collectionName() . '/' . $file->meta['warehouse'] + ]); + } + } + + // Макрос действий после импорта + static::afterImportExcel($warehouse); + + return true; + } + + // Запись ошибки + $this->addError('errors', 'Не пройдена проверка параметров'); + + return false; + } + + /** + * Запись поставок из excel + * + * На данный момент обрабатывает только импорт из + * файлов с расширением .excel + * + * @param File $file Инстанция файла + * + * @return bool + */ + public static function loadExcel(File $file): bool { + // Инициализация + $created = 0; + $updated = 0; + $account = Account::initAccount((int) $file->user); + $data = []; + + $supply = new Supply(); + $supply->scenario = $supply::SCENARIO_IMPORT_EXCEL; + $supply->file_excel = $file->path; + + if ($supply->validate()) { + // Пройдена проверка + + $data[] = Excel::import($supply->file_excel, [ 'setFirstRecordAsKeys' => true, 'setIndexSheetByName' => true, ]); @@ -441,7 +521,7 @@ class Supply extends Product implements ProductInterface, OfferInterface if (count($data) < 1) { // Не найдены строки с товарами - $this->addError('errors', 'Не удалось найти данные товаров'); + $supply->addError('errors', 'Не удалось найти данные товаров'); } else { // Найдены строки с товарами @@ -489,10 +569,11 @@ class Supply extends Product implements ProductInterface, OfferInterface if (!$product = Product::searchByCatnAndProd($article, $prod)) $product = Product::writeEmpty($article, $prod, Account::isMinimalAuthorized($account)); // Инициализация группы товаров - if (!$group = ProductGroup::searchByProduct($product)) $group = ProductGroup::writeEmpty(active: true); + // if (!$group = ProductGroup::searchByProduct($product)) $group = ProductGroup::writeEmpty(active: true); // Инициализация функции создания поставки - $create = function (string $_supply, int|null $amount = null) use ($group, $row, $prod, $analogs, &$created, &$updated, &$imported, $account): bool { + // $create = function (string $_supply, int|null $amount = null) use ($group, $row, $prod, $analogs, &$created, &$updated, &$imported, $account): bool { + $create = function (string $_supply, int|null $amount = null) use ($row, $prod, $analogs, &$created, &$updated, &$imported, $account): bool { // Очистка $_supply = trim($_supply); @@ -660,6 +741,9 @@ class Supply extends Product implements ProductInterface, OfferInterface // Перенос данных в буфер (существующий в базе данных дубликат) $supply->setAttributes($vars, false); + // Запись ребра: АККАУНТ -> ПОСТАВКА + (new AccountEdgeSupply)->write($account->readId(), $supply->readId(), 'import'); + // Перезапись существующего документа $supply->update(); @@ -672,7 +756,7 @@ class Supply extends Product implements ProductInterface, OfferInterface // Проверка не пройдена // Добавление ошибок - foreach ($supply->errors as $attribute => $error) $this->addError($attribute, $error); + foreach ($supply->errors as $attribute => $error) $supply->addError($attribute, $error); // Запись статуса об ошибке $error = true; @@ -689,7 +773,7 @@ class Supply extends Product implements ProductInterface, OfferInterface // } // Добавление в группу аналогов - $group->writeProduct($product); + // $group->writeProduct($product); } return !$error; @@ -698,12 +782,12 @@ class Supply extends Product implements ProductInterface, OfferInterface // Запись поставки $create($article, (int) $amount); - foreach ($analogs as $_supply) { - // Перебор аналогов (если найдены) + // foreach ($analogs as $_supply) { + // // Перебор аналогов (если найдены) - // Запись поставки - $create((string) $_supply); - } + // // Запись поставки + // $create((string) $_supply); + // } } } } @@ -712,26 +796,14 @@ class Supply extends Product implements ProductInterface, OfferInterface if (count($imported) > 0) { // Успешно записана минимум 1 поставка - // Инициализация инстанции импорта - $import = new Import; + foreach ($imported as $supply) { + // Перебор импортированных поставок - $import->file = $path; - $import->name = $filename; + // Инициализация инстанции импорта + $import = Import::searchByFile($file); - if ($import->save()) { - // Инстанция импорта успешно загружена - - if (WarehouseEdgeImport::write(Warehouse::collectionName() . "/$warehouse", $import->readId(), data: ['type' => 'loaded'])) { - // Записано ребро: СКЛАД -> ИНСТАНЦИЯ ПОСТАВОК - - // Запись в журнал инстанции импорта - $import->journal('connect_with_warehouse', [ - 'target' => Warehouse::collectionName() . "/$warehouse" - ]); - } - - foreach ($imported as $supply) { - // Перебор импортированных поставок + if (isset($import)) { + // Найдена интанция импорта if (ImportEdgeSupply::write($import->readId(), $supply->readId(), data: ['type' => 'imported', 'vrsn' => ImportEdgeSupply::searchMaxVersion($supply) + 1])) { // Записано ребро: ИНСТАНЦИЯ ПОСТАВОК -> ПОСТАВКА @@ -745,23 +817,25 @@ class Supply extends Product implements ProductInterface, OfferInterface } } - // Макрос действий после импорта - static::afterImportExcel($created, $updated); + // Запись статуса + $file->stts = 'loaded'; + + if ($file->update() > 0) { + // Удалось записать в базу данных + + // Запись в журнал + $file->journal('loaded'); + } + + // Макрос действий после загрузки + static::afterLoadExcel($account, $created, $updated); // Удаление (важно именно задать null для формы в представлении) - $this->file_excel = null; - - return true; + $supply->file_excel = null; } // Запись ошибки - $this->addError('errors', 'Не пройдена проверка параметров'); - - // Макрос действий после импорта - static::afterImportExcel($created, $updated); - - // Удаление (важно именно задать null для формы в представлении) - $this->file_excel = null; + $supply->addError('errors', 'Не пройдена проверка параметров'); return false; } @@ -974,16 +1048,55 @@ class Supply extends Product implements ProductInterface, OfferInterface } /** - * Вызывается после загрузки поставок из excel-документа + * Вызывается после загрузки excel-документа на сервер * + * @param string|int $warehouse Склад + * + * @return bool Статус выполнения + */ + public static function afterImportExcel(string|int $warehouse = 'неизвестен'): bool + { + // Отправка уведомления о загрузке + $save = Notification::_write("Загружены товары для склада \"$warehouse\""); + + // Инициализация периода + $period = new DatePeriod(new DateTime('@' . strtotime("00:00:00")), new DateInterval('PT5M'), new DateTime('@' . strtotime("next day 00:00:00"))); + + foreach($period as $date){ + // Перебор периодов + + if (($converted = $date->format('U')) > $time = time()) { + // Найден интервал из будущего времени (предполагается, что ближайший по причине остановки выполнения далее) + + // Запись даты + $date = (new DateTime('@' . ($converted - $time)))->format('H:i:s'); + + break; + } + } + + if ($date instanceof DateTime) $date = '5:00'; + + // Отправка уведомления об обработке + $handle = Notification::_write("Следующее обновление товаров начнётся через $date"); + + return $save && $handle; + } + + /** + * Вызывается после загрузки поставок из excel-документа в базу данных + * + * @param static|null $account Аккаунт * @param int $created Количество созданных документов * @param int $updated Количество обновлённых документов + * + * @return bool Статус выполнения */ - public static function afterImportExcel(int $created = 0, int $updated = 0): bool + public static function afterLoadExcel(Account|int $account = null, int $created = 0, int $updated = 0): bool { // Инициализация параметров $model = new Notification; - $account = yii::$app->user->identity; + $account = Account::initAccount($account); // Инициализация часового пояса preg_match_all('/UTC([\+\-0-9:]*)/', $account->zone ?? Settings::searchActive()['timezone_default'] ?? 'UTC+3', $timezone); @@ -993,8 +1106,9 @@ class Supply extends Product implements ProductInterface, OfferInterface $date = (new DateTime('now', new DateTimeZone($timezone)))->format('H:i d.m.Y'); // Настройка - $model->text = yii::$app->controller->renderPartial('@app/views/notification/system/afterImportExcel', compact('created', 'updated', 'date')); + $model->text = yii::$app->controller->renderPartial('@app/views/notification/system/afterLoadExcel', compact('created', 'updated', 'date')); $model->type = $model::TYPE_NOTICE; + $model->account = $account->readId(); // Отправка return (bool) $model->write(); diff --git a/mirzaev/skillparts/system/views/account/index.php b/mirzaev/skillparts/system/views/account/index.php index 20bab7f..09619b8 100644 --- a/mirzaev/skillparts/system/views/account/index.php +++ b/mirzaev/skillparts/system/views/account/index.php @@ -78,6 +78,7 @@ use app\models\AccountForm; 'submitRegistration', 'onclick' => 'return registration_start(this.parentElement, \'' . $target . '\');', 'class' => 'col-12 ml-auto btn btn-success btn-sm button_clean']) ?> + Восстановить пароль zone ?? Settings::searchActive()['timezone_default'] ?? 'UTC+3', $timezone); +$timezone = $timezone[1][0]; +?> + + 1) : ?> + + + + + jrnl ?? [] as $jrnl) { + // Перебор записей в журнале + + if ($jrnl['action'] === 'create') { + // Найдена дата создания + + // Инициализация даты + $create = (new DateTime())->setTimestamp($jrnl['date'])->setTimezone(new DateTimeZone($timezone))->format('d.m.Y') ?? 'Неизвестно'; + + // Выход из цикла + break; + } + } + ?> + +
+
.
+
+ name ?? 'Неизвестно' ?> +
+
+ indx ?? '' ?> +
+
+ agnt ? 'Поставщик' : 'Покупатель' ?> +
+
+ type() ?> +
+
+ +
+ +
+ + + + + diff --git a/mirzaev/skillparts/system/views/cart/index.php b/mirzaev/skillparts/system/views/cart/index.php index 2c1cd06..1410188 100644 --- a/mirzaev/skillparts/system/views/cart/index.php +++ b/mirzaev/skillparts/system/views/cart/index.php @@ -122,7 +122,7 @@ use DateTime;

~$days дн

- {$supply['cost']} {$supply['currency']} + {$supply['supply']->cost} {$supply['currency']}
@@ -223,20 +223,12 @@ use DateTime; - diff --git a/mirzaev/skillparts/system/views/index.php b/mirzaev/skillparts/system/views/index.php index 93c5513..a18f987 100644 --- a/mirzaev/skillparts/system/views/index.php +++ b/mirzaev/skillparts/system/views/index.php @@ -5,10 +5,10 @@ declare(strict_types=1); $this->title = 'SkillParts'; ?> - +
-
+

Проблема с подбором запчастей?

@@ -21,22 +21,22 @@ $this->title = 'SkillParts';

-
- Cummins - Iveco - Komatsu - Case - Isuzu - New Holland - Perkins - John Deere - Caterpillar - Shantui - XCMG - Kobelco - SHEHWA - BOMAG - Hitachi +
+
Cummins
+
Iveco
+
Komatsu
+
Case
+
Isuzu
+
New Holland
+
Perkins
+
John Deere
+
Caterpillar
+
Shantui
+
XCMG
+
Kobelco
+
SHEHWA
+
BOMAG
+
Hitachi
@@ -80,5 +80,13 @@ $this->title = 'SkillParts';
- + + diff --git a/mirzaev/skillparts/system/views/invoice/order/pattern.php b/mirzaev/skillparts/system/views/invoice/order/pattern.php index 4f61725..6ead3da 100644 --- a/mirzaev/skillparts/system/views/invoice/order/pattern.php +++ b/mirzaev/skillparts/system/views/invoice/order/pattern.php @@ -1,6 +1,7 @@ @@ -133,9 +134,7 @@ use app\models\Settings; - Производитель - Товар - Поставщик + Товар Количество Цена Сумма @@ -156,11 +155,13 @@ use app\models\Settings; $supplies) : ?> $deliveries) : ?> $supply) : ?> + name ?? 'Без названия'; + ?> - - - + diff --git a/mirzaev/skillparts/system/views/layouts/main.php b/mirzaev/skillparts/system/views/layouts/main.php index 1f6201b..a6ffbae 100644 --- a/mirzaev/skillparts/system/views/layouts/main.php +++ b/mirzaev/skillparts/system/views/layouts/main.php @@ -50,6 +50,7 @@ AppAsset::register($this);
+