Добавлен универсальный поиск клиента и быстрое создание нового клиента
Реализованы следующие функции: - AJAX API endpoint для поиска клиента по имени, телефону или email одновременно - AJAX API endpoint для создания нового клиента прямо при создании заказа - Интерактивная форма поиска в поле "Клиент" с использованием Select2 - При отсутствии результатов поиска предлагается создать нового клиента с автоматическим заполнением формы введенными данными - Модальное окно для создания клиента во всплывающем окне (не на отдельной странице) - Автоматический выбор созданного клиента после сохранения Изменения: 1. customers/views.py - добавлены endpoints api_search_customers и api_create_customer 2. customers/urls.py - добавлены URL маршруты для новых endpoints 3. orders/templates/orders/order_form.html - обновлена инициализация Select2 для поиска, добавлено модальное окно и стили 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -9,4 +9,8 @@ urlpatterns = [
|
|||||||
path('<int:pk>/', views.customer_detail, name='customer-detail'),
|
path('<int:pk>/', views.customer_detail, name='customer-detail'),
|
||||||
path('<int:pk>/edit/', views.customer_update, name='customer-update'),
|
path('<int:pk>/edit/', views.customer_update, name='customer-update'),
|
||||||
path('<int:pk>/delete/', views.customer_delete, name='customer-delete'),
|
path('<int:pk>/delete/', views.customer_delete, name='customer-delete'),
|
||||||
|
|
||||||
|
# AJAX API endpoints
|
||||||
|
path('api/search/', views.api_search_customers, name='api-search-customers'),
|
||||||
|
path('api/create/', views.api_create_customer, name='api-create-customer'),
|
||||||
]
|
]
|
||||||
@@ -3,7 +3,11 @@ from django.contrib import messages
|
|||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.views.decorators.http import require_http_methods
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
import phonenumbers
|
import phonenumbers
|
||||||
|
import json
|
||||||
from .models import Customer, Address
|
from .models import Customer, Address
|
||||||
from .forms import CustomerForm
|
from .forms import CustomerForm
|
||||||
|
|
||||||
@@ -92,14 +96,186 @@ def customer_update(request, pk):
|
|||||||
def customer_delete(request, pk):
|
def customer_delete(request, pk):
|
||||||
"""Удаление клиента"""
|
"""Удаление клиента"""
|
||||||
customer = get_object_or_404(Customer, pk=pk)
|
customer = get_object_or_404(Customer, pk=pk)
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
customer_name = customer.full_name
|
customer_name = customer.full_name
|
||||||
customer.delete()
|
customer.delete()
|
||||||
messages.success(request, f'Клиент {customer_name} успешно удален.')
|
messages.success(request, f'Клиент {customer_name} успешно удален.')
|
||||||
return redirect('customers:customer-list')
|
return redirect('customers:customer-list')
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'customer': customer
|
'customer': customer
|
||||||
}
|
}
|
||||||
return render(request, 'customers/customer_confirm_delete.html', context)
|
return render(request, 'customers/customer_confirm_delete.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
# === AJAX API ENDPOINTS ===
|
||||||
|
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
@login_required
|
||||||
|
def api_search_customers(request):
|
||||||
|
"""
|
||||||
|
AJAX endpoint для поиска клиента по имени, телефону или email.
|
||||||
|
|
||||||
|
Параметры GET:
|
||||||
|
- q: поисковая строка
|
||||||
|
|
||||||
|
Возвращает JSON с результатами поиска:
|
||||||
|
{
|
||||||
|
"results": [
|
||||||
|
{"id": 1, "text": "Иван Петров (+375291234567)", "name": "Иван Петров", "phone": "+375291234567", "email": "ivan@example.com"},
|
||||||
|
...
|
||||||
|
],
|
||||||
|
"pagination": {"more": false}
|
||||||
|
}
|
||||||
|
|
||||||
|
Если ничего не найдено и заданы параметры поиска, возвращает:
|
||||||
|
{
|
||||||
|
"results": [
|
||||||
|
{"id": null, "text": "Создать клиента: 'Поиск'", "is_create_option": true, "search_text": "Поиск"}
|
||||||
|
],
|
||||||
|
"pagination": {"more": false}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
query = request.GET.get('q', '').strip()
|
||||||
|
|
||||||
|
if not query or len(query) < 1:
|
||||||
|
return JsonResponse({
|
||||||
|
'results': [],
|
||||||
|
'pagination': {'more': False}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Пытаемся нормализовать номер телефона для поиска
|
||||||
|
phone_normalized = normalize_query_phone(query)
|
||||||
|
|
||||||
|
# Ищем по имени, email или телефону
|
||||||
|
customers = Customer.objects.filter(
|
||||||
|
Q(name__icontains=query) |
|
||||||
|
Q(email__icontains=query) |
|
||||||
|
Q(phone__icontains=phone_normalized)
|
||||||
|
).order_by('name')[:20] # Ограничиваем 20 результатами
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# Добавляем найденные клиентов
|
||||||
|
for customer in customers:
|
||||||
|
phone_display = str(customer.phone) if customer.phone else ''
|
||||||
|
text = customer.name
|
||||||
|
if phone_display:
|
||||||
|
text += f' ({phone_display})'
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'id': customer.pk,
|
||||||
|
'text': text,
|
||||||
|
'name': customer.name,
|
||||||
|
'phone': phone_display,
|
||||||
|
'email': customer.email,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Если ничего не найдено, предлагаем создать нового клиента
|
||||||
|
if not results:
|
||||||
|
results.append({
|
||||||
|
'id': None,
|
||||||
|
'text': f'Создать клиента: "{query}"',
|
||||||
|
'is_create_option': True,
|
||||||
|
'search_text': query,
|
||||||
|
})
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'results': results,
|
||||||
|
'pagination': {'more': False}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@require_http_methods(["POST"])
|
||||||
|
@login_required
|
||||||
|
def api_create_customer(request):
|
||||||
|
"""
|
||||||
|
AJAX endpoint для создания нового клиента.
|
||||||
|
|
||||||
|
Принимает POST JSON:
|
||||||
|
{
|
||||||
|
"name": "Иван Петров",
|
||||||
|
"phone": "+375291234567",
|
||||||
|
"email": "ivan@example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
Возвращает JSON:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"id": 123,
|
||||||
|
"name": "Иван Петров",
|
||||||
|
"phone": "+375291234567",
|
||||||
|
"email": "ivan@example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
При ошибке:
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "Клиент с таким номером телефона уже существует"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = json.loads(request.body)
|
||||||
|
|
||||||
|
name = data.get('name', '').strip()
|
||||||
|
phone = data.get('phone', '').strip()
|
||||||
|
email = data.get('email', '').strip()
|
||||||
|
|
||||||
|
# Валидация: имя обязательно
|
||||||
|
if not name:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Имя клиента обязательно'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
# Нормализуем телефон если он указан
|
||||||
|
if phone:
|
||||||
|
phone = normalize_query_phone(phone)
|
||||||
|
|
||||||
|
# Проверяем, не существует ли уже клиент с таким телефоном
|
||||||
|
if phone and Customer.objects.filter(phone=phone).exists():
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Клиент с таким номером телефона уже существует'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
# Проверяем, не существует ли уже клиент с таким email
|
||||||
|
if email and Customer.objects.filter(email=email).exists():
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Клиент с таким email уже существует'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
# Создаем нового клиента
|
||||||
|
customer = Customer.objects.create(
|
||||||
|
name=name,
|
||||||
|
phone=phone if phone else None,
|
||||||
|
email=email if email else None
|
||||||
|
)
|
||||||
|
|
||||||
|
phone_display = str(customer.phone) if customer.phone else ''
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'success': True,
|
||||||
|
'id': customer.pk,
|
||||||
|
'name': customer.name,
|
||||||
|
'phone': phone_display,
|
||||||
|
'email': customer.email,
|
||||||
|
}, status=201)
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Некорректный JSON'
|
||||||
|
}, status=400)
|
||||||
|
except ValidationError as e:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': str(e)
|
||||||
|
}, status=400)
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': f'Ошибка сервера: {str(e)}'
|
||||||
|
}, status=500)
|
||||||
@@ -15,6 +15,31 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Стили для поиска клиента */
|
||||||
|
.customer-option {
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-create-option {
|
||||||
|
color: #28a745;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-create-option i {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Select2 dropdown styling */
|
||||||
|
.select2-results__option.customer-option-item {
|
||||||
|
padding: 8px 8px;
|
||||||
|
border-bottom: 1px solid #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-results__option.customer-option-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -374,7 +399,84 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Инициализация Select2 для обычных полей (не товары)
|
// Инициализация Select2 для поля customer с поиском
|
||||||
|
$('#{{ form.customer.id_for_label }}').select2({
|
||||||
|
theme: 'bootstrap-5',
|
||||||
|
width: '100%',
|
||||||
|
language: 'ru',
|
||||||
|
placeholder: 'Начните вводить имя, телефон или email',
|
||||||
|
minimumInputLength: 1,
|
||||||
|
ajax: {
|
||||||
|
url: '{% url "customers:api-search-customers" %}',
|
||||||
|
dataType: 'json',
|
||||||
|
delay: 250,
|
||||||
|
data: function(params) {
|
||||||
|
return {
|
||||||
|
q: params.term,
|
||||||
|
page: params.page || 1
|
||||||
|
};
|
||||||
|
},
|
||||||
|
processResults: function(data) {
|
||||||
|
return {
|
||||||
|
results: data.results,
|
||||||
|
pagination: {
|
||||||
|
more: data.pagination.more
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
cache: true
|
||||||
|
},
|
||||||
|
templateResult: formatCustomerOption,
|
||||||
|
templateSelection: formatCustomerSelection,
|
||||||
|
escapeMarkup: function(markup) { return markup; }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Форматирование опции в списке
|
||||||
|
function formatCustomerOption(option) {
|
||||||
|
if (!option.id) {
|
||||||
|
return option.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option.is_create_option) {
|
||||||
|
return '<div class="customer-create-option"><i class="bi bi-plus-circle"></i> ' + option.text + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
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.id) {
|
||||||
|
return option.text;
|
||||||
|
}
|
||||||
|
if (option.is_create_option) {
|
||||||
|
return option.text;
|
||||||
|
}
|
||||||
|
return option.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка выбора в Select2
|
||||||
|
$('#{{ form.customer.id_for_label }}').on('select2:select', function(e) {
|
||||||
|
const data = e.params.data;
|
||||||
|
|
||||||
|
if (data.is_create_option) {
|
||||||
|
// Очищаем select2
|
||||||
|
$(this).val(null).trigger('change');
|
||||||
|
// Открываем модальное окно для создания клиента
|
||||||
|
openCreateCustomerModal(data.search_text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Инициализация Select2 для остальных полей
|
||||||
$('.select2:not(.select2-order-item)').select2({
|
$('.select2:not(.select2-order-item)').select2({
|
||||||
theme: 'bootstrap-5',
|
theme: 'bootstrap-5',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -633,6 +735,114 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// === СОЗДАНИЕ НОВОГО КЛИЕНТА ===
|
||||||
|
|
||||||
|
// Функция открытия модального окна создания клиента
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обработчик сохранения нового клиента
|
||||||
|
document.getElementById('save-customer-btn').addEventListener('click', function() {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправляем 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 modal = bootstrap.Modal.getInstance(document.getElementById('createCustomerModal'));
|
||||||
|
modal.hide();
|
||||||
|
|
||||||
|
// Выбираем созданного клиента в Select2
|
||||||
|
const $customerSelect = $('#{{ form.customer.id_for_label }}');
|
||||||
|
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';
|
||||||
|
|
||||||
|
// Показываем успешное сообщение
|
||||||
|
alert(`Клиент "${data.name}" успешно создан!`);
|
||||||
|
} else {
|
||||||
|
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);
|
||||||
|
const errorDiv = document.getElementById('customer-form-errors');
|
||||||
|
errorDiv.innerHTML = '<div class="alert alert-danger mb-0">Ошибка при создании клиента: ' + error.message + '</div>';
|
||||||
|
errorDiv.style.display = 'block';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// === ВРЕМЕННЫЕ КОМПЛЕКТЫ ===
|
// === ВРЕМЕННЫЕ КОМПЛЕКТЫ ===
|
||||||
|
|
||||||
// Модальное окно для создания временного комплекта
|
// Модальное окно для создания временного комплекта
|
||||||
@@ -757,7 +967,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Отправляем AJAX запрос
|
// Отправляем AJAX запрос
|
||||||
fetch('{% url "orders:temporary-kit-create" %}', {
|
fetch('{% url "products:api-temporary-kit-create" %}', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -874,6 +1084,54 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Модальное окно для создания нового клиента -->
|
||||||
|
<div class="modal fade" id="createCustomerModal" tabindex="-1" aria-labelledby="createCustomerModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="createCustomerModalLabel">
|
||||||
|
<i class="bi bi-person-plus"></i> Создать нового клиента
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="customer-form-errors" style="display: none;" class="alert alert-danger"></div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="customer-name" class="form-label">
|
||||||
|
Имя клиента <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" class="form-control" id="customer-name"
|
||||||
|
placeholder="Например: Иван Петров">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="customer-phone" class="form-label">Телефон</label>
|
||||||
|
<input type="tel" class="form-control" id="customer-phone"
|
||||||
|
placeholder="+375291234567 или 8(029)1234567">
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
Введите в любом формате, будет автоматически преобразован
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="customer-email" class="form-label">Email</label>
|
||||||
|
<input type="email" class="form-control" id="customer-email"
|
||||||
|
placeholder="ivan@example.com">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
|
<i class="bi bi-x-circle"></i> Отмена
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-success" id="save-customer-btn">
|
||||||
|
<i class="bi bi-check-circle"></i> Создать клиента
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Модальное окно для создания временного комплекта -->
|
<!-- Модальное окно для создания временного комплекта -->
|
||||||
<div class="modal fade" id="tempKitModal" tabindex="-1" aria-labelledby="tempKitModalLabel" aria-hidden="true">
|
<div class="modal fade" id="tempKitModal" tabindex="-1" aria-labelledby="tempKitModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
|
|||||||
Reference in New Issue
Block a user