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:
@@ -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 %}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user