Исправлен Select2 поиск товаров при создании группы вариантов

Проблема: при создании новой группы вариантов (VariantGroup) поиск
товаров через Select2 не работал. При редактировании существующих
групп всё работало корректно.

Причина: отсутствовали проверки инициализации Select2, обработка
ошибок AJAX запросов и валидация параметров.

Изменения:

1. select2-product-init.html - улучшена функция initProductSelect2:
   - Добавлена валидация входных параметров (element, apiUrl)
   - Добавлена проверка загрузки jQuery и Select2
   - Улучшена проверка повторной инициализации
   - Добавлен try-catch для обработки ошибок
   - Функция возвращает boolean (успех/неудача)
   - Добавлено логирование для отладки

2. variantgroup_form.html - улучшены все функции работы с формой:

   initSelect2ForRow:
   - Добавлена проверка существования row и select элемента
   - Удаление старых обработчиков перед инициализацией
   - Проверка результата инициализации Select2

   updateRowData:
   - Добавлен timeout (5 сек) для fetch запросов
   - Добавлена проверка статуса HTTP ответа
   - Улучшена обработка ошибок с fallback данными
   - Добавлено логирование ошибок

   DOMContentLoaded инициализация:
   - Добавлена валидация контейнера, totalFormsInput и apiUrl
   - Задержка перед инициализацией существующих строк (100ms)
   - Проверка успешности инициализации перед updateRowData

   Добавление нового товара:
   - Задержка (50ms) перед инициализацией Select2
   - Повторная попытка при неудаче (через 500ms)
   - Улучшена надежность работы с динамическими элементами

Результат: Select2 поиск работает корректно как при создании новых
групп, так и при редактировании существующих. Добавлена надежная
обработка ошибок и логирование для отладки.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-03 17:11:47 +03:00
parent e6fd44ef6b
commit 6c3b970395
2 changed files with 149 additions and 48 deletions

View File

@@ -40,10 +40,32 @@
* @param {Element} element - DOM элемент select * @param {Element} element - DOM элемент select
* @param {string} type - Тип поиска ('product' или 'variant') * @param {string} type - Тип поиска ('product' или 'variant')
* @param {string} apiUrl - URL API для поиска * @param {string} apiUrl - URL API для поиска
* @returns {boolean} - true если инициализация прошла успешно, false иначе
*/ */
window.initProductSelect2 = function(element, type, apiUrl) { window.initProductSelect2 = function(element, type, apiUrl) {
if (!element || $(element).data('select2')) { // Валидация входных параметров
return; // Уже инициализирован if (!element) {
console.error('initProductSelect2: element is null or undefined');
return false;
}
if (!apiUrl || typeof apiUrl !== 'string') {
console.error('initProductSelect2: invalid apiUrl', apiUrl);
return false;
}
// Проверка загрузки jQuery и Select2
if (typeof $ === 'undefined' || typeof $.fn.select2 === 'undefined') {
console.error('initProductSelect2: jQuery or Select2 not loaded');
return false;
}
var $element = $(element);
// Улучшенная проверка инициализации
if ($element.hasClass('select2-hidden-accessible')) {
console.log('initProductSelect2: already initialized, skipping', element.name);
return true; // Уже инициализирован - это не ошибка
} }
var placeholders = { var placeholders = {
@@ -51,7 +73,8 @@
'variant': 'Начните вводить название группы...' 'variant': 'Начните вводить название группы...'
}; };
$(element).select2({ try {
$element.select2({
theme: 'bootstrap-5', theme: 'bootstrap-5',
placeholder: placeholders[type] || 'Выберите...', placeholder: placeholders[type] || 'Выберите...',
allowClear: true, allowClear: true,
@@ -83,6 +106,12 @@
templateResult: formatSelectResult, templateResult: formatSelectResult,
templateSelection: formatSelectSelection templateSelection: formatSelectSelection
}); });
console.log('initProductSelect2: successfully initialized for', element.name);
return true;
} catch (error) {
console.error('initProductSelect2: initialization error', error);
return false;
}
}; };
/** /**

View File

@@ -215,12 +215,43 @@ document.addEventListener('DOMContentLoaded', function() {
const totalFormsInput = document.querySelector('[name$="TOTAL_FORMS"]'); const totalFormsInput = document.querySelector('[name$="TOTAL_FORMS"]');
const apiUrl = '{% url "products:api-search-products-variants" %}'; const apiUrl = '{% url "products:api-search-products-variants" %}';
// Валидация
if (!container) {
console.error('items-tbody container not found');
return;
}
if (!totalFormsInput) {
console.error('TOTAL_FORMS input not found');
return;
}
if (!apiUrl || apiUrl.includes('undefined')) {
console.error('Invalid API URL:', apiUrl);
return;
}
console.log('Initializing variant group form with API URL:', apiUrl);
// Инициализируем Select2 для всех селектов товаров // Инициализируем Select2 для всех селектов товаров
function initSelect2ForRow(row) { function initSelect2ForRow(row) {
const productSelect = row.querySelector('[name$="-product"]'); if (!row) {
if (productSelect) { console.error('initSelect2ForRow: row is null');
window.initProductSelect2(productSelect, 'product', apiUrl); return false;
}
const productSelect = row.querySelector('[name$="-product"]');
if (!productSelect) {
console.warn('initSelect2ForRow: product select not found in row');
return false;
}
// Убираем старые обработчики перед повторной инициализацией
$(productSelect).off('select2:select');
const success = window.initProductSelect2(productSelect, 'product', apiUrl);
if (success) {
// Обработчик события при выборе товара // Обработчик события при выборе товара
$(productSelect).on('select2:select', function(e) { $(productSelect).on('select2:select', function(e) {
// Извлекаем числовой ID из формата "product_123" // Извлекаем числовой ID из формата "product_123"
@@ -239,13 +270,20 @@ document.addEventListener('DOMContentLoaded', function() {
} }
updateRowData(row); updateRowData(row);
}); });
return true;
} else {
console.error('initSelect2ForRow: failed to initialize Select2');
return false;
} }
} }
// Функция для обновления данных строки при выборе товара // Функция для обновления данных строки при выборе товара
function updateRowData(row) { function updateRowData(row) {
if (!row) return;
const productSelect = row.querySelector('[name$="-product"]'); const productSelect = row.querySelector('[name$="-product"]');
if (!productSelect || !productSelect.value) { if (!productSelect || !productSelect.value) {
// Сброс данных
row.querySelector('[data-product-sku]').textContent = '-'; row.querySelector('[data-product-sku]').textContent = '-';
row.querySelector('[data-product-price]').textContent = '-'; row.querySelector('[data-product-price]').textContent = '-';
row.querySelector('[data-product-stock]').innerHTML = '-'; row.querySelector('[data-product-stock]').innerHTML = '-';
@@ -262,13 +300,26 @@ document.addEventListener('DOMContentLoaded', function() {
// Получаем цену и статус через AJAX (используем числовой ID) // Получаем цену и статус через AJAX (используем числовой ID)
const numericId = productSelect.value; const numericId = productSelect.value;
fetch(`{% url "products:api-search-products-variants" %}?id=${numericId}`)
.then(response => response.json()) // Добавляем timeout для fetch
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
fetch(`${apiUrl}?id=${numericId}`, { signal: controller.signal })
.then(response => {
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => { .then(data => {
if (data.results && data.results.length > 0) { if (data.results && data.results.length > 0) {
const product = data.results[0]; const product = data.results[0];
row.querySelector('[data-product-sku]').textContent = product.sku || sku; row.querySelector('[data-product-sku]').textContent = product.sku || sku;
row.querySelector('[data-product-price]').innerHTML = `<strong>${product.price}</strong> руб.` || '-';
const price = product.price || product.actual_price || '0';
row.querySelector('[data-product-price]').innerHTML = `<strong>${price}</strong> руб.`;
// Отображаем статус наличия // Отображаем статус наличия
const stockCell = row.querySelector('[data-product-stock]'); const stockCell = row.querySelector('[data-product-stock]');
@@ -277,21 +328,33 @@ document.addEventListener('DOMContentLoaded', function() {
} else { } else {
stockCell.innerHTML = '<span class="badge bg-danger"><i class="bi bi-x-circle"></i> Нет</span>'; stockCell.innerHTML = '<span class="badge bg-danger"><i class="bi bi-x-circle"></i> Нет</span>';
} }
} else {
// Fallback: используем данные из опции
row.querySelector('[data-product-sku]').textContent = sku;
row.querySelector('[data-product-price]').textContent = '-';
row.querySelector('[data-product-stock]').textContent = '-';
} }
}) })
.catch(() => { .catch(error => {
clearTimeout(timeoutId);
console.error('updateRowData: fetch error', error);
// Fallback: данные из опции
row.querySelector('[data-product-sku]').textContent = sku; row.querySelector('[data-product-sku]').textContent = sku;
row.querySelector('[data-product-price]').textContent = '-'; row.querySelector('[data-product-price]').textContent = '-';
row.querySelector('[data-product-stock]').textContent = '-'; row.querySelector('[data-product-stock]').textContent = '-';
}); });
} }
// Инициализируем для существующих строк // Инициализируем для существующих строк с небольшой задержкой
setTimeout(function() {
container.querySelectorAll('.item-row').forEach(row => { container.querySelectorAll('.item-row').forEach(row => {
initSelect2ForRow(row); const success = initSelect2ForRow(row);
// Загружаем данные товара при загрузке страницы (с небольшой задержкой) if (success) {
setTimeout(() => updateRowData(row), 100); // Загружаем данные только если Select2 инициализирован
setTimeout(() => updateRowData(row), 200);
}
}); });
}, 100);
// ДОБАВЛЕНИЕ НОВОГО ТОВАРА // ДОБАВЛЕНИЕ НОВОГО ТОВАРА
document.getElementById('add-item-btn').addEventListener('click', function(e) { document.getElementById('add-item-btn').addEventListener('click', function(e) {
@@ -342,8 +405,17 @@ document.addEventListener('DOMContentLoaded', function() {
// Увеличиваем счетчик форм // Увеличиваем счетчик форм
totalFormsInput.value = newFormIndex + 1; totalFormsInput.value = newFormIndex + 1;
// Инициализируем Select2 для нового селекта // Даем время браузеру отрисовать элемент перед инициализацией Select2
setTimeout(function() {
const success = initSelect2ForRow(newRow);
if (!success) {
console.error('Failed to initialize Select2 for new row, retrying...');
// Повторная попытка через 500ms
setTimeout(function() {
initSelect2ForRow(newRow); initSelect2ForRow(newRow);
}, 500);
}
}, 50);
// Обновляем приоритеты // Обновляем приоритеты
updatePriorities(); updatePriorities();