Add Redis-based persistence for POS cart and customer selection

Implemented Redis caching with 2-hour TTL for POS session data:

Backend changes:
- Added Redis cache configuration in settings.py
- Created save_cart() endpoint to persist cart state
- Added cart and customer loading from Redis in pos_terminal()
- Validates cart items (products/kits) still exist in DB
- Added REDIS_HOST, REDIS_PORT, REDIS_DB to .env

Frontend changes:
- Added saveCartToRedis() with 500ms debounce
- Cart auto-saves on add/remove/quantity change
- Added cart initialization from Redis on page load
- Enhanced customer button with two-line display and reset button
- Red X button appears only for non-system customers

Features:
- Cart persists across page reloads (2 hour TTL)
- Customer selection persists (2 hour TTL)
- Independent cart per user+warehouse combination
- Automatic cleanup of deleted items
- Debounced saves to reduce server load

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-20 09:55:03 +03:00
parent 685c06d94d
commit eac778b06d
5 changed files with 614 additions and 19 deletions

View File

@@ -56,13 +56,22 @@
<div class="card mb-2 flex-grow-1" style="min-height: 0;">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="mb-0">Корзина</h6>
<button class="btn btn-outline-primary btn-sm" id="customerSelectBtn">
<i class="bi bi-person"></i> Выбрать клиента
</button>
<div class="d-flex gap-1 align-items-center">
<button class="btn btn-outline-primary btn-sm d-flex align-items-center" id="customerSelectBtn">
<i class="bi bi-person me-1"></i>
<div class="d-flex flex-column align-items-start lh-1">
<small class="text-muted" style="font-size: 0.65rem;">Клиент</small>
<span id="customerSelectBtnText" class="fw-semibold">Выбрать</span>
</div>
</button>
<button class="btn btn-sm btn-outline-danger" id="resetCustomerBtn" title="Сбросить на системного клиента" style="display: none;">
<i class="bi bi-x-lg"></i>
</button>
</div>
</div>
<div class="card-body d-flex flex-column" style="min-height: 0;">
<div id="cartList" class="flex-grow-1" style="overflow-y: auto;"></div>
<div class="mt-auto">
<div class="d-flex justify-content-between align-items-center py-1 border-top">
<strong class="mb-0">Итого:</strong>
@@ -266,13 +275,21 @@
<div class="row">
<!-- Левая колонка: состав заказа -->
<div class="col-md-7">
<!-- Информация о клиенте -->
<div class="mb-3">
<strong>Клиент</strong>
<div class="border rounded p-2 mt-2 bg-light">
<div class="fw-bold" id="checkoutCustomerName"></div>
</div>
</div>
<div class="mb-3">
<strong>Состав заказа</strong>
<div class="border rounded p-3 mt-2" id="checkoutItems" style="max-height: 280px; overflow-y: auto; background: #f8f9fa;">
<div class="border rounded p-3 mt-2" id="checkoutItems" style="max-height: 240px; overflow-y: auto; background: #f8f9fa;">
<!-- Заполняется из JS -->
</div>
</div>
<!-- Опции оплаты и комментарий -->
<div class="row g-3">
<div class="col-md-6">
@@ -372,8 +389,8 @@
<div class="modal-body">
<div class="list-group" id="warehouseList">
{% for warehouse in warehouses %}
<button type="button" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center warehouse-item"
data-warehouse-id="{{ warehouse.id }}"
<button type="button" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center warehouse-item"
data-warehouse-id="{{ warehouse.id }}"
data-warehouse-name="{{ warehouse.name }}">
<div>
<strong>{{ warehouse.name }}</strong>
@@ -394,6 +411,77 @@
</div>
</div>
</div>
<!-- Модалка: Выбор клиента -->
<div class="modal fade" id="selectCustomerModal" tabindex="-1" aria-labelledby="selectCustomerModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="selectCustomerModalLabel">
<i class="bi bi-person-search"></i> Выбор клиента
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="customerSearchInput" class="form-label">Поиск клиента</label>
<input type="text" class="form-select" id="customerSearchInput"
placeholder="Начните вводить имя, телефон или email (минимум 3 символа)">
</div>
<div class="d-grid gap-2">
<button type="button" class="btn btn-outline-success" id="createNewCustomerBtn">
<i class="bi bi-person-plus"></i> Создать нового клиента
</button>
<button type="button" class="btn btn-outline-secondary" id="selectSystemCustomerBtn">
<i class="bi bi-person"></i> Выбрать системного клиента (анонимный)
</button>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
</div>
</div>
</div>
</div>
<!-- Модалка: Создание нового клиента -->
<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 class="mb-3">
<label for="newCustomerName" class="form-label">Имя *</label>
<input type="text" class="form-control" id="newCustomerName" placeholder="Введите имя клиента" required>
</div>
<div class="mb-3">
<label for="newCustomerPhone" class="form-label">Телефон</label>
<input type="text" class="form-control" id="newCustomerPhone" placeholder="+375XXXXXXXXX">
</div>
<div class="mb-3">
<label for="newCustomerEmail" class="form-label">Email</label>
<input type="email" class="form-control" id="newCustomerEmail" placeholder="email@example.com">
</div>
<div id="createCustomerError" class="alert alert-danger d-none"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-primary" id="confirmCreateCustomerBtn">
<i class="bi bi-check-circle"></i> Создать
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
@@ -401,6 +489,19 @@
<script id="categoriesData" type="application/json">{{ categories_json|safe }}</script>
<script id="itemsData" type="application/json">{{ items_json|safe }}</script>
<script id="showcaseKitsData" type="application/json">{{ showcase_kits_json|safe }}</script>
<script id="systemCustomerData" type="application/json">
{
"id": {{ system_customer.id }},
"name": "{{ system_customer.name|escapejs }}"
}
</script>
<script id="selectedCustomerData" type="application/json">
{
"id": {{ selected_customer.id }},
"name": "{{ selected_customer.name|escapejs }}"
}
</script>
<script id="cartData" type="application/json">{{ cart_data|safe }}</script>
<script src="{% static 'pos/js/terminal.js' %}"></script>
{% endblock %}