- Replace large operation cards with smaller, uniform inventory-card components - Update icon sizes and text styles for better visual hierarchy - Group operations into a cleaner 4x3 grid layout using Bootstrap columns - Simplify card hover effects with subtler shadows and background gradients - Remove unused purple utility classes and old card-body styles - Add focus outline support for accessibility on inventory cards fix(customers): show total debt only when applicable and add external links - Display total debt row only if debt is greater than zero in customer detail - Remove redundant conditional inside debt display cell - Add target="_blank" and rel attributes to order detail links to open in new tab
761 lines
45 KiB
HTML
761 lines
45 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}{{ customer.full_name }}{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="container-fluid">
|
||
<div class="row">
|
||
<div class="col-12">
|
||
<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-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>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<!-- Customer Info -->
|
||
<div class="col-md-6">
|
||
<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 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>
|
||
<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>
|
||
<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>
|
||
{% if total_debt > 0 %}
|
||
<tr>
|
||
<td class="text-muted"><i class="bi bi-exclamation-triangle-fill text-danger"></i> Общий долг:</td>
|
||
<td colspan="2">
|
||
<span class="badge bg-danger">{{ total_debt|floatformat:2 }} руб.</span>
|
||
<small class="text-muted ms-2">(Заказов: {{ active_orders_count }})</small>
|
||
</td>
|
||
</tr>
|
||
{% endif %}
|
||
|
||
<!-- Разделитель -->
|
||
<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>
|
||
<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 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-circle"></i> Добавить
|
||
</button>
|
||
</div>
|
||
<div class="card-body">
|
||
{% if contact_channels %}
|
||
<ul class="list-group list-group-flush">
|
||
{% for channel in contact_channels %}
|
||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||
<div>
|
||
{% if channel.channel_type == 'telegram' %}
|
||
<span class="badge bg-info me-2"><i class="bi bi-telegram"></i> Telegram</span>
|
||
{% elif channel.channel_type == 'instagram' %}
|
||
<span class="badge bg-danger me-2"><i class="bi bi-instagram"></i> Instagram</span>
|
||
{% elif channel.channel_type == 'whatsapp' %}
|
||
<span class="badge bg-success me-2"><i class="bi bi-whatsapp"></i> WhatsApp</span>
|
||
{% elif channel.channel_type == 'viber' %}
|
||
<span class="badge bg-purple me-2" style="background-color: #7360f2 !important;"><i class="bi bi-chat-fill"></i> Viber</span>
|
||
{% elif channel.channel_type == 'vk' %}
|
||
<span class="badge bg-primary me-2">VK</span>
|
||
{% elif channel.channel_type == 'facebook' %}
|
||
<span class="badge bg-primary me-2"><i class="bi bi-facebook"></i> Facebook</span>
|
||
{% elif channel.channel_type == 'phone' %}
|
||
<span class="badge bg-secondary me-2"><i class="bi bi-telephone"></i> Телефон</span>
|
||
{% elif channel.channel_type == 'email' %}
|
||
<span class="badge bg-secondary me-2"><i class="bi bi-envelope"></i> Email</span>
|
||
{% else %}
|
||
<span class="badge bg-dark me-2">{{ channel.get_channel_type_display }}</span>
|
||
{% endif %}
|
||
<strong>{{ channel.value }}</strong>
|
||
{% 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>
|
||
{% else %}
|
||
<p class="text-muted mb-0">Нет дополнительных каналов связи. Добавьте Instagram, Telegram и другие контакты.</p>
|
||
{% 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" target="_blank" rel="noopener noreferrer" title="Открыть в новой вкладке">
|
||
<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 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="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>
|
||
{% elif customer.wallet_balance == 0 %}
|
||
<span class="badge bg-secondary" style="font-size: 1.1em;">{{ customer.wallet_balance|floatformat:2 }} руб.</span>
|
||
{% else %}
|
||
<span class="badge bg-danger" style="font-size: 1.1em;">{{ customer.wallet_balance|floatformat:2 }} руб.</span>
|
||
{% endif %}
|
||
</span>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row">
|
||
<!-- Пополнение -->
|
||
<div class="col-md-6">
|
||
<h6 class="text-success mb-3"><i class="bi bi-plus-circle"></i> Пополнение</h6>
|
||
<form method="post" action="{% url 'customers:wallet-deposit' customer.pk %}">
|
||
{% csrf_token %}
|
||
<div class="mb-3">
|
||
<label for="wallet_deposit_amount" class="form-label">Сумма, руб.</label>
|
||
<input type="number"
|
||
step="0.01"
|
||
min="0.01"
|
||
class="form-control"
|
||
id="wallet_deposit_amount"
|
||
name="amount"
|
||
placeholder="0.00"
|
||
required>
|
||
<div style="height: 1.25rem;"></div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="wallet_deposit_description" class="form-label">Описание</label>
|
||
<textarea class="form-control"
|
||
id="wallet_deposit_description"
|
||
name="description"
|
||
rows="2"
|
||
placeholder="Подарок, компенсация..."
|
||
required></textarea>
|
||
</div>
|
||
<button type="submit" class="btn btn-success w-100"><i class="bi bi-plus-circle"></i> Пополнить</button>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Возврат / списание -->
|
||
<div class="col-md-6">
|
||
<h6 class="text-danger mb-3"><i class="bi bi-dash-circle"></i> Списание</h6>
|
||
<form method="post" action="{% url 'customers:wallet-withdraw' customer.pk %}">
|
||
{% csrf_token %}
|
||
<div class="mb-3">
|
||
<label for="wallet_withdraw_amount" class="form-label">Сумма, руб.</label>
|
||
<input type="number"
|
||
step="0.01"
|
||
min="0.01"
|
||
max="{{ customer.wallet_balance }}"
|
||
class="form-control"
|
||
id="wallet_withdraw_amount"
|
||
name="amount"
|
||
placeholder="0.00"
|
||
required>
|
||
<small class="text-muted d-block" style="height: 1.25rem; line-height: 1.25rem;">Макс: {{ customer.wallet_balance|floatformat:2 }} р.</small>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="wallet_withdraw_description" class="form-label">Описание</label>
|
||
<textarea class="form-control"
|
||
id="wallet_withdraw_description"
|
||
name="description"
|
||
rows="2"
|
||
placeholder="Возврат наличными..."
|
||
required></textarea>
|
||
</div>
|
||
<button type="submit" class="btn btn-danger w-100"><i class="bi bi-dash-circle"></i> Списать</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
<div class="alert alert-info mb-0 mt-3">
|
||
<i class="bi bi-info-circle"></i> Все операции логируются в истории выше.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Конец правой колонки -->
|
||
|
||
<!-- Алерт о необходимости возврата -->
|
||
{% if refund_amount > 0 %}
|
||
<div class="col-md-12">
|
||
<div class="alert alert-warning shadow-sm 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.3em;">
|
||
<i class="bi bi-exclamation-circle"></i> {{ refund_amount|floatformat:2 }} руб.
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Модальное окно добавления канала связи -->
|
||
<div class="modal fade" id="addChannelModal" tabindex="-1" aria-labelledby="addChannelModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<form method="post" action="{% url 'customers:add-contact-channel' customer.pk %}">
|
||
{% csrf_token %}
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="addChannelModalLabel">Добавить канал связи</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="mb-3">
|
||
<label for="channel_type" class="form-label">Тип канала</label>
|
||
<select name="channel_type" id="channel_type" class="form-select" required>
|
||
<option value="telegram">Telegram</option>
|
||
<option value="instagram">Instagram</option>
|
||
<option value="whatsapp">WhatsApp</option>
|
||
<option value="viber">Viber</option>
|
||
<option value="vk">ВКонтакте</option>
|
||
<option value="facebook">Facebook</option>
|
||
<option value="phone">Телефон</option>
|
||
<option value="email">Email</option>
|
||
<option value="other">Другое</option>
|
||
</select>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="channel_value" class="form-label">Значение</label>
|
||
<input type="text" name="value" id="channel_value" class="form-control" placeholder="@username, номер, ссылка..." required>
|
||
<small class="text-muted">Например: @flower_lover, +375291234567, flower.shop</small>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="channel_notes" class="form-label">Примечание <span class="text-muted">(необязательно)</span></label>
|
||
<input type="text" name="notes" id="channel_notes" class="form-control" placeholder="Личный аккаунт, рабочий...">
|
||
</div>
|
||
<div class="form-check">
|
||
<input type="checkbox" name="is_primary" class="form-check-input" id="isPrimary" value="true">
|
||
<label class="form-check-label" for="isPrimary">Основной канал связи</label>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||
<button type="submit" class="btn btn-success"><i class="bi bi-plus"></i> Добавить</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// Автооткрытие collapse при наличии якоря в URL
|
||
const hash = window.location.hash;
|
||
if (hash === '#ordersHistoryCollapse') {
|
||
const collapseElement = document.getElementById('ordersHistoryCollapse');
|
||
if (collapseElement) {
|
||
const bsCollapse = new bootstrap.Collapse(collapseElement, {
|
||
show: true
|
||
});
|
||
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 %} |