]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
ERP-302 Редактирование букета
authormarina <m.zozirova@gmail.com>
Tue, 4 Feb 2025 12:33:46 +0000 (15:33 +0300)
committermarina <m.zozirova@gmail.com>
Tue, 4 Feb 2025 12:33:46 +0000 (15:33 +0300)
erp24/controllers/BouquetController.php
erp24/records/BouquetComposition.php
erp24/records/BouquetCompositionProducts.php
erp24/views/bouquet/update.php
erp24/web/js/bouquet/bouquet.js
erp24/widgets/DualList.php

index c2a7d73bc642068e1f602e06cb11750b6b787ba0..95999c8d547bcfbb36aa075dbf57dacdbe3f6df0 100644 (file)
@@ -1,9 +1,12 @@
 <?php
+
 namespace app\controllers;
 
+use Exception;
 use Yii;
 use yii\helpers\ArrayHelper;
 use yii\web\Controller;
+use yii\web\NotFoundHttpException;
 use yii_app\records\BouquetComposition;
 use yii_app\records\BouquetCompositionProducts;
 use yii_app\records\Products1c;
@@ -13,22 +16,90 @@ use yii_app\records\Products1c;
  */
 class BouquetController extends Controller
 {
-    public function actionIndex() {
+    public function actionIndex()
+    {
         return $this->render('index');
     }
 
-    public function actionView() {
+    public function actionView()
+    {
         return $this->render('view');
     }
 
-    public function actionUpdate() {
-        return $this->render('update');
+    public function actionUpdate($id)
+    {
+        $model = BouquetComposition::findOne($id);
+
+        if (!$model) {
+            throw new NotFoundHttpException('Букет не найден.');
+        }
+//
+//        if (Yii::$app->request->isPost) {
+//            echo '<pre>';
+//            var_dump(Yii::$app->request->post());
+//            echo '</pre>';
+//           die();
+//        }
+
+        if (Yii::$app->request->isPost) {
+            try {
+                if (array_key_exists('products_quantity', Yii::$app->request->post())) {
+                    $bouquetProducts = Yii::$app->request->post('products_quantity');
+                    foreach ($bouquetProducts as $key => $value) {
+                        $product = new BouquetCompositionProducts([
+                            'bouquet_id' => $id,
+                            'product_guid' => $key,
+                            'count' => $value
+                        ]);
+                        $product->save();
+                    }
+
+                    return $this->render('view', [
+                        'model' => $model
+                    ]);
+                }
+            } catch (Exception $exception) {
+                throw new NotFoundHttpException($exception);
+            }
+        }
+        $products = BouquetCompositionProducts::find()
+            ->where(['bouquet_id' => $model->id])
+            ->with('product')
+            ->all();
+
+        $selectedItems = array_map(fn($product) => [
+            'id' => $product->product_guid,
+            'count' => $product->count,
+            'text' => $product->product->name ?? ''
+        ], $products);
+
+        $selectedProductIds = array_column($selectedItems, 'id');
+
+        $availableItems = ArrayHelper::map(
+            Products1c::find()
+                ->where([
+                    'view' => Products1c::IS_VISIBLE,
+                    'tip' => Products1c::TYPE_PRODUCTS
+                ])
+                ->andWhere(['not in', 'id', $selectedProductIds])
+                ->all(),
+            'id',
+            'name'
+        );
+
+        return $this->render('update', [
+            'model' => $model,
+            'selectedItems' => $selectedItems,
+            'availableItems' => $availableItems,
+        ]);
     }
 
     public function actionGetList()
     {
         \Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
 
+        
+
         $products = Products1c::find()
             ->where(['tip' => Products1c::TYPE_PRODUCTS, 'view' => Products1c::IS_VISIBLE])
             ->andWhere(['ilike', 'name', 'роза'])
index 7e14a46e3452c8d065f11eb589c756a6ad0b440b..ec122b94eaa08617663793f3719b773f009f04a8 100644 (file)
@@ -2,7 +2,10 @@
 namespace yii_app\records;
 
 use Yii;
+use yii\behaviors\BlameableBehavior;
+use yii\behaviors\TimestampBehavior;
 use yii\db\ActiveRecord;
+use yii\db\Expression;
 use yii_app\records\BouquetCompositionProducts;
 
 /**
@@ -28,6 +31,24 @@ class BouquetComposition extends ActiveRecord
         return 'erp24.bouquet_composition';
     }
 
+    public function behaviors()
+    {
+        return [
+            [
+                'class' => TimestampBehavior::class,
+                'createdAtAttribute' => 'created_at',
+                'updatedAtAttribute' => 'updated_at',
+                'value' => new Expression('NOW()'),
+            ],
+            [
+                'class' => BlameableBehavior::class,
+                'createdByAttribute' => 'created_by',
+                'updatedByAttribute' => 'updated_by',
+            ],
+        ];
+    }
+
+
     public function rules()
     {
         return [
index c71005ed03bb6bf01c8c43ed2fc8b875d9bcafdb..98d4b778e78a189819af14a211d06b18f78ca616 100644 (file)
@@ -2,7 +2,10 @@
 namespace yii_app\records;
 
 use Yii;
+use yii\behaviors\BlameableBehavior;
+use yii\behaviors\TimestampBehavior;
 use yii\db\ActiveRecord;
+use yii\db\Expression;
 
 /**
  * This is the model class for table "erp24.bouquet_composition_products".
@@ -25,6 +28,24 @@ class BouquetCompositionProducts extends ActiveRecord
         return 'erp24.bouquet_composition_products';
     }
 
+    public function behaviors()
+    {
+        return [
+            [
+                'class' => TimestampBehavior::class,
+                'createdAtAttribute' => 'created_at',
+                'updatedAtAttribute' => 'updated_at',
+                'value' => new Expression('NOW()'),
+            ],
+            [
+                'class' => BlameableBehavior::class,
+                'createdByAttribute' => 'created_by',
+                'updatedByAttribute' => 'updated_by',
+            ],
+        ];
+    }
+
+
     public function rules()
     {
         return [
@@ -55,4 +76,9 @@ class BouquetCompositionProducts extends ActiveRecord
     {
         return $this->hasOne(BouquetComposition::class, ['id' => 'bouquet_id']);
     }
+
+    public function getProduct() {
+        return $this->hasOne(Products1c::class, ['id' => 'product_guid']); // Исправил связь
+    }
+
 }
index 1c9eef7ce1aeac1680b2441f24c73394fe507bca..5de8c3403270a9d8dbae228047a9587f37b26ef0 100644 (file)
@@ -3,6 +3,7 @@
 use app\widgets\DualList;
 use yii\helpers\ArrayHelper;
 use yii\helpers\Html;
+use yii\widgets\ActiveForm;
 use yii_app\records\Products1c;
 
 /** @var yii\web\View $this */
@@ -16,11 +17,14 @@ $this->registerJsFile('/js/bouquet/bouquet.js', ['position' => \yii\web\View::PO
 ?>
 
 <div class="bouquet-update p-4">
-    <?= Html::label('Редактирование букета', null, ['class' => 'h4']) ?>
-    <h1 class="ms-3"><?= Html::encode($this->title) ?></h1>
-
+<!--    --><?php //= Html::label('Редактирование букета', null, ['class' => 'h4']) ?>
+    <h3 class="ms-3 d-inline"><?= Html::encode("Редактирование букета: ") ?></h3>
+    <h2 class="d-inline"><strong><?= Html::encode($this->title) ?></strong></h2>
     <div class="row">
-        <div class="col-md-3 p-3 border rounded ms-3">
+        <div class="col-md-3 p-3 ms-3">
+            <div class="row mb-2">
+                <?= Html::tag('div', Html::label('Фильтры'), ['class' => 'fw-bold fs-5 text-center']) ?>
+            </div>
             <div class="row mb-2">
                 <?= Html::dropDownList('category', null, [], ['class' => 'form-select', 'prompt' => 'Выберите категорию']) ?>
             </div>
@@ -38,39 +42,36 @@ $this->registerJsFile('/js/bouquet/bouquet.js', ['position' => \yii\web\View::PO
             </div>
 
             <?= Html::button('Применить', ['class' => 'btn btn-primary w-100 mb-3', 'id' => 'apply-button']) ?>
+        </div>
 
-            <div class="border-top pt-2">
+        <div class="col-md-8">
+            <div class="row mb-5"></div>
+            <?php $form = ActiveForm::begin(['id' => 'dual-list-form']); ?>
+            <?= DualList::widget([
+                'name' => 'products',
+                'availableLabel' => 'Выбор',
+                'selectedLabel' => 'Состав букета',
+                'availableItems' => $availableItems,
+                'selectedItems' => $selectedItems,
+                'ajaxUrl' => '/bouquet/get-list',
+                'showQuantity' => true,
+                'triggerButton' => 'apply-button',
+            ]) ?>
+        </div>
+
+    </div>
+    <div class="row">
+        <div class="col-md-3">
+            <div class="pt-2">
                 <p class="mb-1"><strong>Себестоимость:</strong> <span class="cost-value">0</span> ₽</p>
                 <p class="mb-1"><strong>Наценка:</strong> <span class="markup-value">0</span> %</p>
                 <p class="mb-0"><strong>Цена:</strong> <span class="price-value">0</span> ₽</p>
             </div>
         </div>
-
-        <div class="col-md-8">
-            <?= DualList::widget([
-                'name' => 'products',
-                'items' => ArrayHelper::map(
-                    Products1c::findAll(['tip' => Products1c::TYPE_PRODUCTS, 'view' => Products1c::IS_VISIBLE]),
-                    'id', 'name'
-                ),
-                'ajaxUrl' => ['/bouquet/get-list'], // для асинхронной загрузки данных
-                'options' => [
-                    'multiple' => true,
-                    'size' => 10,
-                    'id' => 'dual-list-box'
-                ],
-                'triggerButton' => 'apply-button',  // Кнопка, которая будет обновлять список
-                'clientOptions' => [
-                    'moveOnSelect' => true,
-                    'nonSelectedListLabel' => 'Выбор',
-                    'selectedListLabel' => 'Состав букета',
-                    'filterTextClear' => 'Показать всё',
-                    'filterPlaceHolder' => 'Фильтр...',
-                    'infoText' => 'Показано {0}',
-                    'infoTextFiltered' => '<span class="badge bg-info">{0}</span> из {1}',
-                    'infoTextEmpty' => 'Список пуст',
-                ],
-            ]); ?>
+        <div class="col-md-6"></div>
+        <div class="col-md-2 d-flex justify-content-end align-items-end mx-7 px-3 w-100">
+            <?= Html::submitButton('Сохранить', ['class' => 'btn btn-success w-100']) ?>
         </div>
     </div>
+    <?php ActiveForm::end(); ?>
 </div>
index 5f6a623e4d445e68ae1a4bd2f11f17fc197bfbde..390004ebee62e2f25ed501ac158c32ef18546238 100644 (file)
@@ -1,9 +1,7 @@
 const observer = new MutationObserver(() => {
-    $('.removeall').each(function() {
+    $('.btn-group.buttons').each(function() {
         $(this).remove();
     });
-    console.log("Удалены все .removeall!");
 });
 
-// Наблюдаем за изменениями в контейнере дуал-листбокса
 observer.observe(document.body, { childList: true, subtree: true });
index 2021cee1e74ee3e004ca2b3788672c26459c4699..63bca55c771ed8d24bc5aba99de855e87506e0f5 100644 (file)
 
 namespace app\widgets;
 
+use yii\base\Widget;
+use yii\helpers\Html;
 use yii\helpers\Url;
-use softark\duallistbox\DualListbox;
 use yii\web\View;
 
-class DualList extends DualListbox
+class DualList extends Widget
 {
-    public $ajaxUrl; // URL для AJAX-запроса
-    public $triggerButton; // ID кнопки, которая запускает AJAX-запрос
+    public $name;
+    public $availableItems = [];
+    public $selectedItems = [];
+    public $ajaxUrl;
+    public $showQuantity = false;
+
+// Новое свойство для кнопки, вызывающей загрузку
+    public $triggerButton;
+
+    public $availableLabel = 'Доступные элементы';
+    public $selectedLabel = 'Выбранные элементы';
 
     public function run()
     {
-// Рендерим сам виджет DualListbox
-        echo DualListbox::widget([
-            'name' => $this->name,
-            'options' => $this->options,
-            'items' => $this->items, // Передаем начальные данные
-        ]);
-
-// Подключаем AJAX-обработчик, если передан URL и кнопка
-        if ($this->ajaxUrl && $this->triggerButton) {
-            $this->registerAjaxScript();
+        $this->registerAssets();
+        return $this->renderDualListBox();
+    }
+
+    protected function renderDualListBox()
+    {
+        $id = $this->getId();
+
+        $css = <<<CSS
+<style>
+    .dual-list-box {
+        display: flex;
+        align-items: center;
+        gap: 10px;
+    }
+    .list-container {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        position: relative;
+    }
+    .dual-list {
+        background: white;
+        width: 450px;
+        height: 220px;
+        border: 1px solid #ddd;
+        padding: 5px;
+        text-align: left;
+        overflow-y: auto;
+        border-radius: 5px;
+    }
+    .controls {
+        display: flex;
+        flex-direction: column;
+        gap: 5px;
+    }
+    .section-label {
+    font-size: 19px;
+    }
+    .selected-container {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        gap: 5px;
+    }
+    .dual-list option {
+        padding-top: 1px;
+        padding-bottom: 1px;
+    }
+    .selected-item,
+    .dual-list option {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        width: 100%;
+    }
+    .quantity-input {
+        width: 50px;
+        text-align: center;
+        /*-moz-appearance: textfield; !* Для Chrome/Firefox скрывает стрелки *!*/
+        -webkit-appearance: none;
+        appearance: none;
+        border-radius: 4px;
+    }
+    .quantity-input::-webkit-outer-spin-button, .quantity-input::-webkit-inner-spin-button {
+        -webkit-appearance: none;
+        margin: 0;
+    }
+    .selected-item {
+        color: black;
+        font-size: 14px;
+    }
+    .selected-item.selected {
+        padding: 0px 3px;
+        background-color: highlight;
+    }
+    .dual-list option {
+        padding: 3px 0px;
+        margin-bottom: 4px;
+        font-size: 14px;
+    }
+    .dual-list.option.selected {
+        padding: 0px 2px;
+    }
+    /* Фильтры */
+    .filter-input {
+        width: 420px;
+        margin-bottom: 10px;
+        padding: 5px;
+        border: 1px solid #ccc;
+        border-radius: 4px;
+    }
+    .count-label {
+        position: absolute;
+        bottom: 5px;
+        right: 5px;
+        font-size: 12px;
+        color: #555;
+    }
+    .section-label {
+        font-weight: bold;
+        margin-bottom: 5px;
+    }
+</style>
+CSS;
+
+        return $css . Html::tag('div',
+                "<div class='dual-list-box'>
+    <div class='list-container'>
+        <div class='section-label'>{$this->availableLabel}</div>
+        <input type='text' id='{$id}-filter-available' class='filter-input' placeholder='Фильтр доступных элементов'>
+        <div id='{$id}-available-count' class='count-label'>Доступных: " . count($this->availableItems) . "</div>
+        <select id='{$id}-available' multiple class='dual-list'>
+            " . $this->renderOptions($this->availableItems) . "
+        </select>
+    </div>
+    <div class='controls'>
+        <button id='{$id}-add' class='btn btn-primary' type='button'>&gt;</button>
+        <button id='{$id}-remove' class='btn btn-primary' type='button'>&lt;</button>
+    </div>
+    <div class='list-container'>
+        <div class='section-label'>{$this->selectedLabel}</div>
+        <input type='text' id='{$id}-filter-selected' class='filter-input' placeholder='Фильтр выбранных элементов'>
+        <div id='{$id}-selected-count' class='count-label'>Выбрано: " . count($this->selectedItems) . "</div>
+        <div id='{$id}-selected-container' class='dual-list'>
+            " . $this->renderSelectedItems() . "
+        </div>
+    </div>
+</div>",
+                ['id' => $id]
+            );
+    }
+
+    protected function renderOptions($items)
+    {
+        $options = "";
+        foreach ($items as $id => $name) {  // Используем ключ (id) и значение (name)
+            $options .= Html::tag('option', Html::encode($name), ['value' => $id]); // 'value' будет равен ID товара
         }
+        return $options;
     }
 
-    protected function registerAjaxScript()
+    protected function renderSelectedItems()
     {
-        $id = $this->options['id']; // ID DualListbox
-        $ajaxUrl = Url::to($this->ajaxUrl);
-        $buttonId = $this->triggerButton; // ID кнопки
-
-        $script = <<<JS
-    $(document).on('click', '#$buttonId', function() {
-$.ajax({
-url: '$ajaxUrl',
-type: 'GET',
-dataType: 'json',
-success: function(response) {
-let dualListbox = $('#$id');
-
-// Сохраняем уже выбранные элементы
-let selectedOptions = [];
-dualListbox.find('option:selected').each(function() {
-selectedOptions.push($(this).val());
-});
+        $html = "";
+        foreach ($this->selectedItems as $item) {
+            $id = $item['id'];
+            $text = $item['text'];
+            $count = $item['count'] ?? 1; // Значение по умолчанию 1
 
-// Очищаем список, но оставляем выбранные элементы
-dualListbox.find('option').each(function() {
-if (!selectedOptions.includes($(this).val())) {
-$(this).remove();
+            $html .= "<div class='selected-item' data-id='{$id}'>
+            <span>{$text}</span>
+            <input type='hidden' name='{$this->name}[]' value='{$id}'>"; // Скрытый input для передачи ID
+
+            if ($this->showQuantity) {
+                $html .= "<input type='number' class='quantity-input' name='{$this->name}_quantity[{$id}]' min='0.1' step='any' value='{$count}'>";
+            }
+
+            $html .= "</div>";
+        }
+        return $html;
+    }
+
+    protected function registerAssets()
+    {
+        $id = $this->getId();
+        $ajaxUrl = is_string($this->ajaxUrl) ? Url::to($this->ajaxUrl) : '';
+
+        $js = <<<JS
+function loadAvailableItems() {
+    $.getJSON('{$ajaxUrl}', function(data) {
+        if (data && typeof data === 'object') {
+            let select = $('#{$id}-available');
+            select.empty();  // Очищаем только доступные элементы
+            // Преобразуем объект в массив
+            Object.keys(data).forEach(function(id) {
+                let text = data[id];
+                select.append(new Option(text, id));  // Добавляем новые элементы
+            });
+            updateCounts();  // Обновляем количество доступных и выбранных элементов
+        } else {
+            console.error('Полученные данные не являются объектом или не существуют');
+        }
+    }).fail(function(jqXHR, textStatus, errorThrown) {
+        console.error('Ошибка при загрузке данных:', textStatus, errorThrown);
+    });
 }
-});
 
-// Добавляем новые элементы
-$.each(response, function(value, label) {
-if (!dualListbox.find('option[value="'+ value +'"]').length) {
-dualListbox.append(new Option(label, value, false, false));
+
+function filterItems(filterInputId, listSelector, isOption = true) {
+const filterValue = $(filterInputId).val().toLowerCase();
+if (isOption) {
+$(listSelector).children('option').each(function() {
+const text = $(this).text().toLowerCase();
+if (text.indexOf(filterValue) > -1) {
+$(this).show();
+} else {
+$(this).hide();
 }
 });
-
-// Восстанавливаем выбранные элементы
-dualListbox.find('option').each(function() {
-if (selectedOptions.includes($(this).val())) {
-$(this).prop('selected', true);
+} else {
+$(listSelector).children('.selected-item').each(function() {
+const text = $(this).find('span').text().toLowerCase();
+if (text.indexOf(filterValue) > -1) {
+$(this).show();
+} else {
+$(this).hide();
 }
 });
+}
+updateCounts();
+}
 
-// Перезагрузка DualListbox
-dualListbox.bootstrapDualListbox('refresh');
-},
-error: function(xhr) {
-console.error('Ошибка AJAX-запроса:', xhr);
+function updateCounts() {
+const availableCount = $('#{$id}-available option').length;
+const selectedCount = $('#{$id}-selected-container .selected-item').length;
+$('#{$id}-available-count').text('Доступных: ' + availableCount);
+$('#{$id}-selected-count').text('Выбрано: ' + selectedCount);
 }
+
+$(document).ready(function() {
+// Обработчик клика по кнопке для загрузки новых данных
+$('#{$this->triggerButton}').click(function() {
+loadAvailableItems();
+});
+
+$('#{$id}-add').click(function() {
+    $('#{$id}-available option:selected').each(function() {
+        let text = $(this).text();
+        let id = $(this).val();
+
+        let selectedContainer = $('#{$id}-selected-container');
+
+        let newItem = '<div class="selected-item" data-id="' + id + '">' +
+            '<span>' + text + '</span>' +
+            '<input type="hidden" name="' + '{$this->name}[]' + '" value="' + id + '">';
+
+        if ({$this->showQuantity} == true) {
+            newItem += '<input type="number" class="quantity-input" name="' + '{$this->name}_quantity[' + id + ']' + '" min="0.1" step="any" value="1">';
+        }
+
+        newItem += '</div>';
+
+        selectedContainer.append(newItem);
+        $(this).remove();
+        updateCounts();
+    });
+});
+
+$('#{$id}-remove').click(function() {
+$('#{$id}-selected-container .selected-item.selected').each(function() {
+let id = $(this).data('id');
+let text = $(this).find('span').text();
+
+// Добавляем элемент обратно в доступныеtype="button">
+$('#{$id}-available').append(new Option(text, id));
+$(this).remove();
+updateCounts();
+});
+});
+
+$('#{$id}-selected-container').on('click', '.selected-item', function() {
+$(this).toggleClass('selected');
+});
+
+// Фильтрация доступных элементов
+$('#{$id}-filter-available').on('input', function() {
+filterItems('#{$id}-filter-available', '#{$id}-available', true);
+});
+
+// Фильтрация выбранных элементов
+$('#{$id}-filter-selected').on('input', function() {
+filterItems('#{$id}-filter-selected', '#{$id}-selected-container', false);
 });
 });
 JS;
-        $this->view->registerJs($script, View::POS_READY);
+
+        $this->view->registerJs($js, View::POS_READY);
     }
 }