Улучшения UX формы заказа и создания клиента
- Изменен порядок секций формы: товары перемещены выше доставки - Добавлена защита от двойного создания клиента - Улучшена валидация при создании клиента с детализацией ошибок - Добавлен индикатор загрузки при сохранении клиента - Исправлена логика обработки специальной опции создания клиента 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -181,6 +181,137 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Товары в заказе -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Товары в заказе</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{ formset.management_form }}
|
||||
<div id="order-items-container">
|
||||
{% for item_form in formset %}
|
||||
<div class="order-item-form border rounded p-3 mb-3" data-form-index="{{ forloop.counter0 }}">
|
||||
{{ item_form.id }}
|
||||
{{ item_form.product }} <!-- Hidden field -->
|
||||
{{ item_form.product_kit }} <!-- Hidden field -->
|
||||
{{ item_form.is_custom_price }} <!-- Hidden field -->
|
||||
|
||||
<div class="row align-items-end">
|
||||
<div class="col-md-5">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Товар или комплект</label>
|
||||
<select class="form-select select2-order-item" data-form-index="{{ forloop.counter0 }}">
|
||||
<option value=""></option>
|
||||
{% if item_form.instance.product %}
|
||||
<option value="product_{{ item_form.instance.product.id }}" selected data-type="product" data-price="{{ item_form.instance.product.actual_price }}">
|
||||
{{ item_form.instance.product.name }}{% if item_form.instance.product.sku %} ({{ item_form.instance.product.sku }}){% endif %}
|
||||
</option>
|
||||
{% elif item_form.instance.product_kit %}
|
||||
<option value="kit_{{ item_form.instance.product_kit.id }}" selected data-type="kit" data-price="{{ item_form.instance.product_kit.actual_price }}">
|
||||
{{ item_form.instance.product_kit.name }}{% if item_form.instance.product_kit.sku %} ({{ item_form.instance.product_kit.sku }}){% endif %}
|
||||
</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Количество</label>
|
||||
{{ item_form.quantity }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Цена</label>
|
||||
<div class="position-relative">
|
||||
{{ item_form.price }}
|
||||
<span class="custom-price-badge badge bg-warning position-absolute top-0 end-0 mt-1 me-1" style="display: none;">
|
||||
Изменена
|
||||
</span>
|
||||
<small class="text-muted original-price-info position-absolute" style="display: none; top: calc(100% + 2px); left: 0; white-space: nowrap;">
|
||||
Оригинальная: <span class="original-price-value"></span> руб.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 text-end">
|
||||
<div class="mb-2">
|
||||
<label class="form-label d-block"> </label>
|
||||
{% if formset.can_delete %}
|
||||
{{ item_form.DELETE }}
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-danger btn-sm remove-item-btn">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if item_form.errors %}
|
||||
<div class="alert alert-danger mt-2">{{ item_form.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Скрытый шаблон для новых форм -->
|
||||
<template id="empty-form-template">
|
||||
<div class="order-item-form border rounded p-3 mb-3" data-form-index="__prefix__">
|
||||
<input type="hidden" name="items-__prefix__-id" id="id_items-__prefix__-id">
|
||||
<input type="hidden" name="items-__prefix__-product" id="id_items-__prefix__-product">
|
||||
<input type="hidden" name="items-__prefix__-product_kit" id="id_items-__prefix__-product_kit">
|
||||
<input type="hidden" name="items-__prefix__-is_custom_price" id="id_items-__prefix__-is_custom_price" value="false">
|
||||
|
||||
<div class="row align-items-end">
|
||||
<div class="col-md-5">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Товар или комплект</label>
|
||||
<select class="form-select select2-order-item" data-form-index="__prefix__" id="id_items-__prefix__-select">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Количество</label>
|
||||
<input type="number" name="items-__prefix__-quantity" step="1" min="1" value="1" class="form-control" id="id_items-__prefix__-quantity">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Цена</label>
|
||||
<div class="position-relative">
|
||||
<input type="number" name="items-__prefix__-price" step="0.01" min="0" class="form-control" id="id_items-__prefix__-price">
|
||||
<span class="custom-price-badge badge bg-warning position-absolute top-0 end-0 mt-1 me-1" style="display: none;">
|
||||
Изменена
|
||||
</span>
|
||||
<small class="text-muted original-price-info position-absolute" style="display: none; top: calc(100% + 2px); left: 0; white-space: nowrap;">
|
||||
Оригинальная: <span class="original-price-value"></span> руб.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 text-end">
|
||||
<div class="mb-2">
|
||||
<label class="form-label d-block"> </label>
|
||||
<input type="checkbox" name="items-__prefix__-DELETE" id="id_items-__prefix__-DELETE">
|
||||
<button type="button" class="btn btn-danger btn-sm remove-item-btn">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<button type="button" class="btn btn-success" id="add-item-btn">
|
||||
<i class="bi bi-plus-circle"></i> Добавить товар
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary ms-2" id="create-temp-kit-btn" data-bs-toggle="modal" data-bs-target="#tempKitModal">
|
||||
<i class="bi bi-flower1"></i> Создать и добавить комплект
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Доставка -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
@@ -324,28 +455,6 @@
|
||||
<div class="border-top pt-3 mt-3">
|
||||
<h6 class="mb-3">Получатель</h6>
|
||||
|
||||
<!-- Поля получателя из модели Address -->
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="id_address_recipient_name" class="form-label">
|
||||
Имя получателя
|
||||
</label>
|
||||
<input type="text" name="address_recipient_name" id="id_address_recipient_name"
|
||||
class="form-control" placeholder="Имя получателя">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="id_address_recipient_phone" class="form-label">
|
||||
Телефон получателя
|
||||
</label>
|
||||
<input type="text" name="address_recipient_phone" id="id_address_recipient_phone"
|
||||
class="form-control" placeholder="Телефон получателя">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Крупный переключатель "Покупатель = получатель" -->
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch" style="padding-left: 3.5em;">
|
||||
@@ -425,136 +534,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Товары в заказе -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Товары в заказе</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{ formset.management_form }}
|
||||
<div id="order-items-container">
|
||||
{% for item_form in formset %}
|
||||
<div class="order-item-form border rounded p-3 mb-3" data-form-index="{{ forloop.counter0 }}">
|
||||
{{ item_form.id }}
|
||||
{{ item_form.product }} <!-- Hidden field -->
|
||||
{{ item_form.product_kit }} <!-- Hidden field -->
|
||||
{{ item_form.is_custom_price }} <!-- Hidden field -->
|
||||
|
||||
<div class="row align-items-end">
|
||||
<div class="col-md-5">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Товар или комплект</label>
|
||||
<select class="form-select select2-order-item" data-form-index="{{ forloop.counter0 }}">
|
||||
<option value=""></option>
|
||||
{% if item_form.instance.product %}
|
||||
<option value="product_{{ item_form.instance.product.id }}" selected data-type="product" data-price="{{ item_form.instance.product.actual_price }}">
|
||||
{{ item_form.instance.product.name }}{% if item_form.instance.product.sku %} ({{ item_form.instance.product.sku }}){% endif %}
|
||||
</option>
|
||||
{% elif item_form.instance.product_kit %}
|
||||
<option value="kit_{{ item_form.instance.product_kit.id }}" selected data-type="kit" data-price="{{ item_form.instance.product_kit.actual_price }}">
|
||||
{{ item_form.instance.product_kit.name }}{% if item_form.instance.product_kit.sku %} ({{ item_form.instance.product_kit.sku }}){% endif %}
|
||||
</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Количество</label>
|
||||
{{ item_form.quantity }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Цена</label>
|
||||
<div class="position-relative">
|
||||
{{ item_form.price }}
|
||||
<span class="custom-price-badge badge bg-warning position-absolute top-0 end-0 mt-1 me-1" style="display: none;">
|
||||
Изменена
|
||||
</span>
|
||||
<small class="text-muted original-price-info position-absolute" style="display: none; top: calc(100% + 2px); left: 0; white-space: nowrap;">
|
||||
Оригинальная: <span class="original-price-value"></span> руб.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 text-end">
|
||||
<div class="mb-2">
|
||||
<label class="form-label d-block"> </label>
|
||||
{% if formset.can_delete %}
|
||||
{{ item_form.DELETE }}
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-danger btn-sm remove-item-btn">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if item_form.errors %}
|
||||
<div class="alert alert-danger mt-2">{{ item_form.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Скрытый шаблон для новых форм -->
|
||||
<template id="empty-form-template">
|
||||
<div class="order-item-form border rounded p-3 mb-3" data-form-index="__prefix__">
|
||||
<input type="hidden" name="items-__prefix__-id" id="id_items-__prefix__-id">
|
||||
<input type="hidden" name="items-__prefix__-product" id="id_items-__prefix__-product">
|
||||
<input type="hidden" name="items-__prefix__-product_kit" id="id_items-__prefix__-product_kit">
|
||||
<input type="hidden" name="items-__prefix__-is_custom_price" id="id_items-__prefix__-is_custom_price" value="false">
|
||||
|
||||
<div class="row align-items-end">
|
||||
<div class="col-md-5">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Товар или комплект</label>
|
||||
<select class="form-select select2-order-item" data-form-index="__prefix__" id="id_items-__prefix__-select">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Количество</label>
|
||||
<input type="number" name="items-__prefix__-quantity" step="1" min="1" value="1" class="form-control" id="id_items-__prefix__-quantity">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Цена</label>
|
||||
<div class="position-relative">
|
||||
<input type="number" name="items-__prefix__-price" step="0.01" min="0" class="form-control" id="id_items-__prefix__-price">
|
||||
<span class="custom-price-badge badge bg-warning position-absolute top-0 end-0 mt-1 me-1" style="display: none;">
|
||||
Изменена
|
||||
</span>
|
||||
<small class="text-muted original-price-info position-absolute" style="display: none; top: calc(100% + 2px); left: 0; white-space: nowrap;">
|
||||
Оригинальная: <span class="original-price-value"></span> руб.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 text-end">
|
||||
<div class="mb-2">
|
||||
<label class="form-label d-block"> </label>
|
||||
<input type="checkbox" name="items-__prefix__-DELETE" id="id_items-__prefix__-DELETE">
|
||||
<button type="button" class="btn btn-danger btn-sm remove-item-btn">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<button type="button" class="btn btn-success" id="add-item-btn">
|
||||
<i class="bi bi-plus-circle"></i> Добавить товар
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary ms-2" id="create-temp-kit-btn" data-bs-toggle="modal" data-bs-target="#tempKitModal">
|
||||
<i class="bi bi-flower1"></i> Создать и добавить комплект
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Оплата и дополнительно -->
|
||||
<div class="card mb-3">
|
||||
@@ -672,19 +651,7 @@ function initCustomerSelect2() {
|
||||
},
|
||||
templateResult: formatCustomerOption,
|
||||
templateSelection: formatCustomerSelection,
|
||||
escapeMarkup: function(markup) { return markup; },
|
||||
// Очень важно: указываем как отображать уже выбранное значение
|
||||
matcher: function(params, data) {
|
||||
// Если нет поискового термина, показываем все результаты
|
||||
if ($.trim(params.term) === '') {
|
||||
return data;
|
||||
}
|
||||
// Иначе ищем совпадение
|
||||
if (data.text.toUpperCase().indexOf(params.term.toUpperCase()) > -1) {
|
||||
return data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
escapeMarkup: function(markup) { return markup; }
|
||||
});
|
||||
|
||||
console.log('6. Select2 инициализирован');
|
||||
@@ -726,10 +693,12 @@ function initCustomerSelect2() {
|
||||
const data = e.params.data;
|
||||
console.log('9b. Попытка выбрать элемент (перед выбором):', data);
|
||||
|
||||
if (data.is_create_option) {
|
||||
if (data.is_create_option || data.id === '__create_new__') {
|
||||
console.log('9c. Это опция создания клиента - предотвращаем выбор и открываем модаль');
|
||||
// Предотвращаем выбор этой опции
|
||||
e.preventDefault();
|
||||
// Закрываем dropdown
|
||||
$customerSelect.select2('close');
|
||||
// Очищаем значение
|
||||
$customerSelect.val(null).trigger('change.select2');
|
||||
// Открываем модаль
|
||||
@@ -752,7 +721,7 @@ function initCustomerSelect2() {
|
||||
const data = e.params.data;
|
||||
console.log('10b. Выбран элемент:', data);
|
||||
|
||||
if (data.is_create_option) {
|
||||
if (data.is_create_option || data.id === '__create_new__') {
|
||||
console.log('11. Открываем модальное окно для создания клиента');
|
||||
this.value = '';
|
||||
// Триггерим нативное change событие для draft-creator.js
|
||||
@@ -769,12 +738,13 @@ function initCustomerSelect2() {
|
||||
|
||||
// Форматирование опции в списке
|
||||
function formatCustomerOption(option) {
|
||||
if (!option.id) {
|
||||
return option.text;
|
||||
// ВАЖНО: Проверяем is_create_option или специальный ID ПЕРЕД проверкой !option.id
|
||||
if (option.is_create_option || option.id === '__create_new__') {
|
||||
return '<div class="customer-create-option"><i class="bi bi-plus-circle"></i> ' + option.text + '</div>';
|
||||
}
|
||||
|
||||
if (option.is_create_option) {
|
||||
return '<div class="customer-create-option"><i class="bi bi-plus-circle"></i> ' + option.text + '</div>';
|
||||
if (!option.id) {
|
||||
return option.text;
|
||||
}
|
||||
|
||||
let html = '<div class="customer-option">';
|
||||
@@ -791,10 +761,10 @@ function initCustomerSelect2() {
|
||||
|
||||
// Форматирование выбранного значения
|
||||
function formatCustomerSelection(option) {
|
||||
if (!option.id) {
|
||||
if (option.is_create_option || option.id === '__create_new__') {
|
||||
return option.text;
|
||||
}
|
||||
if (option.is_create_option) {
|
||||
if (!option.id) {
|
||||
return option.text;
|
||||
}
|
||||
// Возвращаем name если есть (из AJAX), иначе text (из DOM опции)
|
||||
@@ -1444,6 +1414,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
<!-- Обработчик сохранения нового клиента (ДОЛЖЕН быть ПОСЛЕ модального окна в HTML) -->
|
||||
<script>
|
||||
document.getElementById('save-customer-btn').addEventListener('click', function() {
|
||||
const saveBtn = this;
|
||||
|
||||
// Защита от двойного клика
|
||||
if (saveBtn.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = document.getElementById('customer-name').value.trim();
|
||||
const phone = document.getElementById('customer-phone').value.trim();
|
||||
const email = document.getElementById('customer-email').value.trim();
|
||||
@@ -1461,6 +1438,11 @@ document.getElementById('save-customer-btn').addEventListener('click', function(
|
||||
return;
|
||||
}
|
||||
|
||||
// Блокируем кнопку и меняем текст
|
||||
saveBtn.disabled = true;
|
||||
const originalHTML = saveBtn.innerHTML;
|
||||
saveBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>Создание...';
|
||||
|
||||
// Отправляем AJAX запрос
|
||||
fetch('{% url "customers:api-create-customer" %}', {
|
||||
method: 'POST',
|
||||
@@ -1528,6 +1510,10 @@ document.getElementById('save-customer-btn').addEventListener('click', function(
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
// Разблокируем кнопку при ошибке
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = originalHTML;
|
||||
|
||||
const errorDiv = document.getElementById('customer-form-errors');
|
||||
errorDiv.innerHTML = '<div class="alert alert-danger mb-0">' + data.error + '</div>';
|
||||
errorDiv.style.display = 'block';
|
||||
@@ -1535,6 +1521,11 @@ document.getElementById('save-customer-btn').addEventListener('click', function(
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
|
||||
// Разблокируем кнопку при ошибке
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = originalHTML;
|
||||
|
||||
const errorDiv = document.getElementById('customer-form-errors');
|
||||
errorDiv.innerHTML = '<div class="alert alert-danger mb-0">Ошибка при создании клиента: ' + error.message + '</div>';
|
||||
errorDiv.style.display = 'block';
|
||||
|
||||
Reference in New Issue
Block a user