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:
2026-01-10 11:41:37 +03:00
parent 0d6d62d1ad
commit 5f565555e3
4 changed files with 136 additions and 126 deletions

View File

@@ -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(

View File

@@ -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'],

View File

@@ -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();
} }

View File

@@ -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 %}