From fb4f14f475376954fdc102edc74aee7175757cd5 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Tue, 23 Dec 2025 15:18:02 +0300 Subject: [PATCH] refactor(orders): extract Customer Select2 to separate module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extracted customer selection and creation functionality from order_form.html to a reusable customer_select2.js module for better maintainability. Changes: - Created customer_select2.js (~450 lines) with IIFE pattern - AJAX customer search with Select2 integration - Smart parsing of email/phone/name from search input - Modal-based customer creation with validation - Toast notifications system - Auto-initialization via data-attributes - Global function exports for backward compatibility - Updated order_form.html: - Added CSRF meta-tag for token access - Added data-attributes to customer select element - Included customer_select2.js script - Removed ~370 lines of inline JavaScript Benefits: - Improved code organization and readability - Reusable across other pages requiring customer selection - Better browser caching for static JS - Consistent with existing select2-product-search.js pattern πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../static/orders/js/customer_select2.js | 445 ++++++++++++++++++ .../orders/templates/orders/order_form.html | 415 +--------------- 2 files changed, 468 insertions(+), 392 deletions(-) create mode 100644 myproject/orders/static/orders/js/customer_select2.js 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 %} +