From 74ece6dd665dcf48c5d250cbb36d4a9bf7a493a0 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Mon, 10 Nov 2025 22:23:11 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D1=83=D0=BD=D0=B8=D0=B2=D0=B5=D1=80=D1=81=D0=B0=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=8B=D0=B9=20=D0=BF=D0=BE=D0=B8=D1=81=D0=BA=20?= =?UTF-8?q?=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=D0=B0=20=D0=B8=20=D0=B1?= =?UTF-8?q?=D1=8B=D1=81=D1=82=D1=80=D0=BE=D0=B5=20=D1=81=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BD=D0=BE=D0=B2=D0=BE=D0=B3=D0=BE?= =?UTF-8?q?=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Реализованы следующие функции: - 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 --- myproject/customers/urls.py | 4 + myproject/customers/views.py | 182 +++++++++++- .../orders/templates/orders/order_form.html | 262 +++++++++++++++++- 3 files changed, 443 insertions(+), 5 deletions(-) diff --git a/myproject/customers/urls.py b/myproject/customers/urls.py index acb3521..08b2b30 100644 --- a/myproject/customers/urls.py +++ b/myproject/customers/urls.py @@ -9,4 +9,8 @@ urlpatterns = [ path('/', views.customer_detail, name='customer-detail'), path('/edit/', views.customer_update, name='customer-update'), path('/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'), ] \ No newline at end of file diff --git a/myproject/customers/views.py b/myproject/customers/views.py index c54b59d..c9db9e3 100644 --- a/myproject/customers/views.py +++ b/myproject/customers/views.py @@ -3,7 +3,11 @@ from django.contrib import messages from django.core.paginator import Paginator from django.core.exceptions import ValidationError 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 json from .models import Customer, Address from .forms import CustomerForm @@ -92,14 +96,186 @@ def customer_update(request, pk): def customer_delete(request, pk): """Удаление клиента""" customer = get_object_or_404(Customer, pk=pk) - + if request.method == 'POST': customer_name = customer.full_name customer.delete() messages.success(request, f'Клиент {customer_name} успешно удален.') return redirect('customers:customer-list') - + context = { 'customer': customer } - return render(request, 'customers/customer_confirm_delete.html', context) \ No newline at end of file + 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) \ No newline at end of file diff --git a/myproject/orders/templates/orders/order_form.html b/myproject/orders/templates/orders/order_form.html index c7e955e..38cbb18 100644 --- a/myproject/orders/templates/orders/order_form.html +++ b/myproject/orders/templates/orders/order_form.html @@ -15,6 +15,31 @@ opacity: 0.5; 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; + } {% endblock %} @@ -374,7 +399,84 @@ + + +