refactor(orders): extract Customer Select2 to separate module
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 <noreply@anthropic.com>
This commit is contained in:
445
myproject/orders/static/orders/js/customer_select2.js
Normal file
445
myproject/orders/static/orders/js/customer_select2.js
Normal file
@@ -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 '<div class="customer-create-option">' +
|
||||
'<i class="bi bi-plus-circle"></i> ' + option.text +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
if (!option.id) {
|
||||
return option.text;
|
||||
}
|
||||
|
||||
let html = '<div class="customer-option">';
|
||||
html += '<strong>' + option.name + '</strong>';
|
||||
if (option.phone) {
|
||||
html += '<br><small class="text-muted">Телефон: ' + option.phone + '</small>';
|
||||
}
|
||||
if (option.email) {
|
||||
html += '<br><small class="text-muted">Email: ' + option.email + '</small>';
|
||||
}
|
||||
html += '</div>';
|
||||
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 =
|
||||
'<div style="display: flex; align-items: center; gap: 10px;">' +
|
||||
'<span>' + icon + '</span><span>' + message + '</span></div>';
|
||||
|
||||
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 = '<ul class="mb-0">' +
|
||||
errors.map(function(e) { return '<li>' + e + '</li>'; }).join('') + '</ul>';
|
||||
errorDiv.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
// Блокируем кнопку
|
||||
newBtn.disabled = true;
|
||||
const originalHTML = newBtn.innerHTML;
|
||||
newBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Создание...';
|
||||
|
||||
// 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 = '<div class="alert alert-danger mb-0">' + data.error + '</div>';
|
||||
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 = '<div class="alert alert-danger mb-0">Ошибка при создании клиента: ' +
|
||||
error.message + '</div>';
|
||||
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);
|
||||
@@ -5,6 +5,7 @@
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<meta name="csrf-token" content="{{ csrf_token }}">
|
||||
<style>
|
||||
/* Скрываем DELETE checkbox */
|
||||
input[name$="-DELETE"] {
|
||||
@@ -143,13 +144,28 @@
|
||||
Клиент <span class="text-danger">*</span>
|
||||
</label>
|
||||
{% if preselected_customer %}
|
||||
<select name="customer" class="form-select" id="id_customer">
|
||||
<option value="{{ preselected_customer.pk }}" selected data-name="{{ preselected_customer.name }}" data-phone="{{ preselected_customer.phone|default:'' }}" data-email="{{ preselected_customer.email|default:'' }}">
|
||||
<select name="customer"
|
||||
class="form-select customer-select2-auto"
|
||||
id="id_customer"
|
||||
data-ajax-url="{% url 'customers:api-search-customers' %}"
|
||||
data-create-url="{% url 'customers:api-create-customer' %}"
|
||||
data-modal-id="createCustomerModal">
|
||||
<option value="{{ preselected_customer.pk }}" selected
|
||||
data-name="{{ preselected_customer.name }}"
|
||||
data-phone="{{ preselected_customer.phone|default:'' }}"
|
||||
data-email="{{ preselected_customer.email|default:'' }}">
|
||||
{{ preselected_customer.name }}{% if preselected_customer.phone %} ({{ preselected_customer.phone }}){% endif %}
|
||||
</option>
|
||||
</select>
|
||||
{% else %}
|
||||
{{ form.customer }}
|
||||
<select name="customer"
|
||||
class="form-select customer-select2-auto"
|
||||
id="id_customer"
|
||||
data-ajax-url="{% url 'customers:api-search-customers' %}"
|
||||
data-create-url="{% url 'customers:api-create-customer' %}"
|
||||
data-modal-id="createCustomerModal">
|
||||
{{ form.customer.value|default:'' }}
|
||||
</select>
|
||||
{% endif %}
|
||||
{% if form.customer.errors %}
|
||||
<div class="text-danger">{{ form.customer.errors }}</div>
|
||||
@@ -1224,193 +1240,12 @@ window.initOrderItemSelect2 = function(element) {
|
||||
console.log('[Select2 Clear] Все поля очищены, форма осталась в DOM');
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
// Ждем пока jQuery загрузится
|
||||
function initCustomerSelect2() {
|
||||
if (typeof $ === 'undefined') {
|
||||
setTimeout(initCustomerSelect2, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
const $customerSelect = $('#id_customer');
|
||||
const ajaxUrl = '{% url "customers:api-search-customers" %}';
|
||||
|
||||
// Сохраняем текущее значение перед очисткой
|
||||
const currentValue = $customerSelect.val();
|
||||
const currentText = $customerSelect.find(':selected').text();
|
||||
|
||||
// НЕ очищаем, если у нас есть текущий выбранный клиент
|
||||
if (!currentValue) {
|
||||
$customerSelect.empty();
|
||||
}
|
||||
|
||||
// Инициализируем Select2 с AJAX
|
||||
$customerSelect.select2({
|
||||
theme: 'bootstrap-5',
|
||||
width: '100%',
|
||||
language: 'ru',
|
||||
placeholder: 'Начните вводить имя, телефон или email (минимум 3 символа)',
|
||||
minimumInputLength: 3,
|
||||
allowClear: true,
|
||||
ajax: {
|
||||
url: ajaxUrl,
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
delay: 500,
|
||||
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) {
|
||||
$customerSelect.val(currentValue);
|
||||
}
|
||||
|
||||
// Select2 готов и есть предзаполненное значение
|
||||
if (currentValue && window.DraftCreator) {
|
||||
setTimeout(function() {
|
||||
window.DraftCreator.triggerDraftCreation();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Слушаем события
|
||||
$customerSelect.on('select2:open', function(e) {
|
||||
setTimeout(function() {
|
||||
$('.select2-search__field:visible').first().focus();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Обработчик для перехвата ПЕРЕД выбором
|
||||
$customerSelect.on('select2:selecting', function(e) {
|
||||
|
||||
// Проверяем наличие e.params и e.params.data
|
||||
if (!e.params || !e.params.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = e.params.data;
|
||||
|
||||
if (data.is_create_option || data.id === '__create_new__') {
|
||||
// Предотвращаем выбор этой опции
|
||||
e.preventDefault();
|
||||
$customerSelect.select2('close');
|
||||
$customerSelect.val(null).trigger('change.select2');
|
||||
window.openCreateCustomerModal(data.search_text);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
$customerSelect.on('select2:select', function(e) {
|
||||
|
||||
// Проверяем наличие e.params и e.params.data
|
||||
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);
|
||||
window.openCreateCustomerModal(data.search_text);
|
||||
} else {
|
||||
const changeEvent = new Event('change', { bubbles: true });
|
||||
this.dispatchEvent(changeEvent);
|
||||
}
|
||||
});
|
||||
|
||||
// Форматирование опции в списке
|
||||
function formatCustomerOption(option) {
|
||||
// ВАЖНО: Проверяем is_create_option или специальный ID ПЕРЕД проверкой !option.id
|
||||
if (option.is_create_option || option.id === '__create_new__') {
|
||||
return '<div class="customer-create-option"><i class="bi bi-plus-circle"></i> ' + option.text + '</div>';
|
||||
}
|
||||
|
||||
if (!option.id) {
|
||||
return option.text;
|
||||
}
|
||||
|
||||
let html = '<div class="customer-option">';
|
||||
html += '<strong>' + option.name + '</strong>';
|
||||
if (option.phone) {
|
||||
html += '<br><small class="text-muted">Телефон: ' + option.phone + '</small>';
|
||||
}
|
||||
if (option.email) {
|
||||
html += '<br><small class="text-muted">Email: ' + option.email + '</small>';
|
||||
}
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
// Форматирование выбранного значения
|
||||
function formatCustomerSelection(option) {
|
||||
if (option.is_create_option || option.id === '__create_new__') {
|
||||
return option.text;
|
||||
}
|
||||
if (!option.id) {
|
||||
return option.text;
|
||||
}
|
||||
// Возвращаем name если есть (из AJAX), иначе text (из DOM опции)
|
||||
return option.name || option.text;
|
||||
}
|
||||
}
|
||||
|
||||
// === СОЗДАНИЕ НОВОГО КЛИЕНТА ===
|
||||
// Функция открытия модального окна создания клиента (определяем ДО инициализации Select2)
|
||||
window.openCreateCustomerModal = function(searchText = '') {
|
||||
// Заполняем форму предложенными данными
|
||||
if (searchText) {
|
||||
// Пытаемся распознать введенные данные
|
||||
const phoneRegex = /[\d\s\-\+\(\)]+/;
|
||||
const emailRegex = /[^\s@]+@[^\s@]+\.[^\s@]+/;
|
||||
|
||||
// Ищем email и телефон в строке поиска
|
||||
const emailMatch = searchText.match(emailRegex);
|
||||
const phoneMatch = searchText.match(phoneRegex);
|
||||
|
||||
// Если это похоже на email, заполняем email
|
||||
if (emailMatch) {
|
||||
document.getElementById('customer-email').value = emailMatch[0];
|
||||
}
|
||||
// Если это похоже на телефон (много цифр), заполняем телефон
|
||||
else if (phoneMatch && phoneMatch[0].replace(/\D/g, '').length >= 9) {
|
||||
document.getElementById('customer-phone').value = phoneMatch[0];
|
||||
}
|
||||
// Иначе считаем это имя
|
||||
else {
|
||||
document.getElementById('customer-name').value = searchText;
|
||||
}
|
||||
}
|
||||
|
||||
// Очищаем поля которые не заполнены
|
||||
const customerNameInput = document.getElementById('customer-name');
|
||||
const customerPhoneInput = document.getElementById('customer-phone');
|
||||
const customerEmailInput = document.getElementById('customer-email');
|
||||
const createCustomerModal = new bootstrap.Modal(document.getElementById('createCustomerModal'));
|
||||
|
||||
// Очищаем сообщения об ошибках
|
||||
document.getElementById('customer-form-errors').innerHTML = '';
|
||||
document.getElementById('customer-form-errors').style.display = 'none';
|
||||
|
||||
createCustomerModal.show();
|
||||
};
|
||||
|
||||
// Вызываем инициализацию Select2 для customer
|
||||
initCustomerSelect2();
|
||||
<!-- Customer Select2 Widget -->
|
||||
<script src="{% static 'orders/js/customer_select2.js' %}" defer></script>
|
||||
|
||||
<script>
|
||||
// Инициализация Select2 для остальных полей (после jQuery загружен)
|
||||
if (typeof $ !== 'undefined') {
|
||||
$(document).ready(function() {
|
||||
@@ -2005,210 +1840,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Обработчик сохранения нового клиента (ДОЛЖЕН быть ПОСЛЕ модального окна в HTML) -->
|
||||
<script>
|
||||
document.getElementById('save-customer-btn').addEventListener('click', function() {
|
||||
const saveBtn = this;
|
||||
|
||||
// Защита от двойного клика
|
||||
if (saveBtn.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 = '<ul class="mb-0">' + errors.map(e => '<li>' + e + '</li>').join('') + '</ul>';
|
||||
errorDiv.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
// Блокируем кнопку и меняем текст
|
||||
saveBtn.disabled = true;
|
||||
const originalHTML = saveBtn.innerHTML;
|
||||
saveBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>Создание...';
|
||||
|
||||
// Отправляем AJAX запрос
|
||||
fetch('{% url "customers:api-create-customer" %}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token }}'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
phone: phone || null,
|
||||
email: email || null
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Закрываем модальное окно
|
||||
const modalElement = document.getElementById('createCustomerModal');
|
||||
const modal = bootstrap.Modal.getInstance(modalElement);
|
||||
|
||||
// Слушаем событие полного закрытия модального окна
|
||||
const handleHidden = () => {
|
||||
// Удаляем обработчик после первого запуска
|
||||
modalElement.removeEventListener('hidden.bs.modal', handleHidden);
|
||||
|
||||
// Дополнительная очистка (на случай если Bootstrap не почистил)
|
||||
const backdrop = document.querySelector('.modal-backdrop');
|
||||
if (backdrop) {
|
||||
backdrop.remove();
|
||||
}
|
||||
document.body.classList.remove('modal-open');
|
||||
document.body.style.paddingRight = '';
|
||||
|
||||
// Возвращаем фокус на форму заказа (опционально)
|
||||
const orderForm = document.getElementById('order-form');
|
||||
if (orderForm) {
|
||||
orderForm.focus();
|
||||
}
|
||||
};
|
||||
|
||||
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');
|
||||
|
||||
// Триггерим создание черновика через draft-creator
|
||||
// После выбора клиента должно произойти автоматическое создание черновика
|
||||
// Добавляем небольшую задержку, чтобы Select2 полностью обновился
|
||||
setTimeout(() => {
|
||||
if (window.DraftCreator && typeof window.DraftCreator.triggerDraftCreation === 'function') {
|
||||
window.DraftCreator.triggerDraftCreation();
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
// Разблокируем кнопку при ошибке
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = originalHTML;
|
||||
|
||||
const errorDiv = document.getElementById('customer-form-errors');
|
||||
errorDiv.innerHTML = '<div class="alert alert-danger mb-0">' + data.error + '</div>';
|
||||
errorDiv.style.display = 'block';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
|
||||
// Разблокируем кнопку при ошибке
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = originalHTML;
|
||||
|
||||
const errorDiv = document.getElementById('customer-form-errors');
|
||||
errorDiv.innerHTML = '<div class="alert alert-danger mb-0">Ошибка при создании клиента: ' + error.message + '</div>';
|
||||
errorDiv.style.display = 'block';
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Показывает автоисчезающее уведомление в правом верхнем углу формы заказа
|
||||
* @param {string} message - Текст сообщения
|
||||
* @param {string} type - Тип: 'success', 'error', 'info' (по умолчанию 'info')
|
||||
* @param {number} duration - Время показа в миллисекундах (по умолчанию 4000)
|
||||
*/
|
||||
function showNotification(message, type = 'info', 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 = `
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span>${icon}</span>
|
||||
<span>${message}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Добавляем в контейнер
|
||||
container.appendChild(notification);
|
||||
|
||||
// Удаляем через указанное время
|
||||
setTimeout(() => {
|
||||
notification.style.animation = 'slideOut 0.3s ease-in-out';
|
||||
setTimeout(() => {
|
||||
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);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Модальное окно для создания временного комплекта -->
|
||||
<div class="modal fade" id="tempKitModal" tabindex="-1" aria-labelledby="tempKitModalLabel" aria-hidden="true">
|
||||
|
||||
Reference in New Issue
Block a user