feat: Добавлена функциональность управления заказами и улучшен поиск товаров

Заказы:
- Добавлены миграции для исторических записей с полями оплаты и получателя
- Расширен admin для заказов с инлайнами товаров/комплектов
- Реализованы представления списка, создания, редактирования и удаления заказов
- Добавлен шаблон подтверждения удаления заказа
- Настроены URL-маршруты для работы с заказами

Клиенты:
- Добавлена миграция с новыми полями адресов и подтверждений
- Обновлена модель клиентов с дополнительными полями
- Улучшен admin для работы с клиентами

Товары:
- Значительно улучшен API поиска товаров с поддержкой фильтрации
- Добавлен Select2 виджет для динамического поиска товаров
- Создан статический JS файл для интеграции Select2
- Оптимизирована обработка запросов и ответов API

Прочее:
- Добавлены новые настройки в settings.py
- Обновлена навигация в navbar.html
- Обновлены URL-маршруты проекта

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-07 16:10:19 +03:00
parent a1dfb6a257
commit ec0557c8cf
15 changed files with 974 additions and 70 deletions

View File

@@ -0,0 +1,148 @@
/**
* Select2 Product Search Module
* Переиспользуемый модуль для инициализации Select2 с AJAX поиском товаров
*/
(function(window) {
'use strict';
// Форматирование результата в выпадающем списке
function formatSelectResult(item) {
if (item.loading) return item.text;
// Если это группа (header для товаров/комплектов), просто возвращаем текст
if (item.children) {
return item.text;
}
var $container = $('<div class="select2-result-item d-flex justify-content-between align-items-center">');
// Левая часть: иконка + название
var $left = $('<div class="d-flex align-items-center">');
// Добавляем иконку в зависимости от типа
if (item.type === 'product') {
$left.append($('<span class="me-2">').text('🌹'));
} else if (item.type === 'kit') {
$left.append($('<span class="me-2">').text('💐'));
} else if (item.type === 'variant') {
$left.append($('<span class="me-2">').text('🔄'));
}
// Название
var $name = $('<span>').text(item.text);
$left.append($name);
$container.append($left);
// Правая часть: цена и статус наличия
if (item.actual_price || item.price) {
var priceText = item.actual_price || item.price;
var $price = $('<span class="text-muted small">').text(priceText + ' руб.');
$container.append($price);
}
// Индикатор наличия для товаров
if (item.type === 'product' && item.in_stock !== undefined) {
if (item.in_stock) {
$container.append($('<span class="badge bg-success ms-2 small">').text('В наличии'));
} else {
$container.append($('<span class="badge bg-secondary ms-2 small">').text('Нет'));
}
}
return $container;
}
// Форматирование выбранного элемента
function formatSelectSelection(item) {
// Добавляем иконку для выбранного элемента
if (item.type === 'product') {
return '🌹 ' + item.text;
} else if (item.type === 'kit') {
return '💐 ' + item.text;
} else if (item.type === 'variant') {
return '🔄 ' + item.text;
}
return item.text || item.id;
}
/**
* Инициализирует Select2 для элемента с AJAX поиском товаров
* @param {Element|jQuery} element - DOM элемент или jQuery объект select
* @param {string} type - Тип поиска ('product', 'variant', 'kit' или 'all')
* @param {string} apiUrl - URL API для поиска
* @param {Object} preloadedData - Предзагруженные данные товара
*/
window.initProductSelect2 = function(element, type, apiUrl, preloadedData) {
if (!element) return;
// Преобразуем в jQuery если нужно
var $element = $(element);
// Если уже инициализирован, пропускаем
if ($element.data('select2')) {
return;
}
var placeholders = {
'product': 'Начните вводить название товара...',
'variant': 'Начните вводить название группы...',
'kit': 'Начните вводить название комплекта...',
'all': 'Начните вводить название товара или комплекта...'
};
var config = {
theme: 'bootstrap-5',
placeholder: placeholders[type] || 'Выберите...',
allowClear: true,
language: 'ru',
minimumInputLength: 0,
ajax: {
url: apiUrl,
dataType: 'json',
delay: 250,
data: function (params) {
return {
q: params.term || '',
type: type,
page: params.page || 1
};
},
processResults: function (data) {
return {
results: data.results,
pagination: {
more: data.pagination.more
}
};
},
cache: true
},
templateResult: formatSelectResult,
templateSelection: formatSelectSelection
};
// Если есть предзагруженные данные, создаем option с ними
if (preloadedData) {
var option = new Option(preloadedData.text, preloadedData.id, true, true);
// Сохраняем дополнительные данные для форматирования
$(option).data('data', preloadedData);
$element.append(option);
}
$element.select2(config);
};
/**
* Инициализирует Select2 для всех элементов с данным селектором
* @param {string} selector - CSS селектор элементов
* @param {string} type - Тип поиска ('product' или 'variant')
* @param {string} apiUrl - URL API для поиска
*/
window.initAllProductSelect2 = function(selector, type, apiUrl) {
document.querySelectorAll(selector).forEach(function(element) {
window.initProductSelect2(element, type, apiUrl);
});
};
})(window);