Замена простого select на autocomplete с поиском для привязки атрибутов к товарам/комплектам
- Переиспользован модуль select2-product-search.js из orders - Заменен простой select на Select2 с AJAX поиском через API search_products_and_variants - Добавлена поддержка привязки как ProductKit, так и Product к значениям атрибутов - Обновлен метод _save_attributes_from_cards для обработки item_ids и item_types - Удалены дублирующиеся подключения jQuery и Select2 (используются из base.html) - Улучшен UX: живой поиск, отображение типа товара (🌹/💐), цены и наличия
This commit is contained in:
@@ -29,6 +29,30 @@ input[name*="DELETE"] {
|
||||
align-items: center;
|
||||
min-height: 38px;
|
||||
}
|
||||
|
||||
/* Стили для autocomplete товаров/комплектов */
|
||||
.product-kit-select-wrapper {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
/* Отображение типа товара в Select2 */
|
||||
.select2-results__option .item-type-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.1rem 0.4rem;
|
||||
margin-left: 0.3rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.item-type-kit {
|
||||
background-color: #0d6efd;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.item-type-product {
|
||||
background-color: #198754;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -108,12 +132,6 @@ input[name*="DELETE"] {
|
||||
|
||||
<!-- Данные для JavaScript -->
|
||||
<script>
|
||||
window.AVAILABLE_KITS = [
|
||||
{% for kit in available_kits %}
|
||||
{ id: {{ kit.id }}, name: "{{ kit.name|escapejs }}" }{% if not forloop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
|
||||
// Справочник атрибутов с их значениями
|
||||
window.PRODUCT_ATTRIBUTES = [
|
||||
{% for attr in product_attributes %}
|
||||
@@ -133,7 +151,8 @@ input[name*="DELETE"] {
|
||||
// URL для API
|
||||
window.API_URLS = {
|
||||
createAttribute: "{% url 'products:api-attribute-create' %}",
|
||||
addValue: function(attrId) { return `/products/api/attributes/${attrId}/values/add/`; }
|
||||
addValue: function(attrId) { return `/products/api/attributes/${attrId}/values/add/`; },
|
||||
searchProductsAndVariants: "{% url 'products:api-search-products-variants' %}"
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -266,16 +285,17 @@ input[name*="DELETE"] {
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Переиспользуемый модуль для Select2 поиска товаров -->
|
||||
<script src="{% static 'products/js/select2-product-search.js' %}"></script>
|
||||
|
||||
<script>
|
||||
// === Управление параметрами товара (карточный интерфейс) ===
|
||||
|
||||
// Функция для добавления нового поля значения параметра с выбором ProductKit
|
||||
function addValueField(container, valueText = '', kitId = '') {
|
||||
// Функция для добавления нового поля значения параметра с выбором Product/Kit через Select2
|
||||
function addValueField(container, valueText = '', itemId = '', itemType = '') {
|
||||
const index = container.querySelectorAll('.value-field-group').length;
|
||||
const fieldId = `value-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
// Получаем список доступных комплектов
|
||||
const kitOptionsHtml = getKitOptionsHtml(kitId);
|
||||
const selectId = `product-kit-select-${fieldId}`;
|
||||
|
||||
const html = `
|
||||
<div class="value-field-group d-flex gap-2 mb-2 align-items-start">
|
||||
@@ -283,13 +303,13 @@ function addValueField(container, valueText = '', kitId = '') {
|
||||
placeholder="Введите значение"
|
||||
value="${escapeHtml(valueText)}"
|
||||
data-field-id="${fieldId}"
|
||||
style="min-width: 100px;">
|
||||
<select class="form-select form-select-sm parameter-kit-select"
|
||||
style="flex: 0 0 150px;">
|
||||
<select class="form-select form-select-sm product-kit-select"
|
||||
id="${selectId}"
|
||||
data-field-id="${fieldId}"
|
||||
title="Выберите комплект для этого значения"
|
||||
style="min-width: 150px;">
|
||||
<option value="">-- Выберите комплект --</option>
|
||||
${kitOptionsHtml}
|
||||
data-ajax-url="${window.API_URLS.searchProductsAndVariants}"
|
||||
style="flex: 1; min-width: 200px;">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger remove-value-btn" title="Удалить значение">
|
||||
<i class="bi bi-trash"></i>
|
||||
@@ -299,12 +319,22 @@ function addValueField(container, valueText = '', kitId = '') {
|
||||
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
|
||||
// Установка выбранного комплекта если был передан
|
||||
if (kitId) {
|
||||
const lastSelect = container.querySelector('.value-field-group:last-child .parameter-kit-select');
|
||||
if (lastSelect) {
|
||||
lastSelect.value = kitId;
|
||||
// Инициализируем Select2 для нового поля
|
||||
const newSelect = document.getElementById(selectId);
|
||||
if (newSelect) {
|
||||
// Если есть предзагруженные данные, добавляем option
|
||||
if (itemId && itemType) {
|
||||
const optionValue = `${itemType}_${itemId}`;
|
||||
const option = document.createElement('option');
|
||||
option.value = optionValue;
|
||||
option.selected = true;
|
||||
option.setAttribute('data-type', itemType);
|
||||
// Текст будет установлен через Select2
|
||||
newSelect.appendChild(option);
|
||||
}
|
||||
|
||||
// Инициализируем Select2 с AJAX поиском
|
||||
window.initProductSelect2(newSelect, 'all', window.API_URLS.searchProductsAndVariants);
|
||||
}
|
||||
|
||||
// Обработчик удаления значения
|
||||
@@ -312,6 +342,11 @@ function addValueField(container, valueText = '', kitId = '') {
|
||||
if (lastRemoveBtn) {
|
||||
lastRemoveBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
// Уничтожаем Select2 перед удалением элемента
|
||||
const select = this.closest('.value-field-group').querySelector('.product-kit-select');
|
||||
if (select && $(select).data('select2')) {
|
||||
$(select).select2('destroy');
|
||||
}
|
||||
this.closest('.value-field-group').remove();
|
||||
});
|
||||
}
|
||||
@@ -324,15 +359,6 @@ function escapeHtml(text) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Получить HTML с опциями комплектов
|
||||
function getKitOptionsHtml(selectedKitId = '') {
|
||||
const kitsData = window.AVAILABLE_KITS || [];
|
||||
return kitsData.map(kit => {
|
||||
const selected = kit.id == selectedKitId ? 'selected' : '';
|
||||
return `<option value="${kit.id}" ${selected}>${escapeHtml(kit.name)}</option>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Найти атрибут в справочнике по имени
|
||||
function findAttributeByName(name) {
|
||||
const attributes = window.PRODUCT_ATTRIBUTES || [];
|
||||
@@ -775,24 +801,32 @@ function initParamDeleteToggle(card) {
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для сериализации значений параметров и их комплектов перед отправкой формы
|
||||
// Функция для сериализации значений параметров и их товаров/комплектов перед отправкой формы
|
||||
function serializeAttributeValues() {
|
||||
document.querySelectorAll('.attribute-card').forEach((card, idx) => {
|
||||
const valueGroups = card.querySelectorAll('.value-field-group');
|
||||
const values = [];
|
||||
const kits = [];
|
||||
const itemIds = [];
|
||||
const itemTypes = [];
|
||||
|
||||
valueGroups.forEach(group => {
|
||||
const valueInput = group.querySelector('.parameter-value-input');
|
||||
const kitSelect = group.querySelector('.parameter-kit-select');
|
||||
const itemSelect = group.querySelector('.product-kit-select');
|
||||
|
||||
if (valueInput) {
|
||||
if (valueInput && itemSelect) {
|
||||
const value = valueInput.value.trim();
|
||||
const kitId = kitSelect ? kitSelect.value : '';
|
||||
const selectedValue = itemSelect.value; // Формат: "product_123" или "kit_456"
|
||||
|
||||
if (value && kitId) {
|
||||
values.push(value);
|
||||
kits.push(parseInt(kitId));
|
||||
if (value && selectedValue) {
|
||||
const parts = selectedValue.split('_');
|
||||
if (parts.length === 2) {
|
||||
const type = parts[0]; // 'product' или 'kit'
|
||||
const id = parts[1];
|
||||
|
||||
values.push(value);
|
||||
itemIds.push(parseInt(id));
|
||||
itemTypes.push(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -808,15 +842,25 @@ function serializeAttributeValues() {
|
||||
}
|
||||
valuesField.value = JSON.stringify(values);
|
||||
|
||||
const kitsFieldName = `attributes-${idx}-kits`;
|
||||
let kitsField = document.querySelector(`input[name="${kitsFieldName}"]`);
|
||||
if (!kitsField) {
|
||||
kitsField = document.createElement('input');
|
||||
kitsField.type = 'hidden';
|
||||
kitsField.name = kitsFieldName;
|
||||
card.appendChild(kitsField);
|
||||
const itemIdsFieldName = `attributes-${idx}-item_ids`;
|
||||
let itemIdsField = document.querySelector(`input[name="${itemIdsFieldName}"]`);
|
||||
if (!itemIdsField) {
|
||||
itemIdsField = document.createElement('input');
|
||||
itemIdsField.type = 'hidden';
|
||||
itemIdsField.name = itemIdsFieldName;
|
||||
card.appendChild(itemIdsField);
|
||||
}
|
||||
kitsField.value = JSON.stringify(kits);
|
||||
itemIdsField.value = JSON.stringify(itemIds);
|
||||
|
||||
const itemTypesFieldName = `attributes-${idx}-item_types`;
|
||||
let itemTypesField = document.querySelector(`input[name="${itemTypesFieldName}"]`);
|
||||
if (!itemTypesField) {
|
||||
itemTypesField = document.createElement('input');
|
||||
itemTypesField.type = 'hidden';
|
||||
itemTypesField.name = itemTypesFieldName;
|
||||
card.appendChild(itemTypesField);
|
||||
}
|
||||
itemTypesField.value = JSON.stringify(itemTypes);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -96,11 +96,6 @@ class ConfigurableProductDetailView(LoginRequiredMixin, ManagerOwnerRequiredMixi
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# Добавляем доступные комплекты для выбора (активные, не временные)
|
||||
context['available_kits'] = ProductKit.objects.filter(
|
||||
status='active',
|
||||
is_temporary=False
|
||||
).order_by('name')
|
||||
return context
|
||||
|
||||
|
||||
@@ -138,12 +133,6 @@ class ConfigurableProductCreateView(LoginRequiredMixin, ManagerOwnerRequiredMixi
|
||||
prefix='attributes'
|
||||
)
|
||||
|
||||
# Доступные комплекты для JavaScript (для выбора при добавлении значений атрибутов)
|
||||
context['available_kits'] = ProductKit.objects.filter(
|
||||
status='active',
|
||||
is_temporary=False
|
||||
).order_by('name')
|
||||
|
||||
# Справочник атрибутов для autocomplete
|
||||
context['product_attributes'] = ProductAttribute.objects.prefetch_related('values').order_by('position', 'name')
|
||||
|
||||
@@ -247,10 +236,12 @@ class ConfigurableProductCreateView(LoginRequiredMixin, ManagerOwnerRequiredMixi
|
||||
- attributes-X-visible: видимость
|
||||
- attributes-X-DELETE: помечен ли для удаления
|
||||
- attributes-X-values: JSON массив значений параметра
|
||||
- attributes-X-kits: JSON массив ID комплектов для каждого значения
|
||||
- attributes-X-item_ids: JSON массив ID товаров/комплектов
|
||||
- attributes-X-item_types: JSON массив типов ('product' или 'kit')
|
||||
"""
|
||||
import json
|
||||
from products.models.kits import ProductKit
|
||||
from products.models.products import Product
|
||||
|
||||
# Сначала удаляем все старые атрибуты
|
||||
ConfigurableProductAttribute.objects.filter(parent=self.object).delete()
|
||||
@@ -282,9 +273,10 @@ class ConfigurableProductCreateView(LoginRequiredMixin, ManagerOwnerRequiredMixi
|
||||
|
||||
visible = self.request.POST.get(f'attributes-{idx}-visible') == 'on'
|
||||
|
||||
# Получаем значения и их привязанные комплекты
|
||||
# Получаем значения, ID и типы (kit/product)
|
||||
values_json = self.request.POST.get(f'attributes-{idx}-values', '[]')
|
||||
kits_json = self.request.POST.get(f'attributes-{idx}-kits', '[]')
|
||||
item_ids_json = self.request.POST.get(f'attributes-{idx}-item_ids', '[]')
|
||||
item_types_json = self.request.POST.get(f'attributes-{idx}-item_types', '[]')
|
||||
|
||||
try:
|
||||
values = json.loads(values_json)
|
||||
@@ -292,15 +284,21 @@ class ConfigurableProductCreateView(LoginRequiredMixin, ManagerOwnerRequiredMixi
|
||||
values = []
|
||||
|
||||
try:
|
||||
kit_ids = json.loads(kits_json)
|
||||
item_ids = json.loads(item_ids_json)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
kit_ids = []
|
||||
item_ids = []
|
||||
|
||||
try:
|
||||
item_types = json.loads(item_types_json)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
item_types = []
|
||||
|
||||
# Создаём ConfigurableProductAttribute для каждого значения
|
||||
for value_idx, value in enumerate(values):
|
||||
if value and value.strip():
|
||||
# Получаем соответствующий ID комплекта
|
||||
kit_id = kit_ids[value_idx] if value_idx < len(kit_ids) else None
|
||||
# Получаем соответствующие ID и тип
|
||||
item_id = item_ids[value_idx] if value_idx < len(item_ids) else None
|
||||
item_type = item_types[value_idx] if value_idx < len(item_types) else None
|
||||
|
||||
# Приготавливаем параметры создания
|
||||
create_kwargs = {
|
||||
@@ -311,13 +309,17 @@ class ConfigurableProductCreateView(LoginRequiredMixin, ManagerOwnerRequiredMixi
|
||||
'visible': visible
|
||||
}
|
||||
|
||||
# Добавляем комплект если указан
|
||||
if kit_id:
|
||||
# Добавляем комплект или товар если указан
|
||||
if item_id and item_type:
|
||||
try:
|
||||
kit = ProductKit.objects.get(id=kit_id)
|
||||
create_kwargs['kit'] = kit
|
||||
except ProductKit.DoesNotExist:
|
||||
# Комплект не найден - создаём без привязки
|
||||
if item_type == 'kit':
|
||||
kit = ProductKit.objects.get(id=item_id)
|
||||
create_kwargs['kit'] = kit
|
||||
elif item_type == 'product':
|
||||
product = Product.objects.get(id=item_id)
|
||||
create_kwargs['product'] = product
|
||||
except (ProductKit.DoesNotExist, Product.DoesNotExist):
|
||||
# Комплект/товар не найден - создаём без привязки
|
||||
pass
|
||||
|
||||
ConfigurableProductAttribute.objects.create(**create_kwargs)
|
||||
@@ -527,10 +529,12 @@ class ConfigurableProductUpdateView(LoginRequiredMixin, ManagerOwnerRequiredMixi
|
||||
- attributes-X-visible: видимость
|
||||
- attributes-X-DELETE: помечен ли для удаления
|
||||
- attributes-X-values: JSON массив значений параметра
|
||||
- attributes-X-kits: JSON массив ID комплектов для каждого значения
|
||||
- attributes-X-item_ids: JSON массив ID товаров/комплектов
|
||||
- attributes-X-item_types: JSON массив типов ('product' или 'kit')
|
||||
"""
|
||||
import json
|
||||
from products.models.kits import ProductKit
|
||||
from products.models.products import Product
|
||||
|
||||
# Сначала удаляем все старые атрибуты
|
||||
ConfigurableProductAttribute.objects.filter(parent=self.object).delete()
|
||||
@@ -562,9 +566,10 @@ class ConfigurableProductUpdateView(LoginRequiredMixin, ManagerOwnerRequiredMixi
|
||||
|
||||
visible = self.request.POST.get(f'attributes-{idx}-visible') == 'on'
|
||||
|
||||
# Получаем значения и их привязанные комплекты
|
||||
# Получаем значения, ID и типы (kit/product)
|
||||
values_json = self.request.POST.get(f'attributes-{idx}-values', '[]')
|
||||
kits_json = self.request.POST.get(f'attributes-{idx}-kits', '[]')
|
||||
item_ids_json = self.request.POST.get(f'attributes-{idx}-item_ids', '[]')
|
||||
item_types_json = self.request.POST.get(f'attributes-{idx}-item_types', '[]')
|
||||
|
||||
try:
|
||||
values = json.loads(values_json)
|
||||
@@ -572,15 +577,21 @@ class ConfigurableProductUpdateView(LoginRequiredMixin, ManagerOwnerRequiredMixi
|
||||
values = []
|
||||
|
||||
try:
|
||||
kit_ids = json.loads(kits_json)
|
||||
item_ids = json.loads(item_ids_json)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
kit_ids = []
|
||||
item_ids = []
|
||||
|
||||
try:
|
||||
item_types = json.loads(item_types_json)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
item_types = []
|
||||
|
||||
# Создаём ConfigurableProductAttribute для каждого значения
|
||||
for value_idx, value in enumerate(values):
|
||||
if value and value.strip():
|
||||
# Получаем соответствующий ID комплекта
|
||||
kit_id = kit_ids[value_idx] if value_idx < len(kit_ids) else None
|
||||
# Получаем соответствующие ID и тип
|
||||
item_id = item_ids[value_idx] if value_idx < len(item_ids) else None
|
||||
item_type = item_types[value_idx] if value_idx < len(item_types) else None
|
||||
|
||||
# Приготавливаем параметры создания
|
||||
create_kwargs = {
|
||||
@@ -591,13 +602,17 @@ class ConfigurableProductUpdateView(LoginRequiredMixin, ManagerOwnerRequiredMixi
|
||||
'visible': visible
|
||||
}
|
||||
|
||||
# Добавляем комплект если указан
|
||||
if kit_id:
|
||||
# Добавляем комплект или товар если указан
|
||||
if item_id and item_type:
|
||||
try:
|
||||
kit = ProductKit.objects.get(id=kit_id)
|
||||
create_kwargs['kit'] = kit
|
||||
except ProductKit.DoesNotExist:
|
||||
# Комплект не найден - создаём без привязки
|
||||
if item_type == 'kit':
|
||||
kit = ProductKit.objects.get(id=item_id)
|
||||
create_kwargs['kit'] = kit
|
||||
elif item_type == 'product':
|
||||
product = Product.objects.get(id=item_id)
|
||||
create_kwargs['product'] = product
|
||||
except (ProductKit.DoesNotExist, Product.DoesNotExist):
|
||||
# Комплект/товар не найден - создаём без привязки
|
||||
pass
|
||||
|
||||
ConfigurableProductAttribute.objects.create(**create_kwargs)
|
||||
|
||||
Reference in New Issue
Block a user