From 61ce3f550d3c753f3cbb9677a51d2acfb9d3a2f3 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Wed, 24 Dec 2025 18:25:20 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=20?= =?UTF-8?q?=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=20=D0=B2?= =?UTF-8?q?=D0=B2=D0=BE=D0=B4=D0=B0=20=D0=B4=D0=B0=D1=82=D1=8B=20=D0=B8=20?= =?UTF-8?q?=D0=B2=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=B8=20=D0=B4=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D0=B2=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Исправлены имена полей времени (time_from/time_to вместо delivery_time_start/end) - Поля времени сделаны необязательными (дата остается обязательной) - Добавлен улучшенный UI с быстрыми кнопками для даты и времени - Поля ввода расположены в один ряд, кнопки быстрого выбора ниже - Добавлены CSS и JS файлы для улучшенного интерфейса - Обновлена валидация: время необязательно, но если указано одно - должно быть и другое --- myproject/orders/forms.py | 16 +- .../0004_make_delivery_time_optional.py | 23 ++ myproject/orders/models/delivery.py | 8 +- .../static/orders/css/delivery_datetime.css | 161 ++++++++++++++ .../static/orders/js/delivery_datetime.js | 203 ++++++++++++++++++ .../orders/templates/orders/order_form.html | 82 ++++--- myproject/orders/views.py | 12 +- 7 files changed, 459 insertions(+), 46 deletions(-) create mode 100644 myproject/orders/migrations/0004_make_delivery_time_optional.py create mode 100644 myproject/orders/static/orders/css/delivery_datetime.css create mode 100644 myproject/orders/static/orders/js/delivery_datetime.js diff --git a/myproject/orders/forms.py b/myproject/orders/forms.py index 890a706..2e73257 100644 --- a/myproject/orders/forms.py +++ b/myproject/orders/forms.py @@ -146,13 +146,13 @@ class OrderForm(forms.ModelForm): ) time_from = forms.TimeField( - required=True, + required=False, widget=forms.TimeInput(attrs={'class': 'form-control', 'type': 'time'}), label='Время доставки от' ) time_to = forms.TimeField( - required=True, + required=False, widget=forms.TimeInput(attrs={'class': 'form-control', 'type': 'time'}), label='Время доставки до' ) @@ -321,13 +321,13 @@ class OrderForm(forms.ModelForm): if not delivery_date: raise forms.ValidationError({'delivery_date': 'Необходимо указать дату доставки'}) - if not time_from: - raise forms.ValidationError({'time_from': 'Необходимо указать время начала доставки'}) + # Время необязательно, но если указано одно, должно быть указано и другое + if (time_from and not time_to) or (time_to and not time_from): + raise forms.ValidationError({ + 'time_to': 'Если указано время начала, необходимо указать и время окончания, и наоборот' + }) - if not time_to: - raise forms.ValidationError({'time_to': 'Необходимо указать время окончания доставки'}) - - # Проверяем, что время "до" позже времени "от" + # Проверяем, что время "до" позже времени "от" (если оба указаны) if time_from and time_to and time_from >= time_to: raise forms.ValidationError({ 'time_to': 'Время окончания доставки должно быть позже времени начала' diff --git a/myproject/orders/migrations/0004_make_delivery_time_optional.py b/myproject/orders/migrations/0004_make_delivery_time_optional.py new file mode 100644 index 0000000..c79a8be --- /dev/null +++ b/myproject/orders/migrations/0004_make_delivery_time_optional.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.10 on 2025-12-24 15:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('orders', '0003_remove_customer_is_recipient'), + ] + + operations = [ + migrations.AlterField( + model_name='delivery', + name='time_from', + field=models.TimeField(blank=True, help_text='Начальное время временного интервала доставки (необязательно)', null=True, verbose_name='Время доставки от'), + ), + migrations.AlterField( + model_name='delivery', + name='time_to', + field=models.TimeField(blank=True, help_text='Конечное время временного интервала доставки (необязательно)', null=True, verbose_name='Время доставки до'), + ), + ] diff --git a/myproject/orders/models/delivery.py b/myproject/orders/models/delivery.py index 2ca0714..c8a5454 100644 --- a/myproject/orders/models/delivery.py +++ b/myproject/orders/models/delivery.py @@ -67,13 +67,17 @@ class Delivery(models.Model): ) time_from = models.TimeField( + null=True, + blank=True, verbose_name='Время доставки от', - help_text='Начальное время временного интервала доставки' + help_text='Начальное время временного интервала доставки (необязательно)' ) time_to = models.TimeField( + null=True, + blank=True, verbose_name='Время доставки до', - help_text='Конечное время временного интервала доставки' + help_text='Конечное время временного интервала доставки (необязательно)' ) cost = models.DecimalField( diff --git a/myproject/orders/static/orders/css/delivery_datetime.css b/myproject/orders/static/orders/css/delivery_datetime.css new file mode 100644 index 0000000..53adb69 --- /dev/null +++ b/myproject/orders/static/orders/css/delivery_datetime.css @@ -0,0 +1,161 @@ +/* Стили для улучшенного интерфейса даты и времени доставки */ + +.delivery-datetime-wrapper { + position: relative; +} + +/* Контейнер для всех кнопок быстрого выбора */ +#delivery-datetime-quick-buttons { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 1rem; + margin-top: 0.5rem; +} + +/* Быстрые кнопки для даты */ +.delivery-date-quick-buttons { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.delivery-date-quick-btn { + padding: 0.375rem 0.75rem; + font-size: 0.875rem; + border: 1px solid #dee2e6; + background-color: #fff; + color: #495057; + border-radius: 0.375rem; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; +} + +.delivery-date-quick-btn:hover { + background-color: #f8f9fa; + border-color: #0d6efd; + color: #0d6efd; +} + +.delivery-date-quick-btn.active { + background-color: #0d6efd; + border-color: #0d6efd; + color: #fff; +} + +.delivery-date-quick-btn i { + margin-right: 0.25rem; +} + +/* Предустановленные интервалы времени */ +.delivery-time-presets { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +/* Дополнительный отступ для группы времени */ +.delivery-time-presets { + margin-left: 0.5rem; + padding-left: 0.5rem; + border-left: 1px solid #dee2e6; +} + +.delivery-time-preset-btn { + padding: 0.5rem 0.75rem; + font-size: 0.875rem; + border: 1px solid #dee2e6; + background-color: #fff; + color: #495057; + border-radius: 0.375rem; + cursor: pointer; + transition: all 0.2s ease; + text-align: center; + display: flex; + align-items: center; + justify-content: center; +} + +.delivery-time-preset-btn:hover { + background-color: #f8f9fa; + border-color: #0d6efd; + color: #0d6efd; +} + +.delivery-time-preset-btn.active { + background-color: #0d6efd; + border-color: #0d6efd; + color: #fff; +} + +.delivery-time-preset-btn i { + margin-right: 0.25rem; +} + +/* Группа полей времени */ +.delivery-time-group { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.delivery-time-group .form-control { + flex: 1; +} + +.delivery-time-separator { + color: #6c757d; + font-weight: 500; + user-select: none; +} + +/* Улучшенные поля ввода */ +.delivery-datetime-wrapper .form-control { + position: relative; +} + +.delivery-datetime-wrapper .form-control:focus { + border-color: #0d6efd; + box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); +} + +/* Подсказки под полями */ +.delivery-datetime-hint { + font-size: 0.875rem; + color: #6c757d; + margin-top: 0.25rem; + display: flex; + align-items: center; + gap: 0.25rem; +} + +.delivery-datetime-hint i { + font-size: 0.75rem; +} + +/* Адаптивность */ +@media (max-width: 768px) { + .delivery-date-quick-buttons { + flex-direction: column; + } + + .delivery-date-quick-btn { + width: 100%; + text-align: center; + } + + .delivery-time-presets { + grid-template-columns: 1fr; + } + + .delivery-time-group { + flex-direction: column; + align-items: stretch; + } + + .delivery-time-separator { + display: none; + } +} + diff --git a/myproject/orders/static/orders/js/delivery_datetime.js b/myproject/orders/static/orders/js/delivery_datetime.js new file mode 100644 index 0000000..5614ac8 --- /dev/null +++ b/myproject/orders/static/orders/js/delivery_datetime.js @@ -0,0 +1,203 @@ +/** + * Улучшенный интерфейс для ввода даты и времени доставки + * Добавляет быстрые кнопки для даты и предустановленные интервалы времени + */ + +(function() { + 'use strict'; + + /** + * Инициализация улучшенного интерфейса даты и времени + * @param {string} dateFieldId - ID поля даты + * @param {string} timeFromFieldId - ID поля времени "от" + * @param {string} timeToFieldId - ID поля времени "до" + */ + function initDeliveryDateTime(dateFieldId, timeFromFieldId, timeToFieldId) { + const dateField = document.getElementById(dateFieldId); + const timeFromField = document.getElementById(timeFromFieldId); + const timeToField = document.getElementById(timeToFieldId); + + if (!dateField || !timeFromField || !timeToField) { + console.warn('[Delivery DateTime] Поля не найдены'); + return; + } + + // Находим общий контейнер для кнопок + const buttonsContainer = document.getElementById('delivery-datetime-quick-buttons'); + if (!buttonsContainer) { + console.warn('[Delivery DateTime] Контейнер для кнопок не найден'); + return; + } + + // Добавляем быстрые кнопки для даты + addDateQuickButtons(buttonsContainer, dateField); + + // Добавляем предустановленные интервалы времени + addTimePresets(buttonsContainer, timeFromField, timeToField); + } + + /** + * Добавляет быстрые кнопки для выбора даты + */ + function addDateQuickButtons(container, dateField) { + const quickButtonsContainer = document.createElement('div'); + quickButtonsContainer.className = 'delivery-date-quick-buttons'; + + const buttons = [ + { label: 'Сегодня', days: 0, icon: 'bi-calendar-day' }, + { label: 'Завтра', days: 1, icon: 'bi-calendar-check' } + ]; + + buttons.forEach(button => { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'delivery-date-quick-btn'; + btn.innerHTML = `${button.label}`; + + btn.addEventListener('click', function() { + const date = new Date(); + date.setDate(date.getDate() + button.days); + const dateString = formatDateForInput(date); + dateField.value = dateString; + + // Обновляем активное состояние кнопок + quickButtonsContainer.querySelectorAll('.delivery-date-quick-btn').forEach(b => { + b.classList.remove('active'); + }); + btn.classList.add('active'); + + // Триггерим событие change для валидации + dateField.dispatchEvent(new Event('change', { bubbles: true })); + }); + + quickButtonsContainer.appendChild(btn); + }); + + // Проверяем текущую дату и активируем соответствующую кнопку + if (dateField.value) { + const currentDate = new Date(dateField.value); + const today = new Date(); + today.setHours(0, 0, 0, 0); + currentDate.setHours(0, 0, 0, 0); + + const diffDays = Math.round((currentDate - today) / (1000 * 60 * 60 * 24)); + + if (diffDays === 0) { + quickButtonsContainer.children[0].classList.add('active'); + } else if (diffDays === 1) { + quickButtonsContainer.children[1].classList.add('active'); + } + } + + // Слушаем изменения даты вручную + dateField.addEventListener('change', function() { + quickButtonsContainer.querySelectorAll('.delivery-date-quick-btn').forEach(b => { + b.classList.remove('active'); + }); + }); + + container.appendChild(quickButtonsContainer); + } + + /** + * Добавляет предустановленные интервалы времени + */ + function addTimePresets(container, timeFromField, timeToField) { + // Создаем контейнер для пресетов + const presetsContainer = document.createElement('div'); + presetsContainer.className = 'delivery-time-presets'; + + const presets = [ + { label: 'Утро', from: '09:00', to: '12:00', icon: 'bi-sunrise' }, + { label: 'День', from: '12:00', to: '15:00', icon: 'bi-sun' }, + { label: 'Вечер', from: '15:00', to: '18:00', icon: 'bi-sunset' }, + { label: 'Поздно', from: '18:00', to: '21:00', icon: 'bi-moon' } + ]; + + presets.forEach(preset => { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'delivery-time-preset-btn'; + btn.innerHTML = `${preset.label}`; + + btn.addEventListener('click', function() { + timeFromField.value = preset.from; + timeToField.value = preset.to; + + // Обновляем активное состояние кнопок + presetsContainer.querySelectorAll('.delivery-time-preset-btn').forEach(b => { + b.classList.remove('active'); + }); + btn.classList.add('active'); + + // Триггерим события change для валидации + timeFromField.dispatchEvent(new Event('change', { bubbles: true })); + timeToField.dispatchEvent(new Event('change', { bubbles: true })); + }); + + presetsContainer.appendChild(btn); + }); + + // Проверяем текущие значения и активируем соответствующий пресет + if (timeFromField.value && timeToField.value) { + presets.forEach((preset, index) => { + if (timeFromField.value === preset.from && timeToField.value === preset.to) { + presetsContainer.children[index].classList.add('active'); + } + }); + } + + // Слушаем изменения времени вручную + const clearPresets = function() { + presetsContainer.querySelectorAll('.delivery-time-preset-btn').forEach(b => { + b.classList.remove('active'); + }); + }; + timeFromField.addEventListener('change', clearPresets); + timeToField.addEventListener('change', clearPresets); + + // Вставляем пресеты в контейнер + container.appendChild(presetsContainer); + } + + /** + * Форматирует дату для input type="date" (YYYY-MM-DD) + */ + function formatDateForInput(date) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + } + + /** + * Автоматическая инициализация при загрузке DOM + */ + function autoInit() { + document.addEventListener('DOMContentLoaded', function() { + // Ищем поля по стандартным ID Django форм + const dateField = document.getElementById('id_delivery_date'); + const timeFromField = document.getElementById('id_time_from'); + const timeToField = document.getElementById('id_time_to'); + + if (dateField && timeFromField && timeToField) { + initDeliveryDateTime( + 'id_delivery_date', + 'id_time_from', + 'id_time_to' + ); + } + }); + } + + // Экспортируем функции для глобального использования + window.DeliveryDateTime = { + init: initDeliveryDateTime, + autoInit: autoInit + }; + + // Автоматическая инициализация + autoInit(); + +})(); + diff --git a/myproject/orders/templates/orders/order_form.html b/myproject/orders/templates/orders/order_form.html index 9f93ec0..8af2c28 100644 --- a/myproject/orders/templates/orders/order_form.html +++ b/myproject/orders/templates/orders/order_form.html @@ -6,6 +6,7 @@ {% block extra_css %} +