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

@@ -158,9 +158,12 @@ def pos_terminal(request):
Товары загружаются прогрессивно через API при клике на категорию.
Работает только с одним выбранным складом.
"""
from customers.models import Customer
from django.core.cache import cache
# Получаем текущий склад для POS
current_warehouse = get_pos_warehouse(request)
if not current_warehouse:
# Нет активных складов - показываем ошибку
from django.contrib import messages
@@ -174,11 +177,61 @@ def pos_terminal(request):
'title': 'POS Terminal',
}
return render(request, 'pos/terminal.html', context)
# Получаем или создаём системного клиента
system_customer, _ = Customer.get_or_create_system_customer()
# Пытаемся получить сохраненного клиента из Redis
selected_customer = None
redis_key = f'pos:customer:{request.user.id}:{current_warehouse.id}'
cached_customer_data = cache.get(redis_key)
if cached_customer_data:
# Проверяем что клиент еще существует в БД
try:
customer = Customer.objects.get(id=cached_customer_data['customer_id'])
selected_customer = {
'id': customer.id,
'name': customer.name
}
except Customer.DoesNotExist:
# Клиент был удален - очищаем кэш
cache.delete(redis_key)
# Если нет сохраненного клиента - используем системного
if not selected_customer:
selected_customer = {
'id': system_customer.id,
'name': system_customer.name
}
# Пытаемся получить сохраненную корзину из Redis
cart_redis_key = f'pos:cart:{request.user.id}:{current_warehouse.id}'
cached_cart_data = cache.get(cart_redis_key)
cart_data = {}
if cached_cart_data:
# Валидируем товары и комплекты в корзине
from products.models import Product, ProductKit
for cart_key, item in cached_cart_data.items():
try:
if item['type'] == 'product':
# Проверяем что товар существует
Product.objects.get(id=item['id'])
cart_data[cart_key] = item
elif item['type'] in ('kit', 'showcase_kit'):
# Проверяем что комплект существует
ProductKit.objects.get(id=item['id'])
cart_data[cart_key] = item
except (Product.DoesNotExist, ProductKit.DoesNotExist):
# Товар или комплект удален - пропускаем
continue
# Загружаем только категории
categories_qs = ProductCategory.objects.filter(is_active=True)
categories = [{'id': c.id, 'name': c.name} for c in categories_qs]
# Список всех активных складов для модалки выбора
warehouses = Warehouse.objects.filter(is_active=True).order_by('-is_default', 'name')
warehouses_list = [{
@@ -196,11 +249,92 @@ def pos_terminal(request):
'name': current_warehouse.name
},
'warehouses': warehouses_list,
'system_customer': {
'id': system_customer.id,
'name': system_customer.name
},
'selected_customer': selected_customer, # Текущий выбранный клиент (из Redis или системный)
'cart_data': json.dumps(cart_data), # Сохраненная корзина из Redis
'title': 'POS Terminal',
}
return render(request, 'pos/terminal.html', context)
@login_required
@require_http_methods(["POST"])
def save_cart(request):
"""
Сохранить корзину POS в Redis для текущего пользователя и склада.
TTL: 2 часа (7200 секунд)
"""
from django.core.cache import cache
import json
# Получаем текущий склад
current_warehouse = get_pos_warehouse(request)
if not current_warehouse:
return JsonResponse({'success': False, 'error': 'Не выбран активный склад'}, status=400)
try:
# Получаем данные корзины из тела запроса
body = json.loads(request.body)
cart_data = body.get('cart', {})
# Валидация структуры данных корзины
if not isinstance(cart_data, dict):
return JsonResponse({'success': False, 'error': 'Неверный формат данных корзины'}, status=400)
# Сохраняем в Redis
redis_key = f'pos:cart:{request.user.id}:{current_warehouse.id}'
cache.set(redis_key, cart_data, timeout=7200) # 2 часа
return JsonResponse({
'success': True,
'items_count': len(cart_data)
})
except json.JSONDecodeError:
return JsonResponse({'success': False, 'error': 'Неверный формат JSON'}, status=400)
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=500)
@login_required
@require_http_methods(["POST"])
def set_customer(request, customer_id):
"""
Сохранить выбранного клиента в Redis для текущего пользователя и склада.
TTL: 2 часа (7200 секунд)
"""
from customers.models import Customer
from django.core.cache import cache
# Получаем текущий склад
current_warehouse = get_pos_warehouse(request)
if not current_warehouse:
return JsonResponse({'success': False, 'error': 'Не выбран активный склад'}, status=400)
# Проверяем, что клиент существует
try:
customer = Customer.objects.get(id=customer_id)
except Customer.DoesNotExist:
return JsonResponse({'success': False, 'error': 'Клиент не найден'}, status=404)
# Сохраняем в Redis
redis_key = f'pos:customer:{request.user.id}:{current_warehouse.id}'
customer_data = {
'customer_id': customer.id,
'customer_name': customer.name
}
cache.set(redis_key, customer_data, timeout=7200) # 2 часа
return JsonResponse({
'success': True,
'customer_id': customer.id,
'customer_name': customer.name
})
@login_required
@require_http_methods(["POST"])
def set_warehouse(request, warehouse_id):