From: marina Date: Tue, 4 Feb 2025 12:33:46 +0000 (+0300) Subject: ERP-302 Редактирование букета X-Git-Url: https://gitweb.erp-flowers.ru/?a=commitdiff_plain;h=6d3d1caaffce5d9a297ec9a8ee76482ce4e41708;p=erp24_rep%2Fyii-erp24%2F.git ERP-302 Редактирование букета --- diff --git a/erp24/controllers/BouquetController.php b/erp24/controllers/BouquetController.php index c2a7d73b..95999c8d 100644 --- a/erp24/controllers/BouquetController.php +++ b/erp24/controllers/BouquetController.php @@ -1,9 +1,12 @@ 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 '
';
+//            var_dump(Yii::$app->request->post());
+//            echo '
'; +// 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', 'роза']) diff --git a/erp24/records/BouquetComposition.php b/erp24/records/BouquetComposition.php index 7e14a46e..ec122b94 100644 --- a/erp24/records/BouquetComposition.php +++ b/erp24/records/BouquetComposition.php @@ -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 [ diff --git a/erp24/records/BouquetCompositionProducts.php b/erp24/records/BouquetCompositionProducts.php index c71005ed..98d4b778 100644 --- a/erp24/records/BouquetCompositionProducts.php +++ b/erp24/records/BouquetCompositionProducts.php @@ -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']); // Исправил связь + } + } diff --git a/erp24/views/bouquet/update.php b/erp24/views/bouquet/update.php index 1c9eef7c..5de8c340 100644 --- a/erp24/views/bouquet/update.php +++ b/erp24/views/bouquet/update.php @@ -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 ?>
- 'h4']) ?> -

title) ?>

- + 'h4']) ?> +

+

title) ?>

-
+
+
+ 'fw-bold fs-5 text-center']) ?> +
'form-select', 'prompt' => 'Выберите категорию']) ?>
@@ -38,39 +42,36 @@ $this->registerJsFile('/js/bouquet/bouquet.js', ['position' => \yii\web\View::PO
'btn btn-primary w-100 mb-3', 'id' => 'apply-button']) ?> +
-
+
+
+ 'dual-list-form']); ?> + 'products', + 'availableLabel' => 'Выбор', + 'selectedLabel' => 'Состав букета', + 'availableItems' => $availableItems, + 'selectedItems' => $selectedItems, + 'ajaxUrl' => '/bouquet/get-list', + 'showQuantity' => true, + 'triggerButton' => 'apply-button', + ]) ?> +
+ +
+
+
+

Себестоимость: 0 ₽

Наценка: 0 %

Цена: 0 ₽

- -
- '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' => '{0} из {1}', - 'infoTextEmpty' => 'Список пуст', - ], - ]); ?> +
+
+ 'btn btn-success w-100']) ?>
+
diff --git a/erp24/web/js/bouquet/bouquet.js b/erp24/web/js/bouquet/bouquet.js index 5f6a623e..390004eb 100644 --- a/erp24/web/js/bouquet/bouquet.js +++ b/erp24/web/js/bouquet/bouquet.js @@ -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 }); diff --git a/erp24/widgets/DualList.php b/erp24/widgets/DualList.php index 2021cee1..63bca55c 100644 --- a/erp24/widgets/DualList.php +++ b/erp24/widgets/DualList.php @@ -2,81 +2,304 @@ 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 = << + .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; + } + +CSS; + + return $css . Html::tag('div', + "
+
+ + +
Доступных: " . count($this->availableItems) . "
+ +
+
+ + +
+
+ + +
Выбрано: " . count($this->selectedItems) . "
+
+ " . $this->renderSelectedItems() . " +
+
+
", + ['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 = <<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 .= "
+ {$text} + "; // Скрытый input для передачи ID + + if ($this->showQuantity) { + $html .= ""; + } + + $html .= "
"; + } + return $html; + } + + protected function registerAssets() + { + $id = $this->getId(); + $ajaxUrl = is_string($this->ajaxUrl) ? Url::to($this->ajaxUrl) : ''; + + $js = << -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 = '
' + + '' + text + '' + + ''; + + if ({$this->showQuantity} == true) { + newItem += ''; + } + + newItem += '
'; + + 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); } }