- Добавлена кнопка копирования промокода в клипборд с визуальной обратной связью - Улучшено отображение ошибок валидации в форме заказа (is-invalid класс) - Добавлен флаг _draftFieldsFilled для корректной обработки пустого черновика - Убран value="1" для quantity чтобы избежать конфликтов с draft-data Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
256 lines
12 KiB
HTML
256 lines
12 KiB
HTML
{% extends "system_settings/base_settings.html" %}
|
||
|
||
{% block title %}Промокоды{% endblock %}
|
||
|
||
{% block settings_content %}
|
||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||
<h3>Промокоды</h3>
|
||
<div class="d-flex gap-2">
|
||
<a href="{% url 'system_settings:discounts:list' %}" class="btn btn-outline-secondary">
|
||
<i class="bi bi-arrow-left"></i> К скидкам
|
||
</a>
|
||
{% if can_edit %}
|
||
<a href="{% url 'system_settings:discounts:promo-create' %}" class="btn btn-primary">
|
||
<i class="bi bi-plus-circle"></i> Создать промокод
|
||
</a>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
{% if messages %}
|
||
{% for message in messages %}
|
||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||
{{ message }}
|
||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||
</div>
|
||
{% endfor %}
|
||
{% endif %}
|
||
|
||
<!-- Фильтры -->
|
||
<div class="card mb-3">
|
||
<div class="card-body">
|
||
<form method="get" class="row g-3 align-items-end">
|
||
<div class="col-md-4">
|
||
<label class="form-label">Поиск по коду</label>
|
||
<input type="text" name="search" class="form-control" placeholder="Код промокода"
|
||
value="{{ current_filters.search }}">
|
||
</div>
|
||
<div class="col-md-3">
|
||
<label class="form-label">Статус</label>
|
||
<select name="is_active" class="form-select">
|
||
<option value="">Все</option>
|
||
<option value="active" {% if current_filters.is_active == 'active' %}selected{% endif %}>Активные</option>
|
||
<option value="inactive" {% if current_filters.is_active == 'inactive' %}selected{% endif %}>Неактивные</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-5">
|
||
<button type="submit" class="btn btn-primary me-2">
|
||
<i class="bi bi-funnel"></i> Применить
|
||
</button>
|
||
<a href="{% url 'system_settings:discounts:promo-list' %}" class="btn btn-outline-secondary">
|
||
<i class="bi bi-x-circle"></i> Сбросить
|
||
</a>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-body">
|
||
{% if promocodes %}
|
||
<div class="table-responsive">
|
||
<table class="table table-hover">
|
||
<thead>
|
||
<tr>
|
||
<th>Код</th>
|
||
<th>Скидка</th>
|
||
<th>Тип</th>
|
||
<th>Значение</th>
|
||
<th>Использований</th>
|
||
<th>Период действия</th>
|
||
<th>Статус</th>
|
||
<th>Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for promo in promocodes %}
|
||
<tr>
|
||
<td>
|
||
<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>
|
||
<a href="{% url 'system_settings:discounts:update' promo.discount.id %}" class="text-decoration-none">
|
||
{{ promo.discount.name }}
|
||
</a>
|
||
</td>
|
||
<td>
|
||
{% if promo.discount.discount_type == 'percentage' %}
|
||
<span class="badge bg-primary">%</span>
|
||
{% else %}
|
||
<span class="badge bg-info">руб.</span>
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
{% if promo.discount.discount_type == 'percentage' %}
|
||
{{ promo.discount.value }}%
|
||
{% else %}
|
||
{{ promo.discount.value }} руб.
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
{{ promo.current_uses }}
|
||
{% if promo.max_total_uses %}
|
||
/ {{ promo.max_total_uses }}
|
||
{% else %}
|
||
/ ∞
|
||
{% endif %}
|
||
{% if promo.max_uses_per_user %}
|
||
<br><small class="text-muted">макс. {{ promo.max_uses_per_user }} на пользователя</small>
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
<small class="text-muted">
|
||
{% if promo.start_date and promo.end_date %}
|
||
с {{ promo.start_date|date:"d.m.Y" }}<br>по {{ promo.end_date|date:"d.m.Y" }}
|
||
{% elif promo.start_date %}
|
||
с {{ promo.start_date|date:"d.m.Y" }}
|
||
{% elif promo.end_date %}
|
||
до {{ promo.end_date|date:"d.m.Y" }}
|
||
{% else %}
|
||
Бессрочный
|
||
{% endif %}
|
||
</small>
|
||
</td>
|
||
<td>
|
||
{% if promo.is_active %}
|
||
<span class="badge bg-success">Активен</span>
|
||
{% else %}
|
||
<span class="badge bg-secondary">Неактивен</span>
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
{% if can_edit %}
|
||
<div class="btn-group btn-group-sm">
|
||
<a href="{% url 'system_settings:discounts:promo-update' promo.pk %}"
|
||
class="btn btn-outline-primary" title="Редактировать">
|
||
<i class="bi bi-pencil"></i>
|
||
</a>
|
||
<a href="{% url 'system_settings:discounts:promo-toggle' promo.pk %}"
|
||
class="btn btn-outline-{% if promo.is_active %}warning{% else %}success{% endif %}"
|
||
title="{% if promo.is_active %}Деактивировать{% else %}Активировать{% endif %}">
|
||
<i class="bi bi-{% if promo.is_active %}pause{% else %}play{% endif %}"></i>
|
||
</a>
|
||
<a href="{% url 'system_settings:discounts:promo-delete' promo.pk %}"
|
||
class="btn btn-outline-danger" title="Удалить">
|
||
<i class="bi bi-trash"></i>
|
||
</a>
|
||
</div>
|
||
{% else %}
|
||
<span class="text-muted small"><i class="bi bi-lock"></i></span>
|
||
{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Пагинация -->
|
||
{% if is_paginated %}
|
||
<nav aria-label="Page navigation">
|
||
<ul class="pagination justify-content-center">
|
||
{% if page_obj.has_previous %}
|
||
<li class="page-item">
|
||
<a class="page-link" href="?page=1{% if current_filters.search %}&search={{ current_filters.search }}{% endif %}{% if current_filters.is_active %}&is_active={{ current_filters.is_active }}{% endif %}">«</a>
|
||
</li>
|
||
<li class="page-item">
|
||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if current_filters.search %}&search={{ current_filters.search }}{% endif %}{% if current_filters.is_active %}&is_active={{ current_filters.is_active }}{% endif %}">‹</a>
|
||
</li>
|
||
{% endif %}
|
||
|
||
<li class="page-item active">
|
||
<span class="page-link">{{ page_obj.number }}</span>
|
||
</li>
|
||
|
||
{% if page_obj.has_next %}
|
||
<li class="page-item">
|
||
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if current_filters.search %}&search={{ current_filters.search }}{% endif %}{% if current_filters.is_active %}&is_active={{ current_filters.is_active }}{% endif %}">›</a>
|
||
</li>
|
||
<li class="page-item">
|
||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if current_filters.search %}&search={{ current_filters.search }}{% endif %}{% if current_filters.is_active %}&is_active={{ current_filters.is_active }}{% endif %}">»</a>
|
||
</li>
|
||
{% endif %}
|
||
</ul>
|
||
</nav>
|
||
{% endif %}
|
||
|
||
{% else %}
|
||
<div class="text-center py-5">
|
||
<p class="text-muted">Нет промокодов</p>
|
||
<i class="bi bi-ticket-perforated display-4 text-muted"></i>
|
||
{% if can_edit %}
|
||
<div class="mt-3">
|
||
<a href="{% url 'system_settings:discounts:promo-create' %}" class="btn btn-primary">
|
||
Создать первый промокод
|
||
</a>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
</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 %}
|