diff --git a/mirzaev/skillparts/system/controllers/ErrorController.php b/mirzaev/skillparts/system/controllers/ErrorController.php index f16977e..684b353 100644 --- a/mirzaev/skillparts/system/controllers/ErrorController.php +++ b/mirzaev/skillparts/system/controllers/ErrorController.php @@ -15,16 +15,16 @@ class ErrorController extends Controller // Исключение не выброшено // Запись кода ошибки - $statusCode = $exception->statusCode; + $statusCode = $exception->statusCode ?? $exception->getCode() ?? 0; // Запись названия ошибки - $name = match ($exception->statusCode) { + $name = match ($statusCode) { 404 => '404 (Не найдено)', default => $exception->getName() }; // Запись сообщения об ошибке - $message = match ($exception->statusCode) { + $message = match ($statusCode) { 404 => 'Страница не найдена', default => $exception->getMessage() }; diff --git a/mirzaev/skillparts/system/controllers/ProductController.php b/mirzaev/skillparts/system/controllers/ProductController.php index 29eb3bd..0cccc41 100644 --- a/mirzaev/skillparts/system/controllers/ProductController.php +++ b/mirzaev/skillparts/system/controllers/ProductController.php @@ -2,415 +2,469 @@ declare(strict_types=1); -namespace app\controllers; +namespace app\models; use yii; -use yii\filters\AccessControl; -use yii\web\Controller; -use yii\web\Response; -use yii\web\HttpException; use yii\web\UploadedFile; +use yii\imagine\Image; -use app\models\Product; +use app\models\traits\SearchByEdge; +use Exception; +use moonland\phpexcel\Excel; -class ProductController extends Controller +/** + * Продукт (в ассортименте магазина) + * + * Представляет собой лот состоящий из предложений от поставщиков + * + * @see Supply Поставки для продуктов + */ +class Product extends Document { - public function actionIndex(string $catn): array|string|null + 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 { - 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); - } + return 'product'; } - public function actionEditTitle(string $catn): array|string|null + /** + * Свойства + */ + public function attributes(): array { - // Инициализация - $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('/'); - } + return array_merge( + parent::attributes(), + [ + 'catn', + 'name', + // В библеотеке есть баг на название DESC (неизвестно в моей или нет) + 'dscr', + 'prod', + 'dmns', + 'imgs', + 'time' + ] + ); } - public function actionEditCatn(string $catn): array|string|null + /** + * Метки свойств + */ + public function attributeLabels(): array { - // Инициализация - $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('/'); - } + return array_merge( + parent::attributeLabels(), + [ + 'catn' => 'Каталожный номер (catn)', + 'name' => 'Название (name)', + 'dscr' => 'Описание (dscr)', + 'prod' => 'Производитель (prod)', + 'dmns' => 'Габариты (dmns)', + 'imgs' => 'Изображения (imgs)', + 'time' => 'Срок доставки (time)', + 'file_excel' => 'Документ (file_excel)', + 'file_image' => 'Изображение (file_image)', + 'group' => 'Группа (group)' + ] + ); } - public function actionEditDscr(string $catn): array|string|null + /** + * Правила + */ + public function rules(): array { - // Инициализация - $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->dscr = $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 actionEditDmns(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->post('text') ?? yii::$app->request->get('text') ?? '0'; - $text or $text = '0'; - $dimension = yii::$app->request->post('dimension') ?? yii::$app->request->get('dimension') ?? 'x'; - - $product->dmns = array_merge( - $product->dmns ?? [], + return array_merge( + parent::rules(), + [ [ - $dimension => $text + 'catn', + 'required', + 'message' => 'Заполните поля: {attribute}', + 'on' => self::SCENARIO_WRITE, + 'except' => [self::SCENARIO_IMPORT_EXCEL, self::SCENARIO_IMPORT_IMAGE] + ], + [ + 'catn', + 'string', + 'message' => '{attribute} должен быть строкой' + ], + [ + 'imgs', + 'arrayValidator', + 'message' => '{attribute} должен быть массивом' + ], + [ + 'dmns', + 'arrayWithNumbersValidator', + '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 ] - ); + ] + ); + } - if ($product->save()) { - // Товар обновлён + /** + * Запись пустого продукта + */ + public static function writeEmpty(string $catn): ?self + { + // Инициализация + $model = new self; - $return['dimension'] = $text; + // Настройки + $model->catn = $catn; + + // Запись + return $model->save() ? $model : null; + } + + /** + * Импорт изображений + * + * @return int Количество сохранённых изображений + */ + public function importImages(): int + { + // Инициализация + $amount = 0; + + if ($this->validate()) { + // Проверка пройдена + + 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 ?? $this->imgs = []; + + // Запись в базу данных + $this->imgs = array_merge( + $this->imgs, + [[ + 'covr' => count($this->imgs) === 0 ? true : false, + 'orig' => $catalog . '/' . $file->baseName . '.' . $file->extension, + 'h150' => $catalog_h150 . '/' . $file->baseName . '.' . $file->extension + ]] + ); + + $this->scenario = self::SCENARIO_WRITE; + + if ($this->save()) { + // Изменения сохранены в базе данных + + // Постинкрементация счётчика + $amount++; + } } } - /** - * Конец алгоритма - */ - end: + if ($this->hasErrors()) { + // Получены ошибки - if (yii::$app->request->isPost) { - // POST-запрос + foreach ($this->getErrors() as $attribute => $errors) { + // Перебор атрибутов - yii::$app->response->format = Response::FORMAT_JSON; + foreach ($errors as $error) { + // Перебор ошибок атрибутов - return $return; - } + $label = $this->getAttributeLabel($attribute); - 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 ($product = Product::searchByCatn($catn)) { - // Товар найден - - // Инициализация - $product->file_image = UploadedFile::getInstancesByName('images'); - $product->scenario = $product::SCENARIO_IMPORT_IMAGE; - - if ($product->importImages() > 0) { - // Товар обновлён - - $return['main'] = $this->renderPartial('index', ['model' => $product]); + Notification::_write("$label: $error", type: Notification::TYPE_ERROR); + } } } - /** - * Конец алгоритма - */ - 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('/'); - } + return $amount; } - public function actionDeleteImage(string $catn): array|string|null + /** + * Импорт товаров + * + * На данный момент обрабатывает только импорт из + * файлов с расширением .excel + */ + public function importExcel(): bool { // Инициализация - $return = [ - '_csrf' => yii::$app->request->getCsrfToken() - ]; - $index = yii::$app->request->post('index') ?? yii::$app->request->get('index'); + $data = []; + $amount = 0; - if (is_null($catn) || is_null($index)) { - // Не получены обязательные параметры + if ($this->validate()) { + foreach ($this->file_excel as $file) { + // Перебор файлов - yii::$app->response->statusCode = 500; + // Инициализация + $dir = '../assets/import/' . date('Y_m_d#H-i', time()) . '/excel/'; - goto end; - } + // Сохранение на диск + if (!file_exists($dir)) { + mkdir($dir, 0775, true); + } + $file->saveAs($path = $dir . $file->baseName . '.' . $file->extension); - if ($product = Product::searchByCatn($catn)) { - // Товар найден - - // Инициализация (буфер нужен из-за кривых сеттеров) - $buffer = $product->imgs; - - // Удаление - unset($buffer[$index]); - - // Запись - $product->imgs = $buffer; - - if ($product->save()) { - // Товар обновлён - - $return['main'] = $this->renderPartial('index', ['model' => $product]); + $data[] = Excel::import($path, [ + 'setFirstRecordAsKeys' => true, + 'setIndexSheetByName' => true, + ]); } - } - /** - * Конец алгоритма - */ - end: - if (yii::$app->request->isPost) { - // POST-запрос + foreach ($data as $data) { + // Перебор конвертированных файлов - yii::$app->response->format = Response::FORMAT_JSON; + if (count($data) < 1) { + // Не найдены строки с товарами - return $return; - } - - if ($model = Product::searchByCatn($catn)) { - return $this->render('index', compact('model')); - } else { - return $this->redirect('/'); - } - } - - public function actionWriteCover(string $catn): array|string|null - { - // Инициализация - $return = [ - '_csrf' => yii::$app->request->getCsrfToken() - ]; - $index = yii::$app->request->post('index') ?? yii::$app->request->get('index'); - - if (is_null($catn) || is_null($index)) { - // Не получены обязательные параметры - - yii::$app->response->statusCode = 500; - - goto end; - } - - if ($product = Product::searchByCatn($catn)) { - // Товар найден - - // Инициализация (буфер нужен из-за кривых сеттеров) - $buffer = $product->imgs; - - foreach($buffer as $image_index => &$image) { - // Перебор изображений - - if ($image_index === (int) $index) { - // Найдено запрашиваемое изображение - - // Установка обложки - $image['covr'] = true; + $this->addError('erros', 'Не удалось найти данные товаров'); } else { - $image['covr'] = false; + // Перебор найденных товаров + + 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); + } + } + } } } - // Запись - $product->imgs = $buffer; + // Деинициализация + $this->file_excel = ''; - if ($product->save()) { - // Товар обновлён + static::afterImportExcel($amount); - $return['main'] = $this->renderPartial('index', ['model' => $product]); - } + return true; } - /** - * Конец алгоритма - */ - end: + $this->addError('erros', 'Неизвестная ошибка'); - if (yii::$app->request->isPost) { - // POST-запрос + static::afterImportExcel($amount); - yii::$app->response->format = Response::FORMAT_JSON; + return false; + } - return $return; + /** + * Поиск по каталожному номеру + * + * Ищет продукт и возвращает его, + * либо выполняет поиск через представление + * + * @todo Переделать нормально + */ + public static function searchByCatn(string $catn, int $limit = 1, array $select = []): static|array|null + { + if ($limit <= 1) { + return static::findOne(['catn' => $catn]); } - if ($model = Product::searchByCatn($catn)) { - return $this->render('index', compact('model')); - } else { - return $this->redirect('/'); + $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 afterImport1c(): bool + { + // Инициализация + $model = new Notification; + $date = date('H:i d.m.Y', time()); + + // Настройка + $model->text = yii::$app->controller->renderPartial('/notification/system/afterImport1c', compact('amount', 'date')); + $model->type = $model::TYPE_NOTICE; + + // Отправка + return (bool) $model->write(); } } diff --git a/mirzaev/skillparts/system/models/Supply.php b/mirzaev/skillparts/system/models/Supply.php index f581c16..62075ce 100644 --- a/mirzaev/skillparts/system/models/Supply.php +++ b/mirzaev/skillparts/system/models/Supply.php @@ -170,7 +170,7 @@ class Supply extends Product implements ProductInterface $models = self::searchByAccount($account->readId()); $properties = self::xml2array($properties->xml); - $account->on(ApiController::EVENT_AFTER_OFFER_SYNC, self::afterImport()); + $account->on(ApiController::EVENT_AFTER_OFFER_SYNC, self::afterImport1c()); foreach ($models as $model) { // Перебор записей diff --git a/mirzaev/skillparts/system/views/notification/system/afterImportOnec.php b/mirzaev/skillparts/system/views/notification/system/afterImport1c.php similarity index 100% rename from mirzaev/skillparts/system/views/notification/system/afterImportOnec.php rename to mirzaev/skillparts/system/views/notification/system/afterImport1c.php