Защита удаления заказов и улучшение интерфейса клиентов
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="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h1>Клиент: {{ customer.full_name }}</h1>
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="mb-0"><i class="bi bi-person-badge text-primary"></i> <span id="customer-title">{{ customer.full_name }}</span></h2>
|
||||
<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-danger">Удалить</a>
|
||||
<a href="{% url 'customers:customer-list' %}" class="btn btn-secondary">Назад к списку</a>
|
||||
<a href="{% url 'customers:customer-delete' customer.pk %}" class="btn btn-outline-danger">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</a>
|
||||
<a href="{% url 'customers:customer-list' %}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Назад к списку
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -20,67 +23,161 @@
|
||||
<div class="row">
|
||||
<!-- Customer Info -->
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5>Информация о клиенте</h5>
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0"><i class="bi bi-person-circle text-primary"></i> Информация о клиенте</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-borderless">
|
||||
<tr>
|
||||
<th>Имя:</th>
|
||||
<td>{{ customer.full_name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Email:</th>
|
||||
<td>{{ customer.email|default:"Не указано" }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
<table class="table table-borderless mb-0" id="customer-info-table" data-customer-id="{{ customer.pk }}">
|
||||
<colgroup>
|
||||
<col style="width: 40%;">
|
||||
<col style="width: 50%;">
|
||||
<col style="width: 10%;">
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<!-- Основная информация -->
|
||||
<tr data-field="name">
|
||||
<td class="text-muted"><i class="bi bi-person-fill text-primary"></i> Имя:</td>
|
||||
<td>
|
||||
{% if total_debt > 0 %}
|
||||
<span class="text-danger fw-bold">{{ total_debt|floatformat:2 }} руб.</span>
|
||||
<small class="text-muted">(Кол-во заказов: {{ active_orders_count }})</small>
|
||||
{% else %}
|
||||
<span class="text-success">0.00 руб.</span>
|
||||
{% endif %}
|
||||
<span class="field-value fw-bold">{{ customer.name|default:"—" }}</span>
|
||||
<input type="text" class="field-input form-control form-control-sm d-none" value="{{ customer.name|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.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>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Заметки:</th>
|
||||
<td>{{ customer.notes|default:"Нет" }}</td>
|
||||
<td class="text-muted"><i class="bi bi-calendar-check text-info"></i> За последний год:</td>
|
||||
<td colspan="2">
|
||||
<span class="badge bg-info">{{ last_year_orders_sum|floatformat:2 }} руб.</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Дата создания:</th>
|
||||
<td>{{ customer.created_at|date:"d.m.Y H:i" }}</td>
|
||||
<td class="text-muted"><i class="bi bi-exclamation-triangle-fill text-danger"></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>
|
||||
<th>Дата обновления:</th>
|
||||
<td>{{ customer.updated_at|date:"d.m.Y H:i" }}</td>
|
||||
<td class="text-muted small"><i class="bi bi-arrow-clockwise"></i> Последнее изменение:</td>
|
||||
<td colspan="2" class="small"><span id="updated-at">{{ customer.updated_at|date:"d.m.Y H:i" }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Каналы связи -->
|
||||
<!-- Правая колонка: Каналы связи + История заказов + История кошелька -->
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Каналы связи</h5>
|
||||
<!-- Каналы связи -->
|
||||
<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-chat-dots text-success"></i> Каналы связи</h5>
|
||||
<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>
|
||||
</div>
|
||||
<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.notes %}<small class="text-muted d-block mt-1">{{ channel.notes }}</small>{% endif %}
|
||||
</div>
|
||||
<div class="d-flex gap-1">
|
||||
<button class="btn btn-sm btn-outline-secondary copy-btn" title="Копировать" data-copy-value="{{ channel.value }}">
|
||||
<i class="bi bi-copy"></i>
|
||||
</button>
|
||||
<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>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@@ -126,35 +228,202 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- История заказов -->
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-header bg-light 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-2"><i class="bi bi-cart-check text-primary"></i> История заказов</h5>
|
||||
<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 class="collapse" id="ordersHistoryCollapse">
|
||||
<div class="card-body p-0">
|
||||
{% if orders_page %}
|
||||
<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 refund_amount > 0 %}
|
||||
<div class="col-md-12">
|
||||
<div class="alert alert-warning d-flex justify-content-between align-items-center mb-4" role="alert">
|
||||
<div>
|
||||
<h5 class="alert-heading mb-2">
|
||||
<i class="bi bi-exclamation-triangle-fill"></i> Требуется возврат средств
|
||||
</h5>
|
||||
<p class="mb-0">
|
||||
Клиент имеет отменённые заказы с внесённой оплатой.
|
||||
Общая сумма к возврату: <strong>{{ refund_amount|floatformat:2 }} руб.</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="badge bg-warning text-dark" style="font-size: 1.2em;">
|
||||
{{ refund_amount|floatformat:2 }} руб.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</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 class="card mb-4 shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<button class="btn btn-link w-100 text-start text-decoration-none p-0 d-flex justify-content-between align-items-center"
|
||||
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="col-md-6">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Операции с кошельком клиента</h5>
|
||||
<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>
|
||||
{% if customer.wallet_balance > 0 %}
|
||||
<span class="badge bg-success" style="font-size: 1.1em;">{{ customer.wallet_balance|floatformat:2 }} руб.</span>
|
||||
@@ -229,264 +498,34 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<!-- Конец правой колонки -->
|
||||
|
||||
<!-- История транзакций кошелька -->
|
||||
<!-- Алерт о необходимости возврата -->
|
||||
{% if refund_amount > 0 %}
|
||||
<div class="col-md-12">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<button class="btn btn-link w-100 text-start text-decoration-none p-0 d-flex justify-content-between align-items-center"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#walletHistoryCollapse"
|
||||
aria-expanded="false"
|
||||
aria-controls="walletHistoryCollapse">
|
||||
<h5 class="mb-0">История кошелька (последние 20)</h5>
|
||||
<div class="alert alert-warning shadow-sm d-flex justify-content-between align-items-center mb-4" role="alert">
|
||||
<div>
|
||||
<span class="badge bg-primary me-2">{{ wallet_transactions|length }}</span>
|
||||
<i class="bi bi-chevron-down"></i>
|
||||
<h5 class="alert-heading mb-2">
|
||||
<i class="bi bi-exclamation-triangle-fill"></i> Требуется возврат средств
|
||||
</h5>
|
||||
<p class="mb-0">
|
||||
Клиент имеет отменённые заказы с внесённой оплатой.
|
||||
Общая сумма к возврату: <strong>{{ refund_amount|floatformat:2 }} руб.</strong>
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="collapse" id="walletHistoryCollapse">
|
||||
<div class="card-body">
|
||||
{% if wallet_transactions %}
|
||||
<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>
|
||||
</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> Оплачено
|
||||
<div>
|
||||
<span class="badge bg-warning text-dark" style="font-size: 1.3em;">
|
||||
<i class="bi bi-exclamation-circle"></i> {{ refund_amount|floatformat:2 }} руб.
|
||||
</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>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -539,8 +578,8 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Автооткрытие collapse при наличии якоря в URL
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Автооткрытие collapse при наличии якоря в URL
|
||||
const hash = window.location.hash;
|
||||
if (hash === '#ordersHistoryCollapse') {
|
||||
const collapseElement = document.getElementById('ordersHistoryCollapse');
|
||||
@@ -548,12 +587,177 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const bsCollapse = new bootstrap.Collapse(collapseElement, {
|
||||
show: true
|
||||
});
|
||||
// Прокручиваем к элементу после открытия collapse (с учетом анимации)
|
||||
collapseElement.addEventListener('shown.bs.collapse', function() {
|
||||
collapseElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}, { 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>
|
||||
{% endblock %}
|
||||
@@ -1,31 +1,17 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{% if is_creating %}Добавить нового клиента{% else %}Редактировать клиента{% endif %}
|
||||
{% endblock %}
|
||||
{% block title %}Добавить нового клиента{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1>
|
||||
{% if is_creating %}
|
||||
Добавить нового клиента
|
||||
{% else %}
|
||||
Редактировать клиента
|
||||
{% endif %}
|
||||
</h1>
|
||||
<h1>Добавить нового клиента</h1>
|
||||
|
||||
<form method="post">
|
||||
{% 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 }}
|
||||
@@ -51,39 +37,7 @@
|
||||
<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-header">
|
||||
<h5>Дополнительная информация</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
{{ form.notes.label_tag }}
|
||||
{{ form.notes }}
|
||||
@@ -94,12 +48,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{% if is_creating %}Создать клиента{% else %}Сохранить изменения{% endif %}
|
||||
</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>
|
||||
<button type="submit" class="btn btn-primary">Создать клиента</button>
|
||||
<a href="{% url 'customers:customer-list' %}" class="btn btn-secondary">Отмена</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -78,9 +78,9 @@
|
||||
|
||||
<td class="text-end" onclick="event.stopPropagation();">
|
||||
<a href="{% url 'customers:customer-detail' customer.pk %}"
|
||||
class="btn btn-sm btn-outline-primary">👁</a>
|
||||
<a href="{% url 'customers:customer-update' customer.pk %}"
|
||||
class="btn btn-sm btn-outline-secondary">✎</a>
|
||||
class="btn btn-sm btn-outline-primary" title="Просмотр">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -9,7 +9,6 @@ urlpatterns = [
|
||||
path('import/', views.customer_import, name='customer-import'),
|
||||
path('export/', views.customer_export, name='customer-export'),
|
||||
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>/wallet/deposit/', views.wallet_deposit, name='wallet-deposit'),
|
||||
path('<int:pk>/wallet/withdraw/', views.wallet_withdraw, name='wallet-withdraw'),
|
||||
@@ -21,4 +20,5 @@ urlpatterns = [
|
||||
# AJAX API endpoints
|
||||
path('api/search/', views.api_search_customers, name='api-search-customers'),
|
||||
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})
|
||||
|
||||
|
||||
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):
|
||||
"""Удаление клиента"""
|
||||
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"])
|
||||
def api_create_customer(request):
|
||||
"""
|
||||
|
||||
@@ -26,9 +26,11 @@
|
||||
<a href="{% url 'orders:order-update' order.order_number %}" class="btn btn-primary">
|
||||
<i class="bi bi-pencil"></i> Редактировать
|
||||
</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">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'orders:order-list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> К списку
|
||||
</a>
|
||||
|
||||
@@ -413,13 +413,31 @@ def order_update(request, order_number):
|
||||
|
||||
|
||||
def order_delete(request, 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':
|
||||
order_number = order.order_number
|
||||
order_number_saved = order.order_number
|
||||
order.delete()
|
||||
messages.success(request, f'Заказ #{order_number} успешно удален.')
|
||||
messages.success(request, f'Заказ #{order_number_saved} успешно удален.')
|
||||
return redirect('orders:order-list')
|
||||
|
||||
context = {
|
||||
|
||||
Reference in New Issue
Block a user