/**
* @module recipientPickerHandler
* @description Виджет для выбора получателей с поиском по API сотрудников.
* Отображает список сотрудников с чекбоксами, выбранные отображаются как чипы.
*
* Пример HTML:
*
*
*
*
*
*
*
*
* Использование:
* import { RecipientPicker } from './recipientPickerHandler.js';
* const picker = new RecipientPicker(element, {
* headers: { 'Authorization': 'Bearer TOKEN' },
* apiUrl: '/api/v1/employees/'
* });
*/
import { esc } from '../utils/stringUtils.js';
/**
* Утилита для debounce (ограничение частоты вызовов функции).
* @param {Function} fn - Функция для вызова
* @param {number} ms - Задержка в миллисекундах
* @returns {Function} Обёрнутая функция
*/
function debounce(fn, ms = 300) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
};
}
/**
* Класс для управления выбором получателей.
*/
export class RecipientPicker {
/**
* @param {HTMLElement} root - Корневой элемент виджета
* @param {Object} options - Опции
* @param {Object} [options.headers={}] - HTTP заголовки для запросов
* @param {string} options.apiUrl - URL API для получения списка сотрудников
*/
constructor(root, options) {
this.root = root;
this.headers = options.headers || {};
this.apiUrl = options.apiUrl;
this.searchQuery = '';
this.selected = new Map(); // id -> {id, display_name, email}
this.maxSelections = options.maxSelections || null; // null = без ограничений
// Получаем элементы
this.elSelected = root.querySelector('.rp-selected');
this.elSearch = root.querySelector('.rp-search input');
this.elResults = root.querySelector('.rp-results');
if (!this.elSelected || !this.elSearch || !this.elResults) {
console.warn('RecipientPicker: обязательные элементы не найдены');
return;
}
this._bindEvents();
this._fetchEmployees();
}
/**
* Привязка обработчиков событий.
* @private
*/
_bindEvents() {
// Поиск с debounce
this.elSearch.addEventListener('input', debounce(() => {
this.searchQuery = this.elSearch.value.trim();
this._fetchEmployees();
}, 250));
// Изменение чекбокса в списке
this.elResults.addEventListener('change', (e) => {
const checkbox = e.target.closest('input[type=checkbox][data-id]');
if (!checkbox) return;
const id = Number(checkbox.dataset.id);
const payload = JSON.parse(checkbox.dataset.payload || '{}');
if (checkbox.checked) {
// Проверка лимита выбора
if (this.maxSelections && this.selected.size >= this.maxSelections) {
checkbox.checked = false;
alert(`Можно выбрать не более ${this.maxSelections} получателей`);
return;
}
this.selected.set(id, payload);
} else {
this.selected.delete(id);
}
this._renderSelected();
});
// Удаление выбранного чипа
this.elSelected.addEventListener('click', (e) => {
const button = e.target.closest('button[data-id]');
if (!button) return;
const id = Number(button.dataset.id);
this.selected.delete(id);
// Снимаем галочку в списке результатов
const checkbox = this.elResults.querySelector(`input[type=checkbox][data-id="${id}"]`);
if (checkbox) checkbox.checked = false;
this._renderSelected();
});
}
/**
* Загрузка списка сотрудников с API.
* @private
*/
async _fetchEmployees() {
const url = new URL(this.apiUrl, window.location.origin);
if (this.searchQuery) {
url.searchParams.set('search', this.searchQuery);
}
url.searchParams.set('ordering', 'last_name,first_name');
url.searchParams.set('page_size', '20');
try {
const response = await fetch(url.toString(), { headers: this.headers });
if (!response.ok) {
throw new Error('HTTP ' + response.status);
}
const data = await response.json();
const items = Array.isArray(data) ? data : (data.results || []);
this._renderResults(items);
} catch (error) {
this.elResults.innerHTML = '
Не удалось загрузить сотрудников
';
console.error('RecipientPicker fetch error:', error);
}
}
/**
* Отображение списка результатов поиска.
* @private
* @param {Array} items - Массив объектов сотрудников
*/
_renderResults(items) {
if (!items.length) {
this.elResults.innerHTML = '