refactor(requests): настоящий табличный вид с фиксированными колонками

Структура таблицы:
- Иконка (48px) - тип заявления
- Заголовок (расширяемая, 2fr) - название, тип, превью комментария
- Автор (1fr) - имя сотрудника с ссылкой
- Даты (100px) - date_from и date_to
- Статус (110px) - badge со статусом
- Создано (80px) - дата и время создания
- Действия (80px) - кнопки одобрить/отклонить/редактировать

Особенности:
- Grid layout с фиксированными шириной колонок
- Автоматический перенос текста в пределах ячейки (word-wrap)
- Выравнивание всех элементов по сетке
- Футер с комментариями выровнен с содержимым
- Адаптивный дизайн (планшеты, мобильные)
- На мобильных - две колонки (иконка + остальное)
This commit is contained in:
admin i 2025-12-03 07:39:10 +03:00
parent fd4bde67e0
commit 3ce95a1458
2 changed files with 277 additions and 171 deletions

View File

@ -1,13 +1,8 @@
/**
* request-list.css
*
* Табличный вид списка заявлений без разделяющих линий
* между элементами, но с чистой сеткой как в таблице.
*
* Используется:
* - card-list.css для базовой структуры карточек
* - message-inputs.scss для форм комментариев
* - common.css для утилит и бейджей
* Настоящий табличный вид списка заявлений.
* Все поля идут в одну строку с фиксированной шириной и переносом текста.
*/
/* Контейнер списка заявлений */
@ -17,7 +12,7 @@
padding: 0;
}
/* Строка заявления - табличный вид */
/* Строка заявления */
#reqList .req-row {
padding: 0;
margin: 0;
@ -33,41 +28,108 @@
border-bottom: 0;
}
/* Заголовок заявления - сетка с выравниванием колонок */
#reqList .req-row > .card-header {
/* Табличная сетка */
#reqList .req-table-row {
display: grid;
grid-template-columns: 42px 1fr auto;
grid-template-columns:
48px /* Иконка */
minmax(200px, 2fr) /* Заголовок (расширяемая) */
minmax(120px, 1fr) /* Автор */
100px /* Даты */
110px /* Статус */
80px /* Создано */
80px; /* Действия */
gap: 0.75rem;
align-items: start;
padding: 0.75rem 1rem;
}
/* Ячейка таблицы */
#reqList .req-cell {
min-width: 0; /* Для корректного word-wrap */
display: flex;
align-items: center;
gap: 1rem;
padding: 0.875rem 1rem;
border-bottom: none;
background: transparent;
}
/* Иконка типа заявления */
#reqList .card-icon {
width: 42px;
height: 42px;
flex: 0 0 auto;
/* Контент ячейки с переносом текста */
#reqList .req-cell-content {
width: 100%;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
hyphens: auto;
line-height: 1.4;
}
/* Метаданные заявления */
#reqList .card-meta {
min-width: 0; /* Для корректного truncate */
/* Специфичные стили для ячеек */
#reqList .req-cell-icon {
justify-content: center;
}
/* Тело карточки (комментарий) */
#reqList .req-row > .card-body {
padding: 0 1rem 0.875rem 1rem;
margin-left: calc(42px + 1rem); /* Выравнивание с текстом */
border-bottom: none;
#reqList .req-cell-icon .card-icon {
width: 38px;
height: 38px;
flex-shrink: 0;
}
/* Футер с кнопками действий (комментарии, вложения) */
#reqList .req-row > .card-actions {
#reqList .req-cell-title {
font-size: 0.9rem;
}
#reqList .req-cell-title strong {
display: block;
margin-bottom: 0.125rem;
}
#reqList .req-cell-title .badge {
font-size: 0.7rem;
padding: 0.2rem 0.4rem;
}
#reqList .req-cell-author {
font-size: 0.875rem;
}
#reqList .req-cell-dates {
font-size: 0.8rem;
font-variant-numeric: tabular-nums;
line-height: 1.3;
}
#reqList .req-cell-status {
justify-content: flex-start;
}
#reqList .req-cell-status .badge {
font-size: 0.7rem;
padding: 0.25rem 0.5rem;
white-space: nowrap;
}
#reqList .req-cell-created {
font-size: 0.75rem;
text-align: center;
}
#reqList .req-cell-actions {
justify-content: center;
gap: 0.25rem;
}
#reqList .req-cell-actions .req-cell-content {
display: flex;
gap: 0.25rem;
justify-content: center;
}
/* Футер с комментариями и вложениями */
#reqList .req-footer {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
margin-left: calc(42px + 1rem); /* Выравнивание с текстом */
border-top: none; /* Убираем разделяющую линию */
margin-left: calc(48px + 0.75rem); /* Выравнивание с содержимым */
border-top: none;
background: transparent;
}
@ -77,67 +139,103 @@
background: color-mix(in srgb, var(--bs-tertiary-bg), transparent 50%);
margin: 0;
padding: 1rem;
margin-left: calc(48px + 0.75rem); /* Выравнивание с содержимым */
}
/* Отступ для списка комментариев */
#reqList .comments-list {
margin-left: calc(42px + 1rem); /* Выравнивание с текстом заявления */
/* Адаптивность для планшетов */
@media (max-width: 992px) {
#reqList .req-table-row {
grid-template-columns:
42px /* Иконка */
minmax(150px, 2fr) /* Заголовок */
minmax(100px, 1fr) /* Автор */
90px /* Даты */
100px /* Статус */
70px; /* Создано + Действия объединены */
}
/* Прячем отдельную колонку действий */
#reqList .req-cell-actions {
display: none;
}
/* Добавляем действия в строку заголовка */
#reqList .req-cell-title .req-cell-content::after {
content: '';
display: block;
}
}
/* Форма нового комментария */
#reqList .comment-new {
margin-left: calc(42px + 1rem); /* Выравнивание с текстом заявления */
}
/* Адаптивность для мобильных устройств */
/* Адаптивность для мобильных */
@media (max-width: 768px) {
#reqList .req-row > .card-header {
#reqList .req-table-row {
grid-template-columns: 36px 1fr;
gap: 0.75rem;
gap: 0.5rem;
padding: 0.75rem;
}
#reqList .card-icon {
/* Все ячейки в две колонки */
#reqList .req-cell-icon {
grid-row: 1 / span 3;
align-self: start;
}
#reqList .req-cell-icon .card-icon {
width: 36px;
height: 36px;
}
#reqList .req-row > .card-body {
padding: 0 0.75rem 0.75rem 0.75rem;
margin-left: calc(36px + 0.75rem);
}
#reqList .req-row > .card-actions {
padding: 0.5rem 0.75rem;
margin-left: calc(36px + 0.75rem);
}
#reqList .comments-list,
#reqList .comment-new {
margin-left: 0; /* На мобильных убираем отступ */
}
/* Кнопки действий переносятся в следующую строку */
#reqList .req-row > .card-header {
grid-template-columns: 36px 1fr;
}
#reqList .req-row > .card-header > .ms-auto {
#reqList .req-cell-title {
grid-column: 2;
margin-top: 0.5rem;
margin-left: 0 !important;
justify-self: start;
font-size: 0.9rem;
}
#reqList .req-cell-author {
grid-column: 2;
font-size: 0.8rem;
margin-top: -0.25rem;
}
#reqList .req-cell-dates,
#reqList .req-cell-status,
#reqList .req-cell-created {
grid-column: 2;
font-size: 0.75rem;
}
#reqList .req-cell-actions {
grid-column: 2;
justify-content: flex-start;
margin-top: 0.25rem;
}
#reqList .req-footer {
margin-left: calc(36px + 0.5rem);
padding: 0.5rem 0.75rem;
}
#reqList .comments-block {
margin-left: 0;
padding: 0.75rem;
}
}
/* Дополнительные утилиты */
#reqList .card-title {
font-size: 0.95rem;
line-height: 1.4;
}
#reqList .card-subtitle {
font-size: 0.85rem;
color: var(--bs-secondary-color);
/* Компактный режим для маленьких экранов */
@media (max-width: 576px) {
#reqList .req-table-row {
padding: 0.625rem;
}
#reqList .req-cell-title .small {
display: none; /* Скрываем превью комментария */
}
#reqList .req-cell-dates br {
display: none; /* Даты в одну строку */
}
#reqList .req-cell-dates .req-cell-content::after {
content: ' — ';
}
}

View File

@ -26,28 +26,27 @@
{% if requests %}
{% for obj in requests %}
<article class="req-row" id="req-{{ obj.id }}">
<header class="card-header">
<!-- Иконка типа заявления -->
<div class="card-icon d-flex align-items-center justify-content-center">
{% with tp=obj.type|lower %}
{% if tp == 'vacation' %}<i class="bi-calendar-check text-primary"></i>
{% elif tp == 'sick_leave' %}<i class="bi-capsule text-danger"></i>
{% elif tp == 'transfer' %}<i class="bi-box-arrow-right text-info"></i>
{% elif tp == 'dismissal' %}<i class="bi-door-open text-warning"></i>
{% else %}<i class="bi-clipboard-check text-secondary"></i>
{% endif %}
{% endwith %}
<div class="req-table-row">
<!-- Иконка типа -->
<div class="req-cell req-cell-icon">
<div class="card-icon d-flex align-items-center justify-content-center">
{% with tp=obj.type|lower %}
{% if tp == 'vacation' %}<i class="bi-calendar-check text-primary"></i>
{% elif tp == 'sick_leave' %}<i class="bi-capsule text-danger"></i>
{% elif tp == 'transfer' %}<i class="bi-box-arrow-right text-info"></i>
{% elif tp == 'dismissal' %}<i class="bi-door-open text-warning"></i>
{% else %}<i class="bi-clipboard-check text-secondary"></i>
{% endif %}
{% endwith %}
</div>
</div>
<!-- Основная информация -->
<div class="card-meta">
<div class="card-title d-flex align-items-center gap-2 flex-wrap">
<!-- Заголовок заявления -->
<!-- Заголовок и тип -->
<div class="req-cell req-cell-title">
<div class="req-cell-content">
<strong>{{ obj.title|default:"Без заголовка" }}</strong>
<!-- Тип заявления -->
{% if obj.type %}{% with tp=obj.type|lower %}
<span class="badge text-bg-light">
<span class="badge text-bg-light ms-1">
{% if tp == 'vacation' %}Отпуск
{% elif tp == 'sick_leave' %}Больничный
{% elif tp == 'transfer' %}Перевод
@ -56,29 +55,15 @@
{% else %}{{ obj.type }}{% endif %}
</span>
{% endwith %}{% endif %}
<!-- Статус -->
{% if obj.status %}{% with st=obj.status|lower %}
<span class="badge
{% if st == 'approved' %}text-bg-success
{% elif st == 'rejected' %}text-bg-danger
{% elif st == 'pending' %}text-bg-warning
{% elif st == 'cancelled' %}text-bg-secondary
{% elif st == 'draft' %}text-bg-info
{% else %}text-bg-light{% endif %}">
{% if st == 'draft' %}Черновик
{% elif st == 'pending' %}На рассмотрении
{% elif st == 'approved' %}Одобрено
{% elif st == 'rejected' %}Отклонено
{% elif st == 'cancelled' %}Отменено
{% else %}{{ obj.status }}{% endif %}
</span>
{% endwith %}{% endif %}
{% if obj.comment %}
<div class="small text-body-secondary mt-1">{{ obj.comment|truncatewords:15 }}</div>
{% endif %}
</div>
<!-- Дополнительная информация -->
<div class="card-subtitle">
<!-- Автор -->
</div>
<!-- Автор -->
<div class="req-cell req-cell-author">
<div class="req-cell-content">
{% if obj.employee %}
{% if obj.employee.id == user.id %}
<a href="{% url 'employees:profile' %}" class="text-decoration-none">Вы</a>
@ -88,67 +73,90 @@
</a>
{% endif %}
{% endif %}
<!-- Период -->
</div>
</div>
<!-- Период -->
<div class="req-cell req-cell-dates">
<div class="req-cell-content text-mono">
{% if obj.date_from or obj.date_to %}
<span>·</span>
<span class="text-mono">
{{ obj.date_from|date:"d.m.Y"|default:"—" }} → {{ obj.date_to|date:"d.m.Y"|default:"—" }}
</span>
{% endif %}
<!-- Дата создания -->
{% if obj.created_at %}
<span>·</span>
<span>{{ obj.created_at|date:"d.m.Y H:i" }}</span>
{{ obj.date_from|date:"d.m.Y"|default:"—" }}<br>
{{ obj.date_to|date:"d.m.Y"|default:"—" }}
{% else %}
—
{% endif %}
</div>
</div>
<!-- Кнопки действий -->
{% if obj.employee.id != user.id and can_process %}
<div class="ms-auto d-flex gap-1">
<button type="button" class="btn btn-ghost btn-sm text-success"
data-bs-toggle="modal" data-bs-target="#reqApproveModal"
data-id="{{ obj.id }}" title="Одобрить">
<i class="bi-hand-thumbs-up"></i>
</button>
<button type="button" class="btn btn-ghost btn-sm text-danger"
data-bs-toggle="modal" data-bs-target="#reqRejectModal"
data-id="{{ obj.id }}" title="Отклонить">
<i class="bi-hand-thumbs-down"></i>
</button>
<!-- Статус -->
<div class="req-cell req-cell-status">
<div class="req-cell-content">
{% if obj.status %}{% with st=obj.status|lower %}
<span class="badge
{% if st == 'approved' %}text-bg-success
{% elif st == 'rejected' %}text-bg-danger
{% elif st == 'pending' %}text-bg-warning
{% elif st == 'cancelled' %}text-bg-secondary
{% elif st == 'draft' %}text-bg-info
{% else %}text-bg-light{% endif %}">
{% if st == 'draft' %}Черновик
{% elif st == 'pending' %}На рассмотр.
{% elif st == 'approved' %}Одобрено
{% elif st == 'rejected' %}Отклонено
{% elif st == 'cancelled' %}Отменено
{% else %}{{ obj.status }}{% endif %}
</span>
{% endwith %}{% endif %}
</div>
</div>
{% elif obj.employee.id == user.id %}
<div class="ms-auto d-flex gap-1">
<button type="button" class="btn btn-ghost btn-sm text-primary"
data-bs-toggle="modal" data-bs-target="#reqEditModal"
data-id="{{ obj.id }}"
data-title="{{ obj.title|default_if_none:'' }}"
data-type="{{ obj.type|default_if_none:'' }}"
data-date_from="{{ obj.date_from|default_if_none:'' }}"
data-date_to="{{ obj.date_to|default_if_none:'' }}"
data-comment="{{ obj.comment|default_if_none:'' }}"
data-status="{{ obj.status|lower }}" title="Изменить">
<i class="bi-pencil"></i>
</button>
<button type="button" class="btn btn-ghost btn-sm text-secondary"
data-bs-toggle="modal" data-bs-target="#reqCancelModal"
data-id="{{ obj.id }}" title="Отменить">
<i class="bi-slash-circle"></i>
</button>
<!-- Дата создания -->
<div class="req-cell req-cell-created">
<div class="req-cell-content small text-body-secondary">
{{ obj.created_at|date:"d.m.Y" }}<br>
{{ obj.created_at|date:"H:i" }}
</div>
</div>
{% endif %}
</header>
<!-- Действия -->
<div class="req-cell req-cell-actions">
<div class="req-cell-content">
{% if obj.employee.id != user.id and can_process %}
<button type="button" class="btn btn-ghost btn-sm text-success"
data-bs-toggle="modal" data-bs-target="#reqApproveModal"
data-id="{{ obj.id }}" title="Одобрить">
<i class="bi-hand-thumbs-up"></i>
</button>
<button type="button" class="btn btn-ghost btn-sm text-danger"
data-bs-toggle="modal" data-bs-target="#reqRejectModal"
data-id="{{ obj.id }}" title="Отклонить">
<i class="bi-hand-thumbs-down"></i>
</button>
{% elif obj.employee.id == user.id %}
<button type="button" class="btn btn-ghost btn-sm text-primary"
data-bs-toggle="modal" data-bs-target="#reqEditModal"
data-id="{{ obj.id }}"
data-title="{{ obj.title|default_if_none:'' }}"
data-type="{{ obj.type|default_if_none:'' }}"
data-date_from="{{ obj.date_from|default_if_none:'' }}"
data-date_to="{{ obj.date_to|default_if_none:'' }}"
data-comment="{{ obj.comment|default_if_none:'' }}"
data-status="{{ obj.status|lower }}" title="Изменить">
<i class="bi-pencil"></i>
</button>
<button type="button" class="btn btn-ghost btn-sm text-secondary"
data-bs-toggle="modal" data-bs-target="#reqCancelModal"
data-id="{{ obj.id }}" title="Отменить">
<i class="bi-slash-circle"></i>
</button>
{% endif %}
</div>
</div>
</div>
{% if obj.comment %}
<div class="card-body">
<div class="small text-body-secondary">{{ obj.comment }}</div>
</div>
{% endif %}
<footer class="card-actions">
<button class="btn btn-ghost d-inline-flex align-items-center gap-1"
<!-- Футер с комментариями -->
<footer class="req-footer">
<button class="btn btn-ghost btn-sm d-inline-flex align-items-center gap-1"
type="button"
data-bs-toggle="collapse"
data-bs-target="#rcoll-{{ obj.id }}"
@ -159,7 +167,7 @@
<span class="txt-close">Скрыть</span>
</button>
{% if obj.attachment %}
<a href="{{ obj.attachment }}" class="btn btn-ghost d-inline-flex align-items-center gap-1" target="_blank">
<a href="{{ obj.attachment }}" class="btn btn-ghost btn-sm d-inline-flex align-items-center gap-1" target="_blank">
<i class="bi-paperclip"></i><span>Вложение</span>
</a>
{% endif %}