<?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;
*/
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', 'роза'])
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 */
?>
<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>
</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>
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'>></button>
+ <button id='{$id}-remove' class='btn btn-primary' type='button'><</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);
}
}