feat: Улучшенная форма создания комплектов товаров
Реализованы следующие улучшения для формы создания/редактирования комплектов: 1. **Улучшенный API поиска товаров:** - Добавлен поиск по полю search_keywords для более точных результатов - Добавлены дополнительные поля: display_name, display_price 2. **Предпросмотр загружаемых фото:** - Миниатюры выбранных файлов перед загрузкой - Счетчик выбранных файлов - Возможность удаления отдельных фото до отправки формы 3. **Динамическое добавление товаров:** - Кнопка "Добавить товар в комплект" для создания новых строк - Автопоиск товаров при вводе текста (задержка 300мс) - Автоматическое добавление при клике на результат - Визуальные уведомления об успешном добавлении - Прокрутка к новой форме после добавления 4. **Валидация на дубликаты:** - Предупреждение при попытке добавить существующий товар - Подсветка дубликата на 2 секунды - Предложение изменить количество в существующей строке 5. **Автоматический расчет цены:** - Информационный блок с суммой товаров и их количеством - Пересчет при добавлении/удалении товаров - Пересчет при изменении количества - Асинхронная загрузка цен для существующих товаров 6. **Исправления:** - Снятие disabled с полей select перед отправкой формы - Правильное создание новых форм при добавлении товаров через поиск - Использование display_name для корректного отображения 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -109,6 +109,7 @@
|
||||
<div class="mb-0">
|
||||
<label for="id_photos" class="form-label fw-bold">
|
||||
{% if object %}➕ Добавить новые фото{% else %}📷 Загрузить фото{% endif %}
|
||||
<span id="photoCount" class="badge bg-secondary ms-2" style="display: none;">0 выбрано</span>
|
||||
</label>
|
||||
<input type="file" name="photos" accept="image/*" multiple class="form-control" id="id_photos">
|
||||
<small class="form-text text-muted">
|
||||
@@ -118,6 +119,12 @@
|
||||
Выберите фото для комплекта (можно выбрать несколько, до 10 штук)
|
||||
{% endif %}
|
||||
</small>
|
||||
|
||||
<!-- Контейнер для предпросмотра выбранных фото -->
|
||||
<div id="photoPreviewContainer" class="mt-3" style="display: none;">
|
||||
<h6 class="mb-2">Выбранные фото:</h6>
|
||||
<div id="photoPreview" class="row g-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -184,6 +191,24 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Информационный блок с расчетом стоимости -->
|
||||
<div class="alert alert-info mb-4" id="priceCalculationInfo">
|
||||
<h6 class="mb-2"><i class="bi bi-calculator"></i> Автоматический расчет стоимости</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<strong>Сумма товаров:</strong>
|
||||
<span id="totalProductsPrice" class="fs-5 text-primary">0.00 ₽</span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>Количество товаров:</strong>
|
||||
<span id="totalProductsCount" class="fs-5">0</span>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted mt-2 d-block">
|
||||
Сумма автоматически пересчитывается при добавлении/удалении товаров и изменении количества
|
||||
</small>
|
||||
</div>
|
||||
|
||||
{% for field in form %}
|
||||
{% if field.name not in 'name,sku,categories,description,tags' %}
|
||||
{% if field.name == 'fixed_price' %}
|
||||
@@ -241,26 +266,27 @@
|
||||
<strong>Подсказка:</strong> Добавьте компоненты комплекта ниже. Каждый компонент может быть либо конкретным товаром, либо группой вариантов (например, розы разных размеров).
|
||||
</div>
|
||||
|
||||
<!-- Поиск товаров для быстрого добавления (показываем только при редактировании) -->
|
||||
<div class="card mb-4 bg-light border-info" id="searchCard">
|
||||
<div class="card-header bg-info text-white">
|
||||
<!-- Поиск товаров для быстрого добавления -->
|
||||
<div class="card mb-4 bg-light border-success" id="searchCard">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="bi bi-search"></i> Поиск товара для добавления
|
||||
<i class="bi bi-search"></i> Быстрое добавление товара
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="input-group mb-2">
|
||||
<div class="mb-2">
|
||||
<label for="productSearch" class="form-label">Найти товар:</label>
|
||||
<input type="text"
|
||||
id="productSearch"
|
||||
class="form-control"
|
||||
placeholder="Введите название или артикул товара (минимум 2 символа)..."
|
||||
placeholder="Начните вводить название или артикул (минимум 2 символа)..."
|
||||
autocomplete="off">
|
||||
<button class="btn btn-info" type="button" id="searchBtn">
|
||||
<i class="bi bi-search"></i> Поиск
|
||||
</button>
|
||||
</div>
|
||||
<div id="searchResults" class="list-group" style="display: none; max-height: 300px; overflow-y: auto;"></div>
|
||||
<small class="text-muted">Используйте поиск для быстрого нахождения товара и добавления его в комплект</small>
|
||||
<div id="searchResults" class="list-group mb-2" style="display: none; max-height: 250px; overflow-y: auto;"></div>
|
||||
<small class="text-muted">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
Товар добавится автоматически при клике на результат поиска
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -354,6 +380,13 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Кнопка добавления нового товара -->
|
||||
<div class="mb-3">
|
||||
<button type="button" class="btn btn-success" id="addKitItemBtn">
|
||||
<i class="bi bi-plus-circle"></i> Добавить товар в комплект
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Кнопки действия -->
|
||||
<div class="d-flex justify-content-between mt-4 gap-2 flex-wrap">
|
||||
<a href="{% url 'products:productkit-list' %}" class="btn btn-secondary">Отмена</a>
|
||||
@@ -381,13 +414,6 @@
|
||||
document.querySelector('[name$="-id"]') !== null &&
|
||||
document.querySelector('[name$="-id"]').value !== '';
|
||||
|
||||
// Если режим редактирования - показываем карточку поиска
|
||||
// Если создание - скрываем (будет одна пустая форма)
|
||||
const searchCard = document.getElementById('searchCard');
|
||||
if (!isEditMode && searchCard) {
|
||||
searchCard.style.display = 'none';
|
||||
}
|
||||
|
||||
// ========== ФУНКЦИИ ДЛЯ УПРАВЛЕНИЯ ВИДИМОСТЬЮ ПОЛЕЙ ==========
|
||||
function updateFieldStatus(form) {
|
||||
const productSelect = form.querySelector('[name$="-product"]');
|
||||
@@ -420,17 +446,25 @@
|
||||
// Инициализируем все существующие формы
|
||||
const kititemForms = document.querySelectorAll('.kititem-form');
|
||||
kititemForms.forEach((form) => {
|
||||
updateFieldStatus(form);
|
||||
const productSelect = form.querySelector('[name$="-product"]');
|
||||
const variantGroupSelect = form.querySelector('[name$="-variant_group"]');
|
||||
if (productSelect) productSelect.addEventListener('change', () => updateFieldStatus(form));
|
||||
if (variantGroupSelect) variantGroupSelect.addEventListener('change', () => updateFieldStatus(form));
|
||||
initializeForm(form);
|
||||
// Для существующих форм нужно загрузить цены товаров
|
||||
loadProductPriceForForm(form);
|
||||
});
|
||||
|
||||
// Инициализируем расчет цены при загрузке страницы
|
||||
setTimeout(() => calculateKitPrice(), 500); // Небольшая задержка для загрузки цен
|
||||
|
||||
// Добавляем слушатель на чекбоксы DELETE для пересчета при удалении
|
||||
document.addEventListener('change', (e) => {
|
||||
if (e.target.matches('[name$="-DELETE"]')) {
|
||||
calculateKitPrice();
|
||||
}
|
||||
});
|
||||
|
||||
// ========== ФУНКЦИИ ДЛЯ ПОИСКА И ДОБАВЛЕНИЯ ТОВАРОВ ==========
|
||||
const searchInput = document.getElementById('productSearch');
|
||||
const searchBtn = document.getElementById('searchBtn');
|
||||
const searchResults = document.getElementById('searchResults');
|
||||
const addKitItemBtn = document.getElementById('addKitItemBtn');
|
||||
const managementForm = document.querySelector('[name$="TOTAL_FORMS"]');
|
||||
|
||||
// Функция для выполнения поиска
|
||||
@@ -488,43 +522,76 @@
|
||||
function addItemToFormset(item) {
|
||||
const formsContainer = document.getElementById('kititem-forms');
|
||||
const existingForms = formsContainer.querySelectorAll('.kititem-form');
|
||||
const itemType = item.type === 'product' ? 'product' : 'variant';
|
||||
|
||||
// Ищем первую пустую форму и используем ее
|
||||
let emptyForm = null;
|
||||
// Проверка на дубликаты
|
||||
let isDuplicate = false;
|
||||
let duplicateForm = null;
|
||||
existingForms.forEach((form) => {
|
||||
if (!emptyForm) {
|
||||
const productSelect = form.querySelector('[name$="-product"]');
|
||||
const variantGroupSelect = form.querySelector('[name$="-variant_group"]');
|
||||
const deleteCheckbox = form.querySelector('[name$="-DELETE"]');
|
||||
const deleteCheckbox = form.querySelector('[name$="-DELETE"]');
|
||||
// Пропускаем формы, помеченные на удаление
|
||||
if (deleteCheckbox && deleteCheckbox.checked) return;
|
||||
|
||||
// Если форма пустая и не помечена на удаление
|
||||
if ((!productSelect || !productSelect.value) &&
|
||||
(!variantGroupSelect || !variantGroupSelect.value) &&
|
||||
(!deleteCheckbox || !deleteCheckbox.checked)) {
|
||||
emptyForm = form;
|
||||
if (itemType === 'product') {
|
||||
const productSelect = form.querySelector('[name$="-product"]');
|
||||
if (productSelect && productSelect.value == item.id) {
|
||||
isDuplicate = true;
|
||||
duplicateForm = form;
|
||||
}
|
||||
} else {
|
||||
const variantGroupSelect = form.querySelector('[name$="-variant_group"]');
|
||||
if (variantGroupSelect && variantGroupSelect.value == item.id) {
|
||||
isDuplicate = true;
|
||||
duplicateForm = form;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Если нашли пустую форму, заполняем ее
|
||||
if (emptyForm) {
|
||||
fillFormWithItem(emptyForm, item);
|
||||
} else {
|
||||
// Если нет пустой формы, создаем новую
|
||||
const totalForms = parseInt(document.querySelector('[name*="TOTAL_FORMS"]').value);
|
||||
const newFormHtml = createNewKitItemForm(totalForms, item);
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = newFormHtml;
|
||||
formsContainer.appendChild(tempDiv.firstElementChild);
|
||||
// Если товар уже добавлен - показываем предупреждение
|
||||
if (isDuplicate) {
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = 'alert alert-warning alert-dismissible fade show';
|
||||
alertDiv.setAttribute('role', 'alert');
|
||||
alertDiv.innerHTML = `
|
||||
<strong>⚠️ Внимание!</strong> "${item.display_name || item.name}" уже добавлен в комплект.
|
||||
Если нужно изменить количество, отредактируйте существующую строку.
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.querySelector('.card-body').prepend(alertDiv);
|
||||
setTimeout(() => alertDiv.remove(), 5000);
|
||||
|
||||
// Обновляем счетчик форм
|
||||
document.querySelector('[name*="TOTAL_FORMS"]').value = totalForms + 1;
|
||||
// Подсвечиваем дубликат
|
||||
if (duplicateForm) {
|
||||
duplicateForm.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
duplicateForm.style.border = '2px solid #ffc107';
|
||||
setTimeout(() => {
|
||||
duplicateForm.style.border = '';
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Инициализируем новую форму
|
||||
const newForm = formsContainer.lastElementChild;
|
||||
initializeForm(newForm);
|
||||
// Очищаем поиск
|
||||
searchInput.value = '';
|
||||
searchResults.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
// При добавлении через поиск всегда создаем новую форму
|
||||
const totalForms = parseInt(document.querySelector('[name*="TOTAL_FORMS"]').value);
|
||||
const newFormHtml = createNewKitItemForm(totalForms, item);
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = newFormHtml;
|
||||
formsContainer.appendChild(tempDiv.firstElementChild);
|
||||
|
||||
// Обновляем счетчик форм
|
||||
document.querySelector('[name*="TOTAL_FORMS"]').value = totalForms + 1;
|
||||
|
||||
// Инициализируем новую форму
|
||||
const newForm = formsContainer.lastElementChild;
|
||||
initializeForm(newForm);
|
||||
|
||||
// Прокручиваем к новой форме
|
||||
newForm.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
|
||||
// Очищаем поиск
|
||||
searchInput.value = '';
|
||||
searchResults.style.display = 'none';
|
||||
@@ -534,7 +601,7 @@
|
||||
alertDiv.className = 'alert alert-success alert-dismissible fade show';
|
||||
alertDiv.setAttribute('role', 'alert');
|
||||
alertDiv.innerHTML = `
|
||||
<strong>✓ Товар добавлен!</strong> "${item.name}" добавлен в комплект.
|
||||
<strong>✓ Товар добавлен!</strong> "${item.display_name || item.name}" добавлен в комплект.
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.querySelector('.card-body').prepend(alertDiv);
|
||||
@@ -550,8 +617,15 @@
|
||||
const productSelect = form.querySelector('[name$="-product"]');
|
||||
if (productSelect) {
|
||||
productSelect.value = selectedId;
|
||||
productSelect.innerHTML = `<option value="">---------</option><option value="${selectedId}" selected>${item.name}</option>`;
|
||||
productSelect.innerHTML = `<option value="">---------</option><option value="${selectedId}" selected>${item.display_name || item.name}</option>`;
|
||||
productSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
||||
// Сохраняем цену в data-атрибут поля количества
|
||||
const quantityInput = form.querySelector('[name$="-quantity"]');
|
||||
if (quantityInput && item.price) {
|
||||
quantityInput.setAttribute('data-price', item.price);
|
||||
quantityInput.classList.add('product-quantity');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const variantSelect = form.querySelector('[name$="-variant_group"]');
|
||||
@@ -563,6 +637,11 @@
|
||||
}
|
||||
|
||||
updateFieldStatus(form);
|
||||
|
||||
// Пересчитываем цену комплекта
|
||||
if (typeof calculateKitPrice === 'function') {
|
||||
calculateKitPrice();
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для инициализации формы
|
||||
@@ -570,8 +649,178 @@
|
||||
updateFieldStatus(form);
|
||||
const productSelect = form.querySelector('[name$="-product"]');
|
||||
const variantGroupSelect = form.querySelector('[name$="-variant_group"]');
|
||||
if (productSelect) productSelect.addEventListener('change', () => updateFieldStatus(form));
|
||||
if (variantGroupSelect) variantGroupSelect.addEventListener('change', () => updateFieldStatus(form));
|
||||
const quantityInput = form.querySelector('[name$="-quantity"]');
|
||||
|
||||
if (productSelect) productSelect.addEventListener('change', () => {
|
||||
updateFieldStatus(form);
|
||||
calculateKitPrice();
|
||||
});
|
||||
if (variantGroupSelect) variantGroupSelect.addEventListener('change', () => {
|
||||
updateFieldStatus(form);
|
||||
calculateKitPrice();
|
||||
});
|
||||
if (quantityInput) quantityInput.addEventListener('input', () => {
|
||||
calculateKitPrice();
|
||||
});
|
||||
}
|
||||
|
||||
// Функция для загрузки цены товара для существующей формы
|
||||
async function loadProductPriceForForm(form) {
|
||||
const productSelect = form.querySelector('[name$="-product"]');
|
||||
const quantityInput = form.querySelector('[name$="-quantity"]');
|
||||
|
||||
if (productSelect && productSelect.value && quantityInput) {
|
||||
const productId = productSelect.value;
|
||||
const productName = productSelect.options[productSelect.selectedIndex]?.text;
|
||||
|
||||
// Если цена уже есть в data-атрибуте, не загружаем повторно
|
||||
if (quantityInput.getAttribute('data-price')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Делаем запрос к API для получения информации о товаре
|
||||
// Используем название товара для поиска
|
||||
if (productName) {
|
||||
const response = await fetch(`{% url 'products:api-search-products-variants' %}?q=${encodeURIComponent(productName)}&type=product`);
|
||||
const data = await response.json();
|
||||
|
||||
// Ищем товар с нужным ID
|
||||
const product = data.results.find(item => item.id == productId);
|
||||
if (product && product.price) {
|
||||
quantityInput.setAttribute('data-price', product.price);
|
||||
quantityInput.classList.add('product-quantity');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке цены товара:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для расчета общей стоимости комплекта
|
||||
function calculateKitPrice() {
|
||||
const formsContainer = document.getElementById('kititem-forms');
|
||||
const existingForms = formsContainer.querySelectorAll('.kititem-form');
|
||||
|
||||
let totalPrice = 0;
|
||||
let totalCount = 0;
|
||||
|
||||
existingForms.forEach((form) => {
|
||||
const deleteCheckbox = form.querySelector('[name$="-DELETE"]');
|
||||
// Пропускаем формы, помеченные на удаление
|
||||
if (deleteCheckbox && deleteCheckbox.checked) return;
|
||||
|
||||
const productSelect = form.querySelector('[name$="-product"]');
|
||||
const quantityInput = form.querySelector('[name$="-quantity"]');
|
||||
|
||||
// Считаем только товары (не группы вариантов)
|
||||
if (productSelect && productSelect.value && quantityInput) {
|
||||
const quantity = parseFloat(quantityInput.value) || 0;
|
||||
const price = parseFloat(quantityInput.getAttribute('data-price')) || 0;
|
||||
|
||||
if (quantity > 0 && price > 0) {
|
||||
totalPrice += price * quantity;
|
||||
totalCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Обновляем отображение
|
||||
const totalProductsPriceElement = document.getElementById('totalProductsPrice');
|
||||
const totalProductsCountElement = document.getElementById('totalProductsCount');
|
||||
|
||||
if (totalProductsPriceElement) {
|
||||
totalProductsPriceElement.textContent = totalPrice.toFixed(2) + ' ₽';
|
||||
}
|
||||
if (totalProductsCountElement) {
|
||||
totalProductsCountElement.textContent = totalCount;
|
||||
}
|
||||
|
||||
return totalPrice;
|
||||
}
|
||||
|
||||
// Функция для добавления пустой формы товара
|
||||
function addEmptyKitItemForm() {
|
||||
const formsContainer = document.getElementById('kititem-forms');
|
||||
const totalForms = parseInt(document.querySelector('[name*="TOTAL_FORMS"]').value);
|
||||
|
||||
const newFormHtml = createEmptyKitItemForm(totalForms);
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = newFormHtml;
|
||||
formsContainer.appendChild(tempDiv.firstElementChild);
|
||||
|
||||
// Обновляем счетчик форм
|
||||
document.querySelector('[name*="TOTAL_FORMS"]').value = totalForms + 1;
|
||||
|
||||
// Инициализируем новую форму
|
||||
const newForm = formsContainer.lastElementChild;
|
||||
initializeForm(newForm);
|
||||
|
||||
// Прокручиваем к новой форме
|
||||
newForm.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
// Функция для создания HTML пустой формы
|
||||
function createEmptyKitItemForm(formIndex) {
|
||||
const fieldName = `kititem_set-${formIndex}`;
|
||||
|
||||
return `
|
||||
<div class="card mb-3 kititem-form" data-form-index="${formIndex}">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<span><strong>Компонент #${formIndex + 1}</strong></span>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="${fieldName}-DELETE" name="${fieldName}-DELETE">
|
||||
<label class="form-check-label" for="${fieldName}-DELETE">
|
||||
Удалить
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="${fieldName}-product" class="form-label">
|
||||
Конкретный товар
|
||||
</label>
|
||||
<select class="form-control" id="${fieldName}-product" name="${fieldName}-product" data-new-record="False">
|
||||
<option value="">---------</option>
|
||||
</select>
|
||||
<small class="form-text text-muted">Конкретный товар (если выбран, группа вариантов не нужна)</small>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="${fieldName}-variant_group" class="form-label">
|
||||
Группа вариантов
|
||||
</label>
|
||||
<select class="form-control" id="${fieldName}-variant_group" name="${fieldName}-variant_group" data-new-record="False">
|
||||
<option value="">---------</option>
|
||||
</select>
|
||||
<small class="form-text text-muted">Группа вариантов (если выбрана, конкретный товар не нужен)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="${fieldName}-quantity" class="form-label">
|
||||
Количество
|
||||
</label>
|
||||
<input type="number" class="form-control product-quantity" id="${fieldName}-quantity" name="${fieldName}-quantity"
|
||||
value="1" step="0.001" min="0" data-price="0">
|
||||
</div>
|
||||
|
||||
<div class="col-md-8 mb-3">
|
||||
<label for="${fieldName}-notes" class="form-label">
|
||||
Примечание
|
||||
</label>
|
||||
<input type="text" class="form-control" id="${fieldName}-notes" name="${fieldName}-notes"
|
||||
placeholder="Опциональное примечание">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="${fieldName}-id">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Функция для создания HTML новой формы
|
||||
@@ -599,9 +848,9 @@
|
||||
Конкретный товар
|
||||
</label>
|
||||
<select class="form-select" id="${fieldName}-product" name="${fieldName}-product"
|
||||
data-new-record="False" ${itemType === 'variant' ? 'disabled' : ''} style="${itemType === 'variant' ? 'opacity: 0.6' : ''}">
|
||||
data-new-record="False" style="${itemType === 'variant' ? 'opacity: 0.6' : ''}">
|
||||
<option value="">---------</option>
|
||||
${itemType === 'product' ? `<option value="${selectedId}" selected>${item.name}</option>` : ''}
|
||||
${itemType === 'product' ? `<option value="${selectedId}" selected>${item.display_name || item.name}</option>` : ''}
|
||||
</select>
|
||||
<small class="form-text text-muted">Конкретный товар (если выбран, группа вариантов не нужна)</small>
|
||||
</div>
|
||||
@@ -611,7 +860,7 @@
|
||||
Группа вариантов
|
||||
</label>
|
||||
<select class="form-select" id="${fieldName}-variant_group" name="${fieldName}-variant_group"
|
||||
data-new-record="False" ${itemType === 'product' ? 'disabled' : ''} style="${itemType === 'product' ? 'opacity: 0.6' : ''}">
|
||||
data-new-record="False" style="${itemType === 'product' ? 'opacity: 0.6' : ''}">
|
||||
<option value="">---------</option>
|
||||
${itemType === 'variant' ? `<option value="${selectedId}" selected>${item.name}</option>` : ''}
|
||||
</select>
|
||||
@@ -624,8 +873,8 @@
|
||||
<label for="${fieldName}-quantity" class="form-label">
|
||||
Количество
|
||||
</label>
|
||||
<input type="number" class="form-control" id="${fieldName}-quantity" name="${fieldName}-quantity"
|
||||
value="1" step="0.001" min="0">
|
||||
<input type="number" class="form-control product-quantity" id="${fieldName}-quantity" name="${fieldName}-quantity"
|
||||
value="1" step="0.001" min="0" data-price="${item.price || 0}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-8 mb-3">
|
||||
@@ -644,21 +893,28 @@
|
||||
}
|
||||
|
||||
// Обработчики событий поиска
|
||||
searchBtn.addEventListener('click', () => {
|
||||
performSearch(searchInput.value);
|
||||
});
|
||||
|
||||
searchInput.addEventListener('keyup', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
performSearch(searchInput.value);
|
||||
} else {
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
// Автопоиск с задержкой
|
||||
clearTimeout(searchInput.searchTimeout);
|
||||
searchInput.searchTimeout = setTimeout(() => {
|
||||
performSearch(searchInput.value);
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
searchInput.addEventListener('keyup', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
performSearch(searchInput.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Обработчик кнопки "Добавить товар"
|
||||
if (addKitItemBtn) {
|
||||
addKitItemBtn.addEventListener('click', () => {
|
||||
addEmptyKitItemForm();
|
||||
});
|
||||
}
|
||||
|
||||
// Очистка результатов при клике вне области
|
||||
document.addEventListener('click', (e) => {
|
||||
@@ -673,6 +929,13 @@
|
||||
const kitForm = document.querySelector('form[method="post"]');
|
||||
if (kitForm) {
|
||||
kitForm.addEventListener('submit', function(e) {
|
||||
// Снимаем disabled со всех селектов перед отправкой
|
||||
// (иначе их значения не попадут в POST запрос)
|
||||
const allSelects = kitForm.querySelectorAll('select[disabled]');
|
||||
allSelects.forEach(select => {
|
||||
select.disabled = false;
|
||||
});
|
||||
|
||||
// Отмечаем пустые компоненты для удаления
|
||||
const formsContainer = document.getElementById('kititem-forms');
|
||||
if (formsContainer) {
|
||||
@@ -732,5 +995,80 @@
|
||||
pricingMethodField.addEventListener('change', updatePricingFieldsState);
|
||||
}
|
||||
});
|
||||
|
||||
// ========== ПРЕДПРОСМОТР ЗАГРУЖАЕМЫХ ФОТО ==========
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const photoInput = document.getElementById('id_photos');
|
||||
const photoPreview = document.getElementById('photoPreview');
|
||||
const photoPreviewContainer = document.getElementById('photoPreviewContainer');
|
||||
const photoCount = document.getElementById('photoCount');
|
||||
|
||||
// Массив для хранения выбранных файлов
|
||||
let selectedFiles = [];
|
||||
|
||||
if (photoInput) {
|
||||
photoInput.addEventListener('change', function(e) {
|
||||
const files = Array.from(e.target.files);
|
||||
selectedFiles = files; // Сохраняем выбранные файлы
|
||||
|
||||
if (files.length > 0) {
|
||||
photoCount.style.display = 'inline';
|
||||
photoCount.textContent = `${files.length} выбрано`;
|
||||
photoPreviewContainer.style.display = 'block';
|
||||
photoPreview.innerHTML = '';
|
||||
|
||||
files.forEach((file, index) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function(event) {
|
||||
const col = document.createElement('div');
|
||||
col.className = 'col-6 col-md-4 col-lg-3';
|
||||
|
||||
col.innerHTML = `
|
||||
<div class="card position-relative" data-file-index="${index}">
|
||||
<img src="${event.target.result}" class="card-img-top" alt="Preview" style="height: 150px; object-fit: cover;">
|
||||
<div class="card-body p-2">
|
||||
<small class="text-truncate d-block">${file.name}</small>
|
||||
<button type="button" class="btn btn-sm btn-danger w-100 mt-1 remove-photo-btn" data-file-index="${index}">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
photoPreview.appendChild(col);
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
} else {
|
||||
photoCount.style.display = 'none';
|
||||
photoPreviewContainer.style.display = 'none';
|
||||
photoPreview.innerHTML = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Обработчик удаления отдельных фото
|
||||
photoPreview.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('remove-photo-btn') || e.target.closest('.remove-photo-btn')) {
|
||||
const btn = e.target.classList.contains('remove-photo-btn') ? e.target : e.target.closest('.remove-photo-btn');
|
||||
const fileIndex = parseInt(btn.getAttribute('data-file-index'));
|
||||
|
||||
// Удаляем файл из массива
|
||||
selectedFiles.splice(fileIndex, 1);
|
||||
|
||||
// Создаем новый DataTransfer для обновления input
|
||||
const dataTransfer = new DataTransfer();
|
||||
selectedFiles.forEach(file => {
|
||||
dataTransfer.items.add(file);
|
||||
});
|
||||
photoInput.files = dataTransfer.files;
|
||||
|
||||
// Обновляем отображение
|
||||
photoInput.dispatchEvent(new Event('change'));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user