Добавлены кнопки создания вариативных товаров и групп вариантов на страницы /products/all/ и /products/catalog/. Улучшен табличный режим каталога с фиксированной сеткой колонок, двухстрочными названиями и выравниванием по всей ширине

This commit is contained in:
2026-01-06 14:10:37 +03:00
parent 288716deba
commit 5d6b894ca6
3 changed files with 142 additions and 61 deletions

View File

@@ -69,7 +69,7 @@
font-size: 0.65rem;
padding: 0.15rem 0.35rem;
}
/* Режим списка */
/* Режим списка - табличная структура с фиксированными колонками */
.catalog-list.row {
gap: 1px !important;
}
@@ -79,19 +79,22 @@
padding: 0 !important;
}
.catalog-list .catalog-item .card {
flex-direction: row;
align-items: stretch;
display: grid;
grid-template-columns: 50px 1fr 150px 120px 120px;
align-items: center;
padding: 0;
border-radius: 0.25rem;
overflow: hidden;
min-height: 50px;
}
.catalog-list .catalog-item .card .position-relative {
flex-shrink: 0;
height: 50px;
width: 50px;
align-self: start;
}
.catalog-list .catalog-item .card .position-relative img,
.catalog-list .catalog-item .card .position-relative > div {
height: 100% !important;
min-height: 36px;
height: 50px;
width: 50px;
border-radius: 0;
object-fit: cover;
@@ -100,20 +103,40 @@
display: none;
}
.catalog-list .catalog-item .card-body {
flex-grow: 1;
display: flex;
align-items: center;
padding: 0 0.5rem !important;
gap: 0.75rem;
display: contents;
}
.catalog-list .catalog-item .card-body > div:first-child {
flex-grow: 1;
/* Название товара - ограничиваем 2 строками */
.catalog-list .catalog-item .product-name {
padding: 0.4rem 0.5rem;
line-height: 1.3;
max-height: 3.4em;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
font-size: 0.85rem;
align-self: center;
}
.catalog-list .catalog-item .card-body > div:last-child {
display: flex;
align-items: center;
gap: 0.75rem;
white-space: nowrap;
/* Остатки */
.catalog-list .catalog-item .product-stock {
padding: 0.4rem 0.25rem;
text-align: left;
font-size: 0.8rem;
align-self: center;
}
/* Цена */
.catalog-list .catalog-item .product-price {
padding: 0.4rem 0.25rem;
text-align: left;
font-size: 0.85rem;
align-self: center;
}
/* Артикул */
.catalog-list .catalog-item .product-sku {
padding: 0.4rem 0.5rem 0.4rem 0.25rem;
text-align: left;
font-size: 0.8rem;
align-self: center;
}
.catalog-list .catalog-item .card-body .mt-1 {
margin-top: 0 !important;
@@ -239,6 +262,20 @@
<!-- Сетка товаров справа -->
<div class="col-lg-8 col-md-7">
<!-- Кнопки действий -->
{% if action_buttons %}
<div class="mb-3">
<div class="btn-toolbar" role="toolbar">
{% for button in action_buttons %}
<a href="{{ button.url }}" class="btn {{ button.class|default:'btn-primary' }} btn-sm me-2 mb-2">
{% if button.icon %}<i class="bi bi-{{ button.icon }}"></i>{% endif %}
{{ button.text }}
</a>
{% endfor %}
</div>
</div>
{% endif %}
<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>
@@ -260,6 +297,7 @@
{% for item in items %}
<div class="col-12 catalog-item" data-type="{{ item.item_type }}" data-category-ids="{% for cat in item.cached_categories %}{{ cat.pk }}{% if not forloop.last %},{% endif %}{% endfor %}">
<div class="card h-100 shadow-sm border-0">
<!-- Фото -->
<div class="position-relative">
{% if item.cached_main_photo %}
<img src="{{ item.cached_main_photo.get_thumbnail_url }}" class="card-img-top" alt="{{ item.name }}" style="height: 120px; object-fit: cover;">
@@ -272,8 +310,10 @@
{% if item.item_type == 'kit' %}К{% else %}Т{% endif %}
</span>
</div>
<div class="card-body p-2">
<div class="small text-truncate fw-medium">
<!-- Название товара (2 строки max) -->
<div class="product-name fw-medium">
{% if item.item_type == 'kit' %}
<a href="{% url 'products:productkit-detail' item.pk %}" class="text-decoration-none">{{ item.name }}</a>
{% else %}
@@ -281,72 +321,59 @@
{% endif %}
</div>
<!-- Остатки -->
<div class="product-stock">
{% if item.item_type == 'product' %}
{# Информация об остатках для товаров #}
<div class="mt-1">
<small class="stock-info {% if item.total_free > 0 %}text-success{% else %}text-danger{% endif %}">
<i class="bi bi-box-seam"></i>
<strong>{{ item.total_free|floatformat:"-3" }}</strong> свободно
<span class="text-muted">/ {{ item.total_available|floatformat:"-3" }} всего</span>
<strong>{{ item.total_free|floatformat:"-3" }}</strong>
<span class="text-muted">/ {{ item.total_available|floatformat:"-3" }}</span>
</small>
</div>
{% elif item.item_type == 'kit' %}
{# Информация об остатках для комплектов #}
<div class="mt-1">
<small class="stock-info {% if item.total_free > 0 %}text-success{% else %}text-danger{% endif %}">
<i class="bi bi-box-seam"></i>
<strong>{{ item.total_free|floatformat:"0" }}</strong> компл. доступно
<strong>{{ item.total_free|floatformat:"0" }}</strong> компл.
</small>
</div>
{% endif %}
</div>
<div class="d-flex justify-content-between align-items-center mt-1">
<!-- Цена -->
<div class="product-price">
{% if item.item_type == 'product' %}
<div class="price-edit-container d-flex align-items-center gap-1 flex-wrap">
<div class="price-edit-container">
{% if item.sale_price %}
{# Скидочная цена #}
<span class="editable-price sale-price fw-bold text-success small"
data-product-id="{{ item.pk }}"
data-field="sale_price"
data-current-value="{{ item.sale_price }}"
title="Скидочная цена (клик для редактирования)">
{{ item.sale_price|floatformat:2 }} руб.
title="Скидочная цена">
{{ item.sale_price|floatformat:2 }}
</span>
{# Обычная цена зачеркнутая #}
<br>
<span class="editable-price regular-price text-muted text-decoration-line-through small"
data-product-id="{{ item.pk }}"
data-field="price"
data-current-value="{{ item.price }}"
title="Обычная цена (клик для редактирования)">
{{ item.price|floatformat:2 }} руб.
title="Обычная цена">
{{ item.price|floatformat:2 }}
</span>
{# Кнопка удаления скидки #}
<i class="bi bi-x-circle text-danger remove-sale-price"
data-product-id="{{ item.pk }}"
title="Убрать скидку"
style="cursor: pointer; font-size: 0.85rem;"></i>
{% else %}
{# Только обычная цена #}
<span class="editable-price fw-bold text-primary small"
data-product-id="{{ item.pk }}"
data-field="price"
data-current-value="{{ item.price }}"
title="Цена (клик для редактирования)">
title="Цена">
{{ item.price|floatformat:2 }} руб.
</span>
{# Кнопка добавления скидки #}
<button class="btn btn-outline-secondary btn-sm add-sale-price py-0 px-1"
data-product-id="{{ item.pk }}"
title="Добавить скидочную цену"
style="font-size: 0.7rem; line-height: 1.2;">
+ скидка
</button>
{% endif %}
</div>
{% else %}
{# ProductKit - не редактируется #}
<span class="fw-bold text-primary small">{{ item.actual_price|floatformat:2 }} руб.</span>
{% endif %}
</div>
<!-- Артикул -->
<div class="product-sku">
<small class="text-muted">{{ item.sku }}</small>
</div>
</div>

View File

@@ -6,6 +6,7 @@ from django.views.generic import ListView
from django.db.models import Prefetch, Sum, Value, DecimalField, Q
from django.db.models.functions import Coalesce
from django.core.paginator import Paginator
from django.urls import reverse_lazy
from ..models import Product, ProductKit, ProductCategory, ProductPhoto, ProductKitPhoto, KitItem
@@ -108,4 +109,41 @@ class CatalogView(LoginRequiredMixin, ListView):
category_tree = self.build_category_tree(categories, parent=None)
context['category_tree'] = category_tree
# Кнопки действий
action_buttons = []
if self.request.user.has_perm('products.add_product'):
action_buttons.append({
'url': reverse_lazy('products:product-create'),
'text': 'Создать товар',
'class': 'btn-primary',
'icon': 'plus-circle'
})
if self.request.user.has_perm('products.add_productkit'):
action_buttons.append({
'url': reverse_lazy('products:productkit-create'),
'text': 'Создать комплект',
'class': 'btn-outline-primary',
'icon': 'box-seam'
})
if self.request.user.has_perm('products.add_configurableproduct'):
action_buttons.append({
'url': reverse_lazy('products:configurableproduct-create'),
'text': 'Создать вариативный товар',
'class': 'btn-outline-success',
'icon': 'grid-3x3-gap'
})
if self.request.user.has_perm('products.add_productvariantgroup'):
action_buttons.append({
'url': reverse_lazy('products:variantgroup-create'),
'text': 'Создать группу вариантов',
'class': 'btn-outline-info',
'icon': 'collection'
})
context['action_buttons'] = action_buttons
return context

View File

@@ -402,6 +402,22 @@ class CombinedProductListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Lis
'icon': 'box-seam'
})
if self.request.user.has_perm('products.add_configurableproduct'):
action_buttons.append({
'url': reverse_lazy('products:configurableproduct-create'),
'text': 'Создать вариативный товар',
'class': 'btn-outline-success',
'icon': 'grid-3x3-gap'
})
if self.request.user.has_perm('products.add_productvariantgroup'):
action_buttons.append({
'url': reverse_lazy('products:variantgroup-create'),
'text': 'Создать группу вариантов',
'class': 'btn-outline-info',
'icon': 'collection'
})
context['action_buttons'] = action_buttons
return context