]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
Сортировка
authorVladimir Fomichev <vladimir.fomichev@erp-flowers.ru>
Fri, 3 Oct 2025 14:15:31 +0000 (17:15 +0300)
committerVladimir Fomichev <vladimir.fomichev@erp-flowers.ru>
Fri, 3 Oct 2025 14:15:31 +0000 (17:15 +0300)
erp24/controllers/MatrixErpPropertyController.php
erp24/helpers/ImageHelper.php
erp24/records/MatrixErpMedia.php
erp24/records/MatrixErpProperty.php
erp24/views/matrix_erp_property/_form.php
erp24/web/js/matrix_erp_property/_form.js

index 5ec32f562aa9b6c17aa8246b58429b6579438b3e..771b4f258cb0671d258ac6f14f484665689863fd 100644 (file)
@@ -148,12 +148,36 @@ class MatrixErpPropertyController extends Controller
                     if (Yii::$app->request->isPost) {
                         $postMatrixErpProperty = $this->request->post('MatrixErpProperty');
 
-                        $modelsMatrixErpMedia = MultipleModel::createMultipleModel(MatrixErpMedia::class,
-                            'MatrixErpProperty', 'mediaFiles');
-
+                        // Получаем данные из формы
                         $loadDataMatrixErpMedia = ArrayHelper::getValue($postMatrixErpProperty, 'mediaFiles');
+
+                        // Сортируем данные по foto_order перед обработкой
+                        if (!empty($loadDataMatrixErpMedia)) {
+                            usort($loadDataMatrixErpMedia, function($a, $b) {
+                                $orderA = $a['foto_order'] ?? 999;
+                                $orderB = $b['foto_order'] ?? 999;
+                                return $orderA <=> $orderB;
+                            });
+                        }
+
+                        // Создаем модели вручную, сохраняя правильные ID
+                        $modelsMatrixErpMedia = [];
                         if (!empty($loadDataMatrixErpMedia)) {
-                            MultipleModel::loadMultipleFromArray($modelsMatrixErpMedia, $loadDataMatrixErpMedia , '',[]);
+                            foreach ($loadDataMatrixErpMedia as $index => $mediaData) {
+                                if (!empty($mediaData['id'])) {
+                                    // Для существующих записей находим модель по ID
+                                    $model = MatrixErpMedia::findOne($mediaData['id']);
+                                    if ($model) {
+                                        $model->load($mediaData, '');
+                                        $modelsMatrixErpMedia[] = $model;
+                                    }
+                                } else {
+                                    // Для новых записей создаем новую модель
+                                    $model = new MatrixErpMedia();
+                                    $model->load($mediaData, '');
+                                    $modelsMatrixErpMedia[] = $model;
+                                }
+                            }
                         }
 
                         $loadMediaIds = ArrayHelper::getColumn($modelsMatrixErpMedia, 'id');
@@ -178,23 +202,7 @@ class MatrixErpPropertyController extends Controller
 
                         $infos = FileService::uploadFileMultilevel('MatrixErpProperty', $adminId, ['mediaFiles','mediaFile']);
 
-                        foreach ($modelsMatrixErpMedia as $key => $modelMatrixErpMedia) {
-                            if (!empty($modelMatrixErpMedia->id)) {
-                                $modelMatrixErpMediaRow = MatrixErpMedia::find()->where(['id'=> $modelMatrixErpMedia->id])->one();
-                                /** @var MatrixErpMedia $modelMatrixErpMediaRow */
-                                if (!empty($loadDataMatrixErpMedia[$key])) {
-                                    if (!empty($modelMatrixErpMediaRow)) {
-                                        $modelMatrixErpMediaRow->load($loadDataMatrixErpMedia[$key],'');
-                                    } else {
-                                        $modelMatrixErpMediaRow = $modelMatrixErpMedia;
-                                    }
-                                }
-                            } else {
-                                /** @var MatrixErpMedia $modelMatrixErpMediaRow */
-                                /** @var MatrixErpMedia $modelMatrixErpMedia */
-                                $modelMatrixErpMediaRow = $modelMatrixErpMedia;
-                            }
-
+                        foreach ($modelsMatrixErpMedia as $key => $modelMatrixErpMediaRow) {
                             if (array_key_exists($key, $infos) && !empty($infos[$key])) {
                                 $info = $infos[$key];
 
@@ -214,6 +222,12 @@ class MatrixErpPropertyController extends Controller
                                 $modelMatrixErpMediaRow->created_admin_id = $adminId;
                                 $modelMatrixErpMediaRow->date = date("Y-m-d H:i:s");
                                 $modelMatrixErpMediaRow->created_at = date("Y-m-d H:i:s");
+
+                                // Устанавливаем foto_order - следующий доступный номер для этого guid
+                                $maxOrder = MatrixErpMedia::find()
+                                    ->where(['guid' => $modelEdit->guid])
+                                    ->max('foto_order') ?? 0;
+                                $modelMatrixErpMediaRow->foto_order = $maxOrder + 1;
                             }
                             if ($modelMatrixErpMediaRow->validate()) {
                                 if (empty($modelMatrixErpMediaRow->id)) {
@@ -223,6 +237,9 @@ class MatrixErpPropertyController extends Controller
                             }
                         }
 
+                        // Обновляем основную картинку на первую из мультиформы
+                        $this->updateMainImageFromFirstMedia($modelEdit, $adminId);
+
                         $uploadImage = UploadedFile::getInstanceByName('MatrixErpProperty[imageFile]');
 
                         if ($uploadImage) {
@@ -275,6 +292,93 @@ class MatrixErpPropertyController extends Controller
      * @return MatrixErpProperty the loaded model
      * @throws NotFoundHttpException if the model cannot be found
      */
+    /**
+     * Обновляет основную картинку на первую из мультиформы
+     * @param MatrixErpProperty $modelEdit
+     * @param int $adminId
+     */
+    protected function updateMainImageFromFirstMedia($modelEdit, $adminId)
+    {
+        // Находим первую медиа запись (с минимальным порядком)
+        $firstMedia = MatrixErpMedia::find()
+            ->where(['guid' => $modelEdit->guid])
+            ->andWhere(['>', 'foto_order', 0]) // Исключаем записи с foto_order = 0 (если есть)
+            ->orderBy(['foto_order' => SORT_ASC])
+            ->one();
+
+        if ($firstMedia && $firstMedia->file) {
+            $file = $firstMedia->file;
+
+            // Проверяем, что файл существует и это изображение
+            if ($file->file_type === 'image' && file_exists($file->url)) {
+                Yii::info("Processing main image update for file: {$file->url}", __METHOD__);
+                // Сохраняем старую картинку для возможного удаления
+                $oldImageId = $modelEdit->image_id;
+
+                // Создаем новую запись в Images на основе файла
+                $image = new Images();
+
+                // Генерируем filename как в методе loadImage
+                $originalName = basename($file->url);
+                $ar_file_name = explode('.', $originalName);
+                $ext = '.' . end($ar_file_name);
+                $file_name = md5($originalName . rand(100000, 999999)) . $ext;
+
+                // Используем оригинальное название файла из MatrixErpMedia или имя файла
+                $displayName = !empty($firstMedia->name) ? $firstMedia->name : pathinfo($originalName, PATHINFO_FILENAME);
+                $image->original_name = $displayName . $ext;
+                $image->filename = $file_name;
+                $image->type = mime_content_type($file->url) ?: 'image/jpeg';
+
+                // Получаем размеры изображения
+                $imageInfo = getimagesize($file->url);
+                if ($imageInfo) {
+                    $image->width = $imageInfo[0];
+                    $image->height = $imageInfo[1];
+                }
+
+                $image->size = filesize($file->url);
+                $image->created_at = time();
+                $image->updated_at = time();
+
+                if ($image->save()) {
+                    // Копируем файл в директорию images
+                    $sourcePath = $file->url;
+                    $destDir = \Yii::getAlias('@uploads/images/' . substr($image->filename, 0, 2));
+                    $destPath = $destDir . '/' . $image->filename;
+
+                    if (!is_dir($destDir)) {
+                        mkdir($destDir, 0777, true);
+                    }
+
+                    if (copy($sourcePath, $destPath)) {
+                        // Обновляем основную картинку
+                        $modelEdit->image_id = $image->id;
+                        $modelEdit->external_image_url = MarketplaceService::getProductImageUrl($image->id);
+
+                        if ($modelEdit->save()) {
+                            Yii::info("Main image updated successfully to image_id: {$image->id}", __METHOD__);
+                            // Удаляем старую картинку, если она была
+                            if ($oldImageId && $oldImageId !== $image->id) {
+                                $oldImage = Images::findOne($oldImageId);
+                                if ($oldImage) {
+                                    $oldImage->delete();
+                                    Yii::info("Old image deleted: {$oldImageId}", __METHOD__);
+                                }
+                            }
+                        } else {
+                            Yii::error("Failed to save MatrixErpProperty with new image_id: " . json_encode($modelEdit->getErrors()), __METHOD__);
+                        }
+                    } else {
+                        Yii::error("Failed to copy file from {$sourcePath} to {$destPath}", __METHOD__);
+                        // Если копирование не удалось, удаляем созданную запись
+                        $image->delete();
+                    }
+                }
+            }
+        }
+    }
+
     protected function findModel($id)
     {
         if (($model = MatrixErpProperty::findOne(['id' => $id])) !== null) {
index 0dac85fd3a58b28c49ec7c8351902c7cda434c58..bd6a8f6633f6ca923a091271c4e8179bfdecc13c 100644 (file)
@@ -35,7 +35,8 @@ class ImageHelper
         $modelImage = \yii_app\records\Images::findOne(['id' => $imageId]);
         if ($modelImage && File::src($modelImage->filename, 'images') != null) {
             $fileName = File::src($modelImage->filename, 'images');
-            $relaFileName = File::getRealName($imageId);
+            // Используем original_name для понятного названия
+            $displayName = $modelImage->original_name ?: basename($fileName);
             try {
                 $imageThumbRow = File::getResizedImageByName($modelImage->filename, $width, $height);
             } catch (\Exception $ex) {
@@ -43,7 +44,7 @@ class ImageHelper
             }
             $imageSrcRow = '/' . $imageThumbRow;
             $srcImageThumbRow = '/' . $imageThumbRow;
-            $image = Html::img($srcImageThumbRow, ['class' => 'file-preview-image tumb', 'alt' => $relaFileName]);
+            $image = Html::img($srcImageThumbRow, ['class' => 'file-preview-image tumb', 'alt' => $displayName]);
         }
 
         return $image;
@@ -60,7 +61,8 @@ class ImageHelper
 
             if ($modelImage && File::src($modelImage->filename, 'images') != null) {
                 $fileName = File::src($modelImage->filename, 'images');
-                $relaFileName = File::getRealName($imageIdRow);
+                // Используем original_name для понятного названия
+                $displayName = $modelImage->original_name ?: basename($fileName);
                 try {
                     $imageThumbRow =  File::getResizedImageByName($modelImage->filename, $w , $h);
                 } catch (\Exception $ex) {
@@ -68,16 +70,16 @@ class ImageHelper
                 }
                 $imageSrcRow = '/' . $imageThumbRow;
                 $srcImageThumbRow = '/' . $imageThumbRow;
-                $tagImageThumbRow = Html::img($srcImageThumbRow, ['class' => 'file-preview-image tumb', 'alt' => $relaFileName]);
+                $tagImageThumbRow = Html::img($srcImageThumbRow, ['class' => 'file-preview-image tumb', 'alt' => $displayName]);
                 if (true === $forWidget) {
                     $images[] = [
                         'url' => $fileName,
                         'src' => $imageSrcRow,
 
                         'options' => [
-                            'title' => $relaFileName,
+                            'title' => $displayName,
                             'class' => 'file-preview-image tumb',
-                            'alt' => $relaFileName,
+                            'alt' => $displayName,
                             'data-lightbox' => $fileName,
                         ]
                     ];
@@ -86,7 +88,7 @@ class ImageHelper
                         'class' => 'gallery-item px-2',
                         'target' => "_blank",
                         'rel'=> "noopener noreferrer",
-                        'title' => $relaFileName,
+                        'title' => $displayName,
                     ];
 
                     $imagesResult[] = Html::a($tagImageThumbRow, $fileName , $options);
index 37415baf18a835afbf6d99e86f3bc1ec184596f1..6e8465cf42c1e686cec391f4cbd9ee4954e33d28 100644 (file)
@@ -13,6 +13,7 @@ use Yii;
  * @property string|null $description Описание
  * @property int|null $file_id Файл
  * @property string $date
+ * @property int|null $foto_order Порядок фото
  * @property int|null $created_admin_id
  * @property string|null $created_at
  * @property int|null $updated_admin_id
@@ -39,7 +40,7 @@ class MatrixErpMedia extends \yii\db\ActiveRecord
             [['guid', 'name', 'date'], 'required'],
             [['description'], 'string'],
             [['mediaFiles', 'id'], 'safe'],
-            [['file_id', 'created_admin_id', 'updated_admin_id'], 'integer'],
+            [['file_id', 'foto_order', 'created_admin_id', 'updated_admin_id'], 'integer'],
             [['guid', 'name', 'date', 'created_at', 'updated_at'], 'string', 'max' => 100],
         ];
     }
@@ -55,6 +56,7 @@ class MatrixErpMedia extends \yii\db\ActiveRecord
             'name' => 'Name',
             'description' => 'Description',
             'file_id' => 'File ID',
+            'foto_order' => 'Foto Order',
             'date' => 'Date',
             'created_admin_id' => 'Created Admin ID',
             'created_at' => 'Created At',
index 83a8e4e010672bf1263168af87f6026be518cb63..340b86f64696c8007006276f17a28a5ab4cfae0d 100644 (file)
@@ -92,6 +92,6 @@ class MatrixErpProperty extends \yii\db\ActiveRecord
     }
 
     public function getMediaFiles() {
-        return $this->hasMany(MatrixErpMedia::class, ['guid' => 'guid']);
+        return $this->hasMany(MatrixErpMedia::class, ['guid' => 'guid'])->orderBy(['foto_order' => SORT_ASC]);
     }
 }
index 2069004a875d67dbf01cfd754dd2b3f86570dab5..e4cc6ee0032aa031303742e434bb10b958230a08 100644 (file)
@@ -12,6 +12,7 @@ use yii_app\services\FileService;
 /** @var yii_app\records\MatrixErpProperty $modelMatrixErpProperty */
 /** @var yii\widgets\ActiveForm $form */
 
+$this->registerJsFile('/js/Sortable.js', ['position' => \yii\web\View::POS_END]);
 $this->registerJsFile('/js/matrix_erp_property/_form.js', ['position' => \yii\web\View::POS_END]);
 
 $options = [
@@ -38,6 +39,7 @@ $this->registerJs(
     <?= $form->field($modelMatrixErpProperty, 'id')->hiddenInput()->label(false) ?>
     <?= $form->field($modelMatrixErp, 'id')->hiddenInput()->label(false) ?>
     <?= $form->field($modelMatrixErpProperty, 'guid')->hiddenInput()->label(false) ?>
+    <?= $form->field($modelMatrixErpProperty, 'image_id')->hiddenInput()->label(false) ?>
 
     <?= $form->field($modelMatrixErpProperty, 'display_name')->textInput() ?>
 
@@ -82,8 +84,14 @@ $this->registerJs(
 
     <?= $form->field($modelMatrixErpProperty, 'weight')->textInput(['type' => 'number', 'step' => 0.01]) ?>
 
+    <?php
+    // Загружаем данные для мультиинпута в правильном порядке
+    $mediaFilesData = $modelMatrixErpProperty->mediaFiles;
+    ?>
+
     <div class="form-group">
         <?= $form->field($modelMatrixErpProperty, 'mediaFiles')->widget(MultipleInput::className(), [
+            'data' => $mediaFilesData,
             'min' => 0,
             'max' => 100,
             'columns' => [
@@ -118,10 +126,10 @@ $this->registerJs(
                     ]
                 ],
                 [
-                    'name'  => 'num_row',
+                    'name'  => 'foto_order',
                     'type'  => BaseColumn::TYPE_HIDDEN_INPUT,
                     'value' => function($data) {
-                        return $data['num_row'] ?? '';
+                        return $data['foto_order'] ?? '';
                     },
                     'headerOptions' => [
                         'style' => 'width: 70px;',
@@ -139,13 +147,31 @@ $this->registerJs(
                     'type'  => BaseColumn::TYPE_STATIC,
                     'value' => function($data) {
                         /* @var $data \yii_app\records\MatrixErpMedia */
+                        $result = '';
+
                         if (!empty($data->file)) {
-                            $resultData = FileService::getFile($data->file);
+                            // Получаем ссылку на скачивание файла от FileService
+                            $downloadLink = FileService::getFile($data->file);
+
+                            // Для изображений показываем миниатюру + ссылку на скачивание
+                            if ($data->file->file_type === 'image') {
+                                $result = '<div style="display: flex; flex-direction: row; align-items: center; gap: 4px;">';
+                                $result .= '<img src="' . FileService::padWithSlash($data->file->url) . '" alt="' . htmlspecialchars($data->name ?: basename($data->file->url)) . '" style="max-width: 80px; max-height: 60px; object-fit: cover; border: 1px solid #ddd; border-radius: 4px;" onclick="window.open(this.src, \'_blank\');" />';
+                                $result .= '<div style="font-size: 10px;">' . $downloadLink . '</div>';
+                                $result .= '</div>';
+                            } else {
+                                // Для других файлов показываем иконку + ссылку на скачивание
+                                $result = '<div style="display: flex; align-items: center; gap: 8px;">';
+                                $result .= '<i class="fa fa-file-o" style="font-size: 24px; color: #666;"></i>';
+                                $result .= '<div style="font-size: 12px;">' . $downloadLink . '</div>';
+                                $result .= '</div>';
+                            }
                         }
-                        return $resultData ?? '';
+
+                        return $result;
                     },
                     'headerOptions' => [
-//                    'style' => 'width: 70px;',
+                        'style' => 'width: 120px;',
                     ]
                 ],
 
@@ -186,23 +212,56 @@ $this->registerJs(
     <?php ActiveForm::end(); ?>
 
 </div>
-<script>
-    jQuery(".multiple-input-list__item").on("click", ".multiple-input-list__item", function(e) {
-        console.log('add-item1');
 
-        // jQuery(".multiple-input-list__item input:file").last().attr("required", "required");
-        jQuery(".multiple-input-list__item input:text").last().attr("required", "required");
+<style>
+.sortable-ghost {
+    opacity: 0.4;
+    background: #f0f0f0;
+}
+
+.sortable-chosen {
+    background: #e9ecef;
+}
+
+.sortable-drag {
+    transform: rotate(5deg);
+}
+
+.sortable-item-dragging {
+    opacity: 0.5;
+}
+
+.multiple-input-list__item {
+    transition: all 0.2s ease;
+}
+
+.multiple-input-list__item:hover {
+    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.sortable-handle {
+    cursor: move;
+    user-select: none;
+}
+
+.sortable-handle:hover {
+    background: #e9ecef !important;
+}
+</style>
+<script>
+    // Обработчик добавления новых элементов
+    $(document).on('multipleinput.afterAddRow', function(e, row) {
+        // Добавляем handle для нового элемента в первую ячейку
+        if (!$(row).find('.sortable-handle').length) {
+            $(row).find('td:first').prepend('<div class="sortable-handle" style="cursor: move; padding: 5px; background: #f8f9fa; border-right: 1px solid #dee2e6; display: inline-block; margin-right: 10px; vertical-align: top;"><i class="fa fa-bars"></i></div>');
+            $(row).find('td:first').css('display', 'flex');
+        }
+        updateNumRowValues();
     });
-    jQuery(".multiple-input-list__item").on("click", "input:text", function(e) {
-        console.log('add-item2');
 
-        // jQuery(".multiple-input-list__item input:file").last().attr("required", "required");
-        jQuery(".multiple-input-list__item input:text").last().attr("required", "required");
+    // Обработчик удаления элементов
+    $(document).on('multipleinput.afterRemoveRow', function(e) {
+        updateNumRowValues();
     });
-    jQuery(".multiple-input-list__item").on("click", "input:file", function(e) {
-        console.log('add-item3');
 
-        // jQuery(".multiple-input-list__item input:file").last().attr("required", "required");
-        jQuery(".multiple-input-list__item input:text").last().attr("required", "required");
-    });
 </script>
\ No newline at end of file
index 7415ccbe013badc725f05880fff2dfb5f08e505b..fa347552d3e44430fd36bc8e197710b25da9b9b4 100644 (file)
@@ -60,4 +60,94 @@ function flowwowCategoryChanged(categoryIndex, subCategoryIndex) {
 
 $(document).ready(() => {
     flowwowCategoryChanged($("#matrixerpproperty-flowwow_category").val(), yiiOptions.subCategory);
-});
\ No newline at end of file
+
+    // Добавляем функциональность сортировки файлов с задержкой
+    setTimeout(function() {
+        initSortableMediaFiles();
+    }, 500);
+
+    // Также пытаемся инициализировать после загрузки всех виджетов
+    $(window).on('load', function() {
+        console.log('Window load event');
+        setTimeout(function() {
+            initSortableMediaFiles();
+        }, 100);
+    });
+});
+
+// Глобальная переменная для хранения экземпляра sortable
+let mediaSortableInstance = null;
+
+function initSortableMediaFiles() {
+    // Для таблиц MultipleInput нужно инициализировать sortable на tbody, а не на table
+    let sortableContainer = document.querySelector('.multiple-input-list tbody');
+    if (!sortableContainer) {
+        sortableContainer = document.querySelector('.multiple-input-list');
+    }
+
+    if (sortableContainer && !mediaSortableInstance) {
+        // Инициализируем Sortable.js для перетаскивания строк таблицы
+        mediaSortableInstance = Sortable.create(sortableContainer, {
+            handle: '.sortable-handle',
+            animation: 150,
+            ghostClass: 'sortable-ghost',
+            chosenClass: 'sortable-chosen',
+            dragClass: 'sortable-drag',
+            // Указываем, что перетаскиваем именно tr элементы
+            draggable: 'tr.multiple-input-list__item',
+            onStart: function(evt) {
+                // Добавляем визуальную индикацию перетаскивания
+                $(evt.item).addClass('sortable-item-dragging');
+            },
+            onEnd: function(evt) {
+                $(evt.item).removeClass('sortable-item-dragging');
+                updateNumRowValues();
+            }
+        });
+
+        // Добавляем handle для перетаскивания к каждому элементу
+        $('.multiple-input-list__item').each(function() {
+            if (!$(this).find('.sortable-handle').length) {
+                // Добавляем handle в первую ячейку строки
+                $(this).find('td:first').prepend('<div class="sortable-handle" style="cursor: move; padding: 5px; background: #f8f9fa; border-right: 1px solid #dee2e6; display: inline-block; margin-right: 10px; vertical-align: top;"><i class="fa fa-bars"></i></div>');
+                $(this).find('td:first').css('display', 'flex');
+            }
+        });
+    } else if (sortableContainer && mediaSortableInstance) {
+        // Если sortable уже инициализирован, просто обновляем handles
+        $('.multiple-input-list__item').each(function() {
+            if (!$(this).find('.sortable-handle').length) {
+                $(this).find('td:first').prepend('<div class="sortable-handle" style="cursor: move; padding: 5px; background: #f8f9fa; border-right: 1px solid #dee2e6; display: inline-block; vertical-align: top;"><i class="fa fa-bars"></i></div>');
+            }
+        });
+    }
+}
+
+function updateNumRowValues() {
+    $('.multiple-input-list__item').each(function(index) {
+        // Ищем поле foto_order и устанавливаем порядок (начиная с 1)
+        let fotoOrderInput = $(this).find('input[name*="MatrixErpProperty[mediaFiles]"][name*="[foto_order]"]');
+        if (!fotoOrderInput.length) {
+            fotoOrderInput = $(this).find('input[name*="[foto_order]"]');
+        }
+        if (!fotoOrderInput.length) {
+            fotoOrderInput = $(this).find('input[type="hidden"][name*="foto_order"]');
+        }
+
+        if (fotoOrderInput.length) {
+            // Устанавливаем порядок начиная с 1
+            fotoOrderInput.val(index + 1);
+        }
+    });
+
+    // Обновляем основную картинку на первую в новом порядке
+    updateMainImageFromFirstFile();
+}
+
+function updateMainImageFromFirstFile() {
+    // Основная картинка обновляется автоматически на сервере
+    // после сохранения формы в методе updateMainImageFromFirstMedia
+    // Здесь ничего не делаем - сервер сам обработает обновление
+    console.log('Main image will be updated on server side');
+}
+