Feat: Add catalog page with inline category renaming

- Create catalog view with category tree and product grid
- Add grid/list view toggle for products
- Implement inline category name editing (click to rename)
- Add API endpoint for category rename
- Extract JS to separate catalog.js file
- Remove unused partial templates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-24 00:54:34 +03:00
parent 4549b2c2c2
commit 6517643e0d
7 changed files with 370 additions and 115 deletions

View File

@@ -69,6 +69,66 @@
font-size: 0.65rem;
padding: 0.15rem 0.35rem;
}
/* Режим списка */
.catalog-list.row {
gap: 1px !important;
}
.catalog-list .catalog-item {
flex: 0 0 100%;
max-width: 100%;
padding: 0 !important;
}
.catalog-list .catalog-item .card {
flex-direction: row;
align-items: stretch;
padding: 0;
border-radius: 0.25rem;
overflow: hidden;
}
.catalog-list .catalog-item .card .position-relative {
flex-shrink: 0;
}
.catalog-list .catalog-item .card .position-relative img,
.catalog-list .catalog-item .card .position-relative > div {
height: 100% !important;
min-height: 36px;
width: 50px;
border-radius: 0;
object-fit: cover;
}
.catalog-list .catalog-item .card .position-relative .badge {
display: none;
}
.catalog-list .catalog-item .card-body {
flex-grow: 1;
display: flex;
align-items: center;
padding: 0 0.5rem !important;
gap: 0.75rem;
}
.catalog-list .catalog-item .card-body > div:first-child {
flex-grow: 1;
}
.catalog-list .catalog-item .card-body > div:last-child {
display: flex;
align-items: center;
gap: 0.75rem;
white-space: nowrap;
}
.catalog-list .catalog-item .card-body .mt-1 {
margin-top: 0 !important;
}
/* Inline-редактирование названий категорий */
.category-name-editable {
cursor: text;
padding: 2px 4px;
margin: -2px -4px;
border-radius: 3px;
transition: background-color 0.15s;
}
.category-name-editable:hover {
background-color: #e7f1ff;
}
</style>
{% endblock %}
@@ -76,7 +136,7 @@
<div class="container-fluid mt-4 px-4">
<div class="row">
<!-- Дерево категорий слева -->
<div class="col-lg-3 col-md-4">
<div class="col-lg-4 col-md-5">
<div class="card shadow-sm border-0 mb-3">
<div class="card-header bg-white py-2 d-flex justify-content-between align-items-center">
<strong><i class="bi bi-folder-tree text-primary"></i> Категории</strong>
@@ -100,7 +160,7 @@
</div>
<!-- Сетка товаров справа -->
<div class="col-lg-9 col-md-8">
<div class="col-lg-8 col-md-7">
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-2 d-flex justify-content-between align-items-center">
<strong><i class="bi bi-grid-3x3-gap text-primary"></i> Товары и комплекты</strong>
@@ -111,12 +171,16 @@
<button class="btn btn-outline-primary btn-sm" data-filter="product">Товары</button>
<button class="btn btn-outline-primary btn-sm" data-filter="kit">Комплекты</button>
</div>
<div class="btn-group btn-group-sm ms-2">
<button class="btn btn-outline-secondary btn-sm" id="view-grid" title="Карточки"><i class="bi bi-grid-3x3-gap"></i></button>
<button class="btn btn-outline-secondary btn-sm active" id="view-list" title="Список"><i class="bi bi-list-ul"></i></button>
</div>
</div>
</div>
<div class="card-body">
<div class="row g-3" id="catalog-grid">
<div class="row g-3 catalog-list" id="catalog-grid">
{% for item in items %}
<div class="col-6 col-lg-4 col-xl-3 catalog-item" data-type="{{ item.item_type }}">
<div class="col-12 catalog-item" data-type="{{ item.item_type }}">
<div class="card h-100 shadow-sm border-0">
<div class="position-relative">
{% if item.main_photo %}
@@ -152,66 +216,6 @@
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Раскрытие/сворачивание категорий
document.querySelectorAll('.category-header').forEach(header => {
header.addEventListener('click', function(e) {
e.stopPropagation();
const node = this.closest('.category-node');
const children = node.querySelector('.category-children');
const items = node.querySelector('.category-items');
const toggle = this.querySelector('.category-toggle');
if (children) {
children.classList.toggle('d-none');
}
if (items) {
items.classList.toggle('d-none');
}
if (toggle) {
toggle.classList.toggle('collapsed');
}
});
});
// Развернуть все
document.getElementById('expand-all').addEventListener('click', function() {
document.querySelectorAll('.category-children, .category-items').forEach(el => el.classList.remove('d-none'));
document.querySelectorAll('.category-toggle').forEach(el => el.classList.remove('collapsed'));
});
// Свернуть все
document.getElementById('collapse-all').addEventListener('click', function() {
document.querySelectorAll('.category-children, .category-items').forEach(el => el.classList.add('d-none'));
document.querySelectorAll('.category-toggle').forEach(el => el.classList.add('collapsed'));
});
// Фильтр по типу
document.querySelectorAll('[data-filter]').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('[data-filter]').forEach(b => {
b.classList.remove('active', 'btn-primary');
b.classList.add('btn-outline-primary');
});
this.classList.add('active', 'btn-primary');
this.classList.remove('btn-outline-primary');
const filter = this.dataset.filter;
document.querySelectorAll('.catalog-item').forEach(item => {
item.style.display = (filter === 'all' || item.dataset.type === filter) ? '' : 'none';
});
});
});
// Поиск
document.getElementById('catalog-search').addEventListener('input', function() {
const query = this.value.toLowerCase();
document.querySelectorAll('.catalog-item').forEach(item => {
const text = item.textContent.toLowerCase();
item.style.display = text.includes(query) ? '' : 'none';
});
});
});
</script>
{% load static %}
<script src="{% static 'products/js/catalog.js' %}"></script>
{% endblock %}

View File

@@ -10,7 +10,7 @@
<i class="bi bi-dot text-muted"></i>
{% endif %}
<i class="bi bi-folder{% if node.children %}-fill text-warning{% else %}2 text-secondary{% endif %}"></i>
<span class="flex-grow-1">{{ node.category.name }}</span>
<span class="flex-grow-1 category-name-editable" data-category-id="{{ node.category.pk }}">{{ node.category.name }}</span>
{% with products_count=node.category.products.count kits_count=node.category.kits.count %}
{% if products_count or kits_count %}
<span class="badge bg-light text-dark border" style="font-size: 0.7rem;">{{ products_count|add:kits_count }}</span>

View File

@@ -1,22 +0,0 @@
{% comment %}
Строка товара/комплекта в дереве каталога.
{% endcomment %}
<div class="product-row d-flex align-items-center py-1 px-2 border-bottom" data-type="{{ item.item_type }}" data-id="{{ item.pk }}">
<i class="bi bi-grip-vertical drag-handle me-2 text-muted"></i>
{% if item.main_photo %}
<img src="{{ item.main_photo.image.url }}" class="product-thumb me-2" alt="">
{% else %}
<div class="product-thumb me-2 bg-light d-flex align-items-center justify-content-center">
<i class="bi bi-image text-muted"></i>
</div>
{% endif %}
<div class="flex-grow-1 text-truncate">
<a href="{% if item.item_type == 'kit' %}{% url 'products:productkit-detail' item.pk %}{% else %}{% url 'products:product-detail' item.pk %}{% endif %}" class="text-decoration-none small">
{{ item.name }}
</a>
<div class="text-muted" style="font-size: 0.75rem;">{{ item.sku }}</div>
</div>
<span class="badge {% if item.item_type == 'kit' %}bg-info{% else %}bg-secondary{% endif %} ms-2">
{% if item.item_type == 'kit' %}К{% else %}Т{% endif %}
</span>
</div>

View File

@@ -1,25 +0,0 @@
{% comment %}
Рекурсивный шаблон для отображения узла дерева категорий.
Параметры: nodes - список узлов, level - уровень вложенности
{% endcomment %}
{% for node in nodes %}
<div class="category-node" data-category-id="{{ node.category.pk }}">
<div class="category-item d-flex align-items-center p-2 {% if level > 0 %}ps-{{ level|add:2 }}{% endif %}">
{% if node.children %}
<i class="bi bi-chevron-down category-icon me-2 text-muted"></i>
{% else %}
<i class="bi bi-dot me-2 text-muted"></i>
{% endif %}
<i class="bi bi-folder{% if node.children %}-fill text-warning{% else %}2 text-secondary{% endif %} me-2"></i>
<span class="flex-grow-1">{{ node.category.name }}</span>
<span class="badge bg-light text-dark border ms-2" title="Товаров в категории">
{{ node.category.products.count|default:0 }}
</span>
</div>
{% if node.children %}
<div class="subcategories">
{% include 'products/partials/catalog_tree_node.html' with nodes=node.children level=level|add:1 %}
</div>
{% endif %}
</div>
{% endfor %}