Добавить функционал массового изменения категорий товаров
- Добавлен UI для пакетного выбора товаров с чекбоксами - Реализована возможность выбора всех товаров на странице - Реализована возможность выбора всех отфильтрованных товаров - Добавлено модальное окно для массового управления категориями - Добавлены API эндпоинты: get_filtered_items_ids, bulk_update_categories - Реализованы три режима работы с категориями: добавление, замена, очистка - Добавлен селектор количества элементов на странице (20/50/100) - Улучшена информативность о количестве выбранных элементов
This commit is contained in:
336
myproject/products/static/products/js/batch-selection.js
Normal file
336
myproject/products/static/products/js/batch-selection.js
Normal file
@@ -0,0 +1,336 @@
|
||||
/**
|
||||
* Batch Selection Manager for Product List
|
||||
* Handles checkbox selection, row highlighting, and counter updates
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Selection state
|
||||
let selectedItems = new Set();
|
||||
|
||||
// DOM elements
|
||||
const selectAllCheckbox = document.getElementById('select-all-checkbox');
|
||||
const batchActionsBtn = document.getElementById('batch-actions-btn');
|
||||
const batchActionsDropdown = document.getElementById('batch-actions-dropdown');
|
||||
let selectionCountSpan = document.getElementById('selection-count');
|
||||
|
||||
/**
|
||||
* Initialize the batch selection functionality
|
||||
*/
|
||||
function init() {
|
||||
if (!selectAllCheckbox) {
|
||||
console.warn('Batch selection: select-all checkbox not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Attach event listeners
|
||||
selectAllCheckbox.addEventListener('change', handleSelectAllChange);
|
||||
|
||||
// Attach listeners to all item checkboxes
|
||||
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
|
||||
itemCheckboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', handleItemCheckboxChange);
|
||||
});
|
||||
|
||||
// Initialize state
|
||||
updateUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle "Select All" checkbox change
|
||||
*/
|
||||
function handleSelectAllChange(event) {
|
||||
const isChecked = event.target.checked;
|
||||
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
|
||||
|
||||
itemCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = isChecked;
|
||||
const itemValue = checkbox.value;
|
||||
|
||||
if (isChecked) {
|
||||
selectedItems.add(itemValue);
|
||||
highlightRow(checkbox, true);
|
||||
} else {
|
||||
selectedItems.delete(itemValue);
|
||||
highlightRow(checkbox, false);
|
||||
}
|
||||
});
|
||||
|
||||
updateUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle individual item checkbox change
|
||||
*/
|
||||
function handleItemCheckboxChange(event) {
|
||||
const checkbox = event.target;
|
||||
const itemValue = checkbox.value;
|
||||
const isChecked = checkbox.checked;
|
||||
|
||||
if (isChecked) {
|
||||
selectedItems.add(itemValue);
|
||||
highlightRow(checkbox, true);
|
||||
} else {
|
||||
selectedItems.delete(itemValue);
|
||||
highlightRow(checkbox, false);
|
||||
}
|
||||
|
||||
// Update "Select All" checkbox state
|
||||
updateSelectAllCheckbox();
|
||||
|
||||
// Update UI
|
||||
updateUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight or unhighlight the row containing the checkbox
|
||||
*/
|
||||
function highlightRow(checkbox, highlight) {
|
||||
const row = checkbox.closest('tr');
|
||||
if (row) {
|
||||
if (highlight) {
|
||||
row.classList.add('table-active');
|
||||
} else {
|
||||
row.classList.remove('table-active');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the "Select All" checkbox state based on individual checkboxes
|
||||
*/
|
||||
function updateSelectAllCheckbox() {
|
||||
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
|
||||
const totalCheckboxes = itemCheckboxes.length;
|
||||
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked').length;
|
||||
|
||||
if (checkedCheckboxes === 0) {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
} else if (checkedCheckboxes === totalCheckboxes) {
|
||||
selectAllCheckbox.checked = true;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
} else {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update UI elements based on selection state
|
||||
*/
|
||||
function updateUI() {
|
||||
const count = selectedItems.size;
|
||||
|
||||
// Обновляем счётчик (ищем заново на случай, если он был пересоздан)
|
||||
selectionCountSpan = document.getElementById('selection-count');
|
||||
if (selectionCountSpan) {
|
||||
selectionCountSpan.textContent = count;
|
||||
}
|
||||
|
||||
// Enable/disable batch actions button and restore text if needed
|
||||
const shouldEnable = count > 0;
|
||||
if (batchActionsBtn) {
|
||||
batchActionsBtn.disabled = !shouldEnable;
|
||||
|
||||
// Если в кнопке нет span#selection-count, значит текст был изменён - восстанавливаем
|
||||
if (!batchActionsBtn.querySelector('#selection-count')) {
|
||||
batchActionsBtn.innerHTML = `<i class="bi bi-gear-fill"></i> Действия над выбранными (<span id="selection-count">${count}</span>)`;
|
||||
selectionCountSpan = document.getElementById('selection-count');
|
||||
}
|
||||
}
|
||||
if (batchActionsDropdown) {
|
||||
batchActionsDropdown.disabled = !shouldEnable;
|
||||
}
|
||||
|
||||
// Показываем/скрываем dropdown для выбора всех
|
||||
const selectAllDropdown = document.getElementById('select-all-dropdown-group');
|
||||
if (selectAllDropdown) {
|
||||
// Показываем всегда, когда есть элементы на странице
|
||||
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
|
||||
if (itemCheckboxes.length > 0) {
|
||||
selectAllDropdown.style.display = 'block';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get currently selected items
|
||||
* @returns {Array} Array of selected item identifiers
|
||||
*/
|
||||
function getSelectedItems() {
|
||||
return Array.from(selectedItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all selections
|
||||
*/
|
||||
function clearSelection() {
|
||||
selectedItems.clear();
|
||||
|
||||
// Uncheck all checkboxes
|
||||
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
|
||||
itemCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = false;
|
||||
highlightRow(checkbox, false);
|
||||
});
|
||||
|
||||
if (selectAllCheckbox) {
|
||||
selectAllCheckbox.checked = false;
|
||||
selectAllCheckbox.indeterminate = false;
|
||||
}
|
||||
|
||||
updateUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select all filtered items via AJAX request
|
||||
*/
|
||||
function selectAllFiltered() {
|
||||
// Получаем текущие параметры фильтрации из URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
// Строим URL для API запроса
|
||||
const apiUrl = '/products/api/filtered-items-ids/?' + urlParams.toString();
|
||||
|
||||
// Показываем индикатор загрузки
|
||||
if (batchActionsBtn) {
|
||||
batchActionsBtn.disabled = true;
|
||||
batchActionsBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> Загрузка...';
|
||||
}
|
||||
|
||||
// Выполняем AJAX запрос
|
||||
fetch(apiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Очищаем текущий выбор
|
||||
selectedItems.clear();
|
||||
|
||||
// Добавляем все отфильтрованные элементы
|
||||
data.items.forEach(item => {
|
||||
const itemValue = `${item.type}:${item.id}`;
|
||||
selectedItems.add(itemValue);
|
||||
});
|
||||
|
||||
// Отмечаем чекбоксы на текущей странице
|
||||
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
|
||||
itemCheckboxes.forEach(checkbox => {
|
||||
const itemValue = checkbox.value;
|
||||
if (selectedItems.has(itemValue)) {
|
||||
checkbox.checked = true;
|
||||
highlightRow(checkbox, true);
|
||||
}
|
||||
});
|
||||
|
||||
// Обновляем состояние "Select All" checkbox
|
||||
updateSelectAllCheckbox();
|
||||
|
||||
// Обновляем UI
|
||||
updateUI();
|
||||
|
||||
// Показываем уведомление
|
||||
showNotification(`Выбрано ${data.count} элементов (включая элементы на других страницах)`, 'success');
|
||||
} else {
|
||||
showNotification('Ошибка при загрузке списка элементов: ' + data.error, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching filtered items:', error);
|
||||
showNotification('Ошибка при загрузке списка элементов', 'error');
|
||||
})
|
||||
.finally(() => {
|
||||
// Восстанавливаем кнопку
|
||||
updateUI();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show notification to user
|
||||
*/
|
||||
function showNotification(message, type = 'info') {
|
||||
// Создаём toast уведомление (Bootstrap 5)
|
||||
const toastContainer = document.querySelector('.toast-container') || createToastContainer();
|
||||
|
||||
const toastId = 'toast-' + Date.now();
|
||||
const bgClass = type === 'success' ? 'bg-success' : type === 'error' ? 'bg-danger' : 'bg-info';
|
||||
|
||||
const toastHTML = `
|
||||
<div id="${toastId}" class="toast align-items-center text-white ${bgClass} border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
${message}
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
toastContainer.insertAdjacentHTML('beforeend', toastHTML);
|
||||
|
||||
const toastElement = document.getElementById(toastId);
|
||||
const toast = new bootstrap.Toast(toastElement, { delay: 3000 });
|
||||
toast.show();
|
||||
|
||||
// Удаляем элемент после скрытия
|
||||
toastElement.addEventListener('hidden.bs.toast', () => {
|
||||
toastElement.remove();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create toast container if it doesn't exist
|
||||
*/
|
||||
function createToastContainer() {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'toast-container position-fixed top-0 end-0 p-3';
|
||||
container.style.zIndex = '9999';
|
||||
document.body.appendChild(container);
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update checkboxes on current page based on selection state
|
||||
* Called when navigating between pages
|
||||
*/
|
||||
function syncCheckboxesWithSelection() {
|
||||
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
|
||||
itemCheckboxes.forEach(checkbox => {
|
||||
const itemValue = checkbox.value;
|
||||
if (selectedItems.has(itemValue)) {
|
||||
checkbox.checked = true;
|
||||
highlightRow(checkbox, true);
|
||||
}
|
||||
});
|
||||
|
||||
updateSelectAllCheckbox();
|
||||
updateUI();
|
||||
}
|
||||
|
||||
// Initialize on DOM ready and sync selection state
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
init();
|
||||
syncCheckboxesWithSelection();
|
||||
});
|
||||
} else {
|
||||
init();
|
||||
syncCheckboxesWithSelection();
|
||||
}
|
||||
|
||||
// Export public API
|
||||
window.BatchSelection = {
|
||||
getSelectedItems: getSelectedItems,
|
||||
clearSelection: clearSelection,
|
||||
selectAllFiltered: selectAllFiltered,
|
||||
syncCheckboxesWithSelection: syncCheckboxesWithSelection
|
||||
};
|
||||
|
||||
})();
|
||||
504
myproject/products/static/products/js/bulk-category-modal.js
Normal file
504
myproject/products/static/products/js/bulk-category-modal.js
Normal file
@@ -0,0 +1,504 @@
|
||||
/**
|
||||
* Bulk Category Assignment Modal
|
||||
* Handles modal for assigning categories to multiple products/kits
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// State
|
||||
let allCategories = [];
|
||||
let selectedCategoryIds = new Set();
|
||||
let modalInstance = null;
|
||||
|
||||
// DOM elements
|
||||
const modal = document.getElementById('bulkCategoryModal');
|
||||
const bulkCategoryAction = document.getElementById('bulk-category-action');
|
||||
const applyBtn = document.getElementById('applyBulkCategoriesBtn');
|
||||
const clearAllBtn = document.getElementById('clearAllCategoriesBtn');
|
||||
const categoryListContainer = document.getElementById('categoryListContainer');
|
||||
const categorySearchInput = document.getElementById('categorySearchInput');
|
||||
const clearExistingToggle = document.getElementById('clearExistingCategoriesToggle');
|
||||
const errorAlert = document.getElementById('bulkCategoryError');
|
||||
const selectedItemsCountSpan = document.getElementById('selectedItemsCount');
|
||||
const selectedItemsBreakdownSpan = document.getElementById('selectedItemsBreakdown');
|
||||
|
||||
/**
|
||||
* Initialize the bulk category modal functionality
|
||||
*/
|
||||
function init() {
|
||||
if (!modal || !bulkCategoryAction) {
|
||||
console.warn('Bulk category modal elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize Bootstrap modal instance
|
||||
modalInstance = new bootstrap.Modal(modal);
|
||||
|
||||
// Event listeners
|
||||
bulkCategoryAction.addEventListener('click', handleOpenModal);
|
||||
applyBtn.addEventListener('click', handleApply);
|
||||
clearAllBtn.addEventListener('click', handleClearAll);
|
||||
categorySearchInput.addEventListener('input', handleCategorySearch);
|
||||
|
||||
// Listen for modal close to reset state
|
||||
modal.addEventListener('hidden.bs.modal', resetModalState);
|
||||
|
||||
console.log('Bulk category modal initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle opening the modal
|
||||
*/
|
||||
function handleOpenModal(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Get selected items from batch selection
|
||||
const selectedItems = getSelectedItems();
|
||||
|
||||
if (selectedItems.length === 0) {
|
||||
alert('Выберите товары для изменения категорий');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update modal header with selection info
|
||||
updateSelectionInfo(selectedItems);
|
||||
|
||||
// Reset state
|
||||
selectedCategoryIds.clear();
|
||||
hideError();
|
||||
|
||||
// Open modal and load categories
|
||||
modalInstance.show();
|
||||
loadCategories();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get selected items from batch selection system
|
||||
*/
|
||||
function getSelectedItems() {
|
||||
const items = [];
|
||||
|
||||
// Access the global batch selection state
|
||||
if (window.BatchSelection && window.BatchSelection.getSelectedItems) {
|
||||
const selectedItemsArray = window.BatchSelection.getSelectedItems();
|
||||
selectedItemsArray.forEach(itemKey => {
|
||||
const [type, id] = itemKey.split(':');
|
||||
items.push({ type, id: parseInt(id) });
|
||||
});
|
||||
} else {
|
||||
// Fallback: read from checkboxes directly
|
||||
document.querySelectorAll('.item-checkbox:checked').forEach(checkbox => {
|
||||
const type = checkbox.dataset.itemType;
|
||||
const id = parseInt(checkbox.dataset.itemId);
|
||||
items.push({ type, id });
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update selection info in modal
|
||||
*/
|
||||
function updateSelectionInfo(items) {
|
||||
selectedItemsCountSpan.textContent = items.length;
|
||||
|
||||
// Calculate breakdown
|
||||
const products = items.filter(i => i.type === 'product').length;
|
||||
const kits = items.filter(i => i.type === 'kit').length;
|
||||
|
||||
let breakdown = '';
|
||||
if (products > 0 && kits > 0) {
|
||||
breakdown = `(${products} товаров, ${kits} комплектов)`;
|
||||
} else if (products > 0) {
|
||||
breakdown = `(товары)`;
|
||||
} else if (kits > 0) {
|
||||
breakdown = `(комплекты)`;
|
||||
}
|
||||
|
||||
selectedItemsBreakdownSpan.textContent = breakdown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load categories from server
|
||||
*/
|
||||
async function loadCategories() {
|
||||
categoryListContainer.innerHTML = `
|
||||
<div class="text-center py-3">
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Загрузка...</span>
|
||||
</div>
|
||||
<div class="mt-2 text-muted">Загрузка категорий...</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
// Get active categories (we can reuse the filter endpoint or create a dedicated one)
|
||||
const response = await fetch('/products/api/search-products-variants/?type=category', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Не удалось загрузить категории');
|
||||
}
|
||||
|
||||
// For now, let's use a simpler approach - get categories from the page context
|
||||
// In production, you might want a dedicated API endpoint
|
||||
allCategories = await fetchCategoriesFromDOM();
|
||||
|
||||
renderCategoryList(allCategories);
|
||||
} catch (error) {
|
||||
console.error('Error loading categories:', error);
|
||||
categoryListContainer.innerHTML = `
|
||||
<div class="alert alert-warning">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
Не удалось загрузить категории.
|
||||
<a href="#" onclick="location.reload()">Обновите страницу</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch categories from the page's filter dropdown
|
||||
*/
|
||||
function fetchCategoriesFromDOM() {
|
||||
const categories = [];
|
||||
const categorySelect = document.getElementById('category');
|
||||
|
||||
if (categorySelect) {
|
||||
categorySelect.querySelectorAll('option').forEach(option => {
|
||||
if (option.value) { // Skip empty option
|
||||
categories.push({
|
||||
id: parseInt(option.value),
|
||||
name: option.textContent.trim()
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render category list with checkboxes
|
||||
*/
|
||||
function renderCategoryList(categories) {
|
||||
if (categories.length === 0) {
|
||||
categoryListContainer.innerHTML = `
|
||||
<div class="alert alert-info mb-0">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
Нет доступных категорий.
|
||||
<a href="/products/categories/create/" target="_blank">Создать категорию</a>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<div class="list-group list-group-flush">';
|
||||
|
||||
categories.forEach(category => {
|
||||
const isChecked = selectedCategoryIds.has(category.id);
|
||||
html += `
|
||||
<div class="list-group-item list-group-item-action p-2">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input category-checkbox"
|
||||
type="checkbox"
|
||||
value="${category.id}"
|
||||
id="category_${category.id}"
|
||||
${isChecked ? 'checked' : ''}>
|
||||
<label class="form-check-label w-100" for="category_${category.id}">
|
||||
${category.name}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
categoryListContainer.innerHTML = html;
|
||||
|
||||
// Add event listeners to checkboxes
|
||||
categoryListContainer.querySelectorAll('.category-checkbox').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', handleCategoryToggle);
|
||||
});
|
||||
|
||||
updateApplyButtonState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle category checkbox toggle
|
||||
*/
|
||||
function handleCategoryToggle(e) {
|
||||
const categoryId = parseInt(e.target.value);
|
||||
|
||||
if (e.target.checked) {
|
||||
selectedCategoryIds.add(categoryId);
|
||||
} else {
|
||||
selectedCategoryIds.delete(categoryId);
|
||||
}
|
||||
|
||||
updateApplyButtonState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle category search
|
||||
*/
|
||||
function handleCategorySearch(e) {
|
||||
const searchTerm = e.target.value.toLowerCase().trim();
|
||||
|
||||
if (searchTerm === '') {
|
||||
renderCategoryList(allCategories);
|
||||
} else {
|
||||
const filtered = allCategories.filter(cat =>
|
||||
cat.name.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
renderCategoryList(filtered);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update apply button state
|
||||
*/
|
||||
function updateApplyButtonState() {
|
||||
// Кнопка Применить активна только если выбрана хотя бы одна категория
|
||||
applyBtn.disabled = selectedCategoryIds.size === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle apply button click
|
||||
*/
|
||||
async function handleApply() {
|
||||
if (selectedCategoryIds.size === 0) {
|
||||
showError('Выберите хотя бы одну категорию');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedItems = getSelectedItems();
|
||||
if (selectedItems.length === 0) {
|
||||
showError('Нет выбранных товаров');
|
||||
return;
|
||||
}
|
||||
|
||||
// Определяем режим: если чекбокс включен - replace, иначе - add
|
||||
const actionMode = clearExistingToggle.checked ? 'replace' : 'add';
|
||||
|
||||
// Prepare request data
|
||||
const requestData = {
|
||||
items: selectedItems,
|
||||
category_ids: Array.from(selectedCategoryIds),
|
||||
action_mode: actionMode
|
||||
};
|
||||
|
||||
// Disable button and show loading
|
||||
applyBtn.disabled = true;
|
||||
applyBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Применение...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/products/api/bulk-update-categories/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Show success message
|
||||
showSuccessToast(result.message);
|
||||
|
||||
// Close modal
|
||||
modalInstance.hide();
|
||||
|
||||
// Optionally reload page to show updated categories
|
||||
// For v1, we'll just show message suggesting refresh
|
||||
if (result.updated_count > 0) {
|
||||
setTimeout(() => {
|
||||
if (confirm('Категории обновлены. Обновить страницу для отображения изменений?')) {
|
||||
location.reload();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
} else {
|
||||
showError(result.message || 'Не удалось обновить категории');
|
||||
|
||||
// Show skipped reasons if any
|
||||
if (result.skipped_reasons && result.skipped_reasons.length > 0) {
|
||||
console.warn('Skipped items:', result.skipped_reasons);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error applying bulk categories:', error);
|
||||
showError('Произошла ошибка при обновлении категорий. Попробуйте снова.');
|
||||
} finally {
|
||||
// Reset button
|
||||
applyBtn.innerHTML = '<i class="bi bi-check2-circle"></i> Применить';
|
||||
applyBtn.disabled = false;
|
||||
updateApplyButtonState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle clear all categories button click
|
||||
*/
|
||||
async function handleClearAll() {
|
||||
const selectedItems = getSelectedItems();
|
||||
if (selectedItems.length === 0) {
|
||||
showError('Нет выбранных товаров');
|
||||
return;
|
||||
}
|
||||
|
||||
// Подтверждение опасной операции
|
||||
if (!confirm(`Вы уверены, что хотите удалить ВСЕ категории у ${selectedItems.length} выбранных товаров?\n\nЭто действие нельзя отменить!`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Отправляем запрос с пустым списком категорий и режимом replace
|
||||
const requestData = {
|
||||
items: selectedItems,
|
||||
category_ids: [], // Пустой список = очистить все
|
||||
action_mode: 'clear' // Специальный режим
|
||||
};
|
||||
|
||||
// Disable button and show loading
|
||||
clearAllBtn.disabled = true;
|
||||
clearAllBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Очистка...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/products/api/bulk-update-categories/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showSuccessToast(result.message || `Категории удалены у ${result.updated_count} товаров`);
|
||||
modalInstance.hide();
|
||||
|
||||
setTimeout(() => {
|
||||
if (confirm('Категории очищены. Обновить страницу?')) {
|
||||
location.reload();
|
||||
}
|
||||
}, 500);
|
||||
} else {
|
||||
showError(result.message || 'Не удалось очистить категории');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error clearing categories:', error);
|
||||
showError('Произошла ошибка. Попробуйте снова.');
|
||||
} finally {
|
||||
clearAllBtn.innerHTML = '<i class="bi bi-trash3"></i> Очистить все категории';
|
||||
clearAllBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error message in modal
|
||||
*/
|
||||
function showError(message) {
|
||||
errorAlert.textContent = message;
|
||||
errorAlert.classList.remove('d-none');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide error message
|
||||
*/
|
||||
function hideError() {
|
||||
errorAlert.classList.add('d-none');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show success toast notification
|
||||
*/
|
||||
function showSuccessToast(message) {
|
||||
// Create toast container if it doesn't exist
|
||||
let toastContainer = document.querySelector('.toast-container');
|
||||
if (!toastContainer) {
|
||||
toastContainer = document.createElement('div');
|
||||
toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';
|
||||
toastContainer.style.zIndex = '11000';
|
||||
document.body.appendChild(toastContainer);
|
||||
}
|
||||
|
||||
// Create toast
|
||||
const toastId = 'toast_' + Date.now();
|
||||
const toastHtml = `
|
||||
<div id="${toastId}" class="toast align-items-center text-white bg-success border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
<i class="bi bi-check-circle me-2"></i>${message}
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
|
||||
|
||||
const toastElement = document.getElementById(toastId);
|
||||
const toast = new bootstrap.Toast(toastElement, {
|
||||
autohide: true,
|
||||
delay: 5000
|
||||
});
|
||||
|
||||
toast.show();
|
||||
|
||||
// Remove toast element after it's hidden
|
||||
toastElement.addEventListener('hidden.bs.toast', () => {
|
||||
toastElement.remove();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset modal state
|
||||
*/
|
||||
function resetModalState() {
|
||||
selectedCategoryIds.clear();
|
||||
categorySearchInput.value = '';
|
||||
hideError();
|
||||
|
||||
// Сбрасываем чекбокс очистки
|
||||
clearExistingToggle.checked = false;
|
||||
|
||||
updateApplyButtonState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CSRF token from cookie
|
||||
*/
|
||||
function getCsrfToken() {
|
||||
const name = 'csrftoken';
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
// Initialize on DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user