diff --git a/backend/requests_app/enums.py b/backend/requests_app/enums.py index be1ba10e..afd60865 100644 --- a/backend/requests_app/enums.py +++ b/backend/requests_app/enums.py @@ -7,6 +7,7 @@ class RequestType(TextChoices): VACATION = "vacation", "Отпуск" SICK_LEAVE = "sick_leave", "Больничный" + DAY_OFF = "day_off", "Отгул" TRANSFER = "transfer", "Перевод" DISMISSAL = "dismissal", "Увольнение" OTHER = "other", "Другое" diff --git a/backend/requests_app/migrations/0009_alter_request_type.py b/backend/requests_app/migrations/0009_alter_request_type.py new file mode 100644 index 00000000..1361513b --- /dev/null +++ b/backend/requests_app/migrations/0009_alter_request_type.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.4 on 2025-12-29 06:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('requests_app', '0008_migrate_department_to_departments'), + ] + + operations = [ + migrations.AlterField( + model_name='request', + name='type', + field=models.CharField(choices=[('vacation', 'Отпуск'), ('sick_leave', 'Больничный'), ('day_off', 'Отгул'), ('transfer', 'Перевод'), ('dismissal', 'Увольнение'), ('other', 'Другое')], max_length=32, verbose_name='Тип заявления'), + ), + ] diff --git a/backend/static/css/components/page-header.css b/backend/static/css/components/page-header.css index a53b07db..bf375244 100644 --- a/backend/static/css/components/page-header.css +++ b/backend/static/css/components/page-header.css @@ -257,18 +257,60 @@ /* Мобильные */ @media (max-width: 575.98px) { + .page-header { + margin-bottom: var(--space-2); + padding: var(--space-1) 0; + top: var(--navbar-h, 60px); + } + .page-title { - font-size: 1.25rem; + font-size: 1.125rem; + font-weight: 700; } .page-count { - font-size: 0.75rem; + font-size: 0.7rem; } + .page-scope-switch { + gap: 0.25rem; + } + .page-scope-switch .btn { - min-width: 50px; - font-size: 0.75rem; - padding: 0.25rem 0.5rem; + min-width: auto; + font-size: 0.7rem; + padding: 0.2rem 0.4rem; + } + + .page-filters { + margin-top: 0.5rem; + } + + .page-filters .btn { + padding: 0.4rem 0.75rem; + font-size: 0.85rem; + } + + .page-search { + margin-top: 0.5rem; + } + + /* Скрываем счётчик на очень маленьких экранах */ + @media (max-width: 480px) { + .page-count { + display: none; + } + + .page-header .card { + margin-top: 0; + width: 100%; + } + + .page-header .card button.dept-row, + .page-header .card a.dept-row { + padding: var(--space-1) var(--space-1); + font-size: 0.9rem; + } } } diff --git a/backend/static/css/components/request-list.css b/backend/static/css/components/request-list.css index b0091356..58de7fd9 100644 --- a/backend/static/css/components/request-list.css +++ b/backend/static/css/components/request-list.css @@ -191,19 +191,19 @@ minmax(100px, 1fr) /* Автор */ 90px /* Даты */ 100px /* Статус */ - 70px; /* Создано + Действия объединены */ + 70px; /* Создано */ } - /* Прячем отдельную колонку действий */ - #reqList .req-table-header .req-cell-actions, - #reqList .req-cell-actions { + /* На планшетах скрываем колонку действий как отдельное поле */ + #reqList .req-table-header .req-cell-actions { display: none; } - /* Добавляем действия в строку заголовка */ - #reqList .req-cell-title .req-cell-content::after { - content: ''; - display: block; + /* Но сами кнопки остаются видимыми */ + #reqList .req-cell-actions { + grid-column: 6; + display: flex !important; + justify-content: flex-end; } } @@ -213,57 +213,156 @@ #reqList .req-table-header { display: none; } - + + /* Переходим на flexbox layout */ #reqList .req-table-row { - grid-template-columns: 36px 1fr; - gap: 0.5rem; - padding: 0.75rem; + display: flex; + flex-direction: row; + flex-wrap: wrap; + padding: 1rem 0.75rem; + gap: 0.75rem; + align-items: flex-start; } - - /* Все ячейки в две колонки */ + + /* Сброс для всех ячеек */ + #reqList .req-cell { + display: block; + min-width: 0; + } + + /* Сброс для контента внутри ячеек */ + #reqList .req-cell-content { + width: 100%; + } + + /* Иконка слева сверху */ #reqList .req-cell-icon { - grid-row: 1 / span 3; - align-self: start; + flex: 0 0 36px; + padding-top: 0.25rem; } - + #reqList .req-cell-icon .card-icon { - width: 36px; - height: 36px; + width: 32px; + height: 32px; } + /* Основной контент - заголовок занимает полную ширину минус иконка */ #reqList .req-cell-title { - grid-column: 2; + flex: 1 1 calc(100% - 48px); font-size: 0.9rem; } - - #reqList .req-cell-author { - grid-column: 2; + + #reqList .req-cell-title strong { + display: block; + margin-bottom: 0.25rem; + font-size: 0.95rem; + } + + #reqList .req-cell-title .badge { + display: inline-block; + font-size: 0.7rem; + padding: 0.2rem 0.4rem; + } + + #reqList .req-cell-title .small { + display: block; 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 .req-cell-author, + #reqList .req-cell-dates, + #reqList .req-cell-status { + flex: 0 1 auto; + font-size: 0.8rem; + } + + #reqList .req-cell-author { + flex: 0 1 auto; + } + + #reqList .req-cell-dates { + flex: 0 1 auto; + white-space: nowrap; + } + + #reqList .req-cell-dates br { + display: none; + } + + #reqList .req-cell-status { + flex: 0 1 auto; + } + + #reqList .req-cell-status .badge { + font-size: 0.7rem; + padding: 0.25rem 0.5rem; + white-space: nowrap; } + /* Скрываем дату создания на мобильных */ + #reqList .req-cell-created { + display: none; + } + + /* Кнопки действий занимают полную строку */ + #reqList .req-cell-actions { + display: flex !important; + flex: 1 1 100%; + gap: 0.5rem; + padding-top: 0.75rem; + border-top: 1px solid var(--bs-border-color-translucent); + margin: 0; + } + + #reqList .req-cell-actions .req-cell-content { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + width: 100%; + } + + #reqList .req-cell-actions .btn { + padding: 0.5rem 0.75rem; + font-size: 0.85rem; + } + + /* Футер с комментариями занимает полную ширину */ + #reqList .req-footer { + flex: 1 1 100%; + margin-left: 0; + padding: 0.75rem 0; + border-top: 1px solid var(--bs-border-color-translucent); + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + } + + #reqList .req-footer .btn { + padding: 0.4rem 0.6rem; + font-size: 0.8rem; + } + + /* Блок комментариев */ #reqList .comments-block { margin-left: 0; padding: 0.75rem; } + + /* Строка заявления */ + #reqList .req-row { + padding: 0; + border-bottom: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 50%); + } + + #reqList .req-row:hover { + background-color: transparent; + } + + #reqList .req-row:last-child { + border-bottom: 0; + } } /* Компактный режим для маленьких экранов */ diff --git a/backend/static/js/components/requestCrudHandler.js b/backend/static/js/components/requestCrudHandler.js index d9776fa0..c666a2b5 100644 --- a/backend/static/js/components/requestCrudHandler.js +++ b/backend/static/js/components/requestCrudHandler.js @@ -303,7 +303,11 @@ export function initRequestCrudHandler(options) { try { const response = await fetch(`${apiDetailBase}${reqId}/approve/`, { method: 'POST', - headers: headers + headers: { + ...headers, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({}) }); if (!response.ok) { @@ -330,7 +334,11 @@ export function initRequestCrudHandler(options) { try { const response = await fetch(`${apiDetailBase}${reqId}/reject/`, { method: 'POST', - headers: headers + headers: { + ...headers, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({}) }); if (!response.ok) { @@ -361,7 +369,11 @@ export function initRequestCrudHandler(options) { try { const response = await fetch(`${apiDetailBase}${reqId}/cancel/`, { method: 'POST', - headers: headers + headers: { + ...headers, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({}) }); if (!response.ok) { diff --git a/backend/static/js/components/requestListHandler.js b/backend/static/js/components/requestListHandler.js index c7c2d5f0..2821d497 100644 --- a/backend/static/js/components/requestListHandler.js +++ b/backend/static/js/components/requestListHandler.js @@ -47,6 +47,18 @@ export function initRequestListHandler(options) { // Intersection Observer для бесконечной прокрутки let observerTarget = null; let observer = null; + + // Мобильная оптимизация: слушаем сетевые события + let isOnline = navigator.onLine; + window.addEventListener('online', () => { + isOnline = true; + console.log('Network restored'); + }); + window.addEventListener('offline', () => { + isOnline = false; + console.log('Network lost'); + listElement.innerHTML = '