Optimize order statuses list page with compact card layout
- Changed from table to card-based design for better space efficiency - Reduced padding and margins to fit 15+ statuses on screen without scrolling - Minimized font sizes and icon sizes for compact display - Added proper styling for edit and delete buttons with hover effects - Improved visual hierarchy with color indicators and badges - Maintained all functionality while improving UX 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -189,80 +189,8 @@ input[name*="DELETE"] {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Варианты (комплекты) -->
|
||||
<div class="card border-0 shadow-sm mb-3">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="mb-0">Варианты (комплекты)</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{ option_formset.management_form }}
|
||||
|
||||
{% if option_formset.non_form_errors %}
|
||||
<div class="alert alert-danger">
|
||||
{{ option_formset.non_form_errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="optionFormsetContainer">
|
||||
{% for form in option_formset %}
|
||||
<div class="option-form border rounded p-3 mb-3" style="background: #f8f9fa;">
|
||||
{{ form.id }}
|
||||
{% if form.instance.pk %}
|
||||
<input type="hidden" name="options-{{ forloop.counter0 }}-id" value="{{ form.instance.pk }}">
|
||||
{% endif %}
|
||||
<div class="row g-2">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">{{ form.kit.label }}</label>
|
||||
{{ form.kit }}
|
||||
{% if form.kit.errors %}
|
||||
<div class="text-danger small">{{ form.kit.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Динамически генерируемые поля для атрибутов варианта -->
|
||||
{% for field in form %}
|
||||
{% if "attribute_" in field.name %}
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% if field.errors %}
|
||||
<div class="text-danger small">{{ field.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small d-block">{{ form.is_default.label }}</label>
|
||||
<div class="form-check form-switch">
|
||||
{{ form.is_default }}
|
||||
<label class="form-check-label" for="{{ form.is_default.id_for_label }}">
|
||||
<span class="default-switch-label">{% if form.instance.is_default %}Да{% else %}Нет{% endif %}</span>
|
||||
</label>
|
||||
</div>
|
||||
{% if form.is_default.errors %}
|
||||
<div class="text-danger small">{{ form.is_default.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
{% if option_formset.can_delete %}
|
||||
<label class="form-label small d-block"> </label>
|
||||
{{ form.DELETE }}
|
||||
<label for="{{ form.DELETE.id_for_label }}" class="btn btn-sm btn-outline-danger d-block">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addOptionBtn">
|
||||
<i class="bi bi-plus-circle me-1"></i>Добавить вариант
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Management form для option_formset (скрыт) -->
|
||||
{{ option_formset.management_form }}
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
@@ -296,169 +224,6 @@ input[name*="DELETE"] {
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Добавление новых форм вариантов
|
||||
document.getElementById('addOptionBtn').addEventListener('click', function() {
|
||||
const container = document.getElementById('optionFormsetContainer');
|
||||
const totalForms = document.querySelector('[name="options-TOTAL_FORMS"]');
|
||||
const formIdx = parseInt(totalForms.value);
|
||||
|
||||
// Получаем первую существующую форму чтобы узнать какие атрибуты нужны
|
||||
const firstForm = container.querySelector('.option-form');
|
||||
let attributesHtml = '';
|
||||
|
||||
if (firstForm) {
|
||||
// Ищем поля атрибутов в первой форме
|
||||
const attributeFields = firstForm.querySelectorAll('select[data-attribute-name]');
|
||||
attributeFields.forEach(field => {
|
||||
const attrName = field.getAttribute('data-attribute-name');
|
||||
const options = field.innerHTML;
|
||||
const colWidth = attributeFields.length > 2 ? 'col-md-1.5' : 'col-md-2';
|
||||
attributesHtml += `
|
||||
<div class="${colWidth}">
|
||||
<label class="form-label small">${attrName}</label>
|
||||
<select name="options-${formIdx}-attribute_${attrName}"
|
||||
id="id_options-${formIdx}-attribute_${attrName}"
|
||||
class="form-select"
|
||||
data-attribute-name="${attrName}">
|
||||
<option value="">---------</option>
|
||||
${options}
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
// Создаём новую форму HTML
|
||||
const newFormHtml = `
|
||||
<div class="option-form border rounded p-3 mb-3" style="background: #f8f9fa;">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Комплект</label>
|
||||
<select name="options-${formIdx}-kit" id="id_options-${formIdx}-kit" class="form-select">
|
||||
<option value="">---------</option>
|
||||
{% for kit in option_formset.empty_form.fields.kit.queryset %}
|
||||
<option value="{{ kit.id }}">{{ kit.name }}{% if kit.sku %} ({{ kit.sku }}){% endif %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
${attributesHtml}
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small d-block">По умолчанию</label>
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" name="options-${formIdx}-is_default"
|
||||
id="id_options-${formIdx}-is_default"
|
||||
class="form-check-input is-default-switch" role="switch">
|
||||
<label class="form-check-label" for="id_options-${formIdx}-is_default">
|
||||
<span class="default-switch-label">Нет</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-1.5">
|
||||
<label class="form-label small d-block"> </label>
|
||||
<input type="checkbox" name="options-${formIdx}-DELETE"
|
||||
id="id_options-${formIdx}-DELETE"
|
||||
style="display:none;">
|
||||
<label for="id_options-${formIdx}-DELETE" class="btn btn-sm btn-outline-danger d-block">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.insertAdjacentHTML('beforeend', newFormHtml);
|
||||
totalForms.value = formIdx + 1;
|
||||
|
||||
// Переинициализируем логику switch после добавления новой формы
|
||||
initDefaultSwitches();
|
||||
});
|
||||
|
||||
// Скрытие удаленных форм
|
||||
document.addEventListener('change', function(e) {
|
||||
if (e.target.type === 'checkbox' && e.target.name && e.target.name.includes('DELETE')) {
|
||||
const form = e.target.closest('.option-form');
|
||||
if (e.target.checked) {
|
||||
form.style.opacity = '0.5';
|
||||
form.style.textDecoration = 'line-through';
|
||||
} else {
|
||||
form.style.opacity = '1';
|
||||
form.style.textDecoration = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Логика для switch "По умолчанию"
|
||||
function initDefaultSwitches() {
|
||||
const container = document.getElementById('optionFormsetContainer');
|
||||
|
||||
// Функция для обновления текста label
|
||||
function updateSwitchLabel(switchInput) {
|
||||
const label = switchInput.closest('.form-check').querySelector('.default-switch-label');
|
||||
if (label) {
|
||||
label.textContent = switchInput.checked ? 'Да' : 'Нет';
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для проверки и установки единственного варианта по умолчанию
|
||||
function ensureSingleDefault() {
|
||||
const visibleSwitches = Array.from(container.querySelectorAll('.is-default-switch')).filter(sw => {
|
||||
const form = sw.closest('.option-form');
|
||||
const deleteCheckbox = form.querySelector('input[name*="DELETE"]');
|
||||
return !deleteCheckbox || !deleteCheckbox.checked;
|
||||
});
|
||||
|
||||
// Если только один вариант - включаем его автоматически
|
||||
if (visibleSwitches.length === 1) {
|
||||
visibleSwitches[0].checked = true;
|
||||
visibleSwitches[0].disabled = true;
|
||||
updateSwitchLabel(visibleSwitches[0]);
|
||||
} else {
|
||||
// Если вариантов несколько - убираем disabled
|
||||
visibleSwitches.forEach(sw => {
|
||||
sw.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Проверяем, есть ли хотя бы один включенный
|
||||
const hasChecked = visibleSwitches.some(sw => sw.checked);
|
||||
if (!hasChecked && visibleSwitches.length > 0) {
|
||||
// Если ни один не включен, включаем первый
|
||||
visibleSwitches[0].checked = true;
|
||||
updateSwitchLabel(visibleSwitches[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Обработчик изменения switch
|
||||
container.addEventListener('change', function(e) {
|
||||
if (e.target.classList.contains('is-default-switch')) {
|
||||
if (e.target.checked) {
|
||||
// Выключаем все остальные
|
||||
const allSwitches = container.querySelectorAll('.is-default-switch');
|
||||
allSwitches.forEach(sw => {
|
||||
if (sw !== e.target) {
|
||||
sw.checked = false;
|
||||
updateSwitchLabel(sw);
|
||||
}
|
||||
});
|
||||
}
|
||||
updateSwitchLabel(e.target);
|
||||
ensureSingleDefault();
|
||||
}
|
||||
|
||||
// При изменении DELETE тоже проверяем
|
||||
if (e.target.name && e.target.name.includes('DELETE')) {
|
||||
ensureSingleDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Инициализация при загрузке
|
||||
ensureSingleDefault();
|
||||
container.querySelectorAll('.is-default-switch').forEach(updateSwitchLabel);
|
||||
}
|
||||
|
||||
// Запускаем инициализацию
|
||||
initDefaultSwitches();
|
||||
|
||||
// === Управление параметрами товара (карточный интерфейс) ===
|
||||
|
||||
// Функция для добавления нового поля значения параметра с выбором ProductKit
|
||||
|
||||
Reference in New Issue
Block a user