').text(customer.phone).html());
+ }
+ if (customer.email) {
+ contactInfo.push($('
').text(customer.email).html());
+ }
+
+ if (contactInfo.length > 0) {
+ parts.push('
(' + contactInfo.join(', ') + ')');
+ }
+
+ return $('
' + parts.join('') + '');
+}
+
+/**
+ * Форматирование выбранного клиента в поле Select2
+ * Показывает только имя
+ */
+function formatCustomerSelection(customer) {
+ return customer.name || customer.text;
+}
+
+/**
+ * Открывает модальное окно создания нового клиента
+ * @param {string} prefillName - Предзаполненное имя (из поиска)
+ */
+function openCreateCustomerModal(prefillName = '') {
+ const modal = new bootstrap.Modal(document.getElementById('createCustomerModal'));
+
+ // Очищаем форму
+ document.getElementById('newCustomerName').value = prefillName || '';
+ document.getElementById('newCustomerPhone').value = '';
+ document.getElementById('newCustomerEmail').value = '';
+ document.getElementById('createCustomerError').classList.add('d-none');
+
+ modal.show();
+}
+
+/**
+ * Создаёт нового клиента через API
+ */
+async function createNewCustomer() {
+ const name = document.getElementById('newCustomerName').value.trim();
+ const phone = document.getElementById('newCustomerPhone').value.trim();
+ const email = document.getElementById('newCustomerEmail').value.trim();
+ const errorBlock = document.getElementById('createCustomerError');
+
+ // Валидация
+ if (!name) {
+ errorBlock.textContent = 'Укажите имя клиента';
+ errorBlock.classList.remove('d-none');
+ return;
+ }
+
+ // Скрываем ошибку
+ errorBlock.classList.add('d-none');
+
+ try {
+ const response = await fetch('/customers/api/create/', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': getCsrfToken()
+ },
+ body: JSON.stringify({
+ name: name,
+ phone: phone || null,
+ email: email || null
+ })
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ // Выбираем созданного клиента
+ selectCustomer(data.id, data.name);
+
+ // Закрываем модалку
+ const modal = bootstrap.Modal.getInstance(document.getElementById('createCustomerModal'));
+ modal.hide();
+
+ // Показываем уведомление
+ alert(`Клиент "${data.name}" успешно создан!`);
+ } else {
+ // Показываем ошибку
+ errorBlock.textContent = data.error || 'Ошибка при создании клиента';
+ errorBlock.classList.remove('d-none');
+ }
+ } catch (error) {
+ console.error('Error creating customer:', error);
+ errorBlock.textContent = 'Ошибка сети при создании клиента';
+ errorBlock.classList.remove('d-none');
+ }
+}
+
function renderCategories() {
const grid = document.getElementById('categoryGrid');
grid.innerHTML = '';
@@ -334,20 +621,21 @@ function setupInfiniteScroll() {
function addToCart(item) {
const cartKey = `${item.type}-${item.id}`; // Уникальный ключ: "product-1" или "kit-1"
-
+
if (!cart.has(cartKey)) {
cart.set(cartKey, { id: item.id, name: item.name, price: Number(item.price), qty: 1, type: item.type });
} else {
cart.get(cartKey).qty += 1;
}
-
+
renderCart();
-
+ saveCartToRedis(); // Сохраняем в Redis
+
// Автоматический фокус на поле количества
setTimeout(() => {
const qtyInputs = document.querySelectorAll('.qty-input');
const itemIndex = Array.from(cart.keys()).indexOf(cartKey);
-
+
if (itemIndex !== -1 && qtyInputs[itemIndex]) {
qtyInputs[itemIndex].focus();
qtyInputs[itemIndex].select(); // Выделяем весь текст
@@ -403,6 +691,7 @@ function renderCart() {
} else {
cart.get(cartKey).qty = newQty;
renderCart();
+ saveCartToRedis(); // Сохраняем в Redis при изменении количества
}
};
@@ -434,11 +723,13 @@ function renderCart() {
function removeFromCart(cartKey) {
cart.delete(cartKey);
renderCart();
+ saveCartToRedis(); // Сохраняем в Redis
}
function clearCart() {
cart.clear();
renderCart();
+ saveCartToRedis(); // Сохраняем пустую корзину в Redis
}
document.getElementById('clearCart').onclick = clearCart;
@@ -954,7 +1245,7 @@ function renderCheckoutModal() {
cart.forEach((item) => {
const row = document.createElement('div');
row.className = 'd-flex justify-content-between align-items-center mb-2 pb-2 border-bottom';
-
+
// Иконка для комплектов
let typeIcon = '';
if (item.type === 'kit' || item.type === 'showcase_kit') {
@@ -962,7 +1253,7 @@ function renderCheckoutModal() {
} else {
typeIcon = '
';
}
-
+
row.innerHTML = `
${typeIcon}${item.name}
@@ -974,6 +1265,9 @@ function renderCheckoutModal() {
total += item.qty * item.price;
});
+ // Обновляем информацию о клиенте
+ updateCustomerDisplay();
+
// Обновляем базовую цену и пересчитываем
updateCheckoutPricing(total);
}
@@ -1094,9 +1388,57 @@ document.getElementById('scheduleLater').onclick = async () => {
alert('Функционал будет подключен позже: создание заказа на доставку/самовывоз.');
};
-// Customer selection
+// ===== ОБРАБОТЧИКИ ДЛЯ РАБОТЫ С КЛИЕНТОМ =====
+
+// Кнопка "Выбрать клиента" в корзине
document.getElementById('customerSelectBtn').addEventListener('click', () => {
- alert('Функция выбора клиента будет реализована позже');
+ const modal = new bootstrap.Modal(document.getElementById('selectCustomerModal'));
+ modal.show();
+});
+
+// Кнопка сброса клиента на системного
+document.getElementById('resetCustomerBtn').addEventListener('click', () => {
+ selectCustomer(SYSTEM_CUSTOMER.id, SYSTEM_CUSTOMER.name);
+});
+
+// Кнопка "Создать нового клиента" в модалке выбора
+document.getElementById('createNewCustomerBtn').addEventListener('click', () => {
+ // Закрываем модалку выбора
+ const selectModal = bootstrap.Modal.getInstance(document.getElementById('selectCustomerModal'));
+ selectModal.hide();
+
+ // Открываем модалку создания
+ openCreateCustomerModal();
+});
+
+// Кнопка "Выбрать системного клиента"
+document.getElementById('selectSystemCustomerBtn').addEventListener('click', () => {
+ selectCustomer(SYSTEM_CUSTOMER.id, SYSTEM_CUSTOMER.name);
+
+ // Закрываем модалку
+ const modal = bootstrap.Modal.getInstance(document.getElementById('selectCustomerModal'));
+ modal.hide();
+});
+
+// Кнопка подтверждения создания клиента
+document.getElementById('confirmCreateCustomerBtn').addEventListener('click', () => {
+ createNewCustomer();
+});
+
+// Инициализация Select2 при загрузке страницы
+document.addEventListener('DOMContentLoaded', () => {
+ initCustomerSelect2();
+ updateCustomerDisplay(); // Обновляем UI с системным клиентом
+
+ // Восстанавливаем корзину из Redis (если есть сохраненные данные)
+ const savedCartData = JSON.parse(document.getElementById('cartData').textContent);
+ if (savedCartData && Object.keys(savedCartData).length > 0) {
+ // Конвертируем обычный объект обратно в Map
+ Object.entries(savedCartData).forEach(([key, value]) => {
+ cart.set(key, value);
+ });
+ renderCart(); // Отрисовываем восстановленную корзину
+ }
});
// Смена склада
diff --git a/myproject/pos/templates/pos/terminal.html b/myproject/pos/templates/pos/terminal.html
index cebf626..deb234a 100644
--- a/myproject/pos/templates/pos/terminal.html
+++ b/myproject/pos/templates/pos/terminal.html
@@ -56,13 +56,22 @@
-
+
Итого:
@@ -266,13 +275,21 @@
+
+
+
Состав заказа
-
-
+
@@ -372,8 +389,8 @@
{% for warehouse in warehouses %}
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{% endblock %}
{% block extra_js %}
@@ -401,6 +489,19 @@
+
+
+
{% endblock %}
diff --git a/myproject/pos/urls.py b/myproject/pos/urls.py
index 76971b4..491475a 100644
--- a/myproject/pos/urls.py
+++ b/myproject/pos/urls.py
@@ -9,6 +9,10 @@ urlpatterns = [
path('', views.pos_terminal, name='terminal'),
# Установить текущий склад для POS (сохранение в сессии) [POST]
path('api/set-warehouse/
/', views.set_warehouse, name='set-warehouse'),
+ # Установить текущего клиента для POS (сохранение в Redis с TTL 2 часа) [POST]
+ path('api/set-customer//', views.set_customer, name='set-customer'),
+ # Сохранить корзину POS (сохранение в Redis с TTL 2 часа) [POST]
+ path('api/save-cart/', views.save_cart, name='save-cart'),
# Получить товары и комплекты (пагинация, поиск, сортировка) [GET]
path('api/items/', views.get_items_api, name='items-api'),
# Получить список активных витрин [GET]
diff --git a/myproject/pos/views.py b/myproject/pos/views.py
index 545294e..04c5b13 100644
--- a/myproject/pos/views.py
+++ b/myproject/pos/views.py
@@ -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):