Feat: Add inline category creation in catalog with clickable product names

Added inline category creation functionality to catalog page with user-friendly interface:
- Inline input fields for creating root and nested categories
- '+' button in category tree header for root categories
- '+' icon on hover for each category node to create subcategories
- Clickable product/kit names in catalog grid and list views
- AJAX API endpoint for category creation with validation
- User-friendly error messages for duplicate names and constraints
- Clean implementation using clearTimeout pattern to prevent duplicate requests

Technical details:
- New API endpoint: POST /products/api/categories/create/
- Auto-generates slug and SKU for new categories
- Validates uniqueness, parent existence, and circular references
- Single-request submission with proper race condition handling
- Removes debug logging for production-ready code

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-25 00:24:05 +03:00
parent 03990292a5
commit d566819367
5 changed files with 344 additions and 7 deletions

View File

@@ -942,3 +942,142 @@ def rename_category_api(request, pk):
'success': False,
'error': f'Ошибка при переименовании: {str(e)}'
}, status=500)
def create_category_api(request):
"""
AJAX endpoint для создания новой категории (inline creation).
Принимает JSON:
{
"name": "Название категории",
"parent_id": 123 # опционально, null для корневой категории
}
Возвращает JSON:
{
"success": true,
"category": {
"id": 456,
"name": "Название категории",
"slug": "nazvanie-kategorii",
"parent_id": 123 или null
}
}
"""
if request.method != 'POST':
return JsonResponse({
'success': False,
'error': 'Метод не поддерживается'
}, status=405)
try:
import json
from django.db import IntegrityError
data = json.loads(request.body)
name = data.get('name', '').strip()
parent_id = data.get('parent_id')
# Валидация названия
if not name:
return JsonResponse({
'success': False,
'error': 'Название категории не может быть пустым'
}, status=400)
if len(name) > 200:
return JsonResponse({
'success': False,
'error': 'Название слишком длинное (максимум 200 символов)'
}, status=400)
# Получаем родительскую категорию, если указана
parent = None
if parent_id:
try:
parent = ProductCategory.objects.get(pk=parent_id)
if not parent.is_active:
return JsonResponse({
'success': False,
'error': 'Родительская категория неактивна'
}, status=400)
except ProductCategory.DoesNotExist:
return JsonResponse({
'success': False,
'error': 'Родительская категория не найдена'
}, status=404)
# Создаем новую категорию
category = ProductCategory(
name=name,
parent=parent,
is_active=True
)
# save() автоматически сгенерирует slug и sku
category.save()
return JsonResponse({
'success': True,
'category': {
'id': category.pk,
'name': category.name,
'slug': category.slug,
'parent_id': category.parent_id
}
})
except IntegrityError as e:
# Определяем тип ошибки уникальности
error_str = str(e).lower()
if 'unique_active_category_name' in error_str or 'unique constraint' in error_str:
error_message = 'Категория с таким названием уже существует'
elif 'sku' in error_str:
error_message = 'Ошибка при генерации артикула. Попробуйте ещё раз'
else:
error_message = 'Ошибка: категория с такими данными уже существует'
return JsonResponse({
'success': False,
'error': error_message
}, status=400)
except json.JSONDecodeError:
return JsonResponse({
'success': False,
'error': 'Некорректный JSON'
}, status=400)
except ValidationError as e:
# ValidationError может содержать словарь с ошибками
if hasattr(e, 'message_dict'):
# Извлекаем первое сообщение из словаря
error_messages = []
for field, messages in e.message_dict.items():
if isinstance(messages, list):
error_messages.extend(messages)
else:
error_messages.append(str(messages))
error_text = ' '.join(error_messages)
elif hasattr(e, 'messages'):
error_text = ' '.join(e.messages)
else:
error_text = str(e)
# Заменяем технические сообщения на понятные
if 'unique_active_category_name' in error_text.lower():
error_text = 'Категория с таким названием уже существует'
elif 'циклическая ссылка' in error_text.lower():
error_text = 'Невозможно создать категорию: обнаружена циклическая ссылка'
elif 'слишком глубокая вложенность' in error_text.lower():
error_text = 'Превышена максимальная глубина вложенности категорий'
return JsonResponse({
'success': False,
'error': error_text
}, status=400)
except Exception as e:
logger.error(f'Ошибка при создании категории: {str(e)}')
return JsonResponse({
'success': False,
'error': f'Ошибка при создании категории: {str(e)}'
}, status=500)