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:
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user