from django.shortcuts import render, get_object_or_404, redirect 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 from .forms import CustomerForm def normalize_query_phone(q): """Normalize phone number for search""" try: parsed = phonenumbers.parse(q, "BY") if phonenumbers.is_valid_number(parsed): return phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164) return q except: return q def customer_list(request): """Список всех клиентов""" query = request.GET.get('q') # Исключаем системного клиента из списка customers = Customer.objects.filter(is_system_customer=False) if query: # Используем ту же логику поиска, что и в AJAX API (api_search_customers) # Это обеспечивает согласованность между веб-интерфейсом и API # Нормализуем номер телефона phone_normalized = normalize_query_phone(query) # Определяем стратегию поиска strategy, search_value = determine_search_strategy(query) # Строим Q-объект для поиска (единая функция) q_objects = build_customer_search_query(query, strategy, search_value) # Добавляем поиск по телефону (умная логика) if phone_normalized: q_objects |= Q(phone__icontains=phone_normalized) # Проверяем, похож ли query на номер телефона (только цифры и минимум 3 цифры) query_digits = ''.join(c for c in query if c.isdigit()) should_search_by_phone_digits = is_query_phone_only(query) and len(query_digits) >= 3 if should_search_by_phone_digits: # Ищем клиентов, чьи телефоны содержат введенные цифры # Используем LIKE запрос вместо Python loop для оптимизации при большом количестве клиентов customers_by_phone = Customer.objects.filter( phone__isnull=False, phone__icontains=query_digits # Простой поиск по цифрам в phone строке ) if customers_by_phone.exists(): q_objects |= Q(pk__in=customers_by_phone.values_list('pk', flat=True)) customers = customers.filter(q_objects) customers = customers.order_by('-created_at') # Пагинация paginator = Paginator(customers, 25) # 25 клиентов на страницу page_number = request.GET.get('page') page_obj = paginator.get_page(page_number) context = { 'page_obj': page_obj, 'query': query, } return render(request, 'customers/customer_list.html', context) def customer_detail(request, pk): """Детали клиента""" customer = get_object_or_404(Customer, pk=pk) # Для системного клиента показываем специальную заглушку if customer.is_system_customer: return render(request, 'customers/customer_system.html') context = { 'customer': customer, } return render(request, 'customers/customer_detail.html', context) def customer_create(request): """Создание нового клиента""" if request.method == 'POST': form = CustomerForm(request.POST) if form.is_valid(): customer = form.save() messages.success(request, f'Клиент {customer.full_name} успешно создан.') return redirect('customers:customer-detail', pk=customer.pk) else: form = CustomerForm() return render(request, 'customers/customer_form.html', {'form': form, 'is_creating': True}) def customer_update(request, pk): """Редактирование клиента""" customer = get_object_or_404(Customer, pk=pk) # Проверяем, не системный ли это клиент if customer.is_system_customer: messages.warning(request, 'Системный клиент не может быть изменен. Он создается автоматически и необходим для корректной работы системы.') return redirect('customers:customer-detail', pk=pk) if request.method == 'POST': form = CustomerForm(request.POST, instance=customer) if form.is_valid(): try: form.save() messages.success(request, f'Клиент {customer.full_name} успешно обновлён.') return redirect('customers:customer-detail', pk=customer.pk) except ValidationError as e: messages.error(request, str(e)) else: form = CustomerForm(instance=customer) return render(request, 'customers/customer_form.html', {'form': form, 'is_creating': False}) def customer_delete(request, pk): """Удаление клиента""" customer = get_object_or_404(Customer, pk=pk) # Проверяем, не системный ли это клиент if customer.is_system_customer: messages.error(request, 'Невозможно удалить системного клиента. Он необходим для корректной работы системы.') return redirect('customers:customer-detail', pk=pk) if request.method == 'POST': customer_name = customer.full_name try: customer.delete() messages.success(request, f'Клиент {customer_name} успешно удален.') return redirect('customers:customer-list') except ValidationError as e: messages.error(request, str(e)) return redirect('customers:customer-detail', pk=pk) context = { 'customer': customer } return render(request, 'customers/customer_confirm_delete.html', context) # === AJAX API ENDPOINTS === def determine_search_strategy(query): """ Определяет стратегию поиска на основе содержимого query. На основе наличия и позиции символа @ определяет, по каким полям и как искать. Возвращает tuple (strategy, search_value): - 'name_only': поиск только по имени (для очень коротких запросов) - 'email_prefix': поиск по началу email (query заканчивается на @) - 'email_domain': поиск по домену (query начинается с @) - 'email_full': полный поиск по email (query содержит обе части) - 'universal': поиск везде (имя + email, для средних запросов) Примеры: - 'team_x3m@' → ('email_prefix', 'team_x3m') - '@bk' → ('email_domain', 'bk') - 'test@bk.ru' → ('email_full', 'test@bk.ru') - 'natul' → ('universal', 'natul') - 'te' → ('name_only', 'te') """ if '@' in query: local, _, domain = query.partition('@') if not local: # Начинается с @: ищем по домену return ('email_domain', domain) if not domain: # Заканчивается на @: ищем по префиксу email return ('email_prefix', local) # Есть и локальная часть, и домен: полный поиск return ('email_full', query) else: # Нет @: определяем по длине if len(query) < 2: return ('name_only', query) if len(query) >= 3: return ('universal', query) # 2 символа: только имя (слишком мало для поиска по email) return ('name_only', query) def is_query_phone_only(query): """ Проверяет, содержит ли query только символы телефонного номера. Возвращает True, если query состоит ТОЛЬКО из: - цифр: 0-9 - телефонных символов: +, -, (, ), пробелов Возвращает False, если есть буквы или другие символы (означает, что это поиск по имени/email). Примеры: - '295' → True (только цифры) - '+375291234567' → True (цифры и телефонные символы) - '(029) 123-45' → True (цифры и телефонные символы) - 'x3m' → False (содержит буквы) - 'team_x3m' → False (содержит буквы) - 'Иван' → False (содержит буквы) """ if not query: return False # Проверяем, что query содержит ТОЛЬКО цифры и телефонные символы phone_chars = set('0123456789+- ().') return all(c in phone_chars for c in query) def build_customer_search_query(query, strategy, search_value): """ Строит Q-объект для поиска клиентов на основе стратегии. Используется в customer_list() и api_search_customers() для единообразия. Args: query: Исходный поисковый запрос (для fallback) strategy: Стратегия поиска (из determine_search_strategy) search_value: Значение для поиска (из determine_search_strategy) Returns: Q-объект для фильтрации Customer.objects """ if strategy == 'name_only': return Q(name__icontains=search_value) elif strategy == 'email_prefix': # Query вида "team_x3m@" — ищем email, начинающиеся с "team_x3m" return Q(email__istartswith=search_value) elif strategy == 'email_domain': # Query вида "@bk" — ищем все email с доменом "@bk.*" return Q(email__icontains=f'@{search_value}') elif strategy == 'email_full': # Query вида "test@bk.ru" — полный поиск по email return Q(email__icontains=search_value) elif strategy == 'universal': # Query вида "natul" (3+ символов) — ищем везде return Q(name__icontains=search_value) | Q(email__icontains=search_value) else: # На случай неизвестной стратегии (не должно быть) return Q(name__icontains=query) @require_http_methods(["GET"]) 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) # Для поиска по телефону: извлекаем только цифры и ищем по ним # Это позволит найти клиента независимо от формата ввода query_digits = ''.join(c for c in query if c.isdigit()) # Ищем по имени, email или телефону # Используем Q-объекты для OR условий # Определяем стратегию поиска на основе содержимого query strategy, search_value = determine_search_strategy(query) # Строим Q-объект для поиска (единая функция, используется везде) q_objects = build_customer_search_query(query, strategy, search_value) # Для поиска по номерам телефонов: применяем умную логику # Ищем по телефону ТОЛЬКО если query состоит из ТОЛЬКО цифр и телефонных символов # (никаких букв — потому что если есть буквы, это явно поиск по имени/email, не по телефону) if phone_normalized: q_objects |= Q(phone__icontains=phone_normalized) # Проверяем, похож ли query на номер телефона (только цифры/+/-/() и минимум 3 цифры) should_search_by_phone_digits = is_query_phone_only(query) and len(query_digits) >= 3 if should_search_by_phone_digits: # Ищем клиентов, чьи телефоны содержат введенные цифры # Используем LIKE запрос вместо Python loop для оптимизации при большом количестве клиентов # Номер телефона в E.164 формате: +375291234567 # Мы ищем по цифрам, поэтому можно просто использовать LIKE с цифрами customers_by_phone = Customer.objects.filter( phone__isnull=False, phone__icontains=query_digits # Простой поиск по цифрам в phone строке ) if customers_by_phone.exists(): q_objects |= Q(pk__in=customers_by_phone.values_list('pk', flat=True)) # Исключаем системного клиента из результатов поиска customers = Customer.objects.filter(q_objects).filter(is_system_customer=False).distinct().order_by('name')[: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': '__create_new__', # Специальный ID для опции создания 'text': f'Создать клиента: "{query}"', 'is_create_option': True, 'search_text': query, }) return JsonResponse({ 'results': results, 'pagination': {'more': False} }) @require_http_methods(["POST"]) 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() if data.get('name') else '' phone = data.get('phone', '').strip() if data.get('phone') else '' email = data.get('email', '').strip() if data.get('email') else '' # Подготавливаем данные для формы form_data = { 'name': name, 'phone': phone if phone else None, 'email': email if email else None, 'loyalty_tier': 'no_discount', # Значение по умолчанию для новых клиентов } # Используем форму для валидации и создания form = CustomerForm(data=form_data) if form.is_valid(): # Сохраняем клиента через форму (автоматически вызывает все валидации) customer = form.save() 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 if customer.email else '', }, status=201) else: # Собираем ошибки валидации с указанием полей errors = [] field_labels = { 'name': 'Имя клиента', 'phone': 'Телефон', 'email': 'Email', } for field, field_errors in form.errors.items(): field_label = field_labels.get(field, field) for error in field_errors: errors.append(f'{field_label}: {error}') # Возвращаем все ошибки error_message = '
'.join(errors) if errors else 'Ошибка валидации данных' return JsonResponse({ 'success': False, 'error': error_message, 'errors': form.errors # Добавляем детальную информацию об ошибках }, status=400) except json.JSONDecodeError: return JsonResponse({ 'success': False, 'error': 'Некорректный JSON' }, status=400) except Exception as e: return JsonResponse({ 'success': False, 'error': f'Ошибка сервера: {str(e)}' }, status=500)