Initial commit: Django inventory system
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Удалить категорию{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h4>Подтверждение удаления</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Вы уверены, что хотите удалить категорию <strong>"{{ category.name }}"</strong>?</p>
|
||||
|
||||
{% if products_count > 0 %}
|
||||
<div class="alert alert-danger">
|
||||
<strong>Внимание!</strong> В этой категории есть <strong>{{ products_count }}</strong> товар(ов).
|
||||
<br>Удаление невозможно. Сначала удалите или переместите товары.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if children_count > 0 %}
|
||||
<div class="alert alert-warning">
|
||||
<strong>Внимание!</strong> У этой категории есть <strong>{{ children_count }}</strong> подкатегорий.
|
||||
<br>Удаление невозможно. Сначала удалите или переместите подкатегории.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<a href="{% url 'products:category-detail' category.pk %}" class="btn btn-secondary">Отмена</a>
|
||||
{% if products_count == 0 and children_count == 0 %}
|
||||
<button type="submit" class="btn btn-danger">Да, удалить</button>
|
||||
{% else %}
|
||||
<a href="{% url 'products:category-detail' category.pk %}" class="btn btn-primary">Вернуться к категории</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
105
myproject/products/templates/products/category_detail.html
Normal file
105
myproject/products/templates/products/category_detail.html
Normal file
@@ -0,0 +1,105 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{{ category.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h3>{{ category.name }}</h3>
|
||||
<div>
|
||||
{% if category.is_active %}
|
||||
<span class="badge bg-success">Активна</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Неактивна</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4"><strong>Артикул:</strong></div>
|
||||
<div class="col-md-8"><code>{{ category.sku|default:"—" }}</code></div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4"><strong>URL-идентификатор:</strong></div>
|
||||
<div class="col-md-8"><code>{{ category.slug }}</code></div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4"><strong>Родительская категория:</strong></div>
|
||||
<div class="col-md-8">
|
||||
{% if category.parent %}
|
||||
<a href="{% url 'products:category-detail' category.parent.pk %}">{{ category.parent.name }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">Корневая категория</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Фотографии -->
|
||||
{% if category_photos %}
|
||||
<div class="mt-4">
|
||||
<h5>Фотографии ({{ photos_count }})</h5>
|
||||
<div class="row g-2">
|
||||
{% for photo in category_photos %}
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="card">
|
||||
<img src="{{ photo.image.url }}" class="card-img-top" alt="Фото категории" style="height: 150px; object-fit: cover;">
|
||||
{% if photo.order == 0 %}
|
||||
<div class="card-body p-1 text-center">
|
||||
<span class="badge bg-success">Главное</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Подкатегории -->
|
||||
{% if children_categories %}
|
||||
<div class="mt-4">
|
||||
<h5>Подкатегории ({{ children_categories.count }})</h5>
|
||||
<ul class="list-group">
|
||||
{% for child in children_categories %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<a href="{% url 'products:category-detail' child.pk %}">{{ child.name }}</a>
|
||||
<span class="badge bg-primary">{{ child.sku|default:"—" }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Товары в категории -->
|
||||
{% if products %}
|
||||
<div class="mt-4">
|
||||
<h5>Товары в категории ({{ products_count }})</h5>
|
||||
<div class="list-group">
|
||||
{% for product in products %}
|
||||
<a href="{% url 'products:product-detail' product.pk %}" class="list-group-item list-group-item-action">
|
||||
{{ product.name }} <span class="text-muted">({{ product.sku }})</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% if products_count > 20 %}
|
||||
<div class="list-group-item text-center text-muted">
|
||||
... и еще {{ products_count|add:"-20" }} товар(ов)
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'products:category-list' %}" class="btn btn-secondary">Назад к списку</a>
|
||||
<a href="{% url 'products:category-update' category.pk %}" class="btn btn-primary">Редактировать</a>
|
||||
<a href="{% url 'products:category-delete' category.pk %}" class="btn btn-danger">Удалить</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
191
myproject/products/templates/products/category_form.html
Normal file
191
myproject/products/templates/products/category_form.html
Normal file
@@ -0,0 +1,191 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{% if object %}Редактировать категорию{% else %}Создать категорию{% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{% for error in form.non_field_errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Блок 1: Основная информация -->
|
||||
<div class="mb-4">
|
||||
<!-- Название -->
|
||||
<div class="mb-3">
|
||||
<label for="id_name" class="form-label fw-bold fs-5">Название</label>
|
||||
{{ form.name }}
|
||||
{% if form.name.help_text %}
|
||||
<small class="form-text text-muted">{{ form.name.help_text }}</small>
|
||||
{% endif %}
|
||||
{% if form.name.errors %}
|
||||
<div class="text-danger">{{ form.name.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Артикул и Slug в один ряд -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
{{ form.sku.label_tag }}
|
||||
{{ form.sku }}
|
||||
{% if form.sku.help_text %}
|
||||
<small class="form-text text-muted">{{ form.sku.help_text }}</small>
|
||||
{% endif %}
|
||||
{% if form.sku.errors %}
|
||||
<div class="text-danger">{{ form.sku.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{{ form.slug.label_tag }}
|
||||
{{ form.slug }}
|
||||
{% if form.slug.help_text %}
|
||||
<small class="form-text text-muted">{{ form.slug.help_text }}</small>
|
||||
{% endif %}
|
||||
{% if form.slug.errors %}
|
||||
<div class="text-danger">{{ form.slug.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Родитель и Статус в один ряд -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-8">
|
||||
{{ form.parent.label_tag }}
|
||||
{{ form.parent }}
|
||||
{% if form.parent.help_text %}
|
||||
<small class="form-text text-muted">{{ form.parent.help_text }}</small>
|
||||
{% endif %}
|
||||
{% if form.parent.errors %}
|
||||
<div class="text-danger">{{ form.parent.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-check mt-4">
|
||||
{{ form.is_active }}
|
||||
{{ form.is_active.label_tag }}
|
||||
</div>
|
||||
{% if form.is_active.help_text %}
|
||||
<small class="form-text text-muted">{{ form.is_active.help_text }}</small>
|
||||
{% endif %}
|
||||
{% if form.is_active.errors %}
|
||||
<div class="text-danger">{{ form.is_active.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<!-- Блок 2: Фотографии -->
|
||||
<div class="mb-4 p-3 bg-light rounded">
|
||||
<h5 class="mb-3">Фотографии</h5>
|
||||
|
||||
<!-- Существующие фотографии (только при редактировании) -->
|
||||
{% if object and category_photos %}
|
||||
<div class="mb-3">
|
||||
<h6 class="mb-3">Текущие фотографии ({{ photos_count }})</h6>
|
||||
<div class="row g-2 mb-3">
|
||||
{% for photo in category_photos %}
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div style="width: 100%; height: 150px; display: flex; align-items: center; justify-content: center; background-color: #f8f9fa; cursor: pointer; overflow: hidden;"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#photoModal{{ photo.pk }}"
|
||||
title="Нажмите для увеличения">
|
||||
<img src="{{ photo.image.url }}"
|
||||
alt="Фото категории"
|
||||
style="max-width: 100%; max-height: 100%; object-fit: contain;">
|
||||
</div>
|
||||
<div class="card-body p-2">
|
||||
{% if photo.order == 0 %}
|
||||
<div class="badge bg-success w-100 mb-1">Главное</div>
|
||||
{% else %}
|
||||
<a href="{% url 'products:category-photo-set-main' photo.pk %}"
|
||||
class="btn btn-outline-primary btn-sm w-100 mb-1 py-0"
|
||||
title="Сделать главным">
|
||||
Главным
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<div class="btn-group w-100 mb-1" role="group">
|
||||
<a href="{% url 'products:category-photo-move-up' photo.pk %}"
|
||||
class="btn btn-outline-secondary btn-sm py-0">
|
||||
↑
|
||||
</a>
|
||||
<a href="{% url 'products:category-photo-move-down' photo.pk %}"
|
||||
class="btn btn-outline-secondary btn-sm py-0">
|
||||
↓
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a href="{% url 'products:category-photo-delete' photo.pk %}"
|
||||
class="btn btn-danger btn-sm w-100 py-0"
|
||||
onclick="return confirm('Удалить это фото?');">
|
||||
Удалить
|
||||
</a>
|
||||
|
||||
<small class="text-muted d-block mt-1 text-center">Позиция: {{ photo.order }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для просмотра фото -->
|
||||
<div class="modal fade" id="photoModal{{ photo.pk }}" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Фото категории</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<img src="{{ photo.image.url }}" class="img-fluid" alt="Фото категории" style="max-height: 70vh;">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||||
<a href="{% url 'products:category-photo-delete' photo.pk %}"
|
||||
class="btn btn-danger"
|
||||
onclick="return confirm('Удалить это фото?');">
|
||||
Удалить фото
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Поле для загрузки новых фотографий -->
|
||||
<div class="mb-0">
|
||||
<label for="id_photos" class="form-label fw-bold">
|
||||
{% if object %}Добавить новые фото{% else %}Загрузить фото{% endif %}
|
||||
</label>
|
||||
<input type="file" name="photos" accept="image/*" multiple class="form-control" id="id_photos">
|
||||
<small class="form-text text-muted">
|
||||
Выберите фото для категории (можно выбрать несколько, до 10 штук)
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4 gap-2 flex-wrap">
|
||||
<a href="{% url 'products:category-list' %}" class="btn btn-secondary">Отмена</a>
|
||||
<button type="submit" class="btn btn-primary">{% if object %}Сохранить изменения{% else %}Создать категорию{% endif %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
214
myproject/products/templates/products/category_list.html
Normal file
214
myproject/products/templates/products/category_list.html
Normal file
@@ -0,0 +1,214 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Список категорий{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<h2 class="mb-4">Категории товаров</h2>
|
||||
|
||||
<!-- Панель фильтрации -->
|
||||
{% include 'components/filter_panel.html' with title="Категории" filters=filters action_buttons=action_buttons %}
|
||||
|
||||
<!-- Кнопка "Развернуть/Свернуть все" -->
|
||||
<div class="mb-3">
|
||||
<button id="expandAllBtn" class="btn btn-sm btn-outline-secondary" onclick="expandAll()">
|
||||
Развернуть все
|
||||
</button>
|
||||
<button id="collapseAllBtn" class="btn btn-sm btn-outline-secondary" onclick="collapseAll()">
|
||||
Свернуть все
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{% if category_tree %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Название</th>
|
||||
<th>Артикул</th>
|
||||
<th>Цена</th>
|
||||
<th>Статус</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in category_tree %}
|
||||
<tr data-category-id="{{ item.pk }}"
|
||||
data-item-type="{{ item.item_type }}"
|
||||
data-parent-id="{{ item.parent_id|default:'null' }}"
|
||||
data-depth="{{ item.depth }}"
|
||||
{% if item.depth > 0 %}style="display: none;"{% endif %}
|
||||
class="category-row">
|
||||
|
||||
<!-- Колонка "Название" -->
|
||||
<td style="padding-left: calc(20px * {{ item.depth }} + 12px);">
|
||||
{% if item.item_type == 'category' %}
|
||||
{% if item.has_children %}
|
||||
<button class="btn btn-sm btn-outline-secondary p-0 toggle-btn"
|
||||
data-target="{{ item.pk }}"
|
||||
onclick="toggleCategory({{ item.pk }}); return false;"
|
||||
style="width: 24px; height: 24px; text-align: center; display: inline-flex; align-items: center; justify-content: center; margin-right: 8px; border-radius: 4px; font-weight: bold; font-size: 16px; line-height: 1;">
|
||||
+
|
||||
</button>
|
||||
{% else %}
|
||||
<span style="display: inline-block; width: 24px; margin-right: 8px;"></span>
|
||||
{% endif %}
|
||||
<a href="{% url 'products:category-detail' item.pk %}"
|
||||
style="font-weight: 600; color: #212529;">{{ item.name }}</a>
|
||||
{% if item.has_children %}
|
||||
<span class="badge bg-info text-dark ms-1">{{ item.obj.children.count }} подкат.</span>
|
||||
{% endif %}
|
||||
|
||||
{% elif item.item_type == 'product' %}
|
||||
<span style="display: inline-block; width: 24px; margin-right: 8px;"></span>
|
||||
<a href="{% url 'products:product-detail' item.pk %}"
|
||||
style="color: #6c757d;">{{ item.name }}</a>
|
||||
|
||||
{% elif item.item_type == 'kit' %}
|
||||
<span style="display: inline-block; width: 24px; margin-right: 8px;"></span>
|
||||
<a href="{% url 'products:kit-detail' item.pk %}"
|
||||
style="color: #6c757d;">{{ item.name }}</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<!-- Колонка "Артикул" -->
|
||||
<td>
|
||||
{% if item.sku %}
|
||||
<code>{{ item.sku }}</code>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<!-- Колонка "Цена" -->
|
||||
<td>
|
||||
{% if item.price %}
|
||||
{{ item.price|floatformat:0 }} ₽
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<!-- Колонка "Статус" (только для категорий) -->
|
||||
<td>
|
||||
{% if item.item_type == 'category' %}
|
||||
{% if item.obj.is_active %}
|
||||
<span class="badge bg-success">Активна</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Неактивна</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<!-- Колонка "Действия" -->
|
||||
<td>
|
||||
{% if item.item_type == 'category' %}
|
||||
<a href="{% url 'products:category-update' item.pk %}" class="btn btn-sm btn-outline-primary">Изменить</a>
|
||||
<a href="{% url 'products:category-delete' item.pk %}" class="btn btn-sm btn-outline-danger">Удалить</a>
|
||||
{% elif item.item_type == 'product' %}
|
||||
<a href="{% url 'products:product-update' item.pk %}" class="btn btn-sm btn-outline-primary">Изменить</a>
|
||||
{% elif item.item_type == 'kit' %}
|
||||
<a href="{% url 'products:kit-update' item.pk %}" class="btn btn-sm btn-outline-primary">Изменить</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<p>Категории не найдены.</p>
|
||||
<a href="{% url 'products:category-create' %}" class="btn btn-primary">Создать первую категорию</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Функция для раскрытия/сворачивания категории
|
||||
function toggleCategory(categoryId) {
|
||||
// Находим все прямые дочерние элементы
|
||||
const children = document.querySelectorAll(`tr[data-parent-id="${categoryId}"]`);
|
||||
const toggleBtn = document.querySelector(`button[data-target="${categoryId}"]`);
|
||||
|
||||
if (!toggleBtn) return;
|
||||
|
||||
const isExpanded = toggleBtn.textContent.trim() === '-';
|
||||
|
||||
children.forEach(child => {
|
||||
const childId = child.getAttribute('data-category-id');
|
||||
const childToggleBtn = child.querySelector(`button[data-target="${childId}"]`);
|
||||
|
||||
if (isExpanded) {
|
||||
// Сворачиваем
|
||||
child.style.display = 'none';
|
||||
// Рекурсивно сворачиваем всех потомков
|
||||
collapseAllChildren(childId);
|
||||
if (childToggleBtn) {
|
||||
childToggleBtn.textContent = '+';
|
||||
}
|
||||
} else {
|
||||
// Раскрываем только прямых детей
|
||||
child.style.display = 'table-row';
|
||||
}
|
||||
});
|
||||
|
||||
// Переключаем кнопку
|
||||
toggleBtn.textContent = isExpanded ? '+' : '-';
|
||||
}
|
||||
|
||||
// Рекурсивно сворачивает всех потомков
|
||||
function collapseAllChildren(categoryId) {
|
||||
const children = document.querySelectorAll(`tr[data-parent-id="${categoryId}"]`);
|
||||
children.forEach(child => {
|
||||
const childId = child.getAttribute('data-category-id');
|
||||
child.style.display = 'none';
|
||||
const childToggleBtn = child.querySelector(`button[data-target="${childId}"]`);
|
||||
if (childToggleBtn) {
|
||||
childToggleBtn.textContent = '+';
|
||||
}
|
||||
collapseAllChildren(childId);
|
||||
});
|
||||
}
|
||||
|
||||
// Развернуть все категории
|
||||
function expandAll() {
|
||||
const allRows = document.querySelectorAll('.category-row');
|
||||
const allToggleBtns = document.querySelectorAll('.toggle-btn');
|
||||
|
||||
allRows.forEach(row => {
|
||||
row.style.display = 'table-row';
|
||||
});
|
||||
|
||||
allToggleBtns.forEach(btn => {
|
||||
btn.textContent = '-';
|
||||
});
|
||||
}
|
||||
|
||||
// Свернуть все категории (показать только корневые)
|
||||
function collapseAll() {
|
||||
const allRows = document.querySelectorAll('.category-row');
|
||||
const allToggleBtns = document.querySelectorAll('.toggle-btn');
|
||||
|
||||
allRows.forEach(row => {
|
||||
const depth = parseInt(row.getAttribute('data-depth'));
|
||||
if (depth > 0) {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
allToggleBtns.forEach(btn => {
|
||||
btn.textContent = '+';
|
||||
});
|
||||
}
|
||||
|
||||
// При поиске автоматически раскрываем все категории
|
||||
{% if has_search %}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
expandAll();
|
||||
});
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,30 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Удалить товар{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h3>Подтверждение удаления</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Вы уверены, что хотите удалить товар <strong>"{{ product.name }}"</strong>?</p>
|
||||
<p class="text-muted">Артикул: {{ product.sku }}</p>
|
||||
<p class="text-danger">Внимание! Это действие нельзя будет отменить.</p>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'products:product-list' %}" class="btn btn-secondary">Отмена</a>
|
||||
<button type="submit" class="btn btn-danger">Удалить товар</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
261
myproject/products/templates/products/product_detail.html
Normal file
261
myproject/products/templates/products/product_detail.html
Normal file
@@ -0,0 +1,261 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{{ product.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h3>{{ product.name }}</h3>
|
||||
<div>
|
||||
{% if perms.products.change_product %}
|
||||
<a href="{% url 'products:product-update' product.pk %}" class="btn btn-outline-primary btn-sm">Редактировать</a>
|
||||
{% endif %}
|
||||
{% if perms.products.delete_product %}
|
||||
<a href="{% url 'products:product-delete' product.pk %}" class="btn btn-outline-danger btn-sm">Удалить</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Секция фотографий в начале -->
|
||||
{% if product_photos %}
|
||||
<div class="mb-4">
|
||||
<h5 class="mb-3">Фотографии товара ({{ photos_count }})</h5>
|
||||
<div class="row g-2">
|
||||
{% for photo in product_photos %}
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<!-- Кликабельное фото для открытия галереи -->
|
||||
<div style="width: 100%; height: 150px; display: flex; align-items: center; justify-content: center; background-color: #f8f9fa; cursor: pointer; overflow: hidden;"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#photoGalleryModal"
|
||||
data-bs-slide-to="{{ forloop.counter0 }}"
|
||||
title="Нажмите для увеличения">
|
||||
<img src="{{ photo.image.url }}"
|
||||
alt="Фото товара"
|
||||
style="max-width: 100%; max-height: 100%; object-fit: contain;">
|
||||
</div>
|
||||
<div class="card-body p-2 text-center">
|
||||
{% if photo.order == 0 %}
|
||||
<div class="badge bg-success w-100">Главное</div>
|
||||
{% else %}
|
||||
<small class="text-muted">Позиция: {{ photo.order }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно с галереей (Bootstrap Carousel) -->
|
||||
<div class="modal fade" id="photoGalleryModal" tabindex="-1" aria-labelledby="photoGalleryModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="photoGalleryModalLabel">Галерея фотографий товара</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="photoCarousel" class="carousel slide" data-bs-ride="false">
|
||||
<div class="carousel-inner">
|
||||
{% for photo in product_photos %}
|
||||
<div class="carousel-item {% if forloop.first %}active{% endif %}">
|
||||
<div class="text-center" style="min-height: 60vh; display: flex; align-items: center; justify-content: center; background-color: #f8f9fa;">
|
||||
<img src="{{ photo.image.url }}" class="d-block" alt="Фото товара" style="max-height: 70vh; max-width: 100%; object-fit: contain;">
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Навигация и индикаторы под фото -->
|
||||
{% if photos_count > 1 %}
|
||||
<div class="d-flex justify-content-center align-items-center mt-3 gap-3">
|
||||
<button class="btn btn-outline-secondary" type="button" data-bs-target="#photoCarousel" data-bs-slide="prev">
|
||||
<i class="bi bi-chevron-left"></i> Предыдущее
|
||||
</button>
|
||||
|
||||
<div class="carousel-indicators position-static m-0">
|
||||
{% for photo in product_photos %}
|
||||
<button type="button" data-bs-target="#photoCarousel" data-bs-slide-to="{{ forloop.counter0 }}"
|
||||
{% if forloop.first %}class="active" aria-current="true"{% endif %}
|
||||
aria-label="Слайд {{ forloop.counter }}"
|
||||
style="background-color: #6c757d;"></button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-secondary" type="button" data-bs-target="#photoCarousel" data-bs-slide="next">
|
||||
Следующее <i class="bi bi-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-2">
|
||||
<small class="text-muted">
|
||||
<span id="currentSlide">1</span> из {{ photos_count }}
|
||||
<span id="mainBadge" {% if not product_photos.0.order == 0 %}style="display: none;"{% endif %} class="badge bg-success ms-2">Главное</span>
|
||||
</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
{% endif %}
|
||||
|
||||
<!-- Основная информация о товаре -->
|
||||
<h5 class="mb-3">Основная информация</h5>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Артикул:</th>
|
||||
<td>{{ product.sku }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Описание:</th>
|
||||
<td>{{ product.description|default:"-" }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Категории:</th>
|
||||
<td>
|
||||
{% if product.categories.all %}
|
||||
{% for category in product.categories.all %}
|
||||
<span class="badge bg-primary">{{ category.name }}</span>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Теги:</th>
|
||||
<td>
|
||||
{% if product.tags.all %}
|
||||
{% for tag in product.tags.all %}
|
||||
<span class="badge bg-secondary">{{ tag.name }}</span>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Единица измерения:</th>
|
||||
<td>{{ product.unit }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Себестоимость:</th>
|
||||
<td>{{ product.cost_price }} руб.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Цена продажи:</th>
|
||||
<td>{{ product.sale_price }} руб.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Статус:</th>
|
||||
<td>
|
||||
{% if product.is_active %}
|
||||
<span class="badge bg-success">Активен</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Неактивен</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Дата создания:</th>
|
||||
<td>{{ product.created_at }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Дата обновления:</th>
|
||||
<td>{{ product.updated_at }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4>Действия</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<a href="{% url 'products:product-list' %}" class="btn btn-secondary btn-block">Назад к списку</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Обработка галереи фотографий
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const photoGalleryModal = document.getElementById('photoGalleryModal');
|
||||
const photoCarousel = document.getElementById('photoCarousel');
|
||||
|
||||
if (photoGalleryModal && photoCarousel) {
|
||||
const carousel = bootstrap.Carousel.getOrCreateInstance(photoCarousel);
|
||||
const currentSlideEl = document.getElementById('currentSlide');
|
||||
const mainBadgeEl = document.getElementById('mainBadge');
|
||||
|
||||
// Массив с информацией о фотографиях
|
||||
const photos = [
|
||||
{% for photo in product_photos %}
|
||||
{ order: {{ photo.order }}, index: {{ forloop.counter0 }} }{% if not forloop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
|
||||
// Обработка клика на миниатюру для открытия галереи на нужном слайде
|
||||
photoGalleryModal.addEventListener('show.bs.modal', function (event) {
|
||||
const button = event.relatedTarget;
|
||||
const slideIndex = button.getAttribute('data-bs-slide-to');
|
||||
if (slideIndex !== null) {
|
||||
carousel.to(parseInt(slideIndex));
|
||||
}
|
||||
});
|
||||
|
||||
// Обновление счетчика и бейджа при переключении слайдов
|
||||
photoCarousel.addEventListener('slid.bs.carousel', function (event) {
|
||||
const activeIndex = event.to;
|
||||
if (currentSlideEl) {
|
||||
currentSlideEl.textContent = activeIndex + 1;
|
||||
}
|
||||
if (mainBadgeEl && photos[activeIndex]) {
|
||||
mainBadgeEl.style.display = photos[activeIndex].order === 0 ? 'inline' : 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Обработка нажатий клавиш-стрелок для навигации
|
||||
const handleKeydown = function(event) {
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
carousel.prev();
|
||||
} else if (event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
carousel.next();
|
||||
}
|
||||
};
|
||||
|
||||
// Добавляем обработчик клавиш при открытии модального окна
|
||||
photoGalleryModal.addEventListener('shown.bs.modal', function () {
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
});
|
||||
|
||||
// Удаляем обработчик клавиш при закрытии модального окна
|
||||
photoGalleryModal.addEventListener('hidden.bs.modal', function () {
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
270
myproject/products/templates/products/product_form.html
Normal file
270
myproject/products/templates/products/product_form.html
Normal file
@@ -0,0 +1,270 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{% if object %}Редактировать товар{% else %}Создать товар{% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{% for error in form.non_field_errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Блок 1: Основная информация -->
|
||||
<div class="mb-4">
|
||||
<!-- Название -->
|
||||
<div class="mb-3">
|
||||
<label for="id_name" class="form-label fw-bold fs-5">Название</label>
|
||||
{{ form.name }}
|
||||
{% if form.name.help_text %}
|
||||
<small class="form-text text-muted">{{ form.name.help_text }}</small>
|
||||
{% endif %}
|
||||
{% if form.name.errors %}
|
||||
<div class="text-danger">{{ form.name.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Артикул -->
|
||||
<div class="mb-3">
|
||||
{{ form.sku.label_tag }}
|
||||
{{ form.sku }}
|
||||
{% if form.sku.help_text %}
|
||||
<small class="form-text text-muted">{{ form.sku.help_text }}</small>
|
||||
{% endif %}
|
||||
{% if form.sku.errors %}
|
||||
<div class="text-danger">{{ form.sku.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Описание -->
|
||||
<div class="mb-3">
|
||||
{{ form.description.label_tag }}
|
||||
{{ form.description }}
|
||||
{% if form.description.help_text %}
|
||||
<small class="form-text text-muted">{{ form.description.help_text }}</small>
|
||||
{% endif %}
|
||||
{% if form.description.errors %}
|
||||
<div class="text-danger">{{ form.description.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Единица измерения и Статус в один ряд -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-8">
|
||||
{{ form.unit.label_tag }}
|
||||
{{ form.unit }}
|
||||
{% if form.unit.help_text %}
|
||||
<small class="form-text text-muted">{{ form.unit.help_text }}</small>
|
||||
{% endif %}
|
||||
{% if form.unit.errors %}
|
||||
<div class="text-danger">{{ form.unit.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-check mt-4">
|
||||
{{ form.is_active }}
|
||||
{{ form.is_active.label_tag }}
|
||||
</div>
|
||||
{% if form.is_active.help_text %}
|
||||
<small class="form-text text-muted">{{ form.is_active.help_text }}</small>
|
||||
{% endif %}
|
||||
{% if form.is_active.errors %}
|
||||
<div class="text-danger">{{ form.is_active.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<!-- Блок 2: Ценообразование -->
|
||||
<div class="mb-4">
|
||||
<h5 class="mb-3">Ценообразование</h5>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
{{ form.cost_price.label_tag }}
|
||||
{{ form.cost_price }}
|
||||
{% if form.cost_price.help_text %}
|
||||
<small class="form-text text-muted">{{ form.cost_price.help_text }}</small>
|
||||
{% endif %}
|
||||
{% if form.cost_price.errors %}
|
||||
<div class="text-danger">{{ form.cost_price.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{{ form.sale_price.label_tag }}
|
||||
{{ form.sale_price }}
|
||||
{% if form.sale_price.help_text %}
|
||||
<small class="form-text text-muted">{{ form.sale_price.help_text }}</small>
|
||||
{% endif %}
|
||||
{% if form.sale_price.errors %}
|
||||
<div class="text-danger">{{ form.sale_price.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<!-- Блок 3: Фотографии -->
|
||||
<div class="mb-4 p-3 bg-light rounded">
|
||||
<h5 class="mb-3">Фотографии</h5>
|
||||
|
||||
<!-- Существующие фотографии (только при редактировании) -->
|
||||
{% if object and product_photos %}
|
||||
<div class="mb-3">
|
||||
<h6 class="mb-3">Текущие фотографии ({{ photos_count }})</h6>
|
||||
<div class="row g-2 mb-3">
|
||||
{% for photo in product_photos %}
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<!-- Кликабельное фото для открытия модального окна -->
|
||||
<div style="width: 100%; height: 150px; display: flex; align-items: center; justify-content: center; background-color: #f8f9fa; cursor: pointer; overflow: hidden;"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#photoModal{{ photo.pk }}"
|
||||
title="Нажмите для увеличения">
|
||||
<img src="{{ photo.image.url }}"
|
||||
alt="Фото товара"
|
||||
style="max-width: 100%; max-height: 100%; object-fit: contain;">
|
||||
</div>
|
||||
<div class="card-body p-2">
|
||||
{% if photo.order == 0 %}
|
||||
<div class="badge bg-success w-100 mb-1">⭐ Главное</div>
|
||||
{% else %}
|
||||
<a href="{% url 'products:product-photo-set-main' photo.pk %}"
|
||||
class="btn btn-outline-primary btn-sm w-100 mb-1 py-0"
|
||||
title="Сделать главным">
|
||||
⭐ Главным
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<div class="btn-group w-100 mb-1" role="group">
|
||||
<a href="{% url 'products:product-photo-move-up' photo.pk %}"
|
||||
class="btn btn-outline-secondary btn-sm py-0"
|
||||
title="Переместить вверх">
|
||||
⬆️
|
||||
</a>
|
||||
<a href="{% url 'products:product-photo-move-down' photo.pk %}"
|
||||
class="btn btn-outline-secondary btn-sm py-0"
|
||||
title="Переместить вниз">
|
||||
⬇️
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a href="{% url 'products:product-photo-delete' photo.pk %}"
|
||||
class="btn btn-danger btn-sm w-100 py-0"
|
||||
onclick="return confirm('Удалить это фото?');">
|
||||
🗑️ Удалить
|
||||
</a>
|
||||
|
||||
<small class="text-muted d-block mt-1 text-center">Позиция: {{ photo.order }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для просмотра фото -->
|
||||
<div class="modal fade" id="photoModal{{ photo.pk }}" tabindex="-1" aria-labelledby="photoModalLabel{{ photo.pk }}" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="photoModalLabel{{ photo.pk }}">Фото товара</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<img src="{{ photo.image.url }}" class="img-fluid" alt="Фото товара" style="max-height: 70vh;">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||||
<a href="{% url 'products:product-photo-delete' photo.pk %}"
|
||||
class="btn btn-danger"
|
||||
onclick="return confirm('Удалить это фото?');">
|
||||
🗑️ Удалить фото
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Поле для загрузки новых фотографий -->
|
||||
<div class="mb-0">
|
||||
<label for="id_photos" class="form-label fw-bold">
|
||||
{% if object %}Добавить новые фото{% else %}Загрузить фото{% endif %}
|
||||
</label>
|
||||
<input type="file" name="photos" accept="image/*" multiple class="form-control" id="id_photos">
|
||||
<small class="form-text text-muted">
|
||||
{% if object %}
|
||||
Выберите фото для добавления к товару (можно выбрать несколько, до 10 штук всего)
|
||||
{% else %}
|
||||
Выберите фото для товара (можно выбрать несколько, до 10 штук)
|
||||
{% endif %}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<!-- Блок 4: Классификация -->
|
||||
<div class="mb-4">
|
||||
<h5 class="mb-3">Классификация</h5>
|
||||
|
||||
<!-- Категории -->
|
||||
<div class="mb-3">
|
||||
{{ form.categories.label_tag }}
|
||||
<div class="p-3 bg-light rounded">
|
||||
{{ form.categories }}
|
||||
</div>
|
||||
{% if form.categories.help_text %}
|
||||
<small class="form-text text-muted">{{ form.categories.help_text }}</small>
|
||||
{% endif %}
|
||||
{% if form.categories.errors %}
|
||||
<div class="text-danger">{{ form.categories.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Теги -->
|
||||
<div class="mb-3">
|
||||
{{ form.tags.label_tag }}
|
||||
<div class="p-3 bg-light rounded">
|
||||
{{ form.tags }}
|
||||
</div>
|
||||
{% if form.tags.help_text %}
|
||||
<small class="form-text text-muted">{{ form.tags.help_text }}</small>
|
||||
{% endif %}
|
||||
{% if form.tags.errors %}
|
||||
<div class="text-danger">{{ form.tags.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4 gap-2 flex-wrap">
|
||||
<div>
|
||||
<a href="{% url 'products:product-list' %}" class="btn btn-secondary">Отмена</a>
|
||||
{% if perms.products.add_productkit %}
|
||||
<a href="{% url 'products:productkit-create' %}" class="btn btn-outline-primary">
|
||||
<i class="bi bi-box-seam"></i> Создать комплект
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">{% if object %}Сохранить изменения{% else %}Создать товар{% endif %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
110
myproject/products/templates/products/product_list.html
Normal file
110
myproject/products/templates/products/product_list.html
Normal file
@@ -0,0 +1,110 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Список товаров{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<h2 class="mb-4">Список товаров</h2>
|
||||
|
||||
<!-- Панель фильтрации -->
|
||||
{% include 'components/filter_panel.html' with title="Товары" filters=filters action_buttons=action_buttons %}
|
||||
|
||||
{% if products %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Фото</th>
|
||||
<th>Название</th>
|
||||
<th>Артикул</th>
|
||||
<th>Категория</th>
|
||||
<th>Цена продажи</th>
|
||||
<th>Статус</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for product in products %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if product.photos.all %}
|
||||
{% with photo=product.photos.first %}
|
||||
<img src="{{ photo.image.url }}" alt="{{ product.name }}" style="max-width: 50px; max-height: 50px;" class="img-thumbnail">
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<span class="text-muted">Нет фото</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'products:product-detail' product.pk %}">{{ product.name }}</a>
|
||||
</td>
|
||||
<td>{{ product.sku }}</td>
|
||||
<td>
|
||||
{% if product.categories.all %}
|
||||
{% for category in product.categories.all %}
|
||||
{{ category.name }}{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ product.sale_price }} руб.</td>
|
||||
<td>
|
||||
{% if product.is_active %}
|
||||
<span class="badge bg-success">Активен</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Неактивен</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if perms.products.change_product %}
|
||||
<a href="{% url 'products:product-update' product.pk %}" class="btn btn-sm btn-outline-primary">Изменить</a>
|
||||
{% endif %}
|
||||
{% if perms.products.delete_product %}
|
||||
<a href="{% url 'products:product-delete' product.pk %}" class="btn btn-sm btn-outline-danger">Удалить</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1">Первая</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">Предыдущая</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="page-item active">
|
||||
<span class="page-link">Страница {{ page_obj.number }} из {{ page_obj.paginator.num_pages }}</span>
|
||||
</li>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}">Следующая</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">Последняя</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<p>Товары не найдены.</p>
|
||||
{% if perms.products.add_product %}
|
||||
<a href="{% url 'products:product-create' %}" class="btn btn-primary">Создать первый товар</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,81 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Удалить комплект - {{ kit.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h4 class="mb-0"><i class="bi bi-exclamation-triangle"></i> Подтверждение удаления</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="lead">Вы действительно хотите удалить комплект?</p>
|
||||
|
||||
<div class="alert alert-warning mb-3">
|
||||
<strong>{{ kit.name }}</strong>
|
||||
<br>
|
||||
<small class="text-muted">Артикул: {{ kit.sku }}</small>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger mb-4">
|
||||
<i class="bi bi-exclamation-circle"></i>
|
||||
<strong>Внимание!</strong> Это действие нельзя отменить. Комплект будет удален с все его компоненты и фотографии.
|
||||
</div>
|
||||
|
||||
<form method="post" class="d-flex gap-2">
|
||||
{% csrf_token %}
|
||||
<a href="{% url 'products:productkit-detail' kit.pk %}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Отмена
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="bi bi-trash"></i> Удалить комплект
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Информация о комплекте -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Информация о комплекте</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-5">Название:</dt>
|
||||
<dd class="col-sm-7">{{ kit.name }}</dd>
|
||||
|
||||
<dt class="col-sm-5">Артикул:</dt>
|
||||
<dd class="col-sm-7">{{ kit.sku }}</dd>
|
||||
|
||||
{% if kit.categories.all %}
|
||||
<dt class="col-sm-5">Категории:</dt>
|
||||
<dd class="col-sm-7">
|
||||
{% for category in kit.categories.all %}
|
||||
{{ category.name }}{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
|
||||
<dt class="col-sm-5">Компонентов:</dt>
|
||||
<dd class="col-sm-7">{{ kit.get_total_components_count }}</dd>
|
||||
|
||||
<dt class="col-sm-5">Статус:</dt>
|
||||
<dd class="col-sm-7">
|
||||
{% if kit.is_active %}
|
||||
<span class="badge bg-success">Активен</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Неактивен</span>
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-5">Создан:</dt>
|
||||
<dd class="col-sm-7">{{ kit.created_at|date:"d.m.Y" }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
239
myproject/products/templates/products/productkit_detail.html
Normal file
239
myproject/products/templates/products/productkit_detail.html
Normal file
@@ -0,0 +1,239 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{{ kit.name }} - Комплект{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8">
|
||||
<h2>{{ kit.name }}</h2>
|
||||
<p class="text-muted">Артикул: <strong>{{ kit.sku }}</strong></p>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
{% if perms.products.change_productkit %}
|
||||
<a href="{% url 'products:productkit-update' kit.pk %}" class="btn btn-primary">
|
||||
<i class="bi bi-pencil"></i> Редактировать
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.products.delete_productkit %}
|
||||
<a href="{% url 'products:productkit-delete' kit.pk %}" class="btn btn-danger">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Основная информация -->
|
||||
<div class="col-md-8">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Основная информация</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4">Название:</dt>
|
||||
<dd class="col-sm-8">{{ kit.name }}</dd>
|
||||
|
||||
<dt class="col-sm-4">Артикул:</dt>
|
||||
<dd class="col-sm-8">{{ kit.sku }}</dd>
|
||||
|
||||
{% if kit.categories.all %}
|
||||
<dt class="col-sm-4">Категории:</dt>
|
||||
<dd class="col-sm-8">
|
||||
{% for category in kit.categories.all %}
|
||||
<span class="badge bg-primary">{{ category.name }}</span>
|
||||
{% endfor %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
|
||||
<dt class="col-sm-4">Цена продажи:</dt>
|
||||
<dd class="col-sm-8">
|
||||
<strong class="text-success fs-5">{{ kit.get_sale_price|floatformat:2 }} ₽</strong>
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-4">Ценообразование:</dt>
|
||||
<dd class="col-sm-8">
|
||||
<span class="badge bg-info text-dark">{{ kit.get_pricing_method_display }}</span>
|
||||
</dd>
|
||||
|
||||
{% if kit.fixed_price %}
|
||||
<dt class="col-sm-4">Фиксированная цена:</dt>
|
||||
<dd class="col-sm-8">{{ kit.fixed_price }} ₽</dd>
|
||||
{% endif %}
|
||||
|
||||
{% if kit.markup_percent %}
|
||||
<dt class="col-sm-4">Процент наценки:</dt>
|
||||
<dd class="col-sm-8">{{ kit.markup_percent }}%</dd>
|
||||
{% endif %}
|
||||
|
||||
{% if kit.markup_amount %}
|
||||
<dt class="col-sm-4">Фиксированная наценка:</dt>
|
||||
<dd class="col-sm-8">{{ kit.markup_amount }} ₽</dd>
|
||||
{% endif %}
|
||||
|
||||
<dt class="col-sm-4">Статус:</dt>
|
||||
<dd class="col-sm-8">
|
||||
{% if kit.is_active %}
|
||||
<span class="badge bg-success">Активен</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Неактивен</span>
|
||||
{% endif %}
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
{% if kit.description %}
|
||||
<div class="mt-3">
|
||||
<h6>Описание:</h6>
|
||||
<p>{{ kit.description|linebreaks }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if kit.tags.all %}
|
||||
<div class="mt-3">
|
||||
<h6>Теги:</h6>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% for tag in kit.tags.all %}
|
||||
<span class="badge bg-secondary">{{ tag.name }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Компоненты комплекта -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Состав комплекта ({{ kit_items.count }} компонентов)</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if kit_items %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Компонент</th>
|
||||
<th>Тип</th>
|
||||
<th>Количество</th>
|
||||
<th>Примечание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in kit_items %}
|
||||
<tr>
|
||||
<td>{{ forloop.counter }}</td>
|
||||
<td>
|
||||
{% if item.product %}
|
||||
<a href="{% url 'products:product-detail' item.product.pk %}">
|
||||
{{ item.product.name }}
|
||||
</a>
|
||||
<br>
|
||||
<small class="text-muted">Артикул: {{ item.product.sku }}</small>
|
||||
{% else %}
|
||||
<span class="badge bg-primary">{{ item.variant_group.name }}</span>
|
||||
<br>
|
||||
<small class="text-muted">Варианты: {{ item.variant_group.get_products_count }} товаров</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.product %}
|
||||
<span class="badge bg-success">Товар</span>
|
||||
{% else %}
|
||||
<span class="badge bg-primary">Варианты</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.quantity }}</td>
|
||||
<td>
|
||||
{% if item.notes %}
|
||||
{{ item.notes }}
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted mb-0">Нет компонентов в этом комплекте</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Сбоку: фотографии и информация -->
|
||||
<div class="col-md-4">
|
||||
<!-- Фотографии -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Фотографии ({{ photos_count }})</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if productkit_photos %}
|
||||
<div class="row g-2">
|
||||
{% for photo in productkit_photos %}
|
||||
<div class="col-6">
|
||||
<div class="card">
|
||||
<img src="{{ photo.image.url }}" class="card-img-top" alt="{{ kit.name }}"
|
||||
style="height: 120px; object-fit: cover; cursor: pointer;"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#photoModal{{ photo.pk }}">
|
||||
{% if photo.order == 0 %}
|
||||
<div class="card-footer bg-success text-white text-center small">⭐ Главное</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для просмотра -->
|
||||
<div class="modal fade" id="photoModal{{ photo.pk }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Фото комплекта</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<img src="{{ photo.image.url }}" class="img-fluid" style="max-height: 70vh;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted mb-0">Нет фотографий</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Метаинформация -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Информация</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-8 text-truncate">Создан:</dt>
|
||||
<dd class="col-sm-4">{{ kit.created_at|date:"d.m.Y H:i" }}</dd>
|
||||
|
||||
<dt class="col-sm-8 text-truncate">Обновлен:</dt>
|
||||
<dd class="col-sm-4">{{ kit.updated_at|date:"d.m.Y H:i" }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Кнопка назад -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<a href="{% url 'products:productkit-list' %}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left"></i> К списку комплектов
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
664
myproject/products/templates/products/productkit_form.html
Normal file
664
myproject/products/templates/products/productkit_form.html
Normal file
@@ -0,0 +1,664 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{% if object %}Редактировать комплект{% else %}Создать комплект{% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-10">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>{% if object %}Редактировать комплект{% else %}Создать комплект{% endif %}</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{% for error in form.non_field_errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Секция управления фотографиями -->
|
||||
{% if object %}
|
||||
<div class="mb-4 p-3 bg-light rounded">
|
||||
<h5 class="mb-3">Управление фотографиями</h5>
|
||||
|
||||
<!-- Существующие фотографии (только при редактировании) -->
|
||||
{% if productkit_photos %}
|
||||
<div class="mb-3">
|
||||
<h6 class="mb-3">Текущие фотографии ({{ photos_count }})</h6>
|
||||
<div class="row g-2 mb-3">
|
||||
{% for photo in productkit_photos %}
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<!-- Кликабельное фото для открытия модального окна -->
|
||||
<div style="width: 100%; height: 150px; display: flex; align-items: center; justify-content: center; background-color: #f8f9fa; cursor: pointer; overflow: hidden;"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#photoModal{{ photo.pk }}"
|
||||
title="Нажмите для увеличения">
|
||||
<img src="{{ photo.image.url }}"
|
||||
alt="Фото комплекта"
|
||||
style="max-width: 100%; max-height: 100%; object-fit: contain;">
|
||||
</div>
|
||||
<div class="card-body p-2">
|
||||
{% if photo.order == 0 %}
|
||||
<div class="badge bg-success w-100 mb-1">⭐ Главное</div>
|
||||
{% else %}
|
||||
<a href="{% url 'products:productkit-photo-set-main' photo.pk %}"
|
||||
class="btn btn-outline-primary btn-sm w-100 mb-1 py-0"
|
||||
title="Сделать главным">
|
||||
⭐ Главным
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<div class="btn-group w-100 mb-1" role="group">
|
||||
<a href="{% url 'products:productkit-photo-move-up' photo.pk %}"
|
||||
class="btn btn-outline-secondary btn-sm py-0"
|
||||
title="Переместить вверх">
|
||||
⬆️
|
||||
</a>
|
||||
<a href="{% url 'products:productkit-photo-move-down' photo.pk %}"
|
||||
class="btn btn-outline-secondary btn-sm py-0"
|
||||
title="Переместить вниз">
|
||||
⬇️
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a href="{% url 'products:productkit-photo-delete' photo.pk %}"
|
||||
class="btn btn-danger btn-sm w-100 py-0"
|
||||
onclick="return confirm('Удалить это фото?');">
|
||||
🗑️ Удалить
|
||||
</a>
|
||||
|
||||
<small class="text-muted d-block mt-1 text-center">Позиция: {{ photo.order }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для просмотра фото -->
|
||||
<div class="modal fade" id="photoModal{{ photo.pk }}" tabindex="-1" aria-labelledby="photoModalLabel{{ photo.pk }}" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="photoModalLabel{{ photo.pk }}">Фото комплекта</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<img src="{{ photo.image.url }}" class="img-fluid" alt="Фото комплекта" style="max-height: 70vh;">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||||
<a href="{% url 'products:productkit-photo-delete' photo.pk %}"
|
||||
class="btn btn-danger"
|
||||
onclick="return confirm('Удалить это фото?');">
|
||||
🗑️ Удалить фото
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Поле для загрузки новых фотографий -->
|
||||
<div class="mb-0">
|
||||
<label for="id_photos" class="form-label fw-bold">
|
||||
{% if object %}➕ Добавить новые фото{% else %}📷 Загрузить фото{% endif %}
|
||||
</label>
|
||||
<input type="file" name="photos" accept="image/*" multiple class="form-control" id="id_photos">
|
||||
<small class="form-text text-muted">
|
||||
{% if object %}
|
||||
Выберите фото для добавления к комплекту (можно выбрать несколько, до 10 штук всего)
|
||||
{% else %}
|
||||
Выберите фото для комплекта (можно выбрать несколько, до 10 штук)
|
||||
{% endif %}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
{% endif %}
|
||||
|
||||
<!-- Основная информация о комплекте -->
|
||||
<h5 class="mb-3">Основная информация</h5>
|
||||
|
||||
{% for field in form %}
|
||||
{% if field.name == 'fixed_price' %}
|
||||
<!-- Alert для неактивного поля fixed_price -->
|
||||
<div id="fixed-price-alert" class="alert alert-warning d-none mb-3" role="alert">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
<strong>Внимание!</strong> Поле "Фиксированная цена" неактивно при текущем методе ценообразования.
|
||||
Оно используется только когда выбран метод <strong>"Фиксированная цена"</strong>.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mb-3">
|
||||
{{ field.label_tag }}
|
||||
|
||||
{% if field.name == 'tags' %}
|
||||
<div class="form-control p-3 bg-light">
|
||||
{{ field }}
|
||||
</div>
|
||||
{% else %}
|
||||
{{ field }}
|
||||
{% endif %}
|
||||
|
||||
{% if field.help_text %}
|
||||
<small class="form-text text-muted">{{ field.help_text }}</small>
|
||||
{% endif %}
|
||||
|
||||
{% if field.errors %}
|
||||
<div class="text-danger">{{ field.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<!-- Компоненты комплекта (формсет) -->
|
||||
<h5 class="mb-3">Состав комплекта</h5>
|
||||
|
||||
<div class="alert alert-info mb-3">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
<strong>Подсказка:</strong> Добавьте компоненты комплекта ниже. Каждый компонент может быть либо конкретным товаром, либо группой вариантов (например, розы разных размеров).
|
||||
</div>
|
||||
|
||||
<!-- Поиск товаров для быстрого добавления (показываем только при редактировании) -->
|
||||
<div class="card mb-4 bg-light border-info" id="searchCard">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="bi bi-search"></i> Поиск товара для добавления
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="input-group mb-2">
|
||||
<input type="text"
|
||||
id="productSearch"
|
||||
class="form-control"
|
||||
placeholder="Введите название или артикул товара (минимум 2 символа)..."
|
||||
autocomplete="off">
|
||||
<button class="btn btn-info" type="button" id="searchBtn">
|
||||
<i class="bi bi-search"></i> Поиск
|
||||
</button>
|
||||
</div>
|
||||
<div id="searchResults" class="list-group" style="display: none; max-height: 300px; overflow-y: auto;"></div>
|
||||
<small class="text-muted">Используйте поиск для быстрого нахождения товара и добавления его в комплект</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Management form для формсета -->
|
||||
{{ kititem_formset.management_form }}
|
||||
|
||||
<!-- Ошибки формсета на уровне формсета -->
|
||||
{% if kititem_formset.non_form_errors %}
|
||||
<div class="alert alert-danger mb-3">
|
||||
{% for error in kititem_formset.non_form_errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Формы компонентов -->
|
||||
<div id="kititem-forms" class="mb-4">
|
||||
{% for kititem_form in kititem_formset %}
|
||||
<div class="card mb-3 kititem-form" data-form-index="{{ forloop.counter0 }}">
|
||||
<!-- Скрытые поля для inline formset -->
|
||||
{{ kititem_form.id }}
|
||||
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<span><strong>Компонент #{{ forloop.counter }}</strong></span>
|
||||
{% if kititem_form.DELETE %}
|
||||
<div class="form-check">
|
||||
{{ kititem_form.DELETE }}
|
||||
<label class="form-check-label" for="{{ kititem_form.DELETE.id_for_label }}">
|
||||
Удалить
|
||||
</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
{% if kititem_form.non_field_errors %}
|
||||
<div class="alert alert-danger">
|
||||
{% for error in kititem_form.non_field_errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ kititem_form.product.id_for_label }}" class="form-label">
|
||||
{{ kititem_form.product.label }}
|
||||
</label>
|
||||
{{ kititem_form.product }}
|
||||
{% if kititem_form.product.errors %}
|
||||
<div class="text-danger">{{ kititem_form.product.errors }}</div>
|
||||
{% endif %}
|
||||
<small class="form-text text-muted">Конкретный товар (если выбран, группа вариантов не нужна)</small>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ kititem_form.variant_group.id_for_label }}" class="form-label">
|
||||
{{ kititem_form.variant_group.label }}
|
||||
</label>
|
||||
{{ kititem_form.variant_group }}
|
||||
{% if kititem_form.variant_group.errors %}
|
||||
<div class="text-danger">{{ kititem_form.variant_group.errors }}</div>
|
||||
{% endif %}
|
||||
<small class="form-text text-muted">Группа вариантов (если выбрана, конкретный товар не нужен)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="{{ kititem_form.quantity.id_for_label }}" class="form-label">
|
||||
{{ kititem_form.quantity.label }}
|
||||
</label>
|
||||
{{ kititem_form.quantity }}
|
||||
{% if kititem_form.quantity.errors %}
|
||||
<div class="text-danger">{{ kititem_form.quantity.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-8 mb-3">
|
||||
<label for="{{ kititem_form.notes.id_for_label }}" class="form-label">
|
||||
{{ kititem_form.notes.label }}
|
||||
</label>
|
||||
{{ kititem_form.notes }}
|
||||
{% if kititem_form.notes.errors %}
|
||||
<div class="text-danger">{{ kititem_form.notes.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Кнопки действия -->
|
||||
<div class="d-flex justify-content-between mt-4 gap-2 flex-wrap">
|
||||
<a href="{% url 'products:productkit-list' %}" class="btn btn-secondary">Отмена</a>
|
||||
<div class="btn-group">
|
||||
<button type="submit" name="action" value="exit" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle"></i> Сохранить и выйти
|
||||
</button>
|
||||
<button type="submit" name="action" value="continue" class="btn btn-outline-primary">
|
||||
<i class="bi bi-arrow-repeat"></i> Сохранить и продолжить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// ========== ОПРЕДЕЛЯЕМ РЕЖИМ: СОЗДАНИЕ ИЛИ РЕДАКТИРОВАНИЕ ==========
|
||||
const existingForms = document.querySelectorAll('.kititem-form');
|
||||
const isEditMode = existingForms.length > 0 &&
|
||||
document.querySelector('[name$="-id"]') !== null &&
|
||||
document.querySelector('[name$="-id"]').value !== '';
|
||||
|
||||
// Если режим редактирования - показываем карточку поиска
|
||||
// Если создание - скрываем (будет одна пустая форма)
|
||||
const searchCard = document.getElementById('searchCard');
|
||||
if (!isEditMode && searchCard) {
|
||||
searchCard.style.display = 'none';
|
||||
}
|
||||
|
||||
// ========== ФУНКЦИИ ДЛЯ УПРАВЛЕНИЯ ВИДИМОСТЬЮ ПОЛЕЙ ==========
|
||||
function updateFieldStatus(form) {
|
||||
const productSelect = form.querySelector('[name$="-product"]');
|
||||
const variantGroupSelect = form.querySelector('[name$="-variant_group"]');
|
||||
|
||||
if (productSelect && variantGroupSelect) {
|
||||
const hasProduct = productSelect.value;
|
||||
const hasVariant = variantGroupSelect.value;
|
||||
|
||||
// Если выбран товар - отключаем группу вариантов
|
||||
if (hasProduct) {
|
||||
variantGroupSelect.disabled = true;
|
||||
variantGroupSelect.closest('.col-md-6').style.opacity = '0.6';
|
||||
} else {
|
||||
variantGroupSelect.disabled = false;
|
||||
variantGroupSelect.closest('.col-md-6').style.opacity = '1';
|
||||
}
|
||||
|
||||
// Если выбрана группа - отключаем товар
|
||||
if (hasVariant) {
|
||||
productSelect.disabled = true;
|
||||
productSelect.closest('.col-md-6').style.opacity = '0.6';
|
||||
} else {
|
||||
productSelect.disabled = false;
|
||||
productSelect.closest('.col-md-6').style.opacity = '1';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализируем все существующие формы
|
||||
const kititemForms = document.querySelectorAll('.kititem-form');
|
||||
kititemForms.forEach((form) => {
|
||||
updateFieldStatus(form);
|
||||
const productSelect = form.querySelector('[name$="-product"]');
|
||||
const variantGroupSelect = form.querySelector('[name$="-variant_group"]');
|
||||
if (productSelect) productSelect.addEventListener('change', () => updateFieldStatus(form));
|
||||
if (variantGroupSelect) variantGroupSelect.addEventListener('change', () => updateFieldStatus(form));
|
||||
});
|
||||
|
||||
// ========== ФУНКЦИИ ДЛЯ ПОИСКА И ДОБАВЛЕНИЯ ТОВАРОВ ==========
|
||||
const searchInput = document.getElementById('productSearch');
|
||||
const searchBtn = document.getElementById('searchBtn');
|
||||
const searchResults = document.getElementById('searchResults');
|
||||
const managementForm = document.querySelector('[name$="TOTAL_FORMS"]');
|
||||
|
||||
// Функция для выполнения поиска
|
||||
async function performSearch(query) {
|
||||
if (query.length < 2) {
|
||||
searchResults.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`{% url 'products:api-search-products-variants' %}?q=${encodeURIComponent(query)}`);
|
||||
const data = await response.json();
|
||||
|
||||
searchResults.innerHTML = '';
|
||||
|
||||
if (data.results.length === 0) {
|
||||
searchResults.innerHTML = '<div class="list-group-item text-muted">Товары не найдены</div>';
|
||||
} else {
|
||||
data.results.forEach((item) => {
|
||||
const resultItem = document.createElement('button');
|
||||
resultItem.type = 'button';
|
||||
resultItem.className = 'list-group-item list-group-item-action';
|
||||
resultItem.innerHTML = `
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<h6 class="mb-1">${item.name}</h6>
|
||||
<small class="text-muted">
|
||||
${item.type === 'product' ? '📦 Товар • Цена: ' + item.price + ' ₽' : '📋 Варианты • Вариантов: ' + item.count}
|
||||
</small>
|
||||
</div>
|
||||
<span class="badge ${item.type === 'product' ? 'bg-success' : 'bg-primary'}">
|
||||
${item.type === 'product' ? 'Товар' : 'Варианты'}
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
resultItem.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
addItemToFormset(item);
|
||||
});
|
||||
|
||||
searchResults.appendChild(resultItem);
|
||||
});
|
||||
}
|
||||
|
||||
searchResults.style.display = 'block';
|
||||
} catch (error) {
|
||||
console.error('Ошибка при поиске:', error);
|
||||
searchResults.innerHTML = '<div class="list-group-item text-danger">Ошибка при поиске</div>';
|
||||
searchResults.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для добавления товара в формсет
|
||||
function addItemToFormset(item) {
|
||||
const formsContainer = document.getElementById('kititem-forms');
|
||||
const existingForms = formsContainer.querySelectorAll('.kititem-form');
|
||||
|
||||
// Ищем первую пустую форму и используем ее
|
||||
let emptyForm = null;
|
||||
existingForms.forEach((form) => {
|
||||
if (!emptyForm) {
|
||||
const productSelect = form.querySelector('[name$="-product"]');
|
||||
const variantGroupSelect = form.querySelector('[name$="-variant_group"]');
|
||||
const deleteCheckbox = form.querySelector('[name$="-DELETE"]');
|
||||
|
||||
// Если форма пустая и не помечена на удаление
|
||||
if ((!productSelect || !productSelect.value) &&
|
||||
(!variantGroupSelect || !variantGroupSelect.value) &&
|
||||
(!deleteCheckbox || !deleteCheckbox.checked)) {
|
||||
emptyForm = form;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Если нашли пустую форму, заполняем ее
|
||||
if (emptyForm) {
|
||||
fillFormWithItem(emptyForm, item);
|
||||
} else {
|
||||
// Если нет пустой формы, создаем новую
|
||||
const totalForms = parseInt(document.querySelector('[name*="TOTAL_FORMS"]').value);
|
||||
const newFormHtml = createNewKitItemForm(totalForms, item);
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = newFormHtml;
|
||||
formsContainer.appendChild(tempDiv.firstElementChild);
|
||||
|
||||
// Обновляем счетчик форм
|
||||
document.querySelector('[name*="TOTAL_FORMS"]').value = totalForms + 1;
|
||||
|
||||
// Инициализируем новую форму
|
||||
const newForm = formsContainer.lastElementChild;
|
||||
initializeForm(newForm);
|
||||
}
|
||||
|
||||
// Очищаем поиск
|
||||
searchInput.value = '';
|
||||
searchResults.style.display = 'none';
|
||||
|
||||
// Показываем сообщение
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = 'alert alert-success alert-dismissible fade show';
|
||||
alertDiv.setAttribute('role', 'alert');
|
||||
alertDiv.innerHTML = `
|
||||
<strong>✓ Товар добавлен!</strong> "${item.name}" добавлен в комплект.
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.querySelector('.card-body').prepend(alertDiv);
|
||||
setTimeout(() => alertDiv.remove(), 3000);
|
||||
}
|
||||
|
||||
// Функция для заполнения существующей формы
|
||||
function fillFormWithItem(form, item) {
|
||||
const itemType = item.type === 'product' ? 'product' : 'variant';
|
||||
const selectedId = item.id;
|
||||
|
||||
if (itemType === 'product') {
|
||||
const productSelect = form.querySelector('[name$="-product"]');
|
||||
if (productSelect) {
|
||||
productSelect.value = selectedId;
|
||||
productSelect.innerHTML = `<option value="">---------</option><option value="${selectedId}" selected>${item.name}</option>`;
|
||||
productSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
} else {
|
||||
const variantSelect = form.querySelector('[name$="-variant_group"]');
|
||||
if (variantSelect) {
|
||||
variantSelect.value = selectedId;
|
||||
variantSelect.innerHTML = `<option value="">---------</option><option value="${selectedId}" selected>${item.name}</option>`;
|
||||
variantSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
|
||||
updateFieldStatus(form);
|
||||
}
|
||||
|
||||
// Функция для инициализации формы
|
||||
function initializeForm(form) {
|
||||
updateFieldStatus(form);
|
||||
const productSelect = form.querySelector('[name$="-product"]');
|
||||
const variantGroupSelect = form.querySelector('[name$="-variant_group"]');
|
||||
if (productSelect) productSelect.addEventListener('change', () => updateFieldStatus(form));
|
||||
if (variantGroupSelect) variantGroupSelect.addEventListener('change', () => updateFieldStatus(form));
|
||||
}
|
||||
|
||||
// Функция для создания HTML новой формы
|
||||
function createNewKitItemForm(formIndex, item) {
|
||||
const fieldName = `kititem_set-${formIndex}`;
|
||||
const itemType = item.type === 'product' ? 'product' : 'variant';
|
||||
const selectedId = item.id;
|
||||
const selectedField = itemType === 'product' ? 'product' : 'variant_group';
|
||||
|
||||
return `
|
||||
<div class="card mb-3 kititem-form" data-form-index="${formIndex}">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<span><strong>Компонент #${formIndex + 1}</strong></span>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="${fieldName}-DELETE" name="${fieldName}-DELETE">
|
||||
<label class="form-check-label" for="${fieldName}-DELETE">
|
||||
Удалить
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="${fieldName}-product" class="form-label">
|
||||
Конкретный товар
|
||||
</label>
|
||||
<select class="form-select" id="${fieldName}-product" name="${fieldName}-product"
|
||||
data-new-record="False" ${itemType === 'variant' ? 'disabled' : ''} style="${itemType === 'variant' ? 'opacity: 0.6' : ''}">
|
||||
<option value="">---------</option>
|
||||
${itemType === 'product' ? `<option value="${selectedId}" selected>${item.name}</option>` : ''}
|
||||
</select>
|
||||
<small class="form-text text-muted">Конкретный товар (если выбран, группа вариантов не нужна)</small>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="${fieldName}-variant_group" class="form-label">
|
||||
Группа вариантов
|
||||
</label>
|
||||
<select class="form-select" id="${fieldName}-variant_group" name="${fieldName}-variant_group"
|
||||
data-new-record="False" ${itemType === 'product' ? 'disabled' : ''} style="${itemType === 'product' ? 'opacity: 0.6' : ''}">
|
||||
<option value="">---------</option>
|
||||
${itemType === 'variant' ? `<option value="${selectedId}" selected>${item.name}</option>` : ''}
|
||||
</select>
|
||||
<small class="form-text text-muted">Группа вариантов (если выбрана, конкретный товар не нужен)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="${fieldName}-quantity" class="form-label">
|
||||
Количество
|
||||
</label>
|
||||
<input type="number" class="form-control" id="${fieldName}-quantity" name="${fieldName}-quantity"
|
||||
value="1" step="0.001" min="0">
|
||||
</div>
|
||||
|
||||
<div class="col-md-8 mb-3">
|
||||
<label for="${fieldName}-notes" class="form-label">
|
||||
Примечание
|
||||
</label>
|
||||
<input type="text" class="form-control" id="${fieldName}-notes" name="${fieldName}-notes"
|
||||
placeholder="Опциональное примечание">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="${fieldName}-id">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Обработчики событий поиска
|
||||
searchBtn.addEventListener('click', () => {
|
||||
performSearch(searchInput.value);
|
||||
});
|
||||
|
||||
searchInput.addEventListener('keyup', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
performSearch(searchInput.value);
|
||||
} else {
|
||||
// Автопоиск с задержкой
|
||||
clearTimeout(searchInput.searchTimeout);
|
||||
searchInput.searchTimeout = setTimeout(() => {
|
||||
performSearch(searchInput.value);
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
|
||||
// Очистка результатов при клике вне области
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('#productSearch') && !e.target.closest('#searchResults') && !e.target.closest('#searchBtn')) {
|
||||
// searchResults.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ========== ОЧИСТКА ПУСТЫХ ФОРМ ПЕРЕД СОХРАНЕНИЕМ ==========
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const kitForm = document.querySelector('form[method="post"]');
|
||||
if (kitForm) {
|
||||
kitForm.addEventListener('submit', function(e) {
|
||||
// Отмечаем пустые компоненты для удаления
|
||||
const formsContainer = document.getElementById('kititem-forms');
|
||||
if (formsContainer) {
|
||||
const allForms = formsContainer.querySelectorAll('.kititem-form');
|
||||
allForms.forEach((form) => {
|
||||
const productSelect = form.querySelector('[name$="-product"]');
|
||||
const variantGroupSelect = form.querySelector('[name$="-variant_group"]');
|
||||
const deleteCheckbox = form.querySelector('[name$="-DELETE"]');
|
||||
|
||||
const hasProduct = productSelect && productSelect.value;
|
||||
const hasVariant = variantGroupSelect && variantGroupSelect.value;
|
||||
|
||||
// Если форма пустая - помечаем на удаление
|
||||
if (!hasProduct && !hasVariant && deleteCheckbox) {
|
||||
deleteCheckbox.checked = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
// Продолжаем нормальную отправку формы
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ========== УПРАВЛЕНИЕ ВИДИМОСТЬЮ ПОЛЯ FIXED_PRICE ==========
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const pricingMethodField = document.querySelector('[name="pricing_method"]');
|
||||
const fixedPriceField = document.querySelector('[name="fixed_price"]');
|
||||
const fixedPriceAlert = document.getElementById('fixed-price-alert');
|
||||
|
||||
function updatePricingFieldsState() {
|
||||
if (!pricingMethodField || !fixedPriceField) return;
|
||||
|
||||
const method = pricingMethodField.value;
|
||||
|
||||
if (method === 'fixed') {
|
||||
// Поле активно
|
||||
fixedPriceField.disabled = false;
|
||||
fixedPriceField.style.opacity = '1';
|
||||
if (fixedPriceAlert) {
|
||||
fixedPriceAlert.classList.add('d-none');
|
||||
}
|
||||
} else {
|
||||
// Поле неактивно
|
||||
fixedPriceField.disabled = true;
|
||||
fixedPriceField.style.opacity = '0.6';
|
||||
if (fixedPriceAlert) {
|
||||
fixedPriceAlert.classList.remove('d-none');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализируем состояние при загрузке
|
||||
updatePricingFieldsState();
|
||||
|
||||
// Обновляем при изменении метода ценообразования
|
||||
if (pricingMethodField) {
|
||||
pricingMethodField.addEventListener('change', updatePricingFieldsState);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
127
myproject/products/templates/products/productkit_list.html
Normal file
127
myproject/products/templates/products/productkit_list.html
Normal file
@@ -0,0 +1,127 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Список комплектов (букетов){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<h2 class="mb-4">Список комплектов (букетов)</h2>
|
||||
|
||||
<!-- Панель фильтрации -->
|
||||
{% include 'components/filter_panel.html' with title="Комплекты" filters=filters action_buttons=action_buttons %}
|
||||
|
||||
{% if kits %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Фото</th>
|
||||
<th>Название</th>
|
||||
<th>Артикул</th>
|
||||
<th>Категория</th>
|
||||
<th>Цена продажи</th>
|
||||
<th>Компонентов</th>
|
||||
<th>Статус</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for kit in kits %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if kit.photos.all %}
|
||||
{% with photo=kit.photos.first %}
|
||||
<img src="{{ photo.image.url }}" alt="{{ kit.name }}" style="max-width: 50px; max-height: 50px;" class="img-thumbnail">
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<span class="text-muted">Нет фото</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'products:productkit-detail' kit.pk %}">{{ kit.name }}</a>
|
||||
</td>
|
||||
<td>{{ kit.sku }}</td>
|
||||
<td>
|
||||
{% if kit.categories.all %}
|
||||
{% for category in kit.categories.all %}
|
||||
{{ category.name }}{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ kit.get_sale_price|floatformat:2 }} руб.</td>
|
||||
<td>
|
||||
<span class="badge bg-secondary">{{ kit.get_total_components_count }} шт</span>
|
||||
{% if kit.get_components_with_variants_count > 0 %}
|
||||
<span class="badge bg-primary" title="С вариантами">
|
||||
<i class="bi bi-shuffle"></i> {{ kit.get_components_with_variants_count }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if kit.is_active %}
|
||||
<span class="badge bg-success">Активен</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Неактивен</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="{% url 'products:productkit-detail' kit.pk %}" class="btn btn-outline-info" title="Просмотреть">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
<a href="{% url 'products:productkit-update' kit.pk %}" class="btn btn-outline-primary" title="Редактировать">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<a href="{% url 'products:productkit-delete' kit.pk %}" class="btn btn-outline-danger" title="Удалить">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">Первая</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">Предыдущая</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="page-item active">
|
||||
<span class="page-link">Страница {{ page_obj.number }} из {{ page_obj.paginator.num_pages }}</span>
|
||||
</li>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">Следующая</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">Последняя</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<h4><i class="bi bi-info-circle"></i> Комплекты не найдены</h4>
|
||||
<p>В данный момент нет комплектов, соответствующих выбранным фильтрам.</p>
|
||||
{% if perms.products.add_productkit %}
|
||||
<a href="{% url 'products:productkit-create' %}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Создать первый комплект
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user