Добавлена поддержка черновиков заказов (Этап 3/3): JavaScript, UI и cleanup
JavaScript: - Создан autosave.js: модуль автосохранения черновиков - Debouncing с задержкой 3 секунды - Визуальный индикатор статуса сохранения - Автоматическое отслеживание изменений в полях формы и formset UI обновления: - order_form.html: добавлен data-атрибут is-draft и подключение autosave.js - order_list.html: добавлен badge "Черновик" для черновиков в списке заказов Management команда: - cleanup_draft_orders: очистка старых черновиков (по умолчанию 30 дней) - Поддержка --dry-run для предпросмотра - Автоматическое удаление связанных временных комплектов Использование: - python manage.py cleanup_draft_orders --days=30 - python manage.py cleanup_draft_orders --days=7 --dry-run Система черновиков полностью готова к использованию! 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
95
myproject/orders/management/commands/cleanup_draft_orders.py
Normal file
95
myproject/orders/management/commands/cleanup_draft_orders.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
"""
|
||||||
|
Management :><0=40 4;O >G8AB:8 AB0@KE G5@=>28:>2 70:07>2.
|
||||||
|
|
||||||
|
#40;O5B G5@=>28:8 70:07>2, :>B>@K5 =5 >1=>2;O;8AL 1>;55 C:070==>3> :>;8G5AB20 4=59.
|
||||||
|
"0:65 C40;O5B A2O70==K5 2@5<5==K5 :><?;5:BK.
|
||||||
|
|
||||||
|
A?>;L7>20=85:
|
||||||
|
python manage.py cleanup_draft_orders --days=30
|
||||||
|
python manage.py cleanup_draft_orders --days=7 --dry-run
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from orders.services import DraftOrderService
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = '#40;O5B AB0@K5 G5@=>28:8 70:07>2 8 A2O70==K5 2@5<5==K5 :><?;5:BK'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
'--days',
|
||||||
|
type=int,
|
||||||
|
default=30,
|
||||||
|
help='>;8G5AB2> 4=59, ?>A;5 :>B>@KE G5@=>28: AG8B05BAO AB0@K< (?> C<>;G0=8N: 30)'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--dry-run',
|
||||||
|
action='store_true',
|
||||||
|
help='>:070BL :>;8G5AB2> G5@=>28:>2 4;O C40;5=8O 157 D0:B8G5A:>3> C40;5=8O'
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
days = options['days']
|
||||||
|
dry_run = options['dry_run']
|
||||||
|
|
||||||
|
if days < 1:
|
||||||
|
raise CommandError('>;8G5AB2> 4=59 4>;6=> 1KBL 1>;LH5 0')
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.WARNING(
|
||||||
|
f'>8A: G5@=>28:>2 AB0@H5 {days} 4=59...'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
# @568<5 dry-run B>;L:> ?>4AG8BK205<
|
||||||
|
from datetime import timedelta
|
||||||
|
from django.utils import timezone
|
||||||
|
from orders.models import Order
|
||||||
|
|
||||||
|
cutoff_date = timezone.now() - timedelta(days=days)
|
||||||
|
old_drafts = Order.objects.filter(
|
||||||
|
status='draft',
|
||||||
|
last_autosave_at__lt=cutoff_date
|
||||||
|
)
|
||||||
|
count = old_drafts.count()
|
||||||
|
|
||||||
|
if count == 0:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f'5B G5@=>28:>2 AB0@H5 {days} 4=59'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.WARNING(
|
||||||
|
f'0945=> {count} G5@=>28:>2 4;O C40;5=8O:'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for draft in old_drafts:
|
||||||
|
last_save = draft.last_autosave_at.strftime('%d.%m.%Y %H:%M') if draft.last_autosave_at else '=8:>340'
|
||||||
|
self.stdout.write(
|
||||||
|
f' - 0:07 #{draft.order_number} (?>A;54=55 A>E@0=5=85: {last_save})'
|
||||||
|
)
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.WARNING(
|
||||||
|
f'\n0?CAB8B5 157 --dry-run 4;O D0:B8G5A:>3> C40;5=8O'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# $0:B8G5A:>5 C40;5=85
|
||||||
|
count = DraftOrderService.delete_old_drafts(days=days)
|
||||||
|
|
||||||
|
if count == 0:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f'5B G5@=>28:>2 AB0@H5 {days} 4=59'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f'#A?5H=> C40;5=> {count} G5@=>28:>2 8 A2O70==KE 2@5<5==KE :><?;5:B>2'
|
||||||
|
)
|
||||||
|
)
|
||||||
504
myproject/orders/static/orders/js/autosave.js
Normal file
504
myproject/orders/static/orders/js/autosave.js
Normal file
@@ -0,0 +1,504 @@
|
|||||||
|
/**
|
||||||
|
* Модуль автосохранения черновиков заказов.
|
||||||
|
*
|
||||||
|
* Автоматически сохраняет изменения в черновике заказа при изменении полей формы.
|
||||||
|
* Использует debouncing для уменьшения количества запросов к серверу.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Конфигурация
|
||||||
|
const CONFIG = {
|
||||||
|
AUTOSAVE_DELAY: 3000, // Задержка перед автосохранением (мс)
|
||||||
|
AUTOSAVE_URL_PATTERN: '/orders/{orderId}/autosave/',
|
||||||
|
STATUS_DISPLAY_DURATION: 5000, // Длительность показа статуса (мс)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Состояние модуля
|
||||||
|
let autosaveTimer = null;
|
||||||
|
let isAutosaving = false;
|
||||||
|
let isDraft = false;
|
||||||
|
let orderId = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инициализация модуля автосохранения
|
||||||
|
*/
|
||||||
|
function init() {
|
||||||
|
// Проверяем, что мы на странице редактирования черновика
|
||||||
|
const orderForm = document.querySelector('form[action*="edit"]');
|
||||||
|
if (!orderForm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем ID заказа из URL
|
||||||
|
const urlMatch = window.location.pathname.match(/\/orders\/(\d+)\/edit\//);
|
||||||
|
if (!urlMatch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
orderId = urlMatch[1];
|
||||||
|
|
||||||
|
// Проверяем, является ли заказ черновиком
|
||||||
|
isDraft = checkIfDraft();
|
||||||
|
if (!isDraft) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Autosave] Initialized for draft order #' + orderId);
|
||||||
|
|
||||||
|
// Инициализируем UI индикатора
|
||||||
|
initStatusIndicator();
|
||||||
|
|
||||||
|
// Добавляем обработчики событий
|
||||||
|
attachEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, является ли заказ черновиком
|
||||||
|
*/
|
||||||
|
function checkIfDraft() {
|
||||||
|
// Проверяем через data-атрибут на форме
|
||||||
|
const form = document.querySelector('form[action*="edit"]');
|
||||||
|
if (form && form.dataset.isDraft === 'true') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем через заголовок страницы
|
||||||
|
const title = document.querySelector('h1');
|
||||||
|
if (title && title.textContent.includes('черновик')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создает индикатор статуса автосохранения
|
||||||
|
*/
|
||||||
|
function initStatusIndicator() {
|
||||||
|
// Проверяем, не создан ли уже индикатор
|
||||||
|
if (document.getElementById('autosave-status')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const indicator = document.createElement('div');
|
||||||
|
indicator.id = 'autosave-status';
|
||||||
|
indicator.className = 'alert alert-info';
|
||||||
|
indicator.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 70px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 1050;
|
||||||
|
min-width: 250px;
|
||||||
|
display: none;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||||
|
`;
|
||||||
|
indicator.innerHTML = `
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span id="autosave-icon" class="me-2"></span>
|
||||||
|
<span id="autosave-text">Автосохранение...</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(indicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Показывает статус автосохранения
|
||||||
|
*/
|
||||||
|
function showStatus(type, message) {
|
||||||
|
const indicator = document.getElementById('autosave-status');
|
||||||
|
const icon = document.getElementById('autosave-icon');
|
||||||
|
const text = document.getElementById('autosave-text');
|
||||||
|
|
||||||
|
if (!indicator || !icon || !text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Убираем все классы
|
||||||
|
indicator.className = 'alert';
|
||||||
|
|
||||||
|
// Устанавливаем соответствующий класс и иконку
|
||||||
|
switch (type) {
|
||||||
|
case 'saving':
|
||||||
|
indicator.classList.add('alert-info');
|
||||||
|
icon.innerHTML = '<span class="spinner-border spinner-border-sm" role="status"></span>';
|
||||||
|
break;
|
||||||
|
case 'success':
|
||||||
|
indicator.classList.add('alert-success');
|
||||||
|
icon.innerHTML = '<i class="bi bi-check-circle-fill"></i>';
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
indicator.classList.add('alert-danger');
|
||||||
|
icon.innerHTML = '<i class="bi bi-exclamation-triangle-fill"></i>';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
text.textContent = message;
|
||||||
|
indicator.style.display = 'block';
|
||||||
|
|
||||||
|
// Автоматически скрываем статус (кроме ошибок)
|
||||||
|
if (type !== 'error') {
|
||||||
|
setTimeout(() => {
|
||||||
|
indicator.style.display = 'none';
|
||||||
|
}, CONFIG.STATUS_DISPLAY_DURATION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Прикрепляет обработчики событий к полям формы
|
||||||
|
*/
|
||||||
|
function attachEventListeners() {
|
||||||
|
const form = document.querySelector('form[action*="edit"]');
|
||||||
|
if (!form) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Слушаем изменения в основных полях заказа
|
||||||
|
const fieldsToWatch = [
|
||||||
|
'select[name="customer"]',
|
||||||
|
'input[name="delivery_date"]',
|
||||||
|
'input[name="delivery_time_start"]',
|
||||||
|
'input[name="delivery_time_end"]',
|
||||||
|
'input[name="delivery_cost"]',
|
||||||
|
'select[name="payment_method"]',
|
||||||
|
'textarea[name="special_instructions"]',
|
||||||
|
'input[name="discount_amount"]',
|
||||||
|
'input[type="checkbox"]',
|
||||||
|
'input[type="radio"]',
|
||||||
|
'select[name="delivery_address"]',
|
||||||
|
'select[name="pickup_shop"]',
|
||||||
|
];
|
||||||
|
|
||||||
|
fieldsToWatch.forEach(selector => {
|
||||||
|
const fields = form.querySelectorAll(selector);
|
||||||
|
fields.forEach(field => {
|
||||||
|
// Для select и checkbox используем 'change'
|
||||||
|
if (field.tagName === 'SELECT' || field.type === 'checkbox' || field.type === 'radio') {
|
||||||
|
field.addEventListener('change', scheduleAutosave);
|
||||||
|
} else {
|
||||||
|
// Для текстовых полей используем 'input'
|
||||||
|
field.addEventListener('input', scheduleAutosave);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Слушаем изменения в формах товаров (formset)
|
||||||
|
observeFormsetChanges();
|
||||||
|
|
||||||
|
console.log('[Autosave] Event listeners attached');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Наблюдает за изменениями в формсете товаров
|
||||||
|
*/
|
||||||
|
function observeFormsetChanges() {
|
||||||
|
const formsetContainer = document.getElementById('order-items-formset');
|
||||||
|
if (!formsetContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Наблюдаем за добавлением/удалением форм
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
attachFormsetEventListeners();
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(formsetContainer, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Прикрепляем обработчики к существующим формам
|
||||||
|
attachFormsetEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Прикрепляет обработчики к полям в формах товаров
|
||||||
|
*/
|
||||||
|
function attachFormsetEventListeners() {
|
||||||
|
const itemForms = document.querySelectorAll('.order-item-form');
|
||||||
|
|
||||||
|
itemForms.forEach(form => {
|
||||||
|
// Если уже прикреплены обработчики, пропускаем
|
||||||
|
if (form.dataset.autosaveAttached === 'true') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = form.querySelectorAll('select, input[type="number"], input[type="checkbox"]');
|
||||||
|
fields.forEach(field => {
|
||||||
|
if (field.tagName === 'SELECT' || field.type === 'checkbox') {
|
||||||
|
field.addEventListener('change', scheduleAutosave);
|
||||||
|
} else {
|
||||||
|
field.addEventListener('input', scheduleAutosave);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
form.dataset.autosaveAttached = 'true';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Планирует автосохранение с задержкой (debouncing)
|
||||||
|
*/
|
||||||
|
function scheduleAutosave() {
|
||||||
|
// Отменяем предыдущий таймер
|
||||||
|
if (autosaveTimer) {
|
||||||
|
clearTimeout(autosaveTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем новый таймер
|
||||||
|
autosaveTimer = setTimeout(() => {
|
||||||
|
performAutosave();
|
||||||
|
}, CONFIG.AUTOSAVE_DELAY);
|
||||||
|
|
||||||
|
console.log('[Autosave] Scheduled in ' + CONFIG.AUTOSAVE_DELAY + 'ms');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выполняет автосохранение
|
||||||
|
*/
|
||||||
|
async function performAutosave() {
|
||||||
|
if (isAutosaving) {
|
||||||
|
console.log('[Autosave] Already in progress, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isAutosaving = true;
|
||||||
|
showStatus('saving', 'Сохранение...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Собираем данные формы
|
||||||
|
const formData = collectFormData();
|
||||||
|
|
||||||
|
// Отправляем AJAX запрос
|
||||||
|
const url = CONFIG.AUTOSAVE_URL_PATTERN.replace('{orderId}', orderId);
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': getCsrfToken(),
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && data.success) {
|
||||||
|
const lastSaved = formatDateTime(data.last_saved);
|
||||||
|
showStatus('success', 'Сохранено ' + lastSaved);
|
||||||
|
console.log('[Autosave] Success:', data);
|
||||||
|
} else {
|
||||||
|
showStatus('error', 'Ошибка: ' + (data.error || 'Неизвестная ошибка'));
|
||||||
|
console.error('[Autosave] Error:', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
showStatus('error', 'Ошибка соединения с сервером');
|
||||||
|
console.error('[Autosave] Exception:', error);
|
||||||
|
} finally {
|
||||||
|
isAutosaving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Собирает данные формы для отправки
|
||||||
|
*/
|
||||||
|
function collectFormData() {
|
||||||
|
const form = document.querySelector('form[action*="edit"]');
|
||||||
|
const data = {};
|
||||||
|
|
||||||
|
// Основные поля заказа
|
||||||
|
const customerField = form.querySelector('select[name="customer"]');
|
||||||
|
if (customerField && customerField.value) {
|
||||||
|
data.customer = parseInt(customerField.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const deliveryDateField = form.querySelector('input[name="delivery_date"]');
|
||||||
|
if (deliveryDateField && deliveryDateField.value) {
|
||||||
|
data.delivery_date = deliveryDateField.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deliveryTimeStartField = form.querySelector('input[name="delivery_time_start"]');
|
||||||
|
if (deliveryTimeStartField && deliveryTimeStartField.value) {
|
||||||
|
data.delivery_time_start = deliveryTimeStartField.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deliveryTimeEndField = form.querySelector('input[name="delivery_time_end"]');
|
||||||
|
if (deliveryTimeEndField && deliveryTimeEndField.value) {
|
||||||
|
data.delivery_time_end = deliveryTimeEndField.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deliveryCostField = form.querySelector('input[name="delivery_cost"]');
|
||||||
|
if (deliveryCostField && deliveryCostField.value) {
|
||||||
|
data.delivery_cost = deliveryCostField.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paymentMethodField = form.querySelector('select[name="payment_method"]');
|
||||||
|
if (paymentMethodField && paymentMethodField.value) {
|
||||||
|
data.payment_method = paymentMethodField.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const specialInstructionsField = form.querySelector('textarea[name="special_instructions"]');
|
||||||
|
if (specialInstructionsField) {
|
||||||
|
data.special_instructions = specialInstructionsField.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const discountAmountField = form.querySelector('input[name="discount_amount"]');
|
||||||
|
if (discountAmountField && discountAmountField.value) {
|
||||||
|
data.discount_amount = discountAmountField.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checkbox поля
|
||||||
|
const isDeliveryField = form.querySelector('input[name="is_delivery"]');
|
||||||
|
if (isDeliveryField) {
|
||||||
|
data.is_delivery = isDeliveryField.checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
const customerIsRecipientField = form.querySelector('input[name="customer_is_recipient"]');
|
||||||
|
if (customerIsRecipientField) {
|
||||||
|
data.customer_is_recipient = customerIsRecipientField.checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAnonymousField = form.querySelector('input[name="is_anonymous"]');
|
||||||
|
if (isAnonymousField) {
|
||||||
|
data.is_anonymous = isAnonymousField.checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Адрес доставки или точка самовывоза
|
||||||
|
const deliveryAddressField = form.querySelector('select[name="delivery_address"]');
|
||||||
|
if (deliveryAddressField && deliveryAddressField.value) {
|
||||||
|
data.delivery_address = parseInt(deliveryAddressField.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pickupShopField = form.querySelector('select[name="pickup_shop"]');
|
||||||
|
if (pickupShopField && pickupShopField.value) {
|
||||||
|
data.pickup_shop = parseInt(pickupShopField.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Собираем позиции заказа
|
||||||
|
data.items = collectOrderItems();
|
||||||
|
|
||||||
|
// Флаг для пересчета итоговой суммы
|
||||||
|
data.recalculate = true;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Собирает данные о позициях заказа
|
||||||
|
*/
|
||||||
|
function collectOrderItems() {
|
||||||
|
const items = [];
|
||||||
|
const itemForms = document.querySelectorAll('.order-item-form');
|
||||||
|
|
||||||
|
itemForms.forEach((form, index) => {
|
||||||
|
// Пропускаем удаленные формы
|
||||||
|
const deleteCheckbox = form.querySelector('input[name$="-DELETE"]');
|
||||||
|
if (deleteCheckbox && deleteCheckbox.checked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем выбранный товар/комплект
|
||||||
|
const itemSelect = form.querySelector('.select2-order-item');
|
||||||
|
if (!itemSelect || !itemSelect.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemValue = itemSelect.value;
|
||||||
|
const quantityInput = form.querySelector('input[name$="-quantity"]');
|
||||||
|
const priceInput = form.querySelector('input[name$="-price"]');
|
||||||
|
|
||||||
|
if (!quantityInput || !priceInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
quantity: quantityInput.value || '1',
|
||||||
|
price: priceInput.value || '0'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Определяем тип: товар или комплект
|
||||||
|
if (itemValue.startsWith('product_')) {
|
||||||
|
item.product_id = parseInt(itemValue.replace('product_', ''));
|
||||||
|
} else if (itemValue.startsWith('kit_')) {
|
||||||
|
item.product_kit_id = parseInt(itemValue.replace('kit_', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает CSRF токен из cookies или meta тега
|
||||||
|
*/
|
||||||
|
function getCsrfToken() {
|
||||||
|
// Пробуем получить из cookie
|
||||||
|
const name = 'csrftoken';
|
||||||
|
let cookieValue = null;
|
||||||
|
if (document.cookie && document.cookie !== '') {
|
||||||
|
const cookies = document.cookie.split(';');
|
||||||
|
for (let i = 0; i < cookies.length; i++) {
|
||||||
|
const cookie = cookies[i].trim();
|
||||||
|
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||||
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если не нашли в cookie, пробуем в meta теге
|
||||||
|
if (!cookieValue) {
|
||||||
|
const metaTag = document.querySelector('meta[name="csrf-token"]');
|
||||||
|
if (metaTag) {
|
||||||
|
cookieValue = metaTag.getAttribute('content');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если не нашли в meta теге, пробуем в input поле
|
||||||
|
if (!cookieValue) {
|
||||||
|
const csrfInput = document.querySelector('input[name="csrfmiddlewaretoken"]');
|
||||||
|
if (csrfInput) {
|
||||||
|
cookieValue = csrfInput.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cookieValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Форматирует дату и время для отображения
|
||||||
|
*/
|
||||||
|
function formatDateTime(isoString) {
|
||||||
|
if (!isoString) {
|
||||||
|
return 'только что';
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(isoString);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = now - date;
|
||||||
|
const diffSecs = Math.floor(diffMs / 1000);
|
||||||
|
const diffMins = Math.floor(diffSecs / 60);
|
||||||
|
|
||||||
|
if (diffSecs < 60) {
|
||||||
|
return 'только что';
|
||||||
|
} else if (diffMins < 60) {
|
||||||
|
return diffMins + ' мин. назад';
|
||||||
|
} else {
|
||||||
|
const hours = date.getHours().toString().padStart(2, '0');
|
||||||
|
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||||
|
return 'в ' + hours + ':' + minutes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация при загрузке DOM
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" id="order-form">
|
<form method="post" id="order-form" {% if is_draft %}data-is-draft="true"{% endif %}>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<!-- Основная информация -->
|
<!-- Основная информация -->
|
||||||
@@ -791,4 +791,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Скрипт автосохранения черновиков -->
|
||||||
|
{% if is_draft %}
|
||||||
|
<script src="{% static 'orders/js/autosave.js' %}"></script>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -130,7 +130,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if order.status == 'new' %}
|
{% if order.status == 'draft' %}
|
||||||
|
<span class="badge bg-secondary">
|
||||||
|
<i class="bi bi-pencil"></i> Черновик
|
||||||
|
</span>
|
||||||
|
{% elif order.status == 'new' %}
|
||||||
<span class="badge bg-primary">Новый</span>
|
<span class="badge bg-primary">Новый</span>
|
||||||
{% elif order.status == 'confirmed' %}
|
{% elif order.status == 'confirmed' %}
|
||||||
<span class="badge bg-success">Подтвержден</span>
|
<span class="badge bg-success">Подтвержден</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user