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 = '' +
+ errors.map(function(e) { return '- ' + e + '
'; }).join('') + '
';
+ 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 %}
+