Реализовано: - Frontend валидация: минимум 3 символа для запуска поиска - Динамическое отображение подсказки при попытке отправить поиск < 3 символов - Визуальное выделение ошибки (is-invalid класс для input) - Автоматическое скрытие ошибки при вводе 3+ символов Backend уже использует ту же логику оптимизированного поиска: - Те же стратегии поиска (name_only, universal, email и т.д.) - Тот же SQL LIKE запрос для поиска по цифрам телефона - Тот же API эндпоинт api_search_customers() Теперь обе страницы (создание заказа и список клиентов) используют единую оптимизированную логику поиска и требуют минимум 3 символа. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
174 lines
9.5 KiB
HTML
174 lines
9.5 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}Клиенты{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="container-fluid">
|
||
<div class="row">
|
||
<div class="col-12">
|
||
|
||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||
<h1>Клиенты</h1>
|
||
<a href="{% url 'customers:customer-create' %}" class="btn btn-primary">Добавить клиента</a>
|
||
</div>
|
||
|
||
<!-- Search Form -->
|
||
<div class="card mb-4">
|
||
<div class="card-body">
|
||
<form method="get" class="row g-3" id="search-form">
|
||
<div class="col-md-6">
|
||
<input type="text" class="form-control" name="q"
|
||
value="{{ query|default:'' }}" placeholder="Поиск по имени, email или телефону (минимум 3 символа)..." id="search-input">
|
||
<small class="form-text text-muted" id="search-hint" style="display: none; color: #dc3545 !important;">
|
||
Введите минимум 3 символа для поиска
|
||
</small>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<button type="submit" class="btn btn-outline-primary" id="search-btn">Поиск</button>
|
||
{% if query %}
|
||
<a href="{% url 'customers:customer-list' %}" class="btn btn-outline-secondary">Очистить</a>
|
||
{% endif %}
|
||
</div>
|
||
</form>
|
||
<script>
|
||
document.getElementById('search-form').addEventListener('submit', function(e) {
|
||
const searchInput = document.getElementById('search-input');
|
||
const searchValue = searchInput.value.trim();
|
||
const searchHint = document.getElementById('search-hint');
|
||
|
||
// Если поле пусто или содержит менее 3 символов, не отправляем форму
|
||
if (searchValue && searchValue.length < 3) {
|
||
e.preventDefault();
|
||
searchHint.style.display = 'block';
|
||
searchInput.classList.add('is-invalid');
|
||
return false;
|
||
}
|
||
|
||
// Если поле пусто, тоже не отправляем (это будет просто пусто)
|
||
if (!searchValue) {
|
||
e.preventDefault();
|
||
return false;
|
||
}
|
||
|
||
// Все хорошо, отправляем
|
||
searchHint.style.display = 'none';
|
||
searchInput.classList.remove('is-invalid');
|
||
});
|
||
|
||
// Убираем ошибку при вводе
|
||
document.getElementById('search-input').addEventListener('input', function() {
|
||
const searchValue = this.value.trim();
|
||
const searchHint = document.getElementById('search-hint');
|
||
|
||
if (searchValue.length >= 3) {
|
||
searchHint.style.display = 'none';
|
||
this.classList.remove('is-invalid');
|
||
}
|
||
});
|
||
</script>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Customers Table -->
|
||
<div class="card">
|
||
<div class="card-body">
|
||
|
||
{% if page_obj %}
|
||
<div class="table-responsive">
|
||
<table class="table table-hover align-middle">
|
||
<thead>
|
||
<tr>
|
||
<th>Имя</th>
|
||
<th>Email</th>
|
||
<th>Телефон</th>
|
||
<th>Уровень лояльности</th>
|
||
<th>Сумма покупок</th>
|
||
<th>VIP</th>
|
||
<th class="text-end">Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for customer in page_obj %}
|
||
<tr
|
||
class="{% if customer.is_vip %}table-warning{% endif %}"
|
||
style="cursor:pointer"
|
||
onclick="window.location='{% url 'customers:customer-detail' customer.pk %}'"
|
||
>
|
||
<td class="fw-semibold">{{ customer.full_name }}</td>
|
||
<td>{{ customer.email|default:'—' }}</td>
|
||
<td>{{ customer.phone|default:'—' }}</td>
|
||
|
||
<td>
|
||
<span class="badge
|
||
{% if customer.loyalty_tier == 'no_discount' %}bg-light text-muted
|
||
{% elif customer.loyalty_tier == 'bronze' %}bg-secondary text-white
|
||
{% elif customer.loyalty_tier == 'silver' %}bg-info text-dark
|
||
{% elif customer.loyalty_tier == 'gold' %}bg-warning text-dark
|
||
{% elif customer.loyalty_tier == 'platinum' %}bg-primary text-white
|
||
{% endif %}
|
||
">
|
||
{{ customer.get_loyalty_tier_display }}
|
||
</span>
|
||
</td>
|
||
|
||
<td>{{ customer.total_spent|default:0|floatformat:2 }} руб.</td>
|
||
|
||
<td>
|
||
{% if customer.is_vip %}
|
||
<span class="badge bg-success">Да</span>
|
||
{% else %}
|
||
<span class="badge bg-secondary text-white">Нет</span>
|
||
{% endif %}
|
||
</td>
|
||
|
||
<td class="text-end" onclick="event.stopPropagation();">
|
||
<a href="{% url 'customers:customer-detail' customer.pk %}"
|
||
class="btn btn-sm btn-outline-primary">👁</a>
|
||
<a href="{% url 'customers:customer-update' customer.pk %}"
|
||
class="btn btn-sm btn-outline-secondary">✎</a>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Pagination -->
|
||
{% if page_obj.has_other_pages %}
|
||
<nav aria-label="Page navigation">
|
||
<ul class="pagination justify-content-center">
|
||
{% if page_obj.has_previous %}
|
||
<li class="page-item">
|
||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if query %}&q={{ query }}{% endif %}">Предыдущая</a>
|
||
</li>
|
||
{% endif %}
|
||
|
||
{% for num in page_obj.paginator.page_range %}
|
||
{% if page_obj.number == num %}
|
||
<li class="page-item active"><span class="page-link">{{ num }}</span></li>
|
||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||
<li class="page-item"><a class="page-link" href="?page={{ num }}{% if query %}&q={{ query }}{% endif %}">{{ num }}</a></li>
|
||
{% endif %}
|
||
{% endfor %}
|
||
|
||
{% if page_obj.has_next %}
|
||
<li class="page-item">
|
||
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if query %}&q={{ query }}{% endif %}">Следующая</a>
|
||
</li>
|
||
{% endif %}
|
||
</ul>
|
||
</nav>
|
||
{% endif %}
|
||
|
||
{% else %}
|
||
<p>Клиенты не найдены.</p>
|
||
{% endif %}
|
||
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|