-
+
+
+
+
@@ -364,16 +752,17 @@ document.getElementById('addParameterBtn')?.addEventListener('click', function()
// Инициализируем новую карточку
const newCard = container.querySelector(`[data-formset-index="${formIdx}"]`);
- initAddValueBtn(newCard);
+ initCardHandlers(newCard);
- // Инициализируем удаление параметра
- initParamDeleteToggle(newCard);
+ // Фокус на select
+ newCard.querySelector('.param-name-select')?.focus();
});
// Функция для скрытия удаленного параметра
function initParamDeleteToggle(card) {
const deleteCheckbox = card.querySelector('input[type="checkbox"][name$="-DELETE"]');
- if (deleteCheckbox) {
+ if (deleteCheckbox && !deleteCheckbox.dataset.initialized) {
+ deleteCheckbox.dataset.initialized = 'true';
deleteCheckbox.addEventListener('change', function() {
if (this.checked) {
card.style.opacity = '0.5';
@@ -388,12 +777,7 @@ function initParamDeleteToggle(card) {
// Функция для сериализации значений параметров и их комплектов перед отправкой формы
function serializeAttributeValues() {
- /**
- * Перед отправкой формы нужно сериализовать все значения параметров
- * и их связанные комплекты из инлайн input'ов в скрытые JSON поля
- */
document.querySelectorAll('.attribute-card').forEach((card, idx) => {
- // Получаем все инпуты с значениями и их комплектами внутри этой карточки
const valueGroups = card.querySelectorAll('.value-field-group');
const values = [];
const kits = [];
@@ -406,7 +790,7 @@ function serializeAttributeValues() {
const value = valueInput.value.trim();
const kitId = kitSelect ? kitSelect.value : '';
- if (value && kitId) { // Требуем чтобы оба поля были заполнены
+ if (value && kitId) {
values.push(value);
kits.push(parseInt(kitId));
}
@@ -414,7 +798,6 @@ function serializeAttributeValues() {
});
// Создаем или обновляем скрытые поля JSON
- // поле values: ["50", "60", "70"]
const valuesFieldName = `attributes-${idx}-values`;
let valuesField = document.querySelector(`input[name="${valuesFieldName}"]`);
if (!valuesField) {
@@ -425,7 +808,6 @@ function serializeAttributeValues() {
}
valuesField.value = JSON.stringify(values);
- // поле kits: [1, 2, 3] (id ProductKit)
const kitsFieldName = `attributes-${idx}-kits`;
let kitsField = document.querySelector(`input[name="${kitsFieldName}"]`);
if (!kitsField) {
@@ -438,18 +820,52 @@ function serializeAttributeValues() {
});
}
+// Toast уведомления
+function showToast(message, type = 'info') {
+ // Создаем контейнер если его нет
+ let container = document.getElementById('toastContainer');
+ if (!container) {
+ container = document.createElement('div');
+ container.id = 'toastContainer';
+ container.className = 'toast-container position-fixed bottom-0 end-0 p-3';
+ container.style.zIndex = '1100';
+ document.body.appendChild(container);
+ }
+
+ const bgClass = {
+ 'success': 'bg-success',
+ 'danger': 'bg-danger',
+ 'warning': 'bg-warning',
+ 'info': 'bg-info'
+ }[type] || 'bg-info';
+
+ const textClass = type === 'warning' ? 'text-dark' : 'text-white';
+
+ const toastHtml = `
+
+
+
${escapeHtml(message)}
+
+
+
+ `;
+
+ container.insertAdjacentHTML('beforeend', toastHtml);
+ const toastEl = container.lastElementChild;
+ const toast = new bootstrap.Toast(toastEl, { delay: 4000 });
+ toast.show();
+
+ toastEl.addEventListener('hidden.bs.toast', () => toastEl.remove());
+}
+
// Инициализация при загрузке страницы
document.addEventListener('DOMContentLoaded', function() {
initializeParameterCards();
- document.querySelectorAll('.attribute-card').forEach(card => {
- initParamDeleteToggle(card);
- });
// Добавляем сериализацию значений перед отправкой формы
const form = document.querySelector('form');
if (form) {
form.addEventListener('submit', function(e) {
- // Перед отправкой формы сериализуем все значения параметров
serializeAttributeValues();
});
}
diff --git a/myproject/products/urls.py b/myproject/products/urls.py
index 100dce0..423af84 100644
--- a/myproject/products/urls.py
+++ b/myproject/products/urls.py
@@ -94,6 +94,7 @@ urlpatterns = [
path('attributes/
/delete/', views.ProductAttributeDeleteView.as_view(), name='attribute-delete'),
# API для атрибутов
+ path('api/attributes/', views.get_attributes_list_api, name='api-attributes-list'),
path('api/attributes/create/', views.create_attribute_api, name='api-attribute-create'),
path('api/attributes//values/add/', views.add_attribute_value_api, name='attribute-add-value'),
path('api/attributes//values//delete/', views.delete_attribute_value_api, name='attribute-delete-value'),
diff --git a/myproject/products/views/__init__.py b/myproject/products/views/__init__.py
index ed6fa9f..e493fc0 100644
--- a/myproject/products/views/__init__.py
+++ b/myproject/products/views/__init__.py
@@ -102,6 +102,7 @@ from .attribute_views import (
create_attribute_api,
add_attribute_value_api,
delete_attribute_value_api,
+ get_attributes_list_api,
)
# API представления
@@ -193,6 +194,7 @@ __all__ = [
'create_attribute_api',
'add_attribute_value_api',
'delete_attribute_value_api',
+ 'get_attributes_list_api',
# API
'search_products_and_variants',
diff --git a/myproject/products/views/attribute_views.py b/myproject/products/views/attribute_views.py
index 0b382c6..9991ca6 100644
--- a/myproject/products/views/attribute_views.py
+++ b/myproject/products/views/attribute_views.py
@@ -245,3 +245,30 @@ def delete_attribute_value_api(request, pk, value_id):
return JsonResponse({'success': False, 'error': 'Значение не найдено'})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)})
+
+
+@login_required
+def get_attributes_list_api(request):
+ """
+ API для получения списка всех атрибутов с их значениями.
+ Используется для autocomplete в форме создания вариативного товара.
+ """
+ attributes = ProductAttribute.objects.prefetch_related('values').order_by('position', 'name')
+
+ data = []
+ for attr in attributes:
+ data.append({
+ 'id': attr.pk,
+ 'name': attr.name,
+ 'slug': attr.slug,
+ 'values': [
+ {
+ 'id': val.pk,
+ 'value': val.value,
+ 'slug': val.slug
+ }
+ for val in attr.values.all().order_by('position', 'value')
+ ]
+ })
+
+ return JsonResponse({'success': True, 'attributes': data})
diff --git a/myproject/products/views/configurableproduct_views.py b/myproject/products/views/configurableproduct_views.py
index 81abb03..0bedeb6 100644
--- a/myproject/products/views/configurableproduct_views.py
+++ b/myproject/products/views/configurableproduct_views.py
@@ -13,7 +13,7 @@ from django.contrib.auth.decorators import login_required
from django.db import transaction
from user_roles.mixins import ManagerOwnerRequiredMixin
-from ..models import ConfigurableProduct, ConfigurableProductOption, ProductKit, ConfigurableProductAttribute
+from ..models import ConfigurableProduct, ConfigurableProductOption, ProductKit, ConfigurableProductAttribute, ProductAttribute
from ..forms import (
ConfigurableProductForm,
ConfigurableProductOptionFormSetCreate,
@@ -144,6 +144,9 @@ class ConfigurableProductCreateView(LoginRequiredMixin, ManagerOwnerRequiredMixi
is_temporary=False
).order_by('name')
+ # Справочник атрибутов для autocomplete
+ context['product_attributes'] = ProductAttribute.objects.prefetch_related('values').order_by('position', 'name')
+
return context
def form_valid(self, form):
@@ -420,6 +423,9 @@ class ConfigurableProductUpdateView(LoginRequiredMixin, ManagerOwnerRequiredMixi
is_temporary=False
).order_by('name')
+ # Справочник атрибутов для autocomplete
+ context['product_attributes'] = ProductAttribute.objects.prefetch_related('values').order_by('position', 'name')
+
return context
def form_valid(self, form):