fix(products): improve bulk category modal behavior and initialization
- Move default date initialization from form to view initial data - Add checks for DOM elements existence before event listener attachment - Handle 'clear' mode in bulk category modal with confirmation and API call - Improve CSRF token usage and error handling during bulk update requests - Remove deprecated handleClearAll function and integrate logic into handleApply - Reset modal state properly including input fields and radio button modes - Update modal JS file version reference in products list template
This commit is contained in:
@@ -356,11 +356,6 @@ class IncomingDocumentForm(forms.ModelForm):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['warehouse'].queryset = Warehouse.objects.filter(is_active=True)
|
self.fields['warehouse'].queryset = Warehouse.objects.filter(is_active=True)
|
||||||
|
|
||||||
# Устанавливаем дату по умолчанию - сегодня
|
|
||||||
if not self.initial.get('date'):
|
|
||||||
from django.utils import timezone
|
|
||||||
self.initial['date'] = timezone.now().date()
|
|
||||||
|
|
||||||
# Если есть склад по умолчанию - предвыбираем его
|
# Если есть склад по умолчанию - предвыбираем его
|
||||||
if not self.initial.get('warehouse'):
|
if not self.initial.get('warehouse'):
|
||||||
default_warehouse = Warehouse.objects.filter(
|
default_warehouse = Warehouse.objects.filter(
|
||||||
|
|||||||
@@ -35,6 +35,13 @@ class IncomingDocumentCreateView(LoginRequiredMixin, CreateView):
|
|||||||
form_class = IncomingDocumentForm
|
form_class = IncomingDocumentForm
|
||||||
template_name = 'inventory/incoming_document/incoming_document_form.html'
|
template_name = 'inventory/incoming_document/incoming_document_form.html'
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
"""Устанавливаем начальные значения для формы"""
|
||||||
|
initial = super().get_initial()
|
||||||
|
from django.utils import timezone
|
||||||
|
initial['date'] = timezone.now().date()
|
||||||
|
return initial
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
document = IncomingDocumentService.create_document(
|
document = IncomingDocumentService.create_document(
|
||||||
warehouse=form.cleaned_data['warehouse'],
|
warehouse=form.cleaned_data['warehouse'],
|
||||||
|
|||||||
@@ -37,13 +37,18 @@
|
|||||||
|
|
||||||
// Event listeners
|
// Event listeners
|
||||||
bulkCategoryAction.addEventListener('click', handleOpenModal);
|
bulkCategoryAction.addEventListener('click', handleOpenModal);
|
||||||
|
|
||||||
|
if (applyBtn) {
|
||||||
applyBtn.addEventListener('click', handleApply);
|
applyBtn.addEventListener('click', handleApply);
|
||||||
|
}
|
||||||
|
|
||||||
if (categorySearchInput) {
|
if (categorySearchInput) {
|
||||||
categorySearchInput.addEventListener('input', handleCategorySearch);
|
categorySearchInput.addEventListener('input', handleCategorySearch);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for mode changes
|
// Listen for mode changes
|
||||||
const modeRadios = document.querySelectorAll('input[name="bulkCategoryMode"]');
|
const modeRadios = document.querySelectorAll('input[name="bulkCategoryMode"]');
|
||||||
|
if (modeRadios.length > 0) {
|
||||||
modeRadios.forEach(radio => {
|
modeRadios.forEach(radio => {
|
||||||
radio.addEventListener('change', () => {
|
radio.addEventListener('change', () => {
|
||||||
hideError();
|
hideError();
|
||||||
@@ -51,13 +56,16 @@
|
|||||||
updateApplyButtonState();
|
updateApplyButtonState();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Listen for modal close to reset state
|
// Listen for modal close to reset state
|
||||||
modal.addEventListener('hidden.bs.modal', resetModalState);
|
modal.addEventListener('hidden.bs.modal', resetModalState);
|
||||||
|
|
||||||
// Initial UI state
|
// Initial UI state (only if elements exist)
|
||||||
|
if (modeRadios.length > 0) {
|
||||||
updateModeUI();
|
updateModeUI();
|
||||||
updateApplyButtonState();
|
updateApplyButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Bulk category modal initialized');
|
console.log('Bulk category modal initialized');
|
||||||
}
|
}
|
||||||
@@ -317,6 +325,10 @@
|
|||||||
* Update apply button state
|
* Update apply button state
|
||||||
*/
|
*/
|
||||||
function updateApplyButtonState() {
|
function updateApplyButtonState() {
|
||||||
|
if (!applyBtn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const mode = getCurrentMode();
|
const mode = getCurrentMode();
|
||||||
|
|
||||||
if (mode === 'clear') {
|
if (mode === 'clear') {
|
||||||
@@ -332,28 +344,111 @@
|
|||||||
* Handle apply button click
|
* Handle apply button click
|
||||||
*/
|
*/
|
||||||
async function handleApply() {
|
async function handleApply() {
|
||||||
if (selectedCategoryIds.size === 0) {
|
const mode = getCurrentMode();
|
||||||
showError('Выберите хотя бы одну категорию');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedItems = getSelectedItems();
|
const selectedItems = getSelectedItems();
|
||||||
|
|
||||||
if (selectedItems.length === 0) {
|
if (selectedItems.length === 0) {
|
||||||
showError('Нет выбранных товаров');
|
showError('Нет выбранных товаров');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Определяем режим: если чекбокс включен - replace, иначе - add
|
// Режим очистки категорий
|
||||||
const actionMode = clearExistingToggle.checked ? 'replace' : 'add';
|
if (mode === 'clear') {
|
||||||
|
const confirmed = confirm(
|
||||||
|
`Вы уверены, что хотите удалить ВСЕ категории у ${selectedItems.length} выбранных товаров?\n\nЭто действие нельзя отменить!`
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const csrfToken = getCsrfToken();
|
||||||
|
if (!csrfToken) {
|
||||||
|
showError('CSRF токен не найден. Обновите страницу и попробуйте снова.');
|
||||||
|
console.error('CSRF token not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestData = {
|
||||||
|
items: selectedItems,
|
||||||
|
category_ids: [],
|
||||||
|
action_mode: 'clear'
|
||||||
|
};
|
||||||
|
|
||||||
|
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': csrfToken
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestData)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
let errorMessage = 'Ошибка сервера';
|
||||||
|
try {
|
||||||
|
const responseText = await response.text();
|
||||||
|
try {
|
||||||
|
const errorData = JSON.parse(responseText);
|
||||||
|
errorMessage = errorData.message || errorData.error || errorMessage;
|
||||||
|
} catch (jsonError) {
|
||||||
|
console.error('Server error response:', responseText);
|
||||||
|
errorMessage = `Ошибка ${response.status}: ${response.statusText}`;
|
||||||
|
if (response.status === 403) {
|
||||||
|
errorMessage = 'Доступ запрещён. Возможно, проблема с CSRF токеном. Обновите страницу и попробуйте снова.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (readError) {
|
||||||
|
console.error('Error reading response:', readError);
|
||||||
|
errorMessage = `Ошибка ${response.status}: ${response.statusText}`;
|
||||||
|
}
|
||||||
|
showError(errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 applying clear categories:', error);
|
||||||
|
showError('Произошла ошибка. Попробуйте снова. Ошибка: ' + error.message);
|
||||||
|
} finally {
|
||||||
|
applyBtn.innerHTML = '<i class="bi bi-check2-circle"></i> Применить';
|
||||||
|
applyBtn.disabled = false;
|
||||||
|
updateApplyButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Режим add/replace
|
||||||
|
if (selectedCategoryIds.size === 0) {
|
||||||
|
showError('Выберите хотя бы одну категорию');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionMode = mode === 'replace' ? 'replace' : 'add';
|
||||||
|
|
||||||
// Prepare request data
|
|
||||||
const requestData = {
|
const requestData = {
|
||||||
items: selectedItems,
|
items: selectedItems,
|
||||||
category_ids: Array.from(selectedCategoryIds),
|
category_ids: Array.from(selectedCategoryIds),
|
||||||
action_mode: actionMode
|
action_mode: actionMode
|
||||||
};
|
};
|
||||||
|
|
||||||
// Проверка CSRF токена
|
|
||||||
const csrfToken = getCsrfToken();
|
const csrfToken = getCsrfToken();
|
||||||
if (!csrfToken) {
|
if (!csrfToken) {
|
||||||
showError('CSRF токен не найден. Обновите страницу и попробуйте снова.');
|
showError('CSRF токен не найден. Обновите страницу и попробуйте снова.');
|
||||||
@@ -372,7 +467,7 @@
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-CSRFToken': getCsrfToken()
|
'X-CSRFToken': csrfToken
|
||||||
},
|
},
|
||||||
body: JSON.stringify(requestData)
|
body: JSON.stringify(requestData)
|
||||||
});
|
});
|
||||||
@@ -440,101 +535,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверка CSRF токена
|
|
||||||
const csrfToken = getCsrfToken();
|
|
||||||
if (!csrfToken) {
|
|
||||||
showError('CSRF токен не найден. Обновите страницу и попробуйте снова.');
|
|
||||||
console.error('CSRF token not found');
|
|
||||||
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>Очистка...';
|
|
||||||
|
|
||||||
console.log('Sending clear categories request:', requestData);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/products/api/bulk-update-categories/', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': getCsrfToken()
|
|
||||||
},
|
|
||||||
body: JSON.stringify(requestData)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Проверяем статус ответа
|
|
||||||
if (!response.ok) {
|
|
||||||
let errorMessage = 'Ошибка сервера';
|
|
||||||
try {
|
|
||||||
// Читаем тело один раз как текст
|
|
||||||
const responseText = await response.text();
|
|
||||||
// Пытаемся распарсить как JSON
|
|
||||||
try {
|
|
||||||
const errorData = JSON.parse(responseText);
|
|
||||||
errorMessage = errorData.message || errorData.error || errorMessage;
|
|
||||||
} catch (jsonError) {
|
|
||||||
// Если не JSON, используем текст как есть или формируем сообщение
|
|
||||||
console.error('Server error response:', responseText);
|
|
||||||
errorMessage = `Ошибка ${response.status}: ${response.statusText}`;
|
|
||||||
if (response.status === 403) {
|
|
||||||
errorMessage = 'Доступ запрещён. Возможно, проблема с CSRF токеном. Обновите страницу и попробуйте снова.';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (readError) {
|
|
||||||
console.error('Error reading response:', readError);
|
|
||||||
errorMessage = `Ошибка ${response.status}: ${response.statusText}`;
|
|
||||||
}
|
|
||||||
showError(errorMessage);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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('Произошла ошибка. Попробуйте снова. Ошибка: ' + error.message);
|
|
||||||
} finally {
|
|
||||||
clearAllBtn.innerHTML = '<i class="bi bi-trash3"></i> Очистить все категории';
|
|
||||||
clearAllBtn.disabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show error message in modal
|
* Show error message in modal
|
||||||
*/
|
*/
|
||||||
@@ -597,12 +597,20 @@
|
|||||||
*/
|
*/
|
||||||
function resetModalState() {
|
function resetModalState() {
|
||||||
selectedCategoryIds.clear();
|
selectedCategoryIds.clear();
|
||||||
|
|
||||||
|
if (categorySearchInput) {
|
||||||
categorySearchInput.value = '';
|
categorySearchInput.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
hideError();
|
hideError();
|
||||||
|
|
||||||
// Сбрасываем чекбокс очистки
|
// Сбрасываем режим на "добавить"
|
||||||
clearExistingToggle.checked = false;
|
const addModeRadio = document.getElementById('bulkCategoryModeAdd');
|
||||||
|
if (addModeRadio) {
|
||||||
|
addModeRadio.checked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateModeUI();
|
||||||
updateApplyButtonState();
|
updateApplyButtonState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -486,5 +486,5 @@
|
|||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
<script src="{% static 'products/js/batch-selection.js' %}?v=1.2"></script>
|
<script src="{% static 'products/js/batch-selection.js' %}?v=1.2"></script>
|
||||||
<script src="{% static 'products/js/bulk-category-modal.js' %}?v=1.4"></script>
|
<script src="{% static 'products/js/bulk-category-modal.js' %}?v=1.6"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user