From 5f565555e3853b2b9f538276d9315ed6bd8abed9 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Sat, 10 Jan 2026 11:41:37 +0300 Subject: [PATCH] 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 --- myproject/inventory/forms.py | 5 - .../inventory/views/incoming_document.py | 7 + .../static/products/js/bulk-category-modal.js | 248 +++++++++--------- .../templates/products/products_list.html | 2 +- 4 files changed, 136 insertions(+), 126 deletions(-) diff --git a/myproject/inventory/forms.py b/myproject/inventory/forms.py index 19fe023..7d2af7e 100644 --- a/myproject/inventory/forms.py +++ b/myproject/inventory/forms.py @@ -356,11 +356,6 @@ class IncomingDocumentForm(forms.ModelForm): super().__init__(*args, **kwargs) 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'): default_warehouse = Warehouse.objects.filter( diff --git a/myproject/inventory/views/incoming_document.py b/myproject/inventory/views/incoming_document.py index 1895dcd..1012b12 100644 --- a/myproject/inventory/views/incoming_document.py +++ b/myproject/inventory/views/incoming_document.py @@ -35,6 +35,13 @@ class IncomingDocumentCreateView(LoginRequiredMixin, CreateView): form_class = IncomingDocumentForm 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): document = IncomingDocumentService.create_document( warehouse=form.cleaned_data['warehouse'], diff --git a/myproject/products/static/products/js/bulk-category-modal.js b/myproject/products/static/products/js/bulk-category-modal.js index d410644..bb2e7e3 100644 --- a/myproject/products/static/products/js/bulk-category-modal.js +++ b/myproject/products/static/products/js/bulk-category-modal.js @@ -37,27 +37,35 @@ // Event listeners bulkCategoryAction.addEventListener('click', handleOpenModal); - applyBtn.addEventListener('click', handleApply); + + if (applyBtn) { + applyBtn.addEventListener('click', handleApply); + } + if (categorySearchInput) { categorySearchInput.addEventListener('input', handleCategorySearch); } // Listen for mode changes const modeRadios = document.querySelectorAll('input[name="bulkCategoryMode"]'); - modeRadios.forEach(radio => { - radio.addEventListener('change', () => { - hideError(); - updateModeUI(); - updateApplyButtonState(); + if (modeRadios.length > 0) { + modeRadios.forEach(radio => { + radio.addEventListener('change', () => { + hideError(); + updateModeUI(); + updateApplyButtonState(); + }); }); - }); + } // Listen for modal close to reset state modal.addEventListener('hidden.bs.modal', resetModalState); - // Initial UI state - updateModeUI(); - updateApplyButtonState(); + // Initial UI state (only if elements exist) + if (modeRadios.length > 0) { + updateModeUI(); + updateApplyButtonState(); + } console.log('Bulk category modal initialized'); } @@ -317,6 +325,10 @@ * Update apply button state */ function updateApplyButtonState() { + if (!applyBtn) { + return; + } + const mode = getCurrentMode(); if (mode === 'clear') { @@ -332,28 +344,111 @@ * Handle apply button click */ async function handleApply() { - if (selectedCategoryIds.size === 0) { - showError('Выберите хотя бы одну категорию'); - return; - } - + const mode = getCurrentMode(); const selectedItems = getSelectedItems(); + if (selectedItems.length === 0) { showError('Нет выбранных товаров'); 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 = 'Применение...'; + + 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 = ' Применить'; + applyBtn.disabled = false; + updateApplyButtonState(); + } + + return; + } + + // Режим add/replace + if (selectedCategoryIds.size === 0) { + showError('Выберите хотя бы одну категорию'); + return; + } + + const actionMode = mode === 'replace' ? 'replace' : 'add'; - // Prepare request data const requestData = { items: selectedItems, category_ids: Array.from(selectedCategoryIds), action_mode: actionMode }; - // Проверка CSRF токена const csrfToken = getCsrfToken(); if (!csrfToken) { showError('CSRF токен не найден. Обновите страницу и попробуйте снова.'); @@ -372,7 +467,7 @@ method: 'POST', headers: { 'Content-Type': 'application/json', - 'X-CSRFToken': getCsrfToken() + 'X-CSRFToken': csrfToken }, 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 = 'Очистка...'; - - 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 = ' Очистить все категории'; - clearAllBtn.disabled = false; - } - } - /** * Show error message in modal */ @@ -597,12 +597,20 @@ */ function resetModalState() { selectedCategoryIds.clear(); - categorySearchInput.value = ''; + + if (categorySearchInput) { + categorySearchInput.value = ''; + } + hideError(); - - // Сбрасываем чекбокс очистки - clearExistingToggle.checked = false; - + + // Сбрасываем режим на "добавить" + const addModeRadio = document.getElementById('bulkCategoryModeAdd'); + if (addModeRadio) { + addModeRadio.checked = true; + } + + updateModeUI(); updateApplyButtonState(); } diff --git a/myproject/products/templates/products/products_list.html b/myproject/products/templates/products/products_list.html index 3d88b79..8c93f41 100644 --- a/myproject/products/templates/products/products_list.html +++ b/myproject/products/templates/products/products_list.html @@ -486,5 +486,5 @@ {% block extra_js %} {% load static %} - + {% endblock %}