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 = ` -
- -
- - -
- Заявление будет видно сотрудникам отделов с правами обработки -
-
- - -
-
- -