Защита удаления заказов и улучшение интерфейса клиентов
Orders: - Удаление разрешено только для черновиков (draft) - Запрет удаления заказов с оплатой (amount_paid > 0) - Кнопка "Удалить" скрыта для недопустимых заказов Customers: - Inline-редактирование полей клиента - Улучшен дизайн карточки клиента - Добавлена история заказов и кошелька 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,12 +6,15 @@
|
|||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h1>Клиент: {{ customer.full_name }}</h1>
|
<h2 class="mb-0"><i class="bi bi-person-badge text-primary"></i> <span id="customer-title">{{ customer.full_name }}</span></h2>
|
||||||
<div>
|
<div>
|
||||||
<a href="{% url 'customers:customer-update' customer.pk %}" class="btn btn-primary">Редактировать</a>
|
<a href="{% url 'customers:customer-delete' customer.pk %}" class="btn btn-outline-danger">
|
||||||
<a href="{% url 'customers:customer-delete' customer.pk %}" class="btn btn-danger">Удалить</a>
|
<i class="bi bi-trash"></i> Удалить
|
||||||
<a href="{% url 'customers:customer-list' %}" class="btn btn-secondary">Назад к списку</a>
|
</a>
|
||||||
|
<a href="{% url 'customers:customer-list' %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-arrow-left"></i> Назад к списку
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -20,67 +23,161 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Customer Info -->
|
<!-- Customer Info -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4 shadow-sm">
|
||||||
<div class="card-header">
|
<div class="card-header bg-light">
|
||||||
<h5>Информация о клиенте</h5>
|
<h5 class="mb-0"><i class="bi bi-person-circle text-primary"></i> Информация о клиенте</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table table-borderless">
|
<table class="table table-borderless mb-0" id="customer-info-table" data-customer-id="{{ customer.pk }}">
|
||||||
<tr>
|
<colgroup>
|
||||||
<th>Имя:</th>
|
<col style="width: 40%;">
|
||||||
<td>{{ customer.full_name }}</td>
|
<col style="width: 50%;">
|
||||||
</tr>
|
<col style="width: 10%;">
|
||||||
<tr>
|
</colgroup>
|
||||||
<th>Email:</th>
|
<tbody>
|
||||||
<td>{{ customer.email|default:"Не указано" }}</td>
|
<!-- Основная информация -->
|
||||||
</tr>
|
<tr data-field="name">
|
||||||
<tr>
|
<td class="text-muted"><i class="bi bi-person-fill text-primary"></i> Имя:</td>
|
||||||
<th>Телефон:</th>
|
|
||||||
<td>{{ customer.phone|default:"Не указано" }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Сумма всех успешных заказов:</th>
|
|
||||||
<td><strong>{{ total_orders_sum|floatformat:2 }} руб.</strong></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Сумма заказов за последний год:</th>
|
|
||||||
<td><strong>{{ last_year_orders_sum|floatformat:2 }} руб.</strong></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Общий долг по активным заказам:</th>
|
|
||||||
<td>
|
<td>
|
||||||
{% if total_debt > 0 %}
|
<span class="field-value fw-bold">{{ customer.name|default:"—" }}</span>
|
||||||
<span class="text-danger fw-bold">{{ total_debt|floatformat:2 }} руб.</span>
|
<input type="text" class="field-input form-control form-control-sm d-none" value="{{ customer.name|default:'' }}">
|
||||||
<small class="text-muted">(Кол-во заказов: {{ active_orders_count }})</small>
|
<div class="field-actions d-inline-block ms-2 d-none">
|
||||||
{% else %}
|
<button class="btn btn-sm btn-success save-btn py-0 px-1" title="Сохранить"><i class="bi bi-check-lg"></i></button>
|
||||||
<span class="text-success">0.00 руб.</span>
|
<button class="btn btn-sm btn-outline-secondary cancel-btn py-0 px-1" title="Отменить"><i class="bi bi-x-lg"></i></button>
|
||||||
{% endif %}
|
<span class="save-status ms-1"></span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="d-flex gap-1 justify-content-end">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary copy-btn" title="Копировать" data-copy-value="{{ customer.name|default:'' }}">
|
||||||
|
<i class="bi bi-copy"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-primary edit-btn" title="Редактировать">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr data-field="phone">
|
||||||
|
<td class="text-muted"><i class="bi bi-telephone-fill text-success"></i> Телефон:</td>
|
||||||
|
<td>
|
||||||
|
<span class="field-value">{{ customer.phone|default:"—" }}</span>
|
||||||
|
<input type="tel" class="field-input form-control form-control-sm d-none" value="{{ customer.phone|default:'' }}" placeholder="+375...">
|
||||||
|
<div class="field-actions d-inline-block ms-2 d-none">
|
||||||
|
<button class="btn btn-sm btn-success save-btn py-0 px-1" title="Сохранить"><i class="bi bi-check-lg"></i></button>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary cancel-btn py-0 px-1" title="Отменить"><i class="bi bi-x-lg"></i></button>
|
||||||
|
<span class="save-status ms-1"></span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="d-flex gap-1 justify-content-end">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary copy-btn" title="Копировать" data-copy-value="{{ customer.phone|default:'' }}">
|
||||||
|
<i class="bi bi-copy"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-primary edit-btn" title="Редактировать">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr data-field="email">
|
||||||
|
<td class="text-muted"><i class="bi bi-envelope-fill text-info"></i> Email:</td>
|
||||||
|
<td>
|
||||||
|
<span class="field-value">{{ customer.email|default:"—" }}</span>
|
||||||
|
<input type="email" class="field-input form-control form-control-sm d-none" value="{{ customer.email|default:'' }}">
|
||||||
|
<div class="field-actions d-inline-block ms-2 d-none">
|
||||||
|
<button class="btn btn-sm btn-success save-btn py-0 px-1" title="Сохранить"><i class="bi bi-check-lg"></i></button>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary cancel-btn py-0 px-1" title="Отменить"><i class="bi bi-x-lg"></i></button>
|
||||||
|
<span class="save-status ms-1"></span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="d-flex gap-1 justify-content-end">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary copy-btn" title="Копировать" data-copy-value="{{ customer.email|default:'' }}">
|
||||||
|
<i class="bi bi-copy"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-primary edit-btn" title="Редактировать">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr data-field="notes">
|
||||||
|
<td class="text-muted"><i class="bi bi-card-text text-warning"></i> Заметки:</td>
|
||||||
|
<td>
|
||||||
|
<span class="field-value" style="white-space: pre-wrap;">{{ customer.notes|default:"—" }}</span>
|
||||||
|
<textarea class="field-input form-control form-control-sm d-none" rows="2">{{ customer.notes|default:'' }}</textarea>
|
||||||
|
<div class="field-actions d-inline-block ms-2 d-none">
|
||||||
|
<button class="btn btn-sm btn-success save-btn py-0 px-1" title="Сохранить"><i class="bi bi-check-lg"></i></button>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary cancel-btn py-0 px-1" title="Отменить"><i class="bi bi-x-lg"></i></button>
|
||||||
|
<span class="save-status ms-1"></span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<button class="btn btn-sm btn-outline-primary edit-btn" title="Редактировать">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Разделитель -->
|
||||||
|
<tr>
|
||||||
|
<td colspan="3"><hr class="my-2"></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Финансовая информация -->
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted"><i class="bi bi-cash-stack text-success"></i> Все успешные заказы:</td>
|
||||||
|
<td colspan="2">
|
||||||
|
<span class="badge bg-success">{{ total_orders_sum|floatformat:2 }} руб.</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Заметки:</th>
|
<td class="text-muted"><i class="bi bi-calendar-check text-info"></i> За последний год:</td>
|
||||||
<td>{{ customer.notes|default:"Нет" }}</td>
|
<td colspan="2">
|
||||||
|
<span class="badge bg-info">{{ last_year_orders_sum|floatformat:2 }} руб.</span>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Дата создания:</th>
|
<td class="text-muted"><i class="bi bi-exclamation-triangle-fill text-danger"></i> Общий долг:</td>
|
||||||
<td>{{ customer.created_at|date:"d.m.Y H:i" }}</td>
|
<td colspan="2">
|
||||||
|
{% if total_debt > 0 %}
|
||||||
|
<span class="badge bg-danger">{{ total_debt|floatformat:2 }} руб.</span>
|
||||||
|
<small class="text-muted ms-2">(Заказов: {{ active_orders_count }})</small>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-success">0.00 руб.</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Разделитель -->
|
||||||
|
<tr>
|
||||||
|
<td colspan="3"><hr class="my-2"></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Системная информация -->
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted small"><i class="bi bi-clock-history"></i> Дата создания:</td>
|
||||||
|
<td colspan="2" class="small">{{ customer.created_at|date:"d.m.Y H:i" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Дата обновления:</th>
|
<td class="text-muted small"><i class="bi bi-arrow-clockwise"></i> Последнее изменение:</td>
|
||||||
<td>{{ customer.updated_at|date:"d.m.Y H:i" }}</td>
|
<td colspan="2" class="small"><span id="updated-at">{{ customer.updated_at|date:"d.m.Y H:i" }}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Каналы связи -->
|
<!-- Правая колонка: Каналы связи + История заказов + История кошелька -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card mb-4">
|
<!-- Каналы связи -->
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card mb-4 shadow-sm">
|
||||||
<h5 class="mb-0">Каналы связи</h5>
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-chat-dots text-success"></i> Каналы связи</h5>
|
||||||
<button class="btn btn-sm btn-success" data-bs-toggle="modal" data-bs-target="#addChannelModal">
|
<button class="btn btn-sm btn-success" data-bs-toggle="modal" data-bs-target="#addChannelModal">
|
||||||
<i class="bi bi-plus"></i> Добавить
|
<i class="bi bi-plus-circle"></i> Добавить
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -112,12 +209,17 @@
|
|||||||
{% if channel.is_primary %}<span class="badge bg-warning text-dark ms-1">основной</span>{% endif %}
|
{% if channel.is_primary %}<span class="badge bg-warning text-dark ms-1">основной</span>{% endif %}
|
||||||
{% if channel.notes %}<small class="text-muted d-block mt-1">{{ channel.notes }}</small>{% endif %}
|
{% if channel.notes %}<small class="text-muted d-block mt-1">{{ channel.notes }}</small>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<form method="post" action="{% url 'customers:delete-contact-channel' channel.pk %}" class="d-inline">
|
<div class="d-flex gap-1">
|
||||||
{% csrf_token %}
|
<button class="btn btn-sm btn-outline-secondary copy-btn" title="Копировать" data-copy-value="{{ channel.value }}">
|
||||||
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Удалить канал?')">
|
<i class="bi bi-copy"></i>
|
||||||
<i class="bi bi-trash"></i>
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
<form method="post" action="{% url 'customers:delete-contact-channel' channel.pk %}" class="d-inline">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Удалить канал?')">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -126,35 +228,202 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Алерт о необходимости возврата -->
|
<!-- История заказов -->
|
||||||
{% if refund_amount > 0 %}
|
<div class="card mb-4 shadow-sm">
|
||||||
<div class="col-md-12">
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
<div class="alert alert-warning d-flex justify-content-between align-items-center mb-4" role="alert">
|
<button class="btn btn-link text-start text-decoration-none p-0 d-flex align-items-center flex-grow-1"
|
||||||
<div>
|
type="button"
|
||||||
<h5 class="alert-heading mb-2">
|
data-bs-toggle="collapse"
|
||||||
<i class="bi bi-exclamation-triangle-fill"></i> Требуется возврат средств
|
data-bs-target="#ordersHistoryCollapse"
|
||||||
</h5>
|
aria-expanded="false"
|
||||||
<p class="mb-0">
|
aria-controls="ordersHistoryCollapse"
|
||||||
Клиент имеет отменённые заказы с внесённой оплатой.
|
style="border: none; background: none;">
|
||||||
Общая сумма к возврату: <strong>{{ refund_amount|floatformat:2 }} руб.</strong>
|
<h5 class="mb-0 me-2"><i class="bi bi-cart-check text-primary"></i> История заказов</h5>
|
||||||
</p>
|
<span class="badge bg-primary">{{ orders_page.paginator.count }}</span>
|
||||||
|
</button>
|
||||||
|
<a href="{% url 'orders:order-create' %}?customer={{ customer.pk }}"
|
||||||
|
class="btn btn-sm btn-success ms-2">
|
||||||
|
<i class="bi bi-plus-circle"></i> Новый
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="collapse" id="ordersHistoryCollapse">
|
||||||
<span class="badge bg-warning text-dark" style="font-size: 1.2em;">
|
<div class="card-body p-0">
|
||||||
{{ refund_amount|floatformat:2 }} руб.
|
{% if orders_page %}
|
||||||
</span>
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-hover mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>№</th>
|
||||||
|
<th>Дата</th>
|
||||||
|
<th>Статус</th>
|
||||||
|
<th>Сумма</th>
|
||||||
|
<th>Остаток</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for order in orders_page %}
|
||||||
|
<tr {% if order.status and order.status.is_negative_end and order.amount_paid > 0 %}class="table-warning"{% endif %}>
|
||||||
|
<td><strong>#{{ order.order_number }}</strong></td>
|
||||||
|
<td><small>{{ order.created_at|date:"d.m.y" }}</small></td>
|
||||||
|
<td>
|
||||||
|
{% if order.status %}
|
||||||
|
{% if order.status.code == 'draft' %}
|
||||||
|
<span class="badge bg-secondary">Черновик</span>
|
||||||
|
{% elif order.status.code == 'pending' %}
|
||||||
|
<span class="badge bg-warning">Ожидает</span>
|
||||||
|
{% elif order.status.code == 'in_production' %}
|
||||||
|
<span class="badge bg-info">В пр-ве</span>
|
||||||
|
{% elif order.status.code == 'ready' %}
|
||||||
|
<span class="badge bg-primary">Готов</span>
|
||||||
|
{% elif order.status.code == 'delivered' %}
|
||||||
|
<span class="badge bg-success">Доставлен</span>
|
||||||
|
{% elif order.status.code == 'cancelled' %}
|
||||||
|
<span class="badge bg-danger">Отменён</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">{{ order.status.name }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">-</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td><strong>{{ order.total_amount|floatformat:2 }}</strong></td>
|
||||||
|
<td>
|
||||||
|
{% if order.status and order.status.is_negative_end %}
|
||||||
|
{% if order.amount_paid > 0 %}
|
||||||
|
<span class="badge bg-warning text-dark">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i> {{ order.amount_paid|floatformat:2 }}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
{% elif order.amount_due > 0 %}
|
||||||
|
<span class="text-danger fw-bold">{{ order.amount_due|floatformat:2 }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-success">0.00</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a href="{% url 'orders:order-detail' order.order_number %}" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-eye"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Пагинация -->
|
||||||
|
{% if orders_page.has_other_pages %}
|
||||||
|
<div class="p-3 bg-light border-top">
|
||||||
|
<nav aria-label="Навигация по заказам">
|
||||||
|
<ul class="pagination pagination-sm justify-content-center mb-0">
|
||||||
|
{% if orders_page.has_previous %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ orders_page.previous_page_number }}#ordersHistoryCollapse">«</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<li class="page-item active">
|
||||||
|
<span class="page-link">
|
||||||
|
{{ orders_page.number }} / {{ orders_page.paginator.num_pages }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{% if orders_page.has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ orders_page.next_page_number }}#ordersHistoryCollapse">»</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted mb-0 p-3">У клиента пока нет заказов.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Операции с кошельком -->
|
<!-- История транзакций кошелька -->
|
||||||
<div class="col-md-6">
|
<div class="card mb-4 shadow-sm">
|
||||||
<div class="card mb-4">
|
<div class="card-header bg-light">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<button class="btn btn-link w-100 text-start text-decoration-none p-0 d-flex justify-content-between align-items-center"
|
||||||
<h5 class="mb-0">Операции с кошельком клиента</h5>
|
type="button"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#walletHistoryCollapse"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="walletHistoryCollapse">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<h5 class="mb-0 me-2"><i class="bi bi-clock-history text-info"></i> История кошелька</h5>
|
||||||
|
<span class="badge bg-primary">{{ wallet_transactions|length }}</span>
|
||||||
|
</div>
|
||||||
|
<i class="bi bi-chevron-down"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="collapse" id="walletHistoryCollapse">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if wallet_transactions %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-hover mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Дата</th>
|
||||||
|
<th>Тип</th>
|
||||||
|
<th>Сумма</th>
|
||||||
|
<th>Описание</th>
|
||||||
|
<th>Заказ</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for transaction in wallet_transactions %}
|
||||||
|
<tr>
|
||||||
|
<td><small>{{ transaction.created_at|date:"d.m.y H:i" }}</small></td>
|
||||||
|
<td>
|
||||||
|
{% if transaction.transaction_type == 'deposit' %}
|
||||||
|
<span class="badge bg-success">Пополн.</span>
|
||||||
|
{% elif transaction.transaction_type == 'spend' %}
|
||||||
|
<span class="badge bg-danger">Списан.</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-warning">Корр.</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if transaction.transaction_type == 'deposit' or transaction.transaction_type == 'adjustment' and transaction.amount > 0 %}
|
||||||
|
<span class="text-success fw-bold">+{{ transaction.amount|floatformat:2 }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-danger fw-bold">-{{ transaction.amount|floatformat:2 }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td><small>{{ transaction.description|default:"-"|truncatewords:5 }}</small></td>
|
||||||
|
<td>
|
||||||
|
{% if transaction.order %}
|
||||||
|
<a href="{% url 'orders:order-detail' transaction.order.order_number %}" class="btn btn-sm btn-outline-primary py-0">
|
||||||
|
#{{ transaction.order.order_number }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted mb-0 p-3">История транзакций пуста.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Операции с кошельком -->
|
||||||
|
<div class="card mb-4 shadow-sm">
|
||||||
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-wallet2 text-warning"></i> Операции с кошельком клиента</h5>
|
||||||
<span>
|
<span>
|
||||||
{% if customer.wallet_balance > 0 %}
|
{% if customer.wallet_balance > 0 %}
|
||||||
<span class="badge bg-success" style="font-size: 1.1em;">{{ customer.wallet_balance|floatformat:2 }} руб.</span>
|
<span class="badge bg-success" style="font-size: 1.1em;">{{ customer.wallet_balance|floatformat:2 }} руб.</span>
|
||||||
@@ -229,264 +498,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="alert alert-info mb-0 mt-3">
|
<div class="alert alert-info mb-0 mt-3">
|
||||||
<i class="bi bi-info-circle"></i> Все операции логируются в истории ниже.
|
<i class="bi bi-info-circle"></i> Все операции логируются в истории выше.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Конец правой колонки -->
|
||||||
|
|
||||||
<!-- История транзакций кошелька -->
|
<!-- Алерт о необходимости возврата -->
|
||||||
|
{% if refund_amount > 0 %}
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="card mb-4">
|
<div class="alert alert-warning shadow-sm d-flex justify-content-between align-items-center mb-4" role="alert">
|
||||||
<div class="card-header">
|
<div>
|
||||||
<button class="btn btn-link w-100 text-start text-decoration-none p-0 d-flex justify-content-between align-items-center"
|
<h5 class="alert-heading mb-2">
|
||||||
type="button"
|
<i class="bi bi-exclamation-triangle-fill"></i> Требуется возврат средств
|
||||||
data-bs-toggle="collapse"
|
</h5>
|
||||||
data-bs-target="#walletHistoryCollapse"
|
<p class="mb-0">
|
||||||
aria-expanded="false"
|
Клиент имеет отменённые заказы с внесённой оплатой.
|
||||||
aria-controls="walletHistoryCollapse">
|
Общая сумма к возврату: <strong>{{ refund_amount|floatformat:2 }} руб.</strong>
|
||||||
<h5 class="mb-0">История кошелька (последние 20)</h5>
|
</p>
|
||||||
<div>
|
|
||||||
<span class="badge bg-primary me-2">{{ wallet_transactions|length }}</span>
|
|
||||||
<i class="bi bi-chevron-down"></i>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse" id="walletHistoryCollapse">
|
<div>
|
||||||
<div class="card-body">
|
<span class="badge bg-warning text-dark" style="font-size: 1.3em;">
|
||||||
{% if wallet_transactions %}
|
<i class="bi bi-exclamation-circle"></i> {{ refund_amount|floatformat:2 }} руб.
|
||||||
<div class="table-responsive">
|
</span>
|
||||||
<table class="table table-striped table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Дата</th>
|
|
||||||
<th>Тип</th>
|
|
||||||
<th>Сумма</th>
|
|
||||||
<th>Описание</th>
|
|
||||||
<th>Заказ</th>
|
|
||||||
<th>Создал</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for transaction in wallet_transactions %}
|
|
||||||
<tr>
|
|
||||||
<td><small>{{ transaction.created_at|date:"d.m.Y H:i" }}</small></td>
|
|
||||||
<td>
|
|
||||||
{% if transaction.transaction_type == 'deposit' %}
|
|
||||||
<span class="badge bg-success">Пополнение</span>
|
|
||||||
{% elif transaction.transaction_type == 'spend' %}
|
|
||||||
<span class="badge bg-danger">Списание</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-warning">Корректировка</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if transaction.transaction_type == 'deposit' or transaction.transaction_type == 'adjustment' and transaction.amount > 0 %}
|
|
||||||
<span class="text-success fw-bold">+{{ transaction.amount|floatformat:2 }} руб.</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="text-danger fw-bold">-{{ transaction.amount|floatformat:2 }} руб.</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>{{ transaction.description|default:"-" }}</td>
|
|
||||||
<td>
|
|
||||||
{% if transaction.order %}
|
|
||||||
<a href="{% url 'orders:order-detail' transaction.order.order_number %}" class="btn btn-sm btn-outline-primary">
|
|
||||||
#{{ transaction.order.order_number }}
|
|
||||||
</a>
|
|
||||||
{% else %}
|
|
||||||
-
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td><small>{{ transaction.created_by.username|default:"-" }}</small></td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<p class="text-muted mb-0">История транзакций пуста.</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- История заказов -->
|
|
||||||
<div class="col-md-12">
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<button class="btn btn-link text-start text-decoration-none p-0 d-flex align-items-center flex-grow-1"
|
|
||||||
type="button"
|
|
||||||
data-bs-toggle="collapse"
|
|
||||||
data-bs-target="#ordersHistoryCollapse"
|
|
||||||
aria-expanded="false"
|
|
||||||
aria-controls="ordersHistoryCollapse"
|
|
||||||
style="border: none; background: none;">
|
|
||||||
<h5 class="mb-0 me-3">История заказов</h5>
|
|
||||||
<span class="badge bg-primary">{{ orders_page.paginator.count }}</span>
|
|
||||||
<i class="bi bi-chevron-down ms-auto"></i>
|
|
||||||
</button>
|
|
||||||
<a href="{% url 'orders:order-create' %}?customer={{ customer.pk }}"
|
|
||||||
class="btn btn-sm btn-success ms-2">
|
|
||||||
<i class="bi bi-plus-circle"></i> Новый заказ
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="collapse" id="ordersHistoryCollapse">
|
|
||||||
<div class="card-body">
|
|
||||||
{% if orders_page %}
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-striped table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>№</th>
|
|
||||||
<th>Дата создания</th>
|
|
||||||
<th>Дата доставки</th>
|
|
||||||
<th>Статус</th>
|
|
||||||
<th>Оплата</th>
|
|
||||||
<th>Сумма</th>
|
|
||||||
<th>Оплачено</th>
|
|
||||||
<th>Остаток</th>
|
|
||||||
<th>Возврат</th>
|
|
||||||
<th>Действия</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for order in orders_page %}
|
|
||||||
<tr {% if order.status and order.status.is_negative_end and order.amount_paid > 0 %}class="table-warning"{% endif %}>
|
|
||||||
<td><strong>#{{ order.order_number }}</strong></td>
|
|
||||||
<td><small>{{ order.created_at|date:"d.m.Y H:i" }}</small></td>
|
|
||||||
<td>
|
|
||||||
{% if order.delivery_date %}
|
|
||||||
<strong>{{ order.delivery_date|date:"d.m.Y" }}</strong>
|
|
||||||
{% if order.delivery_time %}
|
|
||||||
<br><small class="text-muted">{{ order.delivery_time }}</small>
|
|
||||||
{% endif %}
|
|
||||||
{% if order.is_delivery %}
|
|
||||||
<br><span class="badge bg-info">Доставка</span>
|
|
||||||
{% else %}
|
|
||||||
<br><span class="badge bg-secondary">Самовывоз</span>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">Не указана</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if order.status %}
|
|
||||||
{% if order.status.code == 'draft' %}
|
|
||||||
<span class="badge bg-secondary">Черновик</span>
|
|
||||||
{% elif order.status.code == 'pending' %}
|
|
||||||
<span class="badge bg-warning">Ожидает</span>
|
|
||||||
{% elif order.status.code == 'in_production' %}
|
|
||||||
<span class="badge bg-info">В производстве</span>
|
|
||||||
{% elif order.status.code == 'ready' %}
|
|
||||||
<span class="badge bg-primary">Готов</span>
|
|
||||||
{% elif order.status.code == 'delivered' %}
|
|
||||||
<span class="badge bg-success">Доставлен</span>
|
|
||||||
{% elif order.status.code == 'cancelled' %}
|
|
||||||
<span class="badge bg-danger">Отменён</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-secondary">{{ order.status.name }}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-secondary">Без статуса</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if order.is_paid %}
|
|
||||||
<span class="badge bg-success">
|
|
||||||
<i class="bi bi-check-circle"></i> Оплачено
|
|
||||||
</span>
|
|
||||||
{% elif order.status and order.status.is_negative_end and order.amount_paid > 0 %}
|
|
||||||
<span class="badge bg-warning text-dark" style="border: 3px solid #dc3545 !important; box-shadow: 0 0 0 1px #dc3545;" title="Требуется возврат: {{ order.amount_paid|floatformat:2 }} руб.">
|
|
||||||
<i class="bi bi-exclamation-triangle"></i> Возврат <strong style="color: #dc3545;">({{ order.amount_paid|floatformat:2 }} руб.)</strong>
|
|
||||||
</span>
|
|
||||||
{% elif order.amount_paid > 0 %}
|
|
||||||
<span class="badge bg-warning">
|
|
||||||
<i class="bi bi-exclamation-circle"></i> Частично ({{ order.amount_paid|floatformat:2 }} руб.)
|
|
||||||
</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-danger">
|
|
||||||
<i class="bi bi-x-circle"></i> Не оплачено
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td><strong>{{ order.total_amount|floatformat:2 }} руб.</strong></td>
|
|
||||||
<td>
|
|
||||||
{% if order.amount_paid > 0 %}
|
|
||||||
<span class="text-success">{{ order.amount_paid|floatformat:2 }} руб.</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">0.00 руб.</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if order.status and order.status.is_negative_end %}
|
|
||||||
<span class="text-muted">—</span>
|
|
||||||
{% elif order.amount_due > 0 %}
|
|
||||||
<span class="text-danger fw-bold">{{ order.amount_due|floatformat:2 }} руб.</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="text-success">0.00 руб.</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if order.status and order.status.is_negative_end and order.amount_paid > 0 %}
|
|
||||||
<span class="badge bg-warning text-dark" style="border: 3px solid #dc3545 !important; box-shadow: 0 0 0 1px #dc3545;" title="Требуется возврат">
|
|
||||||
<i class="bi bi-exclamation-triangle"></i> <strong style="color: #dc3545;">{{ order.amount_paid|floatformat:2 }} руб.</strong>
|
|
||||||
</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">—</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="{% url 'orders:order-detail' order.order_number %}" class="btn btn-sm btn-outline-primary">
|
|
||||||
<i class="bi bi-eye"></i>
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'orders:order-update' order.order_number %}" class="btn btn-sm btn-outline-secondary">
|
|
||||||
<i class="bi bi-pencil"></i>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Пагинация -->
|
|
||||||
{% if orders_page.has_other_pages %}
|
|
||||||
<nav aria-label="Навигация по заказам">
|
|
||||||
<ul class="pagination justify-content-center mt-3">
|
|
||||||
{% if orders_page.has_previous %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page=1#ordersHistoryCollapse">Первая</a>
|
|
||||||
</li>
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page={{ orders_page.previous_page_number }}#ordersHistoryCollapse">Предыдущая</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<li class="page-item active">
|
|
||||||
<span class="page-link">
|
|
||||||
Страница {{ orders_page.number }} из {{ orders_page.paginator.num_pages }}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{% if orders_page.has_next %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page={{ orders_page.next_page_number }}#ordersHistoryCollapse">Следующая</a>
|
|
||||||
</li>
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page={{ orders_page.paginator.num_pages }}#ordersHistoryCollapse">Последняя</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
<p class="text-muted mb-0">У клиента пока нет заказов.</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -539,8 +578,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Автооткрытие collapse при наличии якоря в URL
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Автооткрытие collapse при наличии якоря в URL
|
||||||
const hash = window.location.hash;
|
const hash = window.location.hash;
|
||||||
if (hash === '#ordersHistoryCollapse') {
|
if (hash === '#ordersHistoryCollapse') {
|
||||||
const collapseElement = document.getElementById('ordersHistoryCollapse');
|
const collapseElement = document.getElementById('ordersHistoryCollapse');
|
||||||
@@ -548,12 +587,177 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const bsCollapse = new bootstrap.Collapse(collapseElement, {
|
const bsCollapse = new bootstrap.Collapse(collapseElement, {
|
||||||
show: true
|
show: true
|
||||||
});
|
});
|
||||||
// Прокручиваем к элементу после открытия collapse (с учетом анимации)
|
|
||||||
collapseElement.addEventListener('shown.bs.collapse', function() {
|
collapseElement.addEventListener('shown.bs.collapse', function() {
|
||||||
collapseElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
collapseElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
}, { once: true });
|
}, { once: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== INLINE EDITING ==========
|
||||||
|
const table = document.getElementById('customer-info-table');
|
||||||
|
if (!table) return;
|
||||||
|
|
||||||
|
const customerId = table.dataset.customerId;
|
||||||
|
const updateUrl = `/customers/${customerId}/api/update/`;
|
||||||
|
|
||||||
|
// Находим все редактируемые строки
|
||||||
|
const editableRows = table.querySelectorAll('tr[data-field]');
|
||||||
|
|
||||||
|
editableRows.forEach(row => {
|
||||||
|
const field = row.dataset.field;
|
||||||
|
const fieldValue = row.querySelector('.field-value');
|
||||||
|
const fieldInput = row.querySelector('.field-input');
|
||||||
|
const fieldActions = row.querySelector('.field-actions');
|
||||||
|
const editBtn = row.querySelector('.edit-btn');
|
||||||
|
const saveBtn = row.querySelector('.save-btn');
|
||||||
|
const cancelBtn = row.querySelector('.cancel-btn');
|
||||||
|
const saveStatus = row.querySelector('.save-status');
|
||||||
|
|
||||||
|
let originalValue = '';
|
||||||
|
|
||||||
|
// Клик на карандаш — начать редактирование
|
||||||
|
editBtn.addEventListener('click', function() {
|
||||||
|
originalValue = fieldInput.value;
|
||||||
|
fieldValue.classList.add('d-none');
|
||||||
|
fieldInput.classList.remove('d-none');
|
||||||
|
fieldActions.classList.remove('d-none');
|
||||||
|
editBtn.style.visibility = 'hidden'; // Скрываем карандаш но сохраняем место
|
||||||
|
fieldInput.focus();
|
||||||
|
if (fieldInput.tagName === 'INPUT') {
|
||||||
|
fieldInput.select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Клик на галочку или Enter — сохранить
|
||||||
|
saveBtn.addEventListener('click', saveField);
|
||||||
|
fieldInput.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter' && fieldInput.tagName !== 'TEXTAREA') {
|
||||||
|
e.preventDefault();
|
||||||
|
saveField();
|
||||||
|
}
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
cancelEdit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Клик на крестик — отменить
|
||||||
|
cancelBtn.addEventListener('click', cancelEdit);
|
||||||
|
|
||||||
|
function cancelEdit() {
|
||||||
|
fieldInput.value = originalValue;
|
||||||
|
fieldValue.classList.remove('d-none');
|
||||||
|
fieldInput.classList.add('d-none');
|
||||||
|
fieldActions.classList.add('d-none');
|
||||||
|
editBtn.style.visibility = 'visible';
|
||||||
|
saveStatus.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveField() {
|
||||||
|
const newValue = fieldInput.value.trim();
|
||||||
|
|
||||||
|
// Показываем индикатор загрузки
|
||||||
|
saveStatus.innerHTML = '<span class="spinner-border spinner-border-sm text-primary"></span>';
|
||||||
|
saveBtn.disabled = true;
|
||||||
|
cancelBtn.disabled = true;
|
||||||
|
|
||||||
|
fetch(updateUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': getCookie('csrftoken')
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
field: field,
|
||||||
|
value: newValue
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
cancelBtn.disabled = false;
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// Обновляем отображаемое значение
|
||||||
|
const displayValue = data.value || '—';
|
||||||
|
fieldValue.textContent = displayValue;
|
||||||
|
fieldInput.value = data.value || '';
|
||||||
|
originalValue = data.value || '';
|
||||||
|
|
||||||
|
// Обновляем заголовок страницы если изменилось имя
|
||||||
|
if (field === 'name') {
|
||||||
|
const titleSpan = document.getElementById('customer-title');
|
||||||
|
if (titleSpan) {
|
||||||
|
titleSpan.textContent = displayValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Возвращаем в режим просмотра
|
||||||
|
fieldValue.classList.remove('d-none');
|
||||||
|
fieldInput.classList.add('d-none');
|
||||||
|
fieldActions.classList.add('d-none');
|
||||||
|
editBtn.style.visibility = 'visible';
|
||||||
|
|
||||||
|
// Показываем успех
|
||||||
|
saveStatus.innerHTML = '<i class="bi bi-check-circle-fill text-success"></i>';
|
||||||
|
setTimeout(() => {
|
||||||
|
saveStatus.innerHTML = '';
|
||||||
|
}, 1500);
|
||||||
|
} else {
|
||||||
|
// Показываем ошибку
|
||||||
|
saveStatus.innerHTML = `<span class="text-danger small"><i class="bi bi-exclamation-circle"></i> ${data.error}</span>`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
cancelBtn.disabled = false;
|
||||||
|
saveStatus.innerHTML = '<span class="text-danger small"><i class="bi bi-exclamation-circle"></i> Ошибка сети</span>';
|
||||||
|
console.error('Error:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Функция получения CSRF токена
|
||||||
|
function getCookie(name) {
|
||||||
|
let cookieValue = null;
|
||||||
|
if (document.cookie && document.cookie !== '') {
|
||||||
|
const cookies = document.cookie.split(';');
|
||||||
|
for (let i = 0; i < cookies.length; i++) {
|
||||||
|
const cookie = cookies[i].trim();
|
||||||
|
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||||
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookieValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== COPY TO CLIPBOARD ==========
|
||||||
|
document.querySelectorAll('.copy-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const value = this.dataset.copyValue;
|
||||||
|
if (!value) return;
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(value).then(() => {
|
||||||
|
// Меняем иконку на галочку и стиль кнопки
|
||||||
|
const icon = this.querySelector('i');
|
||||||
|
const originalIconClass = icon.className;
|
||||||
|
const originalBtnClass = this.className;
|
||||||
|
|
||||||
|
// Меняем кнопку на зелёную с белой галочкой
|
||||||
|
this.className = 'btn btn-sm btn-success copy-btn';
|
||||||
|
icon.className = 'bi bi-check-lg';
|
||||||
|
|
||||||
|
// Возвращаем обратно через 1 сек
|
||||||
|
setTimeout(() => {
|
||||||
|
this.className = originalBtnClass;
|
||||||
|
icon.className = originalIconClass;
|
||||||
|
}, 1000);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Ошибка копирования:', err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,89 +1,43 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}Добавить нового клиента{% endblock %}
|
||||||
{% if is_creating %}Добавить нового клиента{% else %}Редактировать клиента{% endif %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h1>
|
<h1>Добавить нового клиента</h1>
|
||||||
{% if is_creating %}
|
|
||||||
Добавить нового клиента
|
|
||||||
{% else %}
|
|
||||||
Редактировать клиента
|
|
||||||
{% endif %}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<!-- Personal Information -->
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5>Личная информация</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ form.name.label_tag }}
|
|
||||||
{{ form.name }}
|
|
||||||
{% if form.name.errors %}
|
|
||||||
<div class="text-danger">{{ form.name.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ form.phone.label_tag }}
|
|
||||||
{{ form.phone }}
|
|
||||||
<div class="form-text">Введите телефон в любом формате, например: +375291234567, 80291234567</div>
|
|
||||||
{% if form.phone.errors %}
|
|
||||||
<div class="text-danger">{{ form.phone.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ form.email.label_tag }}
|
|
||||||
{{ form.email }}
|
|
||||||
{% if form.email.errors %}
|
|
||||||
<div class="text-danger">{{ form.email.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Preferences and Status -->
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5>Предпочтения и статус</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ form.loyalty_tier.label_tag }}
|
|
||||||
{{ form.loyalty_tier }}
|
|
||||||
{% if form.loyalty_tier.errors %}
|
|
||||||
<div class="text-danger">{{ form.loyalty_tier.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Additional Information -->
|
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header">
|
|
||||||
<h5>Дополнительная информация</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.name.label_tag }}
|
||||||
|
{{ form.name }}
|
||||||
|
{% if form.name.errors %}
|
||||||
|
<div class="text-danger">{{ form.name.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.phone.label_tag }}
|
||||||
|
{{ form.phone }}
|
||||||
|
<div class="form-text">Введите телефон в любом формате, например: +375291234567, 80291234567</div>
|
||||||
|
{% if form.phone.errors %}
|
||||||
|
<div class="text-danger">{{ form.phone.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.email.label_tag }}
|
||||||
|
{{ form.email }}
|
||||||
|
{% if form.email.errors %}
|
||||||
|
<div class="text-danger">{{ form.email.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
{{ form.notes.label_tag }}
|
{{ form.notes.label_tag }}
|
||||||
{{ form.notes }}
|
{{ form.notes }}
|
||||||
@@ -93,13 +47,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Form Actions -->
|
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">Создать клиента</button>
|
||||||
{% if is_creating %}Создать клиента{% else %}Сохранить изменения{% endif %}
|
<a href="{% url 'customers:customer-list' %}" class="btn btn-secondary">Отмена</a>
|
||||||
</button>
|
|
||||||
<a href="{% if form.instance.pk %}{% url 'customers:customer-detail' form.instance.pk %}{% else %}{% url 'customers:customer-list' %}{% endif %}" class="btn btn-secondary">Отмена</a>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -78,9 +78,9 @@
|
|||||||
|
|
||||||
<td class="text-end" onclick="event.stopPropagation();">
|
<td class="text-end" onclick="event.stopPropagation();">
|
||||||
<a href="{% url 'customers:customer-detail' customer.pk %}"
|
<a href="{% url 'customers:customer-detail' customer.pk %}"
|
||||||
class="btn btn-sm btn-outline-primary">👁</a>
|
class="btn btn-sm btn-outline-primary" title="Просмотр">
|
||||||
<a href="{% url 'customers:customer-update' customer.pk %}"
|
<i class="bi bi-eye"></i>
|
||||||
class="btn btn-sm btn-outline-secondary">✎</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ urlpatterns = [
|
|||||||
path('import/', views.customer_import, name='customer-import'),
|
path('import/', views.customer_import, name='customer-import'),
|
||||||
path('export/', views.customer_export, name='customer-export'),
|
path('export/', views.customer_export, name='customer-export'),
|
||||||
path('<int:pk>/', views.customer_detail, name='customer-detail'),
|
path('<int:pk>/', views.customer_detail, name='customer-detail'),
|
||||||
path('<int:pk>/edit/', views.customer_update, name='customer-update'),
|
|
||||||
path('<int:pk>/delete/', views.customer_delete, name='customer-delete'),
|
path('<int:pk>/delete/', views.customer_delete, name='customer-delete'),
|
||||||
path('<int:pk>/wallet/deposit/', views.wallet_deposit, name='wallet-deposit'),
|
path('<int:pk>/wallet/deposit/', views.wallet_deposit, name='wallet-deposit'),
|
||||||
path('<int:pk>/wallet/withdraw/', views.wallet_withdraw, name='wallet-withdraw'),
|
path('<int:pk>/wallet/withdraw/', views.wallet_withdraw, name='wallet-withdraw'),
|
||||||
@@ -21,4 +20,5 @@ urlpatterns = [
|
|||||||
# AJAX API endpoints
|
# AJAX API endpoints
|
||||||
path('api/search/', views.api_search_customers, name='api-search-customers'),
|
path('api/search/', views.api_search_customers, name='api-search-customers'),
|
||||||
path('api/create/', views.api_create_customer, name='api-create-customer'),
|
path('api/create/', views.api_create_customer, name='api-create-customer'),
|
||||||
|
path('<int:pk>/api/update/', views.api_update_customer, name='api-update-customer'),
|
||||||
]
|
]
|
||||||
@@ -174,30 +174,6 @@ def customer_create(request):
|
|||||||
return render(request, 'customers/customer_form.html', {'form': form, 'is_creating': True})
|
return render(request, 'customers/customer_form.html', {'form': form, 'is_creating': True})
|
||||||
|
|
||||||
|
|
||||||
def customer_update(request, pk):
|
|
||||||
"""Редактирование клиента"""
|
|
||||||
customer = get_object_or_404(Customer, pk=pk)
|
|
||||||
|
|
||||||
# Проверяем, не системный ли это клиент
|
|
||||||
if customer.is_system_customer:
|
|
||||||
messages.warning(request, 'Системный клиент не может быть изменен. Он создается автоматически и необходим для корректной работы системы.')
|
|
||||||
return redirect('customers:customer-detail', pk=pk)
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
|
||||||
form = CustomerForm(request.POST, instance=customer)
|
|
||||||
if form.is_valid():
|
|
||||||
try:
|
|
||||||
form.save()
|
|
||||||
messages.success(request, f'Клиент {customer.full_name} успешно обновлён.')
|
|
||||||
return redirect('customers:customer-detail', pk=customer.pk)
|
|
||||||
except ValidationError as e:
|
|
||||||
messages.error(request, str(e))
|
|
||||||
else:
|
|
||||||
form = CustomerForm(instance=customer)
|
|
||||||
|
|
||||||
return render(request, 'customers/customer_form.html', {'form': form, 'is_creating': False})
|
|
||||||
|
|
||||||
|
|
||||||
def customer_delete(request, pk):
|
def customer_delete(request, pk):
|
||||||
"""Удаление клиента"""
|
"""Удаление клиента"""
|
||||||
customer = get_object_or_404(Customer, pk=pk)
|
customer = get_object_or_404(Customer, pk=pk)
|
||||||
@@ -503,6 +479,101 @@ def api_search_customers(request):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@require_http_methods(["POST"])
|
||||||
|
def api_update_customer(request, pk):
|
||||||
|
"""
|
||||||
|
AJAX endpoint для обновления отдельного поля клиента (inline-редактирование).
|
||||||
|
|
||||||
|
Принимает POST JSON:
|
||||||
|
{
|
||||||
|
"field": "name",
|
||||||
|
"value": "Новое имя"
|
||||||
|
}
|
||||||
|
|
||||||
|
Возвращает JSON:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"value": "Новое имя"
|
||||||
|
}
|
||||||
|
|
||||||
|
При ошибке:
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "Текст ошибки"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
customer = get_object_or_404(Customer, pk=pk)
|
||||||
|
|
||||||
|
# Защита системного клиента
|
||||||
|
if customer.is_system_customer:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Системный клиент не может быть изменён'
|
||||||
|
}, status=403)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(request.body)
|
||||||
|
field = data.get('field')
|
||||||
|
value = data.get('value', '').strip()
|
||||||
|
|
||||||
|
# Разрешённые поля для редактирования
|
||||||
|
allowed_fields = ['name', 'phone', 'email', 'notes']
|
||||||
|
if field not in allowed_fields:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': f'Поле "{field}" недоступно для редактирования'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
# Валидация через форму
|
||||||
|
form_data = {field: value if value else None}
|
||||||
|
form = CustomerForm(form_data, instance=customer)
|
||||||
|
|
||||||
|
# Проверяем только нужное поле
|
||||||
|
if field in form.fields:
|
||||||
|
form.fields[field].required = False
|
||||||
|
field_value = form.fields[field].clean(value if value else None)
|
||||||
|
|
||||||
|
# Обновляем поле
|
||||||
|
setattr(customer, field, field_value)
|
||||||
|
customer.save(update_fields=[field, 'updated_at'])
|
||||||
|
|
||||||
|
# Возвращаем отформатированное значение
|
||||||
|
display_value = getattr(customer, field)
|
||||||
|
if display_value is None:
|
||||||
|
display_value = ''
|
||||||
|
elif field == 'phone' and display_value:
|
||||||
|
display_value = str(display_value)
|
||||||
|
else:
|
||||||
|
display_value = str(display_value)
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'success': True,
|
||||||
|
'value': display_value
|
||||||
|
})
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Неизвестное поле'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Некорректный JSON'
|
||||||
|
}, status=400)
|
||||||
|
except ValidationError as e:
|
||||||
|
error_msg = e.message if hasattr(e, 'message') else str(e)
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': error_msg
|
||||||
|
}, status=400)
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': str(e)
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
|
||||||
@require_http_methods(["POST"])
|
@require_http_methods(["POST"])
|
||||||
def api_create_customer(request):
|
def api_create_customer(request):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -26,9 +26,11 @@
|
|||||||
<a href="{% url 'orders:order-update' order.order_number %}" class="btn btn-primary">
|
<a href="{% url 'orders:order-update' order.order_number %}" class="btn btn-primary">
|
||||||
<i class="bi bi-pencil"></i> Редактировать
|
<i class="bi bi-pencil"></i> Редактировать
|
||||||
</a>
|
</a>
|
||||||
|
{% if order.status and order.status.code == 'draft' and order.amount_paid == 0 %}
|
||||||
<a href="{% url 'orders:order-delete' order.order_number %}" class="btn btn-danger">
|
<a href="{% url 'orders:order-delete' order.order_number %}" class="btn btn-danger">
|
||||||
<i class="bi bi-trash"></i> Удалить
|
<i class="bi bi-trash"></i> Удалить
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
<a href="{% url 'orders:order-list' %}" class="btn btn-secondary">
|
<a href="{% url 'orders:order-list' %}" class="btn btn-secondary">
|
||||||
<i class="bi bi-arrow-left"></i> К списку
|
<i class="bi bi-arrow-left"></i> К списку
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -413,13 +413,31 @@ def order_update(request, order_number):
|
|||||||
|
|
||||||
|
|
||||||
def order_delete(request, order_number):
|
def order_delete(request, order_number):
|
||||||
"""Удаление заказа с подтверждением"""
|
"""Удаление заказа с подтверждением (только для черновиков без оплаты)"""
|
||||||
order = get_object_or_404(Order, order_number=order_number)
|
order = get_object_or_404(Order, order_number=order_number)
|
||||||
|
|
||||||
|
# Проверка: удалять можно только черновики
|
||||||
|
if not order.status or order.status.code != 'draft':
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
f'Удаление невозможно. Удалять можно только заказы в статусе "Черновик". '
|
||||||
|
f'Текущий статус: {order.status.name if order.status else "не задан"}'
|
||||||
|
)
|
||||||
|
return redirect('orders:order-detail', order_number=order.order_number)
|
||||||
|
|
||||||
|
# Проверка: нельзя удалять заказы с оплатой
|
||||||
|
if order.amount_paid > 0:
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
f'Удаление невозможно. Заказ имеет оплату ({order.amount_paid} руб.). '
|
||||||
|
f'Сначала оформите возврат платежа.'
|
||||||
|
)
|
||||||
|
return redirect('orders:order-detail', order_number=order.order_number)
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
order_number = order.order_number
|
order_number_saved = order.order_number
|
||||||
order.delete()
|
order.delete()
|
||||||
messages.success(request, f'Заказ #{order_number} успешно удален.')
|
messages.success(request, f'Заказ #{order_number_saved} успешно удален.')
|
||||||
return redirect('orders:order-list')
|
return redirect('orders:order-list')
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
|
|||||||
Reference in New Issue
Block a user