refactor: Двухэтапное создание комплектов товаров
Реализован современный двухэтапный подход к созданию комплектов: **Шаг 1: Создание базовой информации** (productkit_create.html) - Упрощённая форма только с базовой информацией - Название, описание, категории, теги - Фотографии с предпросмотром - Настройки ценообразования - Чистый современный UI с breadcrumbs - Прогрессивное раскрытие (collapsible секции) **Шаг 2: Добавление товаров** (productkit_edit.html) - Автоматический redirect после создания - Работа с товарами через существующий функционал - Улучшенный заголовок с указанием шага **Изменения в коде:** - ProductKitCreateView: упрощён, убраны formsets - ProductKitUpdateView: использует новый шаблон - productkit_form.html → productkit_edit.html - Новый шаблон productkit_create.html **Преимущества:** ✅ Простая и надёжная логика сохранения ✅ Нет проблем с disabled полями в formsets ✅ Понятный UX с чёткими шагами ✅ Современный дизайн с иконками Bootstrap ✅ Предпросмотр фото перед загрузкой 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
363
myproject/products/templates/products/productkit_create.html
Normal file
363
myproject/products/templates/products/productkit_create.html
Normal file
@@ -0,0 +1,363 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Создать комплект{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<!-- Breadcrumbs -->
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'products:productkit-list' %}">Комплекты</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Создать комплект</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8 mx-auto">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h4 class="mb-0">
|
||||
<i class="bi bi-box-seam"></i> Создание нового комплекта
|
||||
</h4>
|
||||
<small class="text-white-50">Шаг 1 из 2: Базовая информация</small>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- Информационное сообщение -->
|
||||
<div class="alert alert-info mb-4">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
<strong>Как это работает:</strong> Сначала создайте комплект с базовой информацией,
|
||||
затем вы сможете добавить товары на следующем шаге.
|
||||
</div>
|
||||
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{% for error in form.non_field_errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- 1. ОСНОВНАЯ ИНФОРМАЦИЯ -->
|
||||
<div class="mb-4">
|
||||
<h5 class="border-bottom pb-2 mb-3">
|
||||
<i class="bi bi-pencil-square"></i> Основная информация
|
||||
</h5>
|
||||
|
||||
<!-- Название -->
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.name.id_for_label }}" class="form-label fw-bold required">
|
||||
{{ form.name.label }}
|
||||
</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">
|
||||
<label for="{{ form.description.id_for_label }}" class="form-label">
|
||||
{{ form.description.label }}
|
||||
</label>
|
||||
{{ 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="mb-3">
|
||||
<label class="form-label">{{ form.categories.label }}</label>
|
||||
<div class="border rounded p-3 bg-light">
|
||||
{{ form.categories }}
|
||||
</div>
|
||||
{% if form.categories.errors %}
|
||||
<div class="text-danger">{{ form.categories.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Теги -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ form.tags.label }}</label>
|
||||
<div class="border rounded p-3 bg-light">
|
||||
{{ form.tags }}
|
||||
</div>
|
||||
{% if form.tags.errors %}
|
||||
<div class="text-danger">{{ form.tags.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. ФОТОГРАФИИ -->
|
||||
<div class="mb-4">
|
||||
<h5 class="border-bottom pb-2 mb-3">
|
||||
<i class="bi bi-images"></i> Фотографии комплекта
|
||||
</h5>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="id_photos" class="form-label">
|
||||
Загрузить фото
|
||||
<span id="photoCount" class="badge bg-secondary ms-2" style="display: none;">0 выбрано</span>
|
||||
</label>
|
||||
<input type="file" name="photos" accept="image/*" multiple class="form-control" id="id_photos">
|
||||
<small class="form-text text-muted">
|
||||
Выберите фото для комплекта (можно выбрать несколько, до 10 штук)
|
||||
</small>
|
||||
|
||||
<!-- Контейнер для предпросмотра выбранных фото -->
|
||||
<div id="photoPreviewContainer" class="mt-3" style="display: none;">
|
||||
<h6 class="mb-2">Выбранные фото:</h6>
|
||||
<div id="photoPreview" class="row g-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. ЦЕНООБРАЗОВАНИЕ -->
|
||||
<div class="mb-4">
|
||||
<h5 class="border-bottom pb-2 mb-3">
|
||||
<i class="bi bi-calculator"></i> Ценообразование
|
||||
</h5>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
<strong>Обратите внимание:</strong> Метод ценообразования можно настроить после добавления товаров в комплект.
|
||||
По умолчанию цена будет рассчитываться как сумма цен входящих товаров.
|
||||
</div>
|
||||
|
||||
<!-- Метод ценообразования -->
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.pricing_method.id_for_label }}" class="form-label">
|
||||
{{ form.pricing_method.label }}
|
||||
</label>
|
||||
{{ form.pricing_method }}
|
||||
{% if form.pricing_method.help_text %}
|
||||
<small class="form-text text-muted">{{ form.pricing_method.help_text }}</small>
|
||||
{% endif %}
|
||||
{% if form.pricing_method.errors %}
|
||||
<div class="text-danger">{{ form.pricing_method.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Фиксированная цена (показывается при выборе 'fixed') -->
|
||||
<div id="fixedPriceBlock" style="display: none;">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.fixed_price.id_for_label }}" class="form-label">
|
||||
{{ form.fixed_price.label }}
|
||||
</label>
|
||||
{{ form.fixed_price }}
|
||||
{% if form.fixed_price.errors %}
|
||||
<div class="text-danger">{{ form.fixed_price.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Процент наценки -->
|
||||
<div id="markupPercentBlock" style="display: none;">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.markup_percent.id_for_label }}" class="form-label">
|
||||
{{ form.markup_percent.label }}
|
||||
</label>
|
||||
{{ form.markup_percent }}
|
||||
{% if form.markup_percent.errors %}
|
||||
<div class="text-danger">{{ form.markup_percent.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Фиксированная наценка -->
|
||||
<div id="markupAmountBlock" style="display: none;">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.markup_amount.id_for_label }}" class="form-label">
|
||||
{{ form.markup_amount.label }}
|
||||
</label>
|
||||
{{ form.markup_amount }}
|
||||
{% if form.markup_amount.errors %}
|
||||
<div class="text-danger">{{ form.markup_amount.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. ДОПОЛНИТЕЛЬНЫЕ НАСТРОЙКИ (Collapsible) -->
|
||||
<div class="mb-4">
|
||||
<h5 class="border-bottom pb-2 mb-3">
|
||||
<a class="text-decoration-none text-dark" data-bs-toggle="collapse" href="#additionalSettings" role="button" aria-expanded="false">
|
||||
<i class="bi bi-gear"></i> Дополнительные настройки
|
||||
<i class="bi bi-chevron-down float-end"></i>
|
||||
</a>
|
||||
</h5>
|
||||
|
||||
<div class="collapse" id="additionalSettings">
|
||||
<!-- Артикул -->
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.sku.id_for_label }}" class="form-label">
|
||||
{{ form.sku.label }}
|
||||
</label>
|
||||
{{ 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="form-check">
|
||||
{{ form.is_active }}
|
||||
<label class="form-check-label" for="{{ form.is_active.id_for_label }}">
|
||||
{{ form.is_active.label }}
|
||||
</label>
|
||||
{% if form.is_active.errors %}
|
||||
<div class="text-danger">{{ form.is_active.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 5. КНОПКИ ДЕЙСТВИЙ (Sticky footer) -->
|
||||
<div class="d-flex justify-content-between mt-4 border-top pt-3">
|
||||
<a href="{% url 'products:productkit-list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Отмена
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
<i class="bi bi-arrow-right-circle"></i> Далее: Добавить товары
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Подсказка -->
|
||||
<div class="alert alert-light mt-3">
|
||||
<strong>💡 Совет:</strong> После создания базовой информации вы попадёте на страницу редактирования,
|
||||
где сможете добавить товары в комплект через удобный поиск.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.required::after {
|
||||
content: " *";
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// ========== УПРАВЛЕНИЕ ВИДИМОСТЬЮ ПОЛЕЙ ЦЕНООБРАЗОВАНИЯ ==========
|
||||
const pricingMethodField = document.querySelector('[name="pricing_method"]');
|
||||
const fixedPriceBlock = document.getElementById('fixedPriceBlock');
|
||||
const markupPercentBlock = document.getElementById('markupPercentBlock');
|
||||
const markupAmountBlock = document.getElementById('markupAmountBlock');
|
||||
|
||||
function updatePricingFieldsVisibility() {
|
||||
if (!pricingMethodField) return;
|
||||
|
||||
const method = pricingMethodField.value;
|
||||
|
||||
// Скрываем все блоки
|
||||
fixedPriceBlock.style.display = 'none';
|
||||
markupPercentBlock.style.display = 'none';
|
||||
markupAmountBlock.style.display = 'none';
|
||||
|
||||
// Показываем нужный блок
|
||||
if (method === 'fixed') {
|
||||
fixedPriceBlock.style.display = 'block';
|
||||
} else if (method === 'from_cost_plus_percent') {
|
||||
markupPercentBlock.style.display = 'block';
|
||||
} else if (method === 'from_cost_plus_amount') {
|
||||
markupAmountBlock.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация
|
||||
updatePricingFieldsVisibility();
|
||||
|
||||
// Обработчик изменения метода
|
||||
if (pricingMethodField) {
|
||||
pricingMethodField.addEventListener('change', updatePricingFieldsVisibility);
|
||||
}
|
||||
|
||||
// ========== ПРЕДПРОСМОТР ЗАГРУЖАЕМЫХ ФОТО ==========
|
||||
const photoInput = document.getElementById('id_photos');
|
||||
const photoPreview = document.getElementById('photoPreview');
|
||||
const photoPreviewContainer = document.getElementById('photoPreviewContainer');
|
||||
const photoCount = document.getElementById('photoCount');
|
||||
|
||||
let selectedFiles = [];
|
||||
|
||||
if (photoInput) {
|
||||
photoInput.addEventListener('change', function(e) {
|
||||
const files = Array.from(e.target.files);
|
||||
selectedFiles = files;
|
||||
|
||||
if (files.length > 0) {
|
||||
photoCount.style.display = 'inline';
|
||||
photoCount.textContent = `${files.length} выбрано`;
|
||||
photoPreviewContainer.style.display = 'block';
|
||||
photoPreview.innerHTML = '';
|
||||
|
||||
files.forEach((file, index) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function(event) {
|
||||
const col = document.createElement('div');
|
||||
col.className = 'col-6 col-md-4 col-lg-3';
|
||||
|
||||
col.innerHTML = `
|
||||
<div class="card position-relative" data-file-index="${index}">
|
||||
<img src="${event.target.result}" class="card-img-top" alt="Preview" style="height: 150px; object-fit: cover;">
|
||||
<div class="card-body p-2">
|
||||
<small class="text-truncate d-block">${file.name}</small>
|
||||
<button type="button" class="btn btn-sm btn-danger w-100 mt-1 remove-photo-btn" data-file-index="${index}">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
photoPreview.appendChild(col);
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
} else {
|
||||
photoCount.style.display = 'none';
|
||||
photoPreviewContainer.style.display = 'none';
|
||||
photoPreview.innerHTML = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Обработчик удаления отдельных фото
|
||||
photoPreview.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('remove-photo-btn') || e.target.closest('.remove-photo-btn')) {
|
||||
const btn = e.target.classList.contains('remove-photo-btn') ? e.target : e.target.closest('.remove-photo-btn');
|
||||
const fileIndex = parseInt(btn.getAttribute('data-file-index'));
|
||||
|
||||
selectedFiles.splice(fileIndex, 1);
|
||||
|
||||
const dataTransfer = new DataTransfer();
|
||||
selectedFiles.forEach(file => {
|
||||
dataTransfer.items.add(file);
|
||||
});
|
||||
photoInput.files = dataTransfer.files;
|
||||
|
||||
photoInput.dispatchEvent(new Event('change'));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
1085
myproject/products/templates/products/productkit_edit.html
Normal file
1085
myproject/products/templates/products/productkit_edit.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user