diff --git a/myproject/orders/static/orders/js/draft-creator.js b/myproject/orders/static/orders/js/draft-creator.js new file mode 100644 index 0000000..7f6549e --- /dev/null +++ b/myproject/orders/static/orders/js/draft-creator.js @@ -0,0 +1,422 @@ +/** + * Модуль автоматического создания черновика заказа. + * + * При первом изменении формы создания заказа автоматически создаёт черновик + * и перенаправляет пользователя на страницу редактирования черновика, + * где уже работает обычное автосохранение. + */ + +(function() { + 'use strict'; + + // Конфигурация + const CONFIG = { + CREATE_DRAFT_URL: '/orders/create-draft/', + DEBOUNCE_DELAY: 2000, // Задержка перед созданием черновика (мс) + }; + + // Состояние модуля + let createDraftTimer = null; + let isCreatingDraft = false; + let draftCreated = false; + + /** + * Инициализация модуля + */ + function init() { + // Проверяем, что мы на странице создания заказа + const isCreatePage = window.location.pathname.includes('/orders/create/'); + if (!isCreatePage) { + return; + } + + const orderForm = document.querySelector('form[action*="create"]'); + if (!orderForm) { + return; + } + + console.log('[DraftCreator] Initialized on order create page'); + + // Добавляем обработчики событий + attachEventListeners(); + } + + /** + * Прикрепляет обработчики событий к полям формы + */ + function attachEventListeners() { + const form = document.querySelector('form[action*="create"]'); + if (!form) { + return; + } + + // Слушаем изменения в поле клиента (обязательное поле) + const customerField = form.querySelector('select[name="customer"]'); + if (customerField) { + customerField.addEventListener('change', function() { + if (this.value && !draftCreated) { + scheduleCreateDraft(); + } + }); + } + + // Слушаем изменения в других основных полях + const fieldsToWatch = [ + 'input[name="delivery_date"]', + 'input[name="delivery_time_start"]', + 'input[name="delivery_time_end"]', + 'select[name="payment_method"]', + 'textarea[name="special_instructions"]', + 'input[type="checkbox"]', + 'select[name="delivery_address"]', + 'select[name="pickup_shop"]', + ]; + + fieldsToWatch.forEach(selector => { + const fields = form.querySelectorAll(selector); + fields.forEach(field => { + const eventType = (field.tagName === 'SELECT' || field.type === 'checkbox') ? 'change' : 'input'; + field.addEventListener(eventType, function() { + // Создаём черновик только если выбран клиент + const customer = form.querySelector('select[name="customer"]'); + if (customer && customer.value && !draftCreated) { + scheduleCreateDraft(); + } + }); + }); + }); + + // Слушаем изменения в формах товаров + observeFormsetChanges(); + } + + /** + * Наблюдает за изменениями в формсете товаров + */ + function observeFormsetChanges() { + const formsetContainer = document.getElementById('order-items-formset'); + if (!formsetContainer) { + return; + } + + // Наблюдаем за добавлением новых форм + const observer = new MutationObserver(() => { + attachFormsetEventListeners(); + }); + + observer.observe(formsetContainer, { + childList: true, + subtree: true + }); + + // Прикрепляем обработчики к существующим формам + attachFormsetEventListeners(); + } + + /** + * Прикрепляет обработчики к полям в формах товаров + */ + function attachFormsetEventListeners() { + const itemForms = document.querySelectorAll('.order-item-form'); + + itemForms.forEach(form => { + // Если уже прикреплены обработчики, пропускаем + if (form.dataset.draftCreatorAttached === 'true') { + return; + } + + const fields = form.querySelectorAll('select, input[type="number"]'); + fields.forEach(field => { + const eventType = field.tagName === 'SELECT' ? 'change' : 'input'; + field.addEventListener(eventType, function() { + const customerField = document.querySelector('select[name="customer"]'); + if (customerField && customerField.value && !draftCreated) { + scheduleCreateDraft(); + } + }); + }); + + form.dataset.draftCreatorAttached = 'true'; + }); + } + + /** + * Планирует создание черновика с задержкой (debouncing) + */ + function scheduleCreateDraft() { + // Отменяем предыдущий таймер + if (createDraftTimer) { + clearTimeout(createDraftTimer); + } + + // Устанавливаем новый таймер + createDraftTimer = setTimeout(() => { + createDraft(); + }, CONFIG.DEBOUNCE_DELAY); + + console.log('[DraftCreator] Scheduled draft creation in ' + CONFIG.DEBOUNCE_DELAY + 'ms'); + } + + /** + * Создаёт черновик заказа + */ + async function createDraft() { + if (isCreatingDraft || draftCreated) { + console.log('[DraftCreator] Already creating or created, skipping'); + return; + } + + isCreatingDraft = true; + console.log('[DraftCreator] Creating draft...'); + + try { + // Собираем данные формы + const formData = collectFormData(); + + // Проверяем наличие клиента + if (!formData.customer) { + console.log('[DraftCreator] No customer selected, skipping'); + isCreatingDraft = false; + return; + } + + // Отправляем AJAX запрос + const response = await fetch(CONFIG.CREATE_DRAFT_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCsrfToken(), + }, + body: JSON.stringify(formData) + }); + + const data = await response.json(); + + if (response.ok && data.success) { + console.log('[DraftCreator] Draft created successfully:', data); + draftCreated = true; + + // Показываем уведомление + showNotification('Черновик создан. Перенаправление...'); + + // Перенаправляем на страницу редактирования черновика + setTimeout(() => { + window.location.href = data.redirect_url; + }, 500); + + } else { + console.error('[DraftCreator] Error creating draft:', data); + + // Если ошибка не критичная (например, клиент не выбран), не показываем + if (response.status !== 400) { + showNotification('Ошибка создания черновика: ' + (data.error || 'Неизвестная ошибка'), 'error'); + } + } + + } catch (error) { + console.error('[DraftCreator] Exception:', error); + showNotification('Ошибка соединения с сервером', 'error'); + } finally { + isCreatingDraft = false; + } + } + + /** + * Собирает данные формы для отправки + */ + function collectFormData() { + const form = document.querySelector('form[action*="create"]'); + const data = {}; + + // Основные поля заказа + const customerField = form.querySelector('select[name="customer"]'); + if (customerField && customerField.value) { + data.customer = parseInt(customerField.value); + } + + const deliveryDateField = form.querySelector('input[name="delivery_date"]'); + if (deliveryDateField && deliveryDateField.value) { + data.delivery_date = deliveryDateField.value; + } + + const deliveryTimeStartField = form.querySelector('input[name="delivery_time_start"]'); + if (deliveryTimeStartField && deliveryTimeStartField.value) { + data.delivery_time_start = deliveryTimeStartField.value; + } + + const deliveryTimeEndField = form.querySelector('input[name="delivery_time_end"]'); + if (deliveryTimeEndField && deliveryTimeEndField.value) { + data.delivery_time_end = deliveryTimeEndField.value; + } + + const deliveryCostField = form.querySelector('input[name="delivery_cost"]'); + if (deliveryCostField && deliveryCostField.value) { + data.delivery_cost = deliveryCostField.value; + } + + const paymentMethodField = form.querySelector('select[name="payment_method"]'); + if (paymentMethodField && paymentMethodField.value) { + data.payment_method = paymentMethodField.value; + } + + const specialInstructionsField = form.querySelector('textarea[name="special_instructions"]'); + if (specialInstructionsField) { + data.special_instructions = specialInstructionsField.value; + } + + const discountAmountField = form.querySelector('input[name="discount_amount"]'); + if (discountAmountField && discountAmountField.value) { + data.discount_amount = discountAmountField.value; + } + + // Checkbox поля + const isDeliveryField = form.querySelector('input[name="is_delivery"]'); + if (isDeliveryField) { + data.is_delivery = isDeliveryField.checked; + } + + const customerIsRecipientField = form.querySelector('input[name="customer_is_recipient"]'); + if (customerIsRecipientField) { + data.customer_is_recipient = customerIsRecipientField.checked; + } + + const isAnonymousField = form.querySelector('input[name="is_anonymous"]'); + if (isAnonymousField) { + data.is_anonymous = isAnonymousField.checked; + } + + // Адрес доставки или точка самовывоза + const deliveryAddressField = form.querySelector('select[name="delivery_address"]'); + if (deliveryAddressField && deliveryAddressField.value) { + data.delivery_address = parseInt(deliveryAddressField.value); + } + + const pickupShopField = form.querySelector('select[name="pickup_shop"]'); + if (pickupShopField && pickupShopField.value) { + data.pickup_shop = parseInt(pickupShopField.value); + } + + // Собираем позиции заказа + const items = collectOrderItems(); + if (items.length > 0) { + data.items = items; + } + + return data; + } + + /** + * Собирает данные о позициях заказа + */ + function collectOrderItems() { + const items = []; + const itemForms = document.querySelectorAll('.order-item-form'); + + itemForms.forEach((form, index) => { + // Пропускаем удаленные формы + const deleteCheckbox = form.querySelector('input[name$="-DELETE"]'); + if (deleteCheckbox && deleteCheckbox.checked) { + return; + } + + // Получаем выбранный товар/комплект + const itemSelect = form.querySelector('.select2-order-item'); + if (!itemSelect || !itemSelect.value) { + return; + } + + const itemValue = itemSelect.value; + const quantityInput = form.querySelector('input[name$="-quantity"]'); + const priceInput = form.querySelector('input[name$="-price"]'); + + if (!quantityInput || !priceInput) { + return; + } + + const item = { + quantity: quantityInput.value || '1', + price: priceInput.value || '0' + }; + + // Определяем тип: товар или комплект + if (itemValue.startsWith('product_')) { + item.product_id = parseInt(itemValue.replace('product_', '')); + } else if (itemValue.startsWith('kit_')) { + item.product_kit_id = parseInt(itemValue.replace('kit_', '')); + } + + items.push(item); + }); + + return items; + } + + /** + * Получает CSRF токен + */ + function getCsrfToken() { + // Пробуем получить из cookie + 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; + } + } + } + + // Если не нашли в cookie, пробуем в input поле + if (!cookieValue) { + const csrfInput = document.querySelector('input[name="csrfmiddlewaretoken"]'); + if (csrfInput) { + cookieValue = csrfInput.value; + } + } + + return cookieValue; + } + + /** + * Показывает уведомление пользователю + */ + function showNotification(message, type = 'info') { + // Создаём простое уведомление + const notification = document.createElement('div'); + notification.className = `alert alert-${type === 'error' ? 'danger' : 'info'}`; + notification.style.cssText = ` + position: fixed; + top: 70px; + right: 20px; + z-index: 1050; + min-width: 250px; + padding: 10px 15px; + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0,0,0,0.15); + `; + notification.innerHTML = ` +
+ ${type === 'error' ? '⚠️' : 'ℹ️'} + ${message} +
+ `; + + document.body.appendChild(notification); + + // Автоматически удаляем через 3 секунды + setTimeout(() => { + notification.remove(); + }, 3000); + } + + // Инициализация при загрузке DOM + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } + +})(); diff --git a/myproject/orders/templates/orders/order_form.html b/myproject/orders/templates/orders/order_form.html index 5ad0c39..a4edb9c 100644 --- a/myproject/orders/templates/orders/order_form.html +++ b/myproject/orders/templates/orders/order_form.html @@ -885,8 +885,12 @@ document.addEventListener('DOMContentLoaded', function() { - + {% if is_draft %} - + + +{% else %} + + {% endif %} {% endblock %} diff --git a/myproject/orders/urls.py b/myproject/orders/urls.py index 7114d2c..c10bef6 100644 --- a/myproject/orders/urls.py +++ b/myproject/orders/urls.py @@ -13,6 +13,7 @@ urlpatterns = [ # AJAX endpoints path('/autosave/', views.autosave_draft_order, name='order-autosave'), + path('create-draft/', views.create_draft_from_form, name='order-create-draft'), # Временные комплекты path('temporary-kits/create/', views.create_temporary_kit, name='temporary-kit-create'), diff --git a/myproject/orders/views.py b/myproject/orders/views.py index c354ead..9d5b14b 100644 --- a/myproject/orders/views.py +++ b/myproject/orders/views.py @@ -284,6 +284,104 @@ def autosave_draft_order(request, pk): }, status=500) +@require_http_methods(["POST"]) +@login_required +def create_draft_from_form(request): + """ + AJAX endpoint для создания черновика заказа из формы создания. + + Используется для автоматического создания черновика при первом изменении формы. + После создания возвращает ID черновика для перенаправления. + + Пример запроса: + { + "customer": 1, + "is_delivery": true, + "delivery_date": "2024-01-15" + } + + Ответ при успехе: + { + "success": true, + "order_id": 123, + "order_number": "ORD-000123", + "redirect_url": "/orders/123/edit/" + } + """ + try: + data = json.loads(request.body) + + # Получаем обязательное поле - клиента + customer_id = data.get('customer') + if not customer_id: + return JsonResponse({ + 'success': False, + 'error': 'Необходимо выбрать клиента' + }, status=400) + + from customers.models import Customer + try: + customer = Customer.objects.get(pk=customer_id) + except Customer.DoesNotExist: + return JsonResponse({ + 'success': False, + 'error': 'Клиент не найден' + }, status=404) + + # Создаем черновик через DraftOrderService + order = DraftOrderService.create_draft( + user=request.user, + customer=customer, + data=data + ) + + # Обрабатываем позиции заказа, если они переданы + if 'items' in data: + for item_data in data['items']: + product_id = item_data.get('product_id') + product_kit_id = item_data.get('product_kit_id') + quantity = item_data.get('quantity') + price = item_data.get('price') + + if product_id: + DraftOrderService.add_item_to_draft( + order_id=order.pk, + product_id=product_id, + quantity=quantity, + price=price + ) + elif product_kit_id: + DraftOrderService.add_item_to_draft( + order_id=order.pk, + product_kit_id=product_kit_id, + quantity=quantity, + price=price + ) + + return JsonResponse({ + 'success': True, + 'order_id': order.pk, + 'order_number': order.order_number, + 'redirect_url': f'/orders/{order.pk}/edit/' + }) + + except ValidationError as e: + return JsonResponse({ + 'success': False, + 'error': str(e) + }, status=400) + except json.JSONDecodeError: + return JsonResponse({ + 'success': False, + 'error': 'Некорректный JSON' + }, status=400) + except Exception as e: + return JsonResponse({ + 'success': False, + 'error': f'Ошибка сервера: {str(e)}' + }, status=500) + + # === ВРЕМЕННЫЕ КОМПЛЕКТЫ === @require_http_methods(["POST"])