diff --git a/myproject/orders/static/orders/js/customer_select2.js b/myproject/orders/static/orders/js/customer_select2.js new file mode 100644 index 0000000..074daea --- /dev/null +++ b/myproject/orders/static/orders/js/customer_select2.js @@ -0,0 +1,445 @@ +/** + * Customer Select2 Widget + * Переиспользуемый модуль для выбора и создания клиентов + * + * Использование: + * 1. Автоматическая инициализация: добавьте класс 'customer-select2-auto' и data-атрибуты + * 2. Ручная инициализация: вызовите window.initCustomerSelect2(element, options) + */ + +(function(window) { + 'use strict'; + + // ========== СЕКЦИЯ 1: Утилиты ========== + + /** + * Получить CSRF токен из meta-тега или cookie + */ + function getCSRFToken() { + // Сначала пробуем meta тег + const meta = document.querySelector('meta[name="csrf-token"]'); + if (meta) return meta.content; + + // Fallback на cookie + const cookies = document.cookie.split(';'); + for (let cookie of cookies) { + const [key, value] = cookie.trim().split('='); + if (key === 'csrftoken') return value; + } + return ''; + } + + // ========== СЕКЦИЯ 2: Форматирование результатов ========== + + function formatCustomerOption(option) { + // Опция "Создать нового клиента" + if (option.is_create_option || option.id === '__create_new__') { + return '
' + + ' ' + option.text + + '
'; + } + + if (!option.id) { + return option.text; + } + + let html = '
'; + html += '' + option.name + ''; + if (option.phone) { + html += '
Телефон: ' + option.phone + ''; + } + if (option.email) { + html += '
Email: ' + option.email + ''; + } + html += '
'; + return html; + } + + function formatCustomerSelection(option) { + if (option.is_create_option || option.id === '__create_new__') { + return option.text; + } + if (!option.id) { + return option.text; + } + return option.name || option.text; + } + + // ========== СЕКЦИЯ 3: Парсинг данных ========== + + /** + * Парсит текст поиска и определяет тип данных (email, телефон или имя) + */ + function parseSearchText(searchText) { + if (!searchText) return { name: '', phone: '', email: '' }; + + const phoneRegex = /[\d\s\-\+\(\)]+/; + const emailRegex = /[^\s@]+@[^\s@]+\.[^\s@]+/; + + const emailMatch = searchText.match(emailRegex); + const phoneMatch = searchText.match(phoneRegex); + + if (emailMatch) { + return { name: '', phone: '', email: emailMatch[0] }; + } else if (phoneMatch && phoneMatch[0].replace(/\D/g, '').length >= 9) { + return { name: '', phone: phoneMatch[0], email: '' }; + } else { + return { name: searchText, phone: '', email: '' }; + } + } + + // ========== СЕКЦИЯ 4: Уведомления ========== + + /** + * Показывает автоисчезающее уведомление + */ + function showNotification(message, type, duration) { + type = type || 'info'; + duration = duration || 4000; + + const alertClass = type === 'error' ? 'alert-danger' : + type === 'success' ? 'alert-success' : 'alert-info'; + const icon = type === 'error' ? '⚠️' : type === 'success' ? '✓' : 'ℹ️'; + + // Создаём контейнер если его нет + let container = document.getElementById('notifications-container'); + if (!container) { + container = document.createElement('div'); + container.id = 'notifications-container'; + container.style.cssText = + 'position: fixed; top: 80px; right: 20px; z-index: 1050; max-width: 400px;'; + document.body.appendChild(container); + } + + // Создаём уведомление + const notification = document.createElement('div'); + notification.className = 'alert ' + alertClass; + notification.style.cssText = + 'margin-bottom: 10px; padding: 12px 15px; border-radius: 4px; ' + + 'box-shadow: 0 2px 8px rgba(0,0,0,0.15); animation: slideIn 0.3s ease-in-out;'; + notification.innerHTML = + '
' + + '' + icon + '' + message + '
'; + + container.appendChild(notification); + + // Удаляем через таймаут + setTimeout(function() { + notification.style.animation = 'slideOut 0.3s ease-in-out'; + setTimeout(function() { notification.remove(); }, 300); + }, duration); + + // Добавляем CSS анимации если их нет + if (!document.getElementById('notification-styles')) { + const style = document.createElement('style'); + style.id = 'notification-styles'; + style.textContent = + '@keyframes slideIn { from { transform: translateX(400px); opacity: 0; } ' + + 'to { transform: translateX(0); opacity: 1; } } ' + + '@keyframes slideOut { from { transform: translateX(0); opacity: 1; } ' + + 'to { transform: translateX(400px); opacity: 0; } }'; + document.head.appendChild(style); + } + } + + // ========== СЕКЦИЯ 5: Модальное окно создания клиента ========== + + /** + * Открывает модальное окно для создания нового клиента + */ + function openCreateCustomerModal(searchText) { + searchText = searchText || ''; + + // Парсим данные из поискового текста + const parsed = parseSearchText(searchText); + + // Заполняем поля формы + const nameInput = document.getElementById('customer-name'); + const phoneInput = document.getElementById('customer-phone'); + const emailInput = document.getElementById('customer-email'); + + if (nameInput) nameInput.value = parsed.name; + if (phoneInput) phoneInput.value = parsed.phone; + if (emailInput) emailInput.value = parsed.email; + + // Очищаем ошибки + const errorsDiv = document.getElementById('customer-form-errors'); + if (errorsDiv) { + errorsDiv.innerHTML = ''; + errorsDiv.style.display = 'none'; + } + + // Открываем модал + const modalElement = document.getElementById('createCustomerModal'); + if (modalElement && typeof bootstrap !== 'undefined') { + const modal = new bootstrap.Modal(modalElement); + modal.show(); + } + } + + /** + * Инициализирует обработчик кнопки создания клиента + */ + function initCreateCustomerHandler(options) { + const saveBtn = document.getElementById('save-customer-btn'); + if (!saveBtn) return; + + // Удаляем старые обработчики + const newBtn = saveBtn.cloneNode(true); + saveBtn.parentNode.replaceChild(newBtn, saveBtn); + + newBtn.addEventListener('click', function() { + if (newBtn.disabled) return; + + const name = document.getElementById('customer-name').value.trim(); + const phone = document.getElementById('customer-phone').value.trim(); + const email = document.getElementById('customer-email').value.trim(); + + // Валидация + const errors = []; + if (!name) errors.push('Имя клиента обязательно'); + + if (errors.length > 0) { + const errorDiv = document.getElementById('customer-form-errors'); + errorDiv.innerHTML = ''; + errorDiv.style.display = 'block'; + return; + } + + // Блокируем кнопку + newBtn.disabled = true; + const originalHTML = newBtn.innerHTML; + newBtn.innerHTML = 'Создание...'; + + // AJAX запрос + fetch(options.createUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCSRFToken() + }, + body: JSON.stringify({ + name: name, + phone: phone || null, + email: email || null + }) + }) + .then(function(response) { return response.json(); }) + .then(function(data) { + if (data.success) { + // Закрываем модал + const modalElement = document.getElementById('createCustomerModal'); + const modal = bootstrap.Modal.getInstance(modalElement); + + const handleHidden = function() { + modalElement.removeEventListener('hidden.bs.modal', handleHidden); + + // Очистка backdrop + const backdrop = document.querySelector('.modal-backdrop'); + if (backdrop) backdrop.remove(); + document.body.classList.remove('modal-open'); + document.body.style.paddingRight = ''; + }; + + modalElement.addEventListener('hidden.bs.modal', handleHidden); + modal.hide(); + + // Обновляем Select2 + const $customerSelect = $('select[name="customer"]'); + const newOption = new Option(data.name, data.id, true, true); + $customerSelect.append(newOption).trigger('change'); + + // Очищаем форму + document.getElementById('customer-name').value = ''; + document.getElementById('customer-phone').value = ''; + document.getElementById('customer-email').value = ''; + document.getElementById('customer-form-errors').innerHTML = ''; + document.getElementById('customer-form-errors').style.display = 'none'; + + // Уведомление + showNotification('✓ Клиент "' + data.name + '" успешно создан!', 'success'); + + // Триггерим DraftCreator + setTimeout(function() { + if (window.DraftCreator && window.DraftCreator.triggerDraftCreation) { + window.DraftCreator.triggerDraftCreation(); + } + }, 100); + } else { + // Ошибка от сервера + newBtn.disabled = false; + newBtn.innerHTML = originalHTML; + + const errorDiv = document.getElementById('customer-form-errors'); + errorDiv.innerHTML = '
' + data.error + '
'; + errorDiv.style.display = 'block'; + } + }) + .catch(function(error) { + console.error('Error:', error); + newBtn.disabled = false; + newBtn.innerHTML = originalHTML; + + const errorDiv = document.getElementById('customer-form-errors'); + errorDiv.innerHTML = '
Ошибка при создании клиента: ' + + error.message + '
'; + errorDiv.style.display = 'block'; + }); + }); + } + + // ========== СЕКЦИЯ 6: Инициализация Select2 ========== + + /** + * Инициализирует Customer Select2 + * + * @param {Element|string} element - DOM элемент или селектор + * @param {Object} options - Настройки + * @param {string} options.ajaxUrl - URL для AJAX поиска + * @param {string} options.createUrl - URL для создания клиента + * @param {string} options.modalId - ID модального окна (по умолчанию 'createCustomerModal') + * @param {number} options.minimumInputLength - Минимальная длина для поиска (по умолчанию 3) + * @param {number} options.delay - Задержка AJAX запроса (по умолчанию 500) + */ + function initCustomerSelect2(element, options) { + // Ждём загрузки jQuery + if (typeof $ === 'undefined') { + setTimeout(function() { initCustomerSelect2(element, options); }, 100); + return; + } + + options = options || {}; + + const $element = typeof element === 'string' ? $(element) : $(element); + if (!$element.length) return; + + const ajaxUrl = options.ajaxUrl || $element.data('ajax-url'); + const createUrl = options.createUrl || $element.data('create-url'); + const modalId = options.modalId || $element.data('modal-id') || 'createCustomerModal'; + const minimumInputLength = options.minimumInputLength || 3; + const delay = options.delay || 500; + + if (!ajaxUrl) { + console.error('[CustomerSelect2] AJAX URL not provided'); + return; + } + + // Сохраняем текущее значение + const currentValue = $element.val(); + const currentText = $element.find(':selected').text(); + + if (!currentValue) { + $element.empty(); + } + + // Инициализация Select2 + $element.select2({ + theme: 'bootstrap-5', + width: '100%', + language: 'ru', + placeholder: 'Начните вводить имя, телефон или email (минимум ' + minimumInputLength + ' символа)', + minimumInputLength: minimumInputLength, + allowClear: true, + ajax: { + url: ajaxUrl, + type: 'GET', + dataType: 'json', + delay: delay, + data: function(params) { + return { q: params.term || '' }; + }, + processResults: function(data) { + return { results: data.results || [] }; + } + }, + templateResult: formatCustomerOption, + templateSelection: formatCustomerSelection, + escapeMarkup: function(markup) { return markup; } + }); + + // Восстанавливаем значение + if (currentValue && currentText) { + $element.val(currentValue); + } + + // Интеграция с DraftCreator при загрузке + if (currentValue && window.DraftCreator) { + setTimeout(function() { + window.DraftCreator.triggerDraftCreation(); + }, 100); + } + + // Обработчик открытия Select2 + $element.on('select2:open', function() { + setTimeout(function() { + $('.select2-search__field:visible').first().focus(); + }, 100); + }); + + // Обработчик ПЕРЕД выбором (для создания нового) + $element.on('select2:selecting', function(e) { + if (!e.params || !e.params.data) return; + + const data = e.params.data; + if (data.is_create_option || data.id === '__create_new__') { + e.preventDefault(); + $element.select2('close'); + $element.val(null).trigger('change.select2'); + openCreateCustomerModal(data.search_text); + return false; + } + }); + + // Обработчик выбора + $element.on('select2:select', function(e) { + if (!e.params || !e.params.data) return; + + const data = e.params.data; + if (data.is_create_option || data.id === '__create_new__') { + this.value = ''; + const changeEvent = new Event('change', { bubbles: true }); + this.dispatchEvent(changeEvent); + openCreateCustomerModal(data.search_text); + } else { + const changeEvent = new Event('change', { bubbles: true }); + this.dispatchEvent(changeEvent); + } + }); + + // Инициализируем обработчик создания клиента + if (createUrl) { + initCreateCustomerHandler({ createUrl: createUrl, modalId: modalId }); + } + + return $element; + } + + // ========== СЕКЦИЯ 7: Автоматическая инициализация ========== + + document.addEventListener('DOMContentLoaded', function() { + // Авто-инициализация для элементов с классом 'customer-select2-auto' + const elements = document.querySelectorAll('.customer-select2-auto'); + + elements.forEach(function(element) { + const ajaxUrl = element.dataset.ajaxUrl; + const createUrl = element.dataset.createUrl; + const modalId = element.dataset.modalId; + + if (ajaxUrl) { + initCustomerSelect2(element, { + ajaxUrl: ajaxUrl, + createUrl: createUrl, + modalId: modalId + }); + } + }); + }); + + // ========== СЕКЦИЯ 8: Экспорт ========== + + window.initCustomerSelect2 = initCustomerSelect2; + window.openCreateCustomerModal = openCreateCustomerModal; + window.showNotification = showNotification; + +})(window); diff --git a/myproject/orders/templates/orders/order_form.html b/myproject/orders/templates/orders/order_form.html index d65997b..f5cd303 100644 --- a/myproject/orders/templates/orders/order_form.html +++ b/myproject/orders/templates/orders/order_form.html @@ -5,6 +5,7 @@ {% block title %}{{ title }}{% endblock %} {% block extra_css %} +