From: fomichev Date: Wed, 3 Jun 2026 10:23:46 +0000 (+0300) Subject: ERP-396: оборудование магазина + фото в city_store_params/city_store X-Git-Url: https://gitweb.erp-flowers.ru/?a=commitdiff_plain;h=18e1d8fa0b44d99c2bd1f69c8ed2e90a087abad1;p=erp24_rep%2Fyii-erp24%2F.git ERP-396: оборудование магазина + фото в city_store_params/city_store Оборудование (camera_model, camera_count, microphone_model, router_model, internet_provider, cash_register_info): - Миграция с идемпотентными проверками - CityStoreParams: @property + правила валидации - Controller: возврат в getStore, сохранение в saveCityStoreParams - Карточка: read-only тайлы вместо stub-заглушек - Операционное: редактируемые поля с сабтитлом «Техника» Фото магазина (city_store.image_sm / image_big): - actionUploadStorePhoto: resize 300×200 thumbnail через ImageHelper - Hero: photo-area с превью, кнопкой камеры и click-to-enlarge - Модал просмотра большой фотографии Co-Authored-By: Claude Sonnet 4.6 --- diff --git a/erp24/controllers/CityStoreManagementController.php b/erp24/controllers/CityStoreManagementController.php index 85b0bef2..6932252c 100644 --- a/erp24/controllers/CityStoreManagementController.php +++ b/erp24/controllers/CityStoreManagementController.php @@ -15,6 +15,8 @@ use yii_app\records\StoreCityList; use yii_app\records\StoreDynamic; use yii_app\records\AssortmentLabel; use yii_app\records\StoreType; +use yii\web\UploadedFile; +use yii_app\helpers\ImageHelper; class CityStoreManagementController extends Controller { @@ -152,6 +154,12 @@ class CityStoreManagementController extends Controller 'addressDistrictName' => ($params && !empty($params->address_district)) ? $params->addressDistrict?->name : null, 'matrixType' => $params?->matrix_type ? explode(',', $params->matrix_type) : [], 'isActive' => $params !== null ? (bool)$params->is_active : true, + 'cameraModel' => $params?->camera_model, + 'cameraCount' => $params?->camera_count, + 'microphoneModel' => $params?->microphone_model, + 'routerModel' => $params?->router_model, + 'internetProvider'=> $params?->internet_provider, + 'cashRegisterInfo'=> $params?->cash_register_info, // store_dynamic 'bushChefFloristId' => $bushChefFloristDyn?->value_int, @@ -275,6 +283,8 @@ class CityStoreManagementController extends Controller $allowed = [ 'store_type', 'store_area', 'showcase_volume', 'freeze_area', 'freeze_volume', 'address_region', 'address_city', 'address_district', 'is_active', + 'camera_model', 'camera_count', 'microphone_model', 'router_model', + 'internet_provider', 'cash_register_info', ]; foreach ($allowed as $field) { @@ -386,4 +396,55 @@ class CityStoreManagementController extends Controller Yii::error('StoreDynamic (str) save error: ' . json_encode($record->getErrors()), __CLASS__); } } + + public function actionUploadStorePhoto(): array + { + Yii::$app->response->format = Response::FORMAT_JSON; + + $storeId = (int)Yii::$app->request->post('store_id'); + $store = $storeId ? CityStore::findOne($storeId) : null; + if (!$store) { + return ['success' => false, 'message' => 'Магазин не найден']; + } + + $file = UploadedFile::getInstanceByName('photo'); + if (!$file || $file->error !== UPLOAD_ERR_OK) { + return ['success' => false, 'message' => 'Файл не выбран или ошибка загрузки']; + } + + $ext = strtolower($file->extension); + if ($ext === 'jpeg') { + $ext = 'jpg'; + } + if (!in_array($ext, ['jpg', 'png', 'webp'], true)) { + return ['success' => false, 'message' => 'Допустимые форматы: jpg, png, webp']; + } + + $adminId = Yii::$app->user->id; + $dateDir = date('Y') . '/' . date('m') . '/' . date('d'); + $dir = Yii::getAlias('@uploads') . "/{$adminId}/{$dateDir}/"; + + if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) { + return ['success' => false, 'message' => 'Ошибка создания директории']; + } + + $base = 'store_' . $storeId . '_' . time(); + $bigFile = $dir . $base . '_big.' . $ext; + $smFile = $dir . $base . '_sm.jpg'; + + $file->saveAs($bigFile); + ImageHelper::resizeImage($bigFile, $smFile, 300, 200, 85); + + $urlDir = '/uploads/' . $adminId . '/' . $dateDir . '/'; + $bigUrl = $urlDir . $base . '_big.' . $ext; + $smUrl = $urlDir . $base . '_sm.jpg'; + + $store->image_big = $bigUrl; + $store->image_sm = $smUrl; + if (!$store->save(false, ['image_big', 'image_sm'])) { + return ['success' => false, 'message' => 'Ошибка сохранения в БД']; + } + + return ['success' => true, 'imageSm' => $smUrl, 'imageBig' => $bigUrl]; + } } diff --git a/erp24/migrations/m260603_120000_add_equipment_to_city_store_params.php b/erp24/migrations/m260603_120000_add_equipment_to_city_store_params.php new file mode 100644 index 00000000..b95976f1 --- /dev/null +++ b/erp24/migrations/m260603_120000_add_equipment_to_city_store_params.php @@ -0,0 +1,51 @@ + 'varchar(150)', + 'camera_count' => 'smallint', + 'microphone_model' => 'varchar(150)', + 'router_model' => 'varchar(150)', + 'internet_provider' => 'varchar(150)', + 'cash_register_info' => 'varchar(200)', + ]; + + public function safeUp(): void + { + $schema = $this->db->getTableSchema(self::TABLE, true); + if ($schema === null) { + return; + } + + foreach (self::COLUMNS as $col => $type) { + if ($schema->getColumn($col) === null) { + $this->addColumn(self::TABLE, $col, $type . ' DEFAULT NULL'); + } + } + } + + public function safeDown(): void + { + $schema = $this->db->getTableSchema(self::TABLE, true); + if ($schema === null) { + return; + } + + foreach (array_keys(self::COLUMNS) as $col) { + if ($schema->getColumn($col) !== null) { + $this->dropColumn(self::TABLE, $col); + } + } + } +} diff --git a/erp24/records/CityStoreParams.php b/erp24/records/CityStoreParams.php index 8149549b..15369230 100644 --- a/erp24/records/CityStoreParams.php +++ b/erp24/records/CityStoreParams.php @@ -22,6 +22,12 @@ use yii\db\Expression; * @property float|null $freeze_area * @property float|null $freeze_volume * @property string|null $matrix_type + * @property string|null $camera_model + * @property int|null $camera_count + * @property string|null $microphone_model + * @property string|null $router_model + * @property string|null $internet_provider + * @property string|null $cash_register_info * @property int $created_by * @property string $created_at * @property int|null $updated_by @@ -69,6 +75,9 @@ class CityStoreParams extends ActiveRecord [['address_city', 'address_region', 'address_district', 'matrix_type'], 'string'], [['is_active'], 'boolean'], [['is_active'], 'default', 'value' => true], + [['camera_model', 'microphone_model', 'router_model', 'internet_provider'], 'string', 'max' => 150], + [['cash_register_info'], 'string', 'max' => 200], + [['camera_count'], 'integer', 'min' => 0], ]; } diff --git a/erp24/views/city-store-management/index.php b/erp24/views/city-store-management/index.php index 8e7b8528..87c00f3d 100644 --- a/erp24/views/city-store-management/index.php +++ b/erp24/views/city-store-management/index.php @@ -21,6 +21,7 @@ $this->registerJs(' getStore: ' . json_encode(Url::to(['/city-store-management/get-store'])) . ', saveCityStore: ' . json_encode(Url::to(['/city-store-management/save-city-store'])) . ', saveCityStoreParams: ' . json_encode(Url::to(['/city-store-management/save-city-store-params'])) . ', + uploadStorePhoto: ' . json_encode(Url::to(['/city-store-management/upload-store-photo'])) . ', assortmentLabels: ' . json_encode(Url::to(['/assortment-label/index'])) . ', }; ', \yii\web\View::POS_HEAD); @@ -67,6 +68,7 @@ $this->registerJs('
+
@@ -123,6 +125,16 @@ $this->registerJs('
+ + + + + +
'; document.getElementById('contentArea').innerHTML = h; @@ -260,8 +271,14 @@ function renderOps() { + numField('col-md-4', 'fSquareStore', s.squareStore, 'Договор (square_store, м²)', 'city_store') + '
' + '
' - + '
' - + 'Техника (камеры, роутеры, касса) — требует миграции БД stub
' + + '
Техника
' + + textField('col-md-5', 'fCameraModel', s.cameraModel, 'Модель камеры') + + '
' + + '
' + + textField('col-md-5', 'fMicrophoneModel', s.microphoneModel, 'Модель микрофона') + + textField('col-md-4', 'fRouterModel', s.routerModel, 'Модель роутера') + + textField('col-md-4', 'fInternetProvider', s.internetProvider, 'Интернет-провайдер') + + textField('col-md-4', 'fCashRegisterInfo', s.cashRegisterInfo, 'Касса / эквайринг') + '
'; // 5. Адрес @@ -432,6 +449,9 @@ function renderSvc() { {k:'freeze_area',v:s.freezeArea},{k:'freeze_volume',v:s.freezeVolume}, {k:'is_active',v:s.isActive?'true':'false'}, {k:'matrix_type',v:s.matrixType ? s.matrixType.join(',') : null}, + {k:'camera_model',v:s.cameraModel},{k:'camera_count',v:s.cameraCount}, + {k:'microphone_model',v:s.microphoneModel},{k:'router_model',v:s.routerModel}, + {k:'internet_provider',v:s.internetProvider},{k:'cash_register_info',v:s.cashRegisterInfo}, ]}, { title: 'city_store_params — Адрес FK', src: 'CSP', fields: [ {k:'address_region',v:s.addressRegion},{k:'address_region_name',v:s.addressRegionName}, @@ -532,6 +552,12 @@ function saveChanges() { territorial_manager: val('fTerritorialManager'), is_active: checked('fIsActive') ? 1 : 0, square_store: val('fSquareStore'), + camera_model: val('fCameraModel'), + camera_count: val('fCameraCount'), + microphone_model: val('fMicrophoneModel'), + router_model: val('fRouterModel'), + internet_provider: val('fInternetProvider'), + cash_register_info: val('fCashRegisterInfo'), label_ids: (D.labelIds || []).join(','), }; @@ -775,9 +801,60 @@ function eqItem(icon, label, value, unit) { + ''; } -function eqItemStub(icon, label) { - return '
' - + '
' + esc(label) + ' stub
' - + '
—
' +function eqText(icon, label, value) { + var hasData = value !== null && value !== undefined && value !== ''; + return '
' + + '
' + esc(label) + '
' + + '
' + (hasData ? value : '—') + '
' + '
'; } + +// ===== ФОТО МАГАЗИНА ===== +function openPhotoModal() { + if (!D || !D.imageBig) return; + document.getElementById('photoModalImg').src = D.imageBig; + document.getElementById('photoModal').style.display = 'flex'; +} + +function closePhotoModal() { + document.getElementById('photoModal').style.display = 'none'; + document.getElementById('photoModalImg').src = ''; +} + +function triggerPhotoUpload(ev) { + if (ev) ev.stopPropagation(); + document.getElementById('photoFileInput').click(); +} + +function handlePhotoUpload(input) { + var file = input.files[0]; + if (!file || !D) return; + input.value = ''; + + var ph = document.getElementById('heroPhoto'); + ph.innerHTML = '
Загрузка…
'; + + var fd = new FormData(); + fd.append('photo', file); + fd.append('store_id', D.id); + + fetch(CSM_URLS.uploadStorePhoto, { + method: 'POST', + headers: { 'X-CSRF-Token': csrfToken }, + body: fd, + }) + .then(function (r) { return r.json(); }) + .then(function (resp) { + if (!resp.success) { toast(resp.message || 'Ошибка загрузки фото', 'err'); renderHero(); return; } + D.imageSm = resp.imageSm; + D.imageBig = resp.imageBig; + renderHero(); + // обновить поля SEO-вкладки если они отрисованы + var fSm = document.getElementById('fImageSm'); + var fBig = document.getElementById('fImageBig'); + if (fSm) fSm.value = resp.imageSm; + if (fBig) fBig.value = resp.imageBig; + toast('Фото загружено', 'ok'); + }) + .catch(function () { toast('Ошибка сети при загрузке фото', 'err'); renderHero(); }); +}