Добавлена модель атрибутов для вариативных товаров (ConfigurableKitProductAttribute)
- Создана модель ConfigurableKitProductAttribute с полями name, option, position, visible - Добавлены формы и formsets для управления атрибутами родительского товара - Обновлены CRUD представления для работы с атрибутами (создание/редактирование) - Добавлен блок атрибутов в шаблоны создания/редактирования - Обновлена страница детального просмотра с отображением атрибутов товара - Добавлен JavaScript для динамического добавления форм атрибутов - Реализована валидация дубликатов атрибутов в formset - Атрибуты сохраняются в transaction.atomic() вместе с вариантами Теперь можно определять схему атрибутов для экспорта на WooCommerce без использования JSON или ID, только name и option.
This commit is contained in:
274
myproject/products/static/products/js/configurablekit_detail.js
Normal file
274
myproject/products/static/products/js/configurablekit_detail.js
Normal file
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* Управление вариантами вариативного товара (ConfigurableKitProduct)
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Проверяем наличие данных конфигурации
|
||||
const configDataEl = document.getElementById('configurableKitData');
|
||||
if (!configDataEl) return;
|
||||
|
||||
const config = JSON.parse(configDataEl.textContent);
|
||||
const modal = new bootstrap.Modal(document.getElementById('addOptionModal'));
|
||||
|
||||
// Элементы формы
|
||||
const saveBtn = document.getElementById('saveOptionBtn');
|
||||
const kitSelect = document.getElementById('kitSelect');
|
||||
const attributesInput = document.getElementById('attributesInput');
|
||||
const isDefaultCheck = document.getElementById('isDefaultCheck');
|
||||
const errorDiv = document.getElementById('addOptionError');
|
||||
const spinner = document.getElementById('saveOptionSpinner');
|
||||
|
||||
// Добавление варианта
|
||||
saveBtn.addEventListener('click', async function() {
|
||||
const kitId = kitSelect.value;
|
||||
if (!kitId) {
|
||||
showError('Пожалуйста, выберите комплект');
|
||||
return;
|
||||
}
|
||||
|
||||
// Показываем спиннер
|
||||
spinner.classList.remove('d-none');
|
||||
saveBtn.disabled = true;
|
||||
hideError();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('kit_id', kitId);
|
||||
formData.append('attributes', attributesInput.value.trim());
|
||||
formData.append('is_default', isDefaultCheck.checked ? 'true' : 'false');
|
||||
formData.append('csrfmiddlewaretoken', getCsrfToken());
|
||||
|
||||
try {
|
||||
const response = await fetch(config.addOptionUrl, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Добавляем строку в таблицу
|
||||
addOptionToTable(data.option);
|
||||
|
||||
// Закрываем модальное окно
|
||||
modal.hide();
|
||||
|
||||
// Очищаем форму
|
||||
resetForm();
|
||||
|
||||
// Показываем уведомление
|
||||
showSuccessMessage('Вариант успешно добавлен');
|
||||
} else {
|
||||
showError(data.error || 'Ошибка при добавлении варианта');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showError('Произошла ошибка при сохранении');
|
||||
} finally {
|
||||
spinner.classList.add('d-none');
|
||||
saveBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Удаление варианта
|
||||
document.addEventListener('click', async function(e) {
|
||||
if (e.target.closest('.remove-option-btn')) {
|
||||
const btn = e.target.closest('.remove-option-btn');
|
||||
const optionId = btn.dataset.optionId;
|
||||
|
||||
if (!confirm('Вы уверены, что хотите удалить этот вариант?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const url = config.removeOptionUrlTemplate.replace('{optionId}', optionId);
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Удаляем строку из таблицы
|
||||
const row = btn.closest('tr');
|
||||
row.remove();
|
||||
|
||||
// Проверяем, есть ли ещё варианты
|
||||
checkIfTableEmpty();
|
||||
|
||||
showSuccessMessage('Вариант удалён');
|
||||
} else {
|
||||
showError(data.error || 'Ошибка при удалении');
|
||||
btn.disabled = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Произошла ошибка при удалении');
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Установка по умолчанию
|
||||
document.addEventListener('click', async function(e) {
|
||||
if (e.target.closest('.set-default-btn')) {
|
||||
const btn = e.target.closest('.set-default-btn');
|
||||
const optionId = btn.dataset.optionId;
|
||||
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const url = config.setDefaultUrlTemplate.replace('{optionId}', optionId);
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Обновляем UI
|
||||
updateDefaultBadges(optionId);
|
||||
showSuccessMessage('Вариант установлен как по умолчанию');
|
||||
} else {
|
||||
showError(data.error || 'Ошибка при установке');
|
||||
btn.disabled = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Произошла ошибка');
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Вспомогательные функции
|
||||
function addOptionToTable(option) {
|
||||
const tbody = document.querySelector('#optionsTable tbody');
|
||||
const noOptionsMsg = document.getElementById('noOptionsMessage');
|
||||
|
||||
// Если таблицы нет, создаём её
|
||||
if (!tbody) {
|
||||
const container = document.getElementById('optionsTableContainer');
|
||||
if (noOptionsMsg) noOptionsMsg.remove();
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm mb-0" id="optionsTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Комплект</th>
|
||||
<th>Артикул</th>
|
||||
<th>Цена</th>
|
||||
<th>Атрибуты</th>
|
||||
<th style="width: 120px;">По умолчанию</th>
|
||||
<th style="width: 150px;">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const newTbody = document.querySelector('#optionsTable tbody');
|
||||
const kitDetailUrl = config.kitDetailUrlTemplate.replace('{kitId}', option.kit_id);
|
||||
|
||||
const row = document.createElement('tr');
|
||||
row.dataset.optionId = option.id;
|
||||
row.innerHTML = `
|
||||
<td>
|
||||
<a href="${kitDetailUrl}" class="text-decoration-none">
|
||||
${option.kit_name}
|
||||
</a>
|
||||
</td>
|
||||
<td><small class="text-muted">${option.kit_sku}</small></td>
|
||||
<td><strong>${option.kit_price}</strong> руб.</td>
|
||||
<td><small class="text-muted option-attributes">${option.attributes}</small></td>
|
||||
<td class="text-center">
|
||||
${option.is_default
|
||||
? '<span class="badge bg-primary default-badge">Да</span>'
|
||||
: `<button class="btn btn-sm btn-outline-secondary set-default-btn" data-option-id="${option.id}">Установить</button>`
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-danger remove-option-btn" data-option-id="${option.id}">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
newTbody.appendChild(row);
|
||||
}
|
||||
|
||||
function updateDefaultBadges(newDefaultId) {
|
||||
// Убираем все badges "По умолчанию"
|
||||
document.querySelectorAll('.default-badge').forEach(badge => {
|
||||
const td = badge.closest('td');
|
||||
const optionId = badge.closest('tr').dataset.optionId;
|
||||
td.innerHTML = `<button class="btn btn-sm btn-outline-secondary set-default-btn" data-option-id="${optionId}">Установить</button>`;
|
||||
});
|
||||
|
||||
// Добавляем badge к новому default
|
||||
const targetRow = document.querySelector(`tr[data-option-id="${newDefaultId}"]`);
|
||||
if (targetRow) {
|
||||
const td = targetRow.querySelector('td:nth-child(5)');
|
||||
td.innerHTML = '<span class="badge bg-primary default-badge">Да</span>';
|
||||
}
|
||||
}
|
||||
|
||||
function checkIfTableEmpty() {
|
||||
const tbody = document.querySelector('#optionsTable tbody');
|
||||
if (tbody && tbody.children.length === 0) {
|
||||
const container = document.getElementById('optionsTableContainer');
|
||||
container.innerHTML = '<p class="text-muted text-center py-4" id="noOptionsMessage">Нет вариантов. Нажмите "Добавить вариант" для добавления.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
kitSelect.value = '';
|
||||
attributesInput.value = '';
|
||||
isDefaultCheck.checked = false;
|
||||
hideError();
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
errorDiv.textContent = message;
|
||||
errorDiv.classList.remove('d-none');
|
||||
}
|
||||
|
||||
function hideError() {
|
||||
errorDiv.classList.add('d-none');
|
||||
}
|
||||
|
||||
function getCsrfToken() {
|
||||
return document.querySelector('[name=csrfmiddlewaretoken]').value;
|
||||
}
|
||||
|
||||
function showSuccessMessage(message) {
|
||||
// Простое alert, можно заменить на toast-уведомление
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = 'alert alert-success alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-3';
|
||||
alertDiv.style.zIndex = '9999';
|
||||
alertDiv.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(alertDiv);
|
||||
|
||||
setTimeout(() => {
|
||||
alertDiv.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Сброс формы при закрытии модального окна
|
||||
document.getElementById('addOptionModal').addEventListener('hidden.bs.modal', function() {
|
||||
resetForm();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user