From aa32439e8b61a8bcaffa720c0f22926d2e2a7434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=9D=D0=B0=D0=B4=D0=B5?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Tue, 2 Dec 2025 18:09:19 +0300 Subject: [PATCH] =?UTF-8?q?refactor(requests):=20=D0=A3=D0=BB=D1=83=D1=87?= =?UTF-8?q?=D1=88=D0=B5=D0=BD=20UX=20=D0=B2=D1=8B=D0=B1=D0=BE=D1=80=D0=B0?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B0=D1=82=D0=B5=D0=BB=D0=B5?= =?UTF-8?q?=D0=B9=20=D0=B2=20=D1=81=D1=82=D0=B8=D0=BB=D0=B5=20email-=D0=BA?= =?UTF-8?q?=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Переработан RecipientPicker компонент: ✨ Chip-based UI (как в Gmail/Outlook) - Пользователи отображаются как chips с крестиком для удаления - Autocomplete с живым поиском по имени/email - Dropdown с аватарами и должностями пользователей � Интуитивный интерфейс: - Структура To:/CC:/Отделы: (как в почтовых клиентах) - Получатели всегда видны (убраны collapse) - Кнопка '+ Копия' для показа поля CC - Inline флаг 'Всем сотрудникам' � Улучшенная верстка: - Модальные окна расширены до modal-lg - Получатели перенесены в начало (как тема в email) - Добавлены иконки для визуальной группировки - Добавлены emoji в типы заявлений для удобства � CSS обновлен: - Email-style компактный дизайн - Фокус-состояния для inputs - Hover-эффекты для dropdown items - Адаптивность для мобильных устройств - Поддержка темной темы � UX улучшения: - Поиск с автодополнением вместо select - Визуальные аватары пользователей (инициалы) - Показ должности и email в dropdown - Автофокус после добавления пользователя - Ограничение результатов поиска до 10 --- .../static/css/components/recipientPicker.css | 274 ++++++++-- .../static/js/components/recipientPicker.js | 481 +++++++++++++----- .../requests_app/request_list_full.html | 112 ++-- 3 files changed, 632 insertions(+), 235 deletions(-) diff --git a/backend/static/css/components/recipientPicker.css b/backend/static/css/components/recipientPicker.css index 92c67c4a..23a94928 100644 --- a/backend/static/css/components/recipientPicker.css +++ b/backend/static/css/components/recipientPicker.css @@ -1,67 +1,269 @@ -/* Recipient Picker Component Styles */ -.recipient-picker { - padding: 0; +/* Recipient Picker - Email-style Component */ +.recipient-picker-email { + font-size: 0.9375rem; } -.recipient-picker .form-label { +/* Строка получателей (To:/CC:/Отделы:) */ +.recipient-row { + display: flex; + align-items: flex-start; + gap: 0.75rem; + padding: 0.75rem 0; + border-bottom: 1px solid var(--bs-border-color); +} + +.recipient-row:last-child { + border-bottom: none; +} + +/* Метка (To:/CC:/Отделы:) */ +.recipient-label { + min-width: 80px; + padding-top: 0.5rem; + color: var(--bs-secondary); font-weight: 500; - margin-bottom: 0.5rem; + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0; } -.recipient-picker .form-label i { - color: var(--bs-primary); - margin-right: 0.25rem; +.recipient-label i { + font-size: 1.1em; } -.recipient-list { - min-height: 2rem; - padding: 0.5rem; - background-color: var(--bs-light); +/* Поле ввода */ +.recipient-field { + flex: 1; + display: flex; + align-items: flex-start; + gap: 0.5rem; + flex-wrap: wrap; +} + +/* Wrapper для chips + input */ +.recipient-input-wrapper { + flex: 1; + min-width: 300px; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.375rem; + padding: 0.375rem 0.5rem; + background: var(--bs-body-bg); border: 1px solid var(--bs-border-color); border-radius: var(--bs-border-radius); + position: relative; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } -.recipient-list .badge { - font-size: 0.875rem; - padding: 0.375rem 0.5rem; - display: inline-flex; - align-items: center; - gap: 0.25rem; +.recipient-input-wrapper:focus-within { + border-color: var(--bs-primary); + box-shadow: 0 0 0 0.15rem rgba(var(--bs-primary-rgb), 0.15); } -.recipient-list .btn-close-sm { - width: 0.75em; - height: 0.75em; - padding: 0; - background-size: 0.75em; +/* Input внутри wrapper */ +.recipient-input { + flex: 1; + min-width: 200px; + border: none; + outline: none; + padding: 0.25rem; + font-size: inherit; + background: transparent; +} + +.recipient-input::placeholder { + color: var(--bs-secondary-color); opacity: 0.7; } -.recipient-list .btn-close-sm:hover { +/* Chips (выбранные пользователи/отделы) */ +.recipient-chip { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.25rem 0.5rem; + background: var(--bs-primary); + color: white; + border-radius: 1rem; + font-size: 0.875rem; + white-space: nowrap; + transition: background-color 0.15s; +} + +.recipient-chip:hover { + background: var(--bs-primary-dark, #0056b3); +} + +.recipient-chip.chip-cc { + background: var(--bs-secondary); +} + +.recipient-chip.chip-cc:hover { + background: var(--bs-secondary-dark, #5a6268); +} + +.chip-remove { + background: none; + border: none; + padding: 0; + color: white; + cursor: pointer; + display: flex; + align-items: center; + opacity: 0.8; + transition: opacity 0.15s; +} + +.chip-remove:hover { opacity: 1; } -.recipient-picker select[multiple] { - min-height: 80px; +.chip-remove i { + font-size: 1.1em; + line-height: 1; } -.recipient-picker .form-check { - padding-left: 1.75rem; +/* Chips для отделов */ +.recipient-chips { + display: inline-flex; + flex-wrap: wrap; + gap: 0.375rem; + align-items: center; } -.recipient-picker .form-check-input { - margin-top: 0.15rem; +/* Dropdown с autocomplete */ +.recipient-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + margin-top: 0.25rem; + background: white; + border: 1px solid var(--bs-border-color); + border-radius: var(--bs-border-radius); + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + max-height: 300px; + overflow-y: auto; + z-index: 1050; } -.recipient-picker .alert { - margin-top: 1rem; +.dropdown-item-user { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.625rem 1rem; + cursor: pointer; + transition: background-color 0.15s; +} + +.dropdown-item-user:hover { + background-color: var(--bs-light); +} + +.dropdown-item-empty { padding: 0.75rem 1rem; + color: var(--bs-secondary); + text-align: center; + font-size: 0.875rem; +} + +/* Аватар пользователя */ +.user-avatar-small { + width: 32px; + height: 32px; + border-radius: 50%; + background: var(--bs-primary); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 0.75rem; + flex-shrink: 0; +} + +/* Информация о пользователе */ +.user-info { + flex: 1; + min-width: 0; +} + +.user-name { + font-weight: 500; + color: var(--bs-body-color); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.user-subtitle { + font-size: 0.8125rem; + color: var(--bs-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Подсказки */ +.alert-sm { + padding: 0.5rem 0.75rem; + margin-top: 0.75rem; + font-size: 0.875rem; } /* Адаптивность */ -@media (max-width: 576px) { - .recipient-list .badge { - font-size: 0.8rem; - padding: 0.3rem 0.4rem; +@media (max-width: 768px) { + .recipient-row { + flex-direction: column; + gap: 0.5rem; + } + + .recipient-label { + min-width: auto; + padding-top: 0; + } + + .recipient-input-wrapper { + min-width: 100%; + } + + .recipient-input { + min-width: 150px; + } +} + +@media (max-width: 576px) { + .recipient-chip { + font-size: 0.8125rem; + padding: 0.2rem 0.4rem; + } + + .user-avatar-small { + width: 28px; + height: 28px; + font-size: 0.7rem; + } + + .dropdown-item-user { + padding: 0.5rem 0.75rem; + } +} + +/* Темная тема (если нужна) */ +@media (prefers-color-scheme: dark) { + .recipient-input-wrapper { + background: var(--bs-dark); + border-color: var(--bs-gray-700); + } + + .recipient-dropdown { + background: var(--bs-dark); + border-color: var(--bs-gray-700); + } + + .dropdown-item-user:hover { + background-color: var(--bs-gray-800); } } diff --git a/backend/static/js/components/recipientPicker.js b/backend/static/js/components/recipientPicker.js index 26447ef9..9549513d 100644 --- a/backend/static/js/components/recipientPicker.js +++ b/backend/static/js/components/recipientPicker.js @@ -1,10 +1,13 @@ /** - * RecipientPicker - компонент для выбора получателей заявления - * Поддерживает выбор: - * - Нескольких отделов - * - Основных получателей (recipients) - * - Пользователей в копии (CC) - * - Флаг "Отправить всем сотрудникам отделов" + * RecipientPicker - компонент для выбора получателей заявления в стиле email-клиента + * Использует Token/Chip input с autocomplete для удобного выбора получателей + * + * Особенности: + * - Autocomplete с поиском по имени/email + * - Chip-based UI (как в Gmail) + * - Разделение To/CC (как в почтовых клиентах) + * - Выбор отделов через dropdown + * - Опция "Всем сотрудникам отделов" */ export class RecipientPicker { @@ -13,6 +16,9 @@ export class RecipientPicker { this.options = { apiUsersUrl: '/api/v1/employees/', apiDepartmentsUrl: '/api/v1/departments/', + placeholder: 'Начните вводить имя или выберите из списка...', + showCC: true, + showDepartments: true, onChange: null, ...options }; @@ -22,7 +28,12 @@ export class RecipientPicker { recipients: [], ccUsers: [], sendToAllDepartment: false, - loading: false + showCC: false, + loading: false, + searchQuery: '', + searchQueryCC: '', + showDropdown: false, + showDropdownCC: false }; this.users = []; @@ -63,133 +74,206 @@ export class RecipientPicker { render() { this.container.innerHTML = ` -
- -
- - -
- Заявление будет видно сотрудникам отделов с правами обработки -
-
- - -
-
- -