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:
228
myproject/products/static/products/js/catalog.js
Normal file
228
myproject/products/static/products/js/catalog.js
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
/**
|
||||||
|
* Каталог товаров - JavaScript функционал
|
||||||
|
* - Раскрытие/сворачивание категорий
|
||||||
|
* - Фильтрация и поиск товаров
|
||||||
|
* - Переключение вида (карточки/список)
|
||||||
|
* - Inline-редактирование названий категорий
|
||||||
|
*/
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Раскрытие/сворачивание категорий
|
||||||
|
// ========================================
|
||||||
|
document.querySelectorAll('.category-header').forEach(header => {
|
||||||
|
header.addEventListener('click', function(e) {
|
||||||
|
// Игнорируем клик по редактируемому полю
|
||||||
|
if (e.target.classList.contains('category-name-editable') ||
|
||||||
|
e.target.classList.contains('category-name-input')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Развернуть все
|
||||||
|
const expandAllBtn = document.getElementById('expand-all');
|
||||||
|
if (expandAllBtn) {
|
||||||
|
expandAllBtn.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'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Свернуть все
|
||||||
|
const collapseAllBtn = document.getElementById('collapse-all');
|
||||||
|
if (collapseAllBtn) {
|
||||||
|
collapseAllBtn.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';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Поиск
|
||||||
|
// ========================================
|
||||||
|
const searchInput = document.getElementById('catalog-search');
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.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';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Переключение вида: карточки / список
|
||||||
|
// ========================================
|
||||||
|
const catalogGrid = document.getElementById('catalog-grid');
|
||||||
|
const viewGridBtn = document.getElementById('view-grid');
|
||||||
|
const viewListBtn = document.getElementById('view-list');
|
||||||
|
|
||||||
|
if (viewGridBtn && viewListBtn && catalogGrid) {
|
||||||
|
viewGridBtn.addEventListener('click', function() {
|
||||||
|
catalogGrid.classList.remove('catalog-list');
|
||||||
|
viewGridBtn.classList.add('active');
|
||||||
|
viewListBtn.classList.remove('active');
|
||||||
|
// Восстановить колонки для карточек
|
||||||
|
document.querySelectorAll('.catalog-item').forEach(item => {
|
||||||
|
item.className = item.className.replace(/col-\d+|col-\w+-\d+/g, '');
|
||||||
|
item.classList.add('col-6', 'col-lg-4', 'col-xl-3', 'catalog-item');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
viewListBtn.addEventListener('click', function() {
|
||||||
|
catalogGrid.classList.add('catalog-list');
|
||||||
|
viewListBtn.classList.add('active');
|
||||||
|
viewGridBtn.classList.remove('active');
|
||||||
|
// Убрать колонки для списка
|
||||||
|
document.querySelectorAll('.catalog-item').forEach(item => {
|
||||||
|
item.className = item.className.replace(/col-\d+|col-\w+-\d+/g, '');
|
||||||
|
item.classList.add('col-12', 'catalog-item');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Inline-редактирование названий категорий
|
||||||
|
// ========================================
|
||||||
|
document.querySelectorAll('.category-name-editable').forEach(nameSpan => {
|
||||||
|
nameSpan.addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// Уже редактируется?
|
||||||
|
if (this.querySelector('input')) return;
|
||||||
|
|
||||||
|
const categoryId = this.dataset.categoryId;
|
||||||
|
const currentName = this.textContent.trim();
|
||||||
|
|
||||||
|
// Создаем input
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'text';
|
||||||
|
input.value = currentName;
|
||||||
|
input.className = 'category-name-input';
|
||||||
|
input.style.cssText = 'width: 100%; border: 1px solid #0d6efd; border-radius: 3px; padding: 2px 6px; font-size: inherit; outline: none;';
|
||||||
|
|
||||||
|
// Сохраняем оригинальный текст
|
||||||
|
this.dataset.originalName = currentName;
|
||||||
|
this.textContent = '';
|
||||||
|
this.appendChild(input);
|
||||||
|
input.focus();
|
||||||
|
input.select();
|
||||||
|
|
||||||
|
// Функция сохранения
|
||||||
|
const saveCategory = async () => {
|
||||||
|
const newName = input.value.trim();
|
||||||
|
|
||||||
|
if (!newName) {
|
||||||
|
// Отмена - пустое название
|
||||||
|
cancelEdit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newName === this.dataset.originalName) {
|
||||||
|
// Название не изменилось
|
||||||
|
cancelEdit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показываем спиннер
|
||||||
|
input.disabled = true;
|
||||||
|
input.style.opacity = '0.5';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/products/api/categories/${categoryId}/rename/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': getCsrfToken()
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ name: newName })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// Успешно сохранено
|
||||||
|
this.textContent = data.name;
|
||||||
|
} else {
|
||||||
|
// Ошибка
|
||||||
|
alert(data.error || 'Ошибка при сохранении');
|
||||||
|
cancelEdit();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
alert('Ошибка сети');
|
||||||
|
cancelEdit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция отмены
|
||||||
|
const cancelEdit = () => {
|
||||||
|
this.textContent = this.dataset.originalName;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Enter - сохранить
|
||||||
|
input.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
saveCategory();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
cancelEdit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Потеря фокуса - сохранить
|
||||||
|
input.addEventListener('blur', function() {
|
||||||
|
// Небольшая задержка чтобы не конфликтовать с Enter
|
||||||
|
setTimeout(saveCategory, 100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получение CSRF токена
|
||||||
|
function getCsrfToken() {
|
||||||
|
const cookieValue = document.cookie
|
||||||
|
.split('; ')
|
||||||
|
.find(row => row.startsWith('csrftoken='))
|
||||||
|
?.split('=')[1];
|
||||||
|
return cookieValue || '';
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -69,6 +69,66 @@
|
|||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
padding: 0.15rem 0.35rem;
|
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>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -76,7 +136,7 @@
|
|||||||
<div class="container-fluid mt-4 px-4">
|
<div class="container-fluid mt-4 px-4">
|
||||||
<div class="row">
|
<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 shadow-sm border-0 mb-3">
|
||||||
<div class="card-header bg-white py-2 d-flex justify-content-between align-items-center">
|
<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>
|
<strong><i class="bi bi-folder-tree text-primary"></i> Категории</strong>
|
||||||
@@ -100,7 +160,7 @@
|
|||||||
</div>
|
</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 shadow-sm border-0">
|
||||||
<div class="card-header bg-white py-2 d-flex justify-content-between align-items-center">
|
<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>
|
<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="product">Товары</button>
|
||||||
<button class="btn btn-outline-primary btn-sm" data-filter="kit">Комплекты</button>
|
<button class="btn btn-outline-primary btn-sm" data-filter="kit">Комплекты</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<div class="card-body">
|
<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 %}
|
{% 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="card h-100 shadow-sm border-0">
|
||||||
<div class="position-relative">
|
<div class="position-relative">
|
||||||
{% if item.main_photo %}
|
{% if item.main_photo %}
|
||||||
@@ -152,66 +216,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
{% load static %}
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
<script src="{% static 'products/js/catalog.js' %}"></script>
|
||||||
// Раскрытие/сворачивание категорий
|
|
||||||
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>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<i class="bi bi-dot text-muted"></i>
|
<i class="bi bi-dot text-muted"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<i class="bi bi-folder{% if node.children %}-fill text-warning{% else %}2 text-secondary{% endif %}"></i>
|
<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 %}
|
{% with products_count=node.category.products.count kits_count=node.category.kits.count %}
|
||||||
{% if products_count or 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>
|
<span class="badge bg-light text-dark border" style="font-size: 0.7rem;">{{ products_count|add:kits_count }}</span>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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 %}
|
|
||||||
@@ -47,6 +47,7 @@ urlpatterns = [
|
|||||||
path('api/kits/temporary/create/', views.create_temporary_kit_api, name='api-temporary-kit-create'),
|
path('api/kits/temporary/create/', views.create_temporary_kit_api, name='api-temporary-kit-create'),
|
||||||
path('api/tags/create/', api_views.create_tag_api, name='api-tag-create'),
|
path('api/tags/create/', api_views.create_tag_api, name='api-tag-create'),
|
||||||
path('api/tags/<int:pk>/toggle/', api_views.toggle_tag_status_api, name='api-tag-toggle'),
|
path('api/tags/<int:pk>/toggle/', api_views.toggle_tag_status_api, name='api-tag-toggle'),
|
||||||
|
path('api/categories/<int:pk>/rename/', api_views.rename_category_api, name='api-category-rename'),
|
||||||
|
|
||||||
# Photo processing status API (for AJAX polling)
|
# Photo processing status API (for AJAX polling)
|
||||||
path('api/photos/status/<str:task_id>/', photo_status_api.photo_processing_status, name='api-photo-status'),
|
path('api/photos/status/<str:task_id>/', photo_status_api.photo_processing_status, name='api-photo-status'),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from django.core.cache import cache
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from ..models import Product, ProductVariantGroup, ProductKit
|
from ..models import Product, ProductVariantGroup, ProductKit, ProductCategory
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -873,3 +873,72 @@ def toggle_tag_status_api(request, pk):
|
|||||||
'success': False,
|
'success': False,
|
||||||
'error': f'Ошибка при обновлении тега: {str(e)}'
|
'error': f'Ошибка при обновлении тега: {str(e)}'
|
||||||
}, status=500)
|
}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
def rename_category_api(request, pk):
|
||||||
|
"""
|
||||||
|
AJAX endpoint для переименования категории (inline editing).
|
||||||
|
|
||||||
|
Принимает JSON:
|
||||||
|
{
|
||||||
|
"name": "Новое название"
|
||||||
|
}
|
||||||
|
|
||||||
|
Возвращает JSON:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"name": "Новое название"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
if request.method != 'POST':
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Метод не поддерживается'
|
||||||
|
}, status=405)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
|
||||||
|
data = json.loads(request.body)
|
||||||
|
name = data.get('name', '').strip()
|
||||||
|
|
||||||
|
# Валидация
|
||||||
|
if not name:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Название категории не может быть пустым'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
if len(name) > 255:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Название слишком длинное (максимум 255 символов)'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
# Получаем категорию
|
||||||
|
category = ProductCategory.objects.get(pk=pk)
|
||||||
|
|
||||||
|
# Обновляем название
|
||||||
|
category.name = name
|
||||||
|
category.save(update_fields=['name'])
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'success': True,
|
||||||
|
'name': category.name
|
||||||
|
})
|
||||||
|
|
||||||
|
except ProductCategory.DoesNotExist:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Категория не найдена'
|
||||||
|
}, status=404)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Некорректный JSON'
|
||||||
|
}, status=400)
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': f'Ошибка при переименовании: {str(e)}'
|
||||||
|
}, status=500)
|
||||||
|
|||||||
Reference in New Issue
Block a user