/** * skillsManager.js * Управление навыками сотрудника (добавление/удаление через AJAX) * * @module skillsManager * @version 1.0.0 */ import { esc } from '../utils/stringUtils.js'; /** * Инициализация менеджера навыков * @param {Object} options - Опции конфигурации * @param {string} options.blockId - ID блока с навыками * @param {string} options.formId - ID формы добавления * @param {string} options.collapseId - ID collapse элемента формы * @param {string} options.removeUrl - URL для удаления навыка * @returns {Object|null} Публичный API или null если элементы не найдены */ export function initSkillsManager(options = {}) { const block = document.getElementById(options.blockId || 'skillsBlock'); // Проверяем, что блок существует и редактируемый if (!block || block.dataset.editable !== '1') { console.log('skillsManager: skip (block not found or not editable)'); return null; } const elems = { form: document.getElementById(options.formId || 'skillAddForm'), chipsWrap: block.querySelector('.chips'), addBtn: block.querySelector('.chip-add'), collapseEl: document.getElementById(options.collapseId || 'skillsForm') }; if (!elems.form || !elems.chipsWrap) { console.warn('skillsManager: required elements not found'); return null; } const config = { removeUrl: options.removeUrl || elems.form.dataset.removeUrl || '', addUrl: elems.form.action }; const csrf = elems.form.querySelector('input[name="csrfmiddlewaretoken"]')?.value || ''; // Bootstrap Collapse instance let bsCollapse = null; if (window.bootstrap && elems.collapseEl) { bsCollapse = new bootstrap.Collapse(elems.collapseEl, { toggle: false }); } /** * Добавить навык * @param {string} skillName - Название навыка */ async function addSkill(skillName) { const input = elems.form.querySelector('input[name="skill_name"]'); const submitBtn = elems.form.querySelector('[type="submit"]'); if (!skillName.trim()) { input?.focus(); return; } submitBtn?.setAttribute('disabled', ''); try { const resp = await fetch(config.addUrl, { method: 'POST', credentials: 'same-origin', headers: { 'X-CSRFToken': csrf, 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }, body: new URLSearchParams({ skill_name: skillName }) }); if (!resp.ok) throw new Error('HTTP ' + resp.status); let data = null; try { data = await resp.json(); } catch (_) {} const displayName = (data && (data.name || data?.skill?.name || data?.title)) || skillName; const skillId = (data && (data.id || data?.skill?.id)) || ''; // Проверяем, не добавлен ли уже этот навык const exists = [...elems.chipsWrap.querySelectorAll('.chip-skill')].some( el => (el.dataset.skillName || el.textContent).trim().toLowerCase() === displayName.toLowerCase() ); if (!exists) { removeEmptyNote(); const chip = createSkillChip(skillId, displayName); elems.chipsWrap.insertBefore(chip, elems.addBtn); // Анимация пульсации кнопки "добавить" elems.addBtn?.classList.add('pulse'); setTimeout(() => elems.addBtn?.classList.remove('pulse'), 180); } // Очистить input и закрыть форму if (input) input.value = ''; if (bsCollapse) { bsCollapse.hide(); } else { elems.collapseEl?.classList.remove('show'); } } catch (e) { console.error('skillsManager: add error', e); location.reload(); } finally { submitBtn?.removeAttribute('disabled'); } } /** * Удалить навык * @param {HTMLElement} chip - Элемент chip с навыком */ async function removeSkill(chip) { const skillId = chip.dataset.skillId || ''; const skillName = chip.dataset.skillName || chip.textContent.trim(); const delBtn = chip.querySelector('.chip-del-btn'); if (delBtn) delBtn.disabled = true; try { const resp = await fetch(config.removeUrl, { method: 'POST', credentials: 'same-origin', headers: { 'X-CSRFToken': csrf, 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }, body: new URLSearchParams({ skill_id: skillId, skill_name: skillName }) }); if (!resp.ok) throw new Error('HTTP ' + resp.status); chip.remove(); // Если больше нет навыков, показать заглушку if (!elems.chipsWrap.querySelector('.chip-skill')) { showEmptyNote(); } } catch (e) { console.error('skillsManager: remove error', e); location.reload(); } finally { if (delBtn) delBtn.disabled = false; } } /** * Создать chip элемент для навыка * @param {string} skillId - ID навыка * @param {string} displayName - Отображаемое имя * @returns {HTMLElement} Chip элемент */ function createSkillChip(skillId, displayName) { const chip = document.createElement('span'); chip.className = 'chip chip-skill'; chip.dataset.skillId = String(skillId || ''); chip.dataset.skillName = displayName; chip.innerHTML = ` ${esc(displayName)} `; return chip; } /** * Показать заглушку "нет навыков" */ function showEmptyNote() { const note = document.createElement('span'); note.className = 'skills-empty'; note.textContent = ''; elems.chipsWrap.insertBefore(note, elems.addBtn); } /** * Удалить заглушку "нет навыков" */ function removeEmptyNote() { elems.chipsWrap.querySelector('.skills-empty')?.remove(); } // ===== Event listeners ===== // Обработка отправки формы elems.form.addEventListener('submit', async (e) => { e.preventDefault(); const input = elems.form.querySelector('input[name="skill_name"]'); const skillName = (input?.value || '').trim(); await addSkill(skillName); }); // Обработка удаления навыка elems.chipsWrap.addEventListener('click', async (e) => { const delBtn = e.target.closest('.chip-del-btn'); if (!delBtn) return; const chip = delBtn.closest('.chip-skill'); if (!chip) return; await removeSkill(chip); }); console.log('skillsManager: initialized'); // Публичный API return { addSkill, refresh: () => location.reload() }; }