feat(ui): улучшения UX для промокодов и форм заказа

- Добавлена кнопка копирования промокода в клипборд с визуальной обратной связью
- Улучшено отображение ошибок валидации в форме заказа (is-invalid класс)
- Добавлен флаг _draftFieldsFilled для корректной обработки пустого черновика
- Убран value="1" для quantity чтобы избежать конфликтов с draft-data

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-11 19:07:19 +03:00
parent ed4d509a4e
commit 2369cfc997
3 changed files with 68 additions and 5 deletions

View File

@@ -76,7 +76,14 @@
{% for promo in promocodes %} {% for promo in promocodes %}
<tr> <tr>
<td> <td>
<code class="fs-5">{{ promo.code }}</code> <div class="d-flex align-items-center gap-2">
<code class="fs-5 promo-code-text">{{ promo.code }}</code>
<button class="btn btn-sm btn-outline-secondary btn-copy-promo"
data-code="{{ promo.code }}"
title="Скопировать">
<i class="bi bi-copy"></i>
</button>
</div>
</td> </td>
<td> <td>
<a href="{% url 'system_settings:discounts:update' promo.discount.id %}" class="text-decoration-none"> <a href="{% url 'system_settings:discounts:update' promo.discount.id %}" class="text-decoration-none">
@@ -199,4 +206,50 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const copyButtons = document.querySelectorAll('.btn-copy-promo');
copyButtons.forEach(button => {
button.addEventListener('click', async function() {
const code = this.getAttribute('data-code');
const icon = this.querySelector('i');
try {
await navigator.clipboard.writeText(code);
// Визуальная обратная связь - зелёная галочка на белом
icon.className = 'bi bi-check2 text-success';
this.classList.remove('btn-outline-secondary');
this.classList.add('btn-light', 'border-success');
setTimeout(() => {
icon.className = 'bi bi-copy';
this.classList.remove('btn-light', 'border-success');
this.classList.add('btn-outline-secondary');
}, 1500);
} catch (err) {
// Fallback для старых браузеров
const textArea = document.createElement('textarea');
textArea.value = code;
textArea.style.position = 'fixed';
textArea.style.left = '-9999px';
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
icon.className = 'bi bi-check2 text-success';
this.classList.remove('btn-outline-secondary');
this.classList.add('btn-light', 'border-success');
setTimeout(() => {
icon.className = 'bi bi-copy';
this.classList.remove('btn-light', 'border-success');
this.classList.add('btn-outline-secondary');
}, 1500);
}
});
});
});
</script>
{% endblock %} {% endblock %}

View File

@@ -459,7 +459,7 @@ class OrderItemForm(forms.ModelForm):
fields = ['product', 'product_kit', 'sales_unit', 'quantity', 'price', 'is_custom_price', 'is_from_showcase'] fields = ['product', 'product_kit', 'sales_unit', 'quantity', 'price', 'is_custom_price', 'is_from_showcase']
# ВАЖНО: НЕ включаем 'id' в fields - это предотвращает ошибку валидации # ВАЖНО: НЕ включаем 'id' в fields - это предотвращает ошибку валидации
widgets = { widgets = {
'quantity': forms.NumberInput(attrs={'min': 1, 'value': 1}), 'quantity': forms.NumberInput(attrs={'min': 1}),
# Скрываем поля product и product_kit - они будут заполняться через JS # Скрываем поля product и product_kit - они будут заполняться через JS
'product': forms.HiddenInput(), 'product': forms.HiddenInput(),
'product_kit': forms.HiddenInput(), 'product_kit': forms.HiddenInput(),
@@ -474,7 +474,11 @@ class OrderItemForm(forms.ModelForm):
# Bootstrap классы # Bootstrap классы
for field_name, field in self.fields.items(): for field_name, field in self.fields.items():
if not isinstance(field.widget, forms.HiddenInput): if not isinstance(field.widget, forms.HiddenInput):
field.widget.attrs.update({'class': 'form-control'}) css_class = 'form-control'
# Добавляем is-invalid если есть ошибки в поле
if self.errors.get(field_name):
css_class += ' is-invalid'
field.widget.attrs.update({'class': css_class})
# Поля product и product_kit опциональны # Поля product и product_kit опциональны
self.fields['product'].required = False self.fields['product'].required = False

View File

@@ -272,8 +272,11 @@
</div> </div>
</div> </div>
</div> </div>
{% if item_form.errors %} {% if item_form.non_field_errors %}
<div class="alert alert-danger mt-2">{{ item_form.errors }}</div> <div class="alert alert-danger mt-2">
<strong>Ошибка валидации:</strong>
{{ item_form.non_field_errors }}
</div>
{% endif %} {% endif %}
</div> </div>
{% endfor %} {% endfor %}
@@ -2058,6 +2061,9 @@ document.addEventListener('DOMContentLoaded', function() {
const draftItems = JSON.parse(draftItemsJson); const draftItems = JSON.parse(draftItemsJson);
if (draftItems.length === 0) { if (draftItems.length === 0) {
console.log('[Draft Items] Пустой массив черновика, устанавливаем флаг и выходим');
// ВАЖНО: Устанавливаем флаг, чтобы initExistingOrderItems мог продолжить!
window._draftFieldsFilled = true;
return; return;
} }