-FROM php:7.4-rc-fpm-alpine
+FROM php:8.1-fpm-alpine
# Install tools required for build stage
RUN apk add --update --no-cache \
bash curl wget rsync ca-certificates openssl openssh git tzdata openntpd \
libxrender fontconfig libc6-compat
-RUN apk add --no-cache zlib libpng icu \
+RUN apk add --no-cache zlib libpng icu zip libzip-dev \
&& apk add --no-cache --virtual .deps zlib-dev libpng-dev icu-dev \
+ && docker-php-ext-configure zip \
+ && docker-php-ext-install zip \
&& docker-php-ext-install -j$(nproc) gd mysqli pdo pdo_mysql intl calendar opcache \
&& apk del .deps
namespace yii_app\actions\motivation;
+use PhpOffice\PhpSpreadsheet\IOFactory;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
use Yii;
use yii\base\Action;
use yii\base\DynamicModel;
use yii\helpers\ArrayHelper;
+use yii\web\UploadedFile;
use yii_app\records\CityStore;
+use yii_app\records\Motivation;
+use yii_app\records\MotivationCostsItem;
+use yii_app\services\MotivationService;
class IndexAction extends Action
{
public function run() {
+ if (Yii::$app->request->isPost) {
+ $file = UploadedFile::getInstanceByName('myfile');
+ if ($file) {
+ $path1 = Yii::getAlias('@uploads') . '/template_plan_temp.xlsx';
+ $file->saveAs($path1);
+
+ $data = MotivationService::uploadTemplatePlan($path1);
+
+ return implode('<br>', $data['errors']);
+ } else {
+ return 'not ok';
+ }
+ }
+
$model = DynamicModel::validateData([
'store_id' => null, 'year' => null, 'month' => null
], [
]);
$model->load(Yii::$app->request->get());
+ $motivations = Motivation::find()->all();
+ $possibleStoreIds = ArrayHelper::getColumn($motivations, 'store_id');
+
$stores = ArrayHelper::map(CityStore::find()->all(), 'id', 'name');
+ $stores = array_filter($stores, function ($k, $v) use($possibleStoreIds) {
+ return in_array($v, $possibleStoreIds);
+ }, ARRAY_FILTER_USE_BOTH);
- $years = [2023, 2024, 2025, 2026];
- $months = ['Январь',' Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'];
+ $possibleYears = ArrayHelper::getColumn($motivations, 'year');
+ $years = array_filter(range(2023, 20100), function ($k) use ($possibleYears) {
+ return in_array($k, $possibleYears);
+ });
+ $possibleMonth = ArrayHelper::getColumn($motivations, 'month');
+ $months = array_filter(['Январь',' Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'], function ($k, $v) use ($possibleMonth) {
+ return in_array($v + 1, $possibleMonth);
+ }, ARRAY_FILTER_USE_BOTH);
return $this->controller->render('index',
compact('model', 'stores', 'years', 'months'));
"unclead/yii2-multiple-input": "~2.0",
"kartik-v/yii2-widget-fileinput": "dev-master",
"yiisoft/yii2-imagine": "^2.3",
- "kartik-v/yii2-builder": "dev-master"
+ "kartik-v/yii2-builder": "dev-master",
+ "phpoffice/phpspreadsheet": "^2.2"
},
"require-dev": {
"yiisoft/yii2-debug": "~2.1.0",
--- /dev/null
+<?php
+
+namespace yii_app\records;
+
+use Yii;
+use yii\behaviors\TimestampBehavior;
+use yii\db\Expression;
+
+/**
+ * This is the model class for table "motivation".
+ *
+ * @property int $id
+ * @property int $store_id ID магазина
+ * @property int $year Год
+ * @property int $month Месяц
+ * @property string $updated_at Дата изменения записи
+ * @property string $created_at Дата создания записи
+ */
+class Motivation extends \yii\db\ActiveRecord
+{
+ /**
+ * {@inheritdoc}
+ */
+ public static function tableName()
+ {
+ return 'motivation';
+ }
+
+ public function behaviors()
+ {
+ return [
+ [
+ 'class' => TimestampBehavior::class,
+ 'createdAtAttribute' => 'created_at',
+ 'updatedAtAttribute' => 'updated_at',
+ 'value' => new Expression('NOW()'),
+ ]
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function rules()
+ {
+ return [
+ [['store_id', 'year', 'month'], 'required'],
+ [['store_id', 'year', 'month'], 'default', 'value' => null],
+ [['store_id', 'year', 'month'], 'integer'],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function attributeLabels()
+ {
+ return [
+ 'id' => 'ID',
+ 'store_id' => 'Store ID',
+ 'year' => 'Year',
+ 'month' => 'Month',
+ 'updated_at' => 'Updated At',
+ 'created_at' => 'Created At',
+ ];
+ }
+}
--- /dev/null
+<?php
+
+namespace yii_app\records;
+
+use Yii;
+
+/**
+ * This is the model class for table "motivation_value".
+ *
+ * @property int $id
+ * @property int $motivation_id ID motivation
+ * @property int $motivation_group_id ID motivation
+ * @property int $value_id ID motivation
+ * @property string $value_type тип значения
+ * @property int|null $value_int value int
+ * @property float|null $value_float value float
+ * @property string|null $value_string value string
+ */
+class MotivationValue extends \yii\db\ActiveRecord
+{
+ /**
+ * {@inheritdoc}
+ */
+ public static function tableName()
+ {
+ return 'motivation_value';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function rules()
+ {
+ return [
+ [['motivation_id', 'motivation_group_id', 'value_id', 'value_type'], 'required'],
+ [['motivation_id', 'motivation_group_id', 'value_id', 'value_int'], 'default', 'value' => null],
+ [['motivation_id', 'motivation_group_id', 'value_id', 'value_int'], 'integer'],
+ [['value_float'], 'number'],
+ [['value_type'], 'string', 'max' => 10],
+ [['value_string'], 'string', 'max' => 255],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function attributeLabels()
+ {
+ return [
+ 'id' => 'ID',
+ 'motivation_id' => 'Motivation ID',
+ 'motivation_group_id' => 'Motivation Group ID',
+ 'value_id' => 'Value ID',
+ 'value_type' => 'Value Type',
+ 'value_int' => 'Value Int',
+ 'value_float' => 'Value Float',
+ 'value_string' => 'Value String',
+ ];
+ }
+}
--- /dev/null
+<?php
+
+namespace yii_app\records;
+
+use Yii;
+
+/**
+ * This is the model class for table "motivation_value_group".
+ *
+ * @property int $id
+ * @property string $name Название
+ * @property string $alias Алиас
+ */
+class MotivationValueGroup extends \yii\db\ActiveRecord
+{
+ /**
+ * {@inheritdoc}
+ */
+ public static function tableName()
+ {
+ return 'motivation_value_group';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function rules()
+ {
+ return [
+ [['name', 'alias'], 'required'],
+ [['name', 'alias'], 'string', 'max' => 80],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function attributeLabels()
+ {
+ return [
+ 'id' => 'ID',
+ 'name' => 'Name',
+ 'alias' => 'Alias',
+ ];
+ }
+}
--- /dev/null
+<?php
+
+namespace yii_app\services;
+
+use PhpOffice\PhpSpreadsheet\IOFactory;
+use yii_app\records\Motivation;
+use yii_app\records\MotivationValue;
+use yii_app\records\MotivationValueGroup;
+use yii_app\records\CityStore;
+use yii_app\records\MotivationCostsItem;
+
+class MotivationService
+{
+ public static function uploadTemplatePlan($path) {
+ $motivationCostsItems = MotivationCostsItem::find()->indexBy('code')->all();
+ try {
+ $spreadsheets = IOFactory::load($path);
+ } catch (\Exception $ex) {
+ return ['errors' => ['Некорректный файл. Загрузите файл, заполненный по шаблону.']];
+ }
+ $errors = [];
+ foreach ($spreadsheets->getAllSheets() as $indSS => $spreadSheet) {
+ $rows = [];
+ $storeStr = true;
+ $error = '';
+ $motivation = null;
+ foreach ($spreadSheet->getRowIterator() as $ind => $spreadSheetRow) {
+ if (in_array($ind, [1, 3])) {
+ continue;
+ }
+ $row = [];
+ foreach ($spreadSheetRow->getCellIterator() as $spreadSheetRowCell) {
+ $value = $spreadSheetRowCell->getValue();
+ $row []= $value;
+ if ($row[0] == '###') {
+ break;
+ }
+ }
+ if ($row[0] == '###') {
+ break;
+ }
+ if ($storeStr) {
+ if (!is_int($row[0] ?? -1)) {
+ $error = "Индекс магазина не корректен [0,0] '" . $row[0] . "'";
+ break;
+ }
+ $store = CityStore::find()->where(['id' => $row[0] ?? -1])->one();
+ if (!$store) {
+ $error = "Не найден магазин с таким индексом [0,0]";
+ break;
+ } elseif ($store->name != ($row[1] ?? 'NO_NAME')) {
+ $error = "Не найден магазин с таким названием [1,0]" . $row[1] . ' ' . $store->name;
+ break;
+ }
+ $year = ((int)$row[2]) ?? -1;
+ $month = ((int)$row[3]) ?? -1;
+ if ($year > 2030 || $year < 2023) {
+ $error = "Не корректно указан год [2,0]";
+ break;
+ }
+ if ($month < 1 || $month > 12) {
+ $error = "Не корректно указан месяц [3,0]";
+ break;
+ }
+
+ $motivation = Motivation::find()->where(['store_id' => $store->id, 'year' => $year, 'month' => $month])->one();
+ /** @var $motivation Motivation */
+ if (!$motivation) {
+ $motivation = new Motivation;
+ $motivation->store_id = $store->id;
+ $motivation->year = $year;
+ $motivation->month = $month;
+ }
+
+ $storeStr = false;
+ } else {
+ if (!isset($motivationCostsItems[$row[0] ?? -1])) {
+ $error = "Не корректен код элемента " . ($row[0] ?? '') . "[$ind,0]";
+ break;
+ }
+ /** @var $motivationCostsItems MotivationCostsItem[] */
+ if ($motivationCostsItems[$row[0]]->name != ($row[1] ?? 'NO_NAME')) {
+ $error = "Не корректно название элемента '" . ($row[1] ?? '') . "' Ожидается: '" . $motivationCostsItems[$row[0]]->name . "' [$ind,1]";
+ break;
+ }
+ if (trim($row[2]) == '') {
+ $rows []= $row;
+ continue;
+ }
+ switch ($motivationCostsItems[$row[0]]->data_type) {
+ case MotivationCostsItem::DATA_TYPE_INT: { if (is_int($row[2])) { $value = (int)$row[2]; } else { $error = "Не целое число [$ind,2] '" . $row[2] . "'"; }; break; }
+ case MotivationCostsItem::DATA_TYPE_FLOAT: { if (is_int($row[2]) || is_float($row[2])) { $value = (float)$row[2]; } else {$error = "Не дробь [$ind,2] '" . $row[2] . "'"; } break; }
+ case MotivationCostsItem::DATA_TYPE_STRING: { $value = $row[2]; break; }
+ }
+ if (!empty($error)) {
+ break;
+ }
+ $rows []= $row;
+ }
+ }
+ if (empty($error)) {
+ if ($motivation) {
+ $motivation->save();
+ if ($motivation->getErrors()) {
+ $error = json_encode($motivation->getErrors());
+ }
+ } else {
+ $error = 'Не указан магазин, год и месяц [0,0]';
+ }
+ }
+ $motivationCostsItemsCount = count(array_keys($motivationCostsItems));
+ if (empty($error) && (count($rows) != $motivationCostsItemsCount)) {
+ $keys = array_keys($motivationCostsItems);
+ foreach ($rows as $row) {
+ if (($key = array_search($row[0], $keys)) !== false) {
+ unset($keys[$key]);
+ }
+ }
+ $keys = array_values($keys);
+ $error = "Указаны не все элементы справочника. В листе: " . count($rows) . " В справочнике: " . $motivationCostsItemsCount
+ . " Ожидается, например: '" . $motivationCostsItems[$keys[0]]->name . "'";
+ }
+ if (empty($error)) {
+ $motivationValueGroupPlan = MotivationValueGroup::find()->where(['alias' => 'plan'])->one();
+ /** @var $motivationValueGroupPlan MotivationValueGroup */
+ foreach ($rows as $row) {
+ $motivationValue = MotivationValue::find()->where([
+ 'motivation_id' => $motivation->id,
+ 'motivation_group_id' => $motivationValueGroupPlan->id,
+ 'value_id' => $row[0]
+ ])->one();
+ /** @var $motivationValue MotivationValue */
+ if (!$motivationValue) {
+ $motivationValue = new MotivationValue;
+ $motivationValue->motivation_id = $motivation->id;
+ $motivationValue->motivation_group_id = $motivationValueGroupPlan->id;
+ $motivationValue->value_id = $row[0];
+ }
+ $motivationValue->value_type = $motivationCostsItems[$row[0]]->data_type;
+ switch ($motivationValue->value_type) {
+ case MotivationCostsItem::DATA_TYPE_INT: { $motivationValue->value_int = (int)$row[2]; break; }
+ case MotivationCostsItem::DATA_TYPE_FLOAT: { $motivationValue->value_float = (float)$row[2]; break; }
+ case MotivationCostsItem::DATA_TYPE_STRING: { $motivationValue->value_string = '' . $row[2]; break; }
+ }
+ $motivationValue->save();
+ if ($motivationValue->getErrors()) {
+ $error = json_encode($motivationValue->getErrors());
+ break;
+ }
+ }
+ }
+ if (!empty($error)) {
+ $errors []= ($indSS + 1) . ":" . $error;
+ }
+ }
+
+ return compact('errors');
+ }
+}
\ No newline at end of file
/** @var $years array */
/** @var $months array */
+$this->registerJsFile('/js/motivation/index.js', ['position' => \yii\web\View::POS_END]);
+
?>
<div class="motivationIndex m-5">
<div class="d-flex justify-content-around align-items-center gap-2">
<div class="mb-3"><?= Html::submitButton('Применить', ['class' => 'btn btn-secondary btn-sm'])?></div>
</div>
+ <div class="d-flex justify-content-around align-items-center gap-2">
+ <div class="mb-3"><?= Html::button('Загрузка плановых значений', ['class' => 'btn btn-success btn-sm',
+ 'onclick' => 'openUploadDictionary();'])?></div>
+ </div>
</div>
</div>
--- /dev/null
+/* jshint esversion: 8 */
+
+const param10 = $("meta[name=csrf-param]").attr("content");
+const token10 = $("meta[name=csrf-token]").attr("content");
+
+/* jshint unused: false */
+function openUploadDictionary() {
+ 'use strict'
+ const $mainModal = $('#mainModal');
+ const $modalBody = $mainModal.find('.modal-body');
+ const $modalFooter = $mainModal.find('.modal-footer');
+ $mainModal.find('.close').on('click', () => { $mainModal.modal('hide'); });
+ $mainModal.find('.modal-title').html('Загрузка плановых значений');
+ $modalFooter.html('');
+ $modalBody.html('<div class="row"><div class="col-12">'
+ + '<form class="d-flex justify-content-left align-items-center" enctype="multipart/form-data">'
+ + '<div class="d-none"><input type="file" name="myfile" accept=".xlsx"/></div>'
+ + '<div><input class="btn btn-success btn-sm" type="submit" value="Загрузить" /></div></form></div></div>'
+ + '<div class="row mt-2"><div class="col-12" id="infoModal"></div></div>'
+ + '<div class="row mt-2"><div class="col-12 border-2 bg-gray-200"></div></div>'
+ + '<div class="row mt-5"><div class="col-12" style="font-size: 1rem;">Скачать шаблон</div></div>'
+ + '<div class="row mt-2"><div class="col-12">'
+ + '<a href="/files/download?url=/uploads/template_plan.xlsx" style="font-size: 1rem; color: #23389c;'
+ + ' text-decoration: underline; " target="_blank">Шаблон. Загрузка плановых значений</a></div></div>');
+ const browse = $modalBody.find('input[type=file]').get(0);
+ const btn = $modalBody.find('input[type=submit]').get(0);
+ const info = $modalBody.find('#infoModal').get(0);
+ const form = $modalBody.find('form').get(0);
+ async function UploadDict() {
+ info.innerHTML = 'загрузка...';
+ if (browse.files[0].size > 200000) {
+ info.innerHTML = '<span style="color:red">Некорректный файл. Загрузите файл, заполненный по шаблону.</span>';
+ return;
+ }
+ const formData = new FormData(form);
+ formData.append(param10, token10);
+ try {
+ const response = await fetch("/motivation/index", {
+ method: "POST",
+ body: formData,
+ });
+ const text = await response.text();
+ if (text === 'not ok') {
+ info.innerHTML = '<span style="color:red">Не смог загрузить файл</span>';
+ } else if (text.replaceAll('<br>', '') === '') {
+ info.innerHTML = '<span style="color:green">Успешно загружен</span>';
+ } else {
+ info.innerHTML = '<span style="color:red">' + text + '</span>';
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ browse.addEventListener('change', (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ UploadDict();
+ })
+
+ btn.addEventListener('click', (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ browse.click();
+ })
+
+ $mainModal.modal('show');
+}
\ No newline at end of file