Tul xxx Tul
User / IP
:
216.73.216.159
Host / Server
:
45.84.207.204 / aircan.me
System
:
Linux lt-bnk-web1726.main-hosting.eu 5.14.0-611.36.1.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Tue Mar 3 11:23:52 EST 2026 x86_64
Command
|
Upload
|
Create
Mass Deface
|
Jumping
|
Symlink
|
Reverse Shell
Ping
|
Port Scan
|
DNS Lookup
|
Whois
|
Header
|
cURL
:
/
home
/
u931257429
/
domains
/
emprendo.com.co
/
public_html
/
soy4
/
admin
/
public
/
js
/
Viewing: admin.js
(function(){ const qs = (s, el=document) => el.querySelector(s); const qsa = (s, el=document) => Array.from(el.querySelectorAll(s)); const showFlash = (message, type = 'success') => { const container = document.querySelector('[data-flash-stack]'); if (container) { const item = document.createElement('div'); item.className = `flash flash--${type}`; item.setAttribute('role', 'alert'); const icon = document.createElement('span'); icon.className = 'flash__icon'; icon.setAttribute('aria-hidden', 'true'); item.appendChild(icon); const body = document.createElement('div'); body.className = 'flash__body'; body.textContent = message; item.appendChild(body); container.appendChild(item); window.setTimeout(() => { item.classList.add('is-leaving'); window.setTimeout(() => { item.remove(); }, 400); }, 4000); return; } const modal = document.querySelector('[data-flash-modal]'); if (!modal) return; const panel = modal.querySelector('[data-flash-modal-panel]') || modal.querySelector('.team-modal__panel'); const titleEl = modal.querySelector('[data-flash-modal-title]'); const messageEl = modal.querySelector('[data-flash-modal-message]'); const iconEl = modal.querySelector('[data-flash-modal-icon]'); const closeEls = modal.querySelectorAll('[data-flash-modal-close]'); const iconInner = iconEl ? iconEl.querySelector('i') : null; const titles = { success: 'Operación exitosa', error: 'Ha ocurrido un problema', warning: 'Revisa esta información', info: 'Aviso', }; const typeClass = { success: 'is-success', error: 'is-error', warning: 'is-warning', info: 'is-info', }[type] || 'is-success'; const iconMap = { success: 'fa-circle-check', error: 'fa-circle-xmark', warning: 'fa-triangle-exclamation', info: 'fa-circle-info', }; const clearTimer = () => { if (typeof modal.flashModalTimer === 'number') { window.clearTimeout(modal.flashModalTimer); modal.flashModalTimer = undefined; } }; const closeModal = () => { clearTimer(); modal.classList.remove('is-open'); const onEnd = () => { modal.hidden = true; modal.removeEventListener('transitionend', onEnd); }; modal.addEventListener('transitionend', onEnd); window.setTimeout(() => { if (!modal.classList.contains('is-open')) { modal.hidden = true; } }, 240); }; closeEls.forEach((btn) => { if (btn.dataset.bindFlashClose === '1') return; btn.dataset.bindFlashClose = '1'; btn.addEventListener('click', (event) => { event.preventDefault(); closeModal(); }); }); const defaultMessage = message || ''; if (messageEl) { messageEl.textContent = defaultMessage; } if (titleEl) { titleEl.textContent = titles[type] || titles.success; } if (iconEl) { iconEl.dataset.type = type; } if (iconInner) { iconInner.className = `fa-solid ${iconMap[type] || iconMap.success}`; } if (panel) { panel.classList.remove('is-success', 'is-error', 'is-warning', 'is-info'); panel.classList.add(typeClass); } const openModal = () => { modal.hidden = false; requestAnimationFrame(() => modal.classList.add('is-open')); }; openModal(); clearTimer(); modal.flashModalTimer = window.setTimeout(() => { closeModal(); }, 4200); }; const initDiagnosticoSummary = () => { const questionCountEl = document.querySelector('[data-question-progress-count]'); const questionBarWrapper = document.querySelector('[data-question-progress-bar-wrapper]'); const questionBarEl = document.querySelector('[data-question-progress-bar]'); const documentCountEl = document.querySelector('[data-document-progress-count]'); const documentBarWrapper = document.querySelector('[data-document-progress-bar-wrapper]'); const documentBarEl = document.querySelector('[data-document-progress-bar]'); window.diagnosticoSummaryRefresh = (payload) => { if (!payload || typeof payload !== 'object') { return; } if (payload.questions && typeof payload.questions === 'object') { const { answered = 0, total = 0, percent = 0 } = payload.questions; if (questionCountEl) { questionCountEl.textContent = `${answered} / ${total}`; } if (questionBarWrapper) { questionBarWrapper.setAttribute('aria-valuenow', String(percent)); } if (questionBarEl) { questionBarEl.style.width = `${percent}%`; } } if (payload.documents && typeof payload.documents === 'object') { const { uploaded = 0, total = 0, percent = 0 } = payload.documents; if (documentCountEl) { documentCountEl.textContent = `${uploaded} / ${total}`; } if (documentBarWrapper) { documentBarWrapper.setAttribute('aria-valuenow', String(percent)); } if (documentBarEl) { documentBarEl.style.width = `${percent}%`; } } if (!payload.questions && payload.percent !== undefined) { if (documentCountEl && payload.uploaded !== undefined && payload.total !== undefined) { documentCountEl.textContent = `${payload.uploaded} / ${payload.total}`; } if (documentBarWrapper) { documentBarWrapper.setAttribute('aria-valuenow', String(payload.percent ?? 0)); } if (documentBarEl) { documentBarEl.style.width = `${payload.percent ?? 0}%`; } } }; }; const initDiagnosticoEntrevista = () => { const interviewRoot = document.querySelector('[data-diagnostico-entrevista]'); if (!interviewRoot) return; const fetchUrl = interviewRoot.dataset.fetch || ''; const saveUrl = interviewRoot.dataset.save || ''; const mode = interviewRoot.dataset.mode || 'edit'; const readOnly = mode === 'readonly'; const listContainer = interviewRoot.querySelector('[data-entrevista-lista]'); const emptyState = interviewRoot.querySelector('[data-entrevista-empty]'); const form = interviewRoot.querySelector('[data-entrevista-form]'); const saveBtn = interviewRoot.querySelector('[data-entrevista-save]'); const statusEl = interviewRoot.querySelector('[data-entrevista-status]'); const summaryRefresh = window.diagnosticoSummaryRefresh; if (!fetchUrl || (!saveUrl && !readOnly) || !form || !listContainer) { return; } const state = { loading: false, questions: [], answers: {}, changed: false, }; const escapeHtml = (value) => String(value ?? '') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); const setLoading = (value) => { state.loading = value; interviewRoot.classList.toggle('is-loading', value); if (saveBtn) { saveBtn.disabled = value || readOnly; } }; const setStatus = (text, type = 'idle') => { if (!statusEl) return; statusEl.dataset.status = type; statusEl.textContent = text || ''; statusEl.hidden = !text; }; const showError = (message, errors) => { let text = message || 'Ocurrió un error inesperado.'; if (errors && typeof errors === 'object') { const extra = Object.values(errors) .flat() .map((item) => (Array.isArray(item) ? item.join(' ') : String(item))) .filter(Boolean) .join(' '); if (extra) { text = `${text} ${extra}`; } } window.alert(text); }; const request = (url, options = {}) => { const config = { credentials: 'same-origin', headers: { Accept: 'application/json', }, ...options, }; if (config.body && !(config.body instanceof FormData)) { config.headers['Content-Type'] = 'application/json'; } return fetch(url, config).then(async (response) => { let payload = null; try { payload = await response.json(); } catch (error) { payload = null; } if (!response.ok || (payload && payload.status === 'error')) { const message = payload && payload.message ? payload.message : 'Solicitud fallida.'; const errors = payload && payload.errors ? payload.errors : {}; const err = new Error(message); err.payload = { message, errors, status: payload ? payload.status : null }; throw err; } return payload; }); }; const getAnswerValue = (question, inputEl) => { const type = String(question.tipo_respuesta || '').toLowerCase(); if (!inputEl) return null; switch (type) { case 'multiple': return Array.from(inputEl.querySelectorAll('input[type="checkbox"]:checked')).map((el) => el.value); case 'booleano': { const checked = inputEl.querySelector('input[type="radio"]:checked'); return checked ? checked.value : null; } default: return inputEl.value; } }; const renderQuestions = () => { listContainer.innerHTML = ''; if (!state.questions.length) { if (emptyState) { emptyState.hidden = false; emptyState.removeAttribute('hidden'); } return; } if (emptyState) { emptyState.hidden = true; emptyState.setAttribute('hidden', ''); } state.questions.forEach((question, index) => { const wrapper = document.createElement('article'); wrapper.className = 'diagnostico-question diagnostico-question--inline'; wrapper.dataset.questionId = String(question.id); const type = String(question.tipo_respuesta || '').toLowerCase(); const required = Boolean(question.es_obligatoria); const current = state.answers[question.id] ?? {}; const value = current.valor ?? ''; const valueJson = current.valor_json ?? null; const assistanceTexts = []; if (question.descripcion) { assistanceTexts.push(escapeHtml(question.descripcion)); } if (question.ayuda_contextual) { assistanceTexts.push(escapeHtml(question.ayuda_contextual)); } const assistanceText = assistanceTexts.join(' '); const assistanceHtml = assistanceText ? `<span class="diagnostico-question__assist" role="note" aria-label="Más información" title="${assistanceText}"><i class="fa-regular fa-circle-question" aria-hidden="true"></i></span>` : ''; const headerHtml = ` <header class="diagnostico-question__header"> <div class="diagnostico-question__title"> <span class="diagnostico-question__index">${index + 1}.</span> <span class="diagnostico-question__text">${escapeHtml(question.titulo || 'Pregunta')}</span> ${required ? '<span class="diagnostico-question__required" title="Respuesta obligatoria" aria-hidden="true">*</span>' : ''} ${assistanceHtml} </div> <div class="diagnostico-question__meta"></div> </header>`; const options = Array.isArray(question.opciones) ? question.opciones : []; const renderOptions = () => { const toValueLabel = (option) => { if (option && typeof option === 'object') { const valueKey = option.value ?? option.label ?? ''; return { value: String(valueKey), label: String(option.label ?? valueKey) }; } const text = String(option ?? ''); return { value: text, label: text }; }; switch (type) { case 'opciones': { const opts = options.map((option) => { const { value: optValue, label: optLabel } = toValueLabel(option); const selected = String(valueJson && valueJson[0] ? valueJson[0] : value).toLowerCase(); const isSelected = selected !== '' && selected === optValue.toLowerCase(); return `<option value="${escapeHtml(optValue)}" ${isSelected ? 'selected' : ''}>${escapeHtml(optLabel)}</option>`; }).join(''); return ` <select ${required ? 'required' : ''}> <option value="">Selecciona una opción</option> ${opts} </select>`; } case 'multiple': { const selectedSet = new Set(Array.isArray(valueJson) ? valueJson.map((item) => String(item).toLowerCase()) : []); const items = options.map((option) => { const { value: optValue, label: optLabel } = toValueLabel(option); const isChecked = selectedSet.has(optValue.toLowerCase()); return ` <label class="form-check"> <input type="checkbox" value="${escapeHtml(optValue)}" ${isChecked ? 'checked' : ''} /> <span>${escapeHtml(optLabel)}</span> </label>`; }).join(''); return `<div class="diagnostico-question__options diagnostico-question__options--grid">${items}</div>`; } case 'booleano': { const bool = valueJson && typeof valueJson.bool === 'boolean' ? valueJson.bool : (typeof value === 'string' ? value.toLowerCase() === 'sí' || value.toLowerCase() === 'si' : null); return ` <div class="diagnostico-question__options"> <label class="form-check"> <input type="radio" name="q_${question.id}" value="si" ${bool === true ? 'checked' : ''} /> <span>Sí</span> </label> <label class="form-check"> <input type="radio" name="q_${question.id}" value="no" ${bool === false ? 'checked' : ''} /> <span>No</span> </label> </div>`; } default: return ''; } }; const fieldHtml = (() => { const commonAttrs = required ? 'required' : ''; const safeValue = Array.isArray(value) ? '' : String(value ?? ''); switch (type) { case 'texto': return `<input type="text" ${commonAttrs} value="${escapeHtml(safeValue)}" />`; case 'numero': return `<input type="number" ${commonAttrs} value="${escapeHtml(safeValue)}" />`; case 'textarea': return `<textarea rows="3" ${commonAttrs}>${escapeHtml(safeValue)}</textarea>`; case 'fecha': return `<input type="date" ${commonAttrs} value="${escapeHtml(safeValue)}" />`; case 'opciones': case 'multiple': case 'booleano': return renderOptions(); default: return `<input type="text" ${commonAttrs} value="${escapeHtml(safeValue)}" />`; } })(); const fieldWrapper = document.createElement('div'); fieldWrapper.className = 'diagnostico-question__field'; fieldWrapper.innerHTML = fieldHtml; fieldWrapper.dataset.questionId = String(question.id); fieldWrapper.dataset.inputType = type; wrapper.innerHTML = headerHtml; wrapper.appendChild(fieldWrapper); listContainer.appendChild(wrapper); }); }; const readFormState = () => { const map = {}; Array.from(listContainer.querySelectorAll('.diagnostico-question__field')).forEach((fieldWrapper) => { const id = parseInt(fieldWrapper.dataset.questionId, 10); if (!id) return; const question = state.questions.find((item) => Number(item.id) === Number(id)); if (!question) return; let value; if (['multiple', 'booleano', 'opciones'].includes(fieldWrapper.dataset.inputType)) { value = getAnswerValue(question, fieldWrapper); } else { const input = fieldWrapper.querySelector('input, textarea, select'); value = input ? input.value : ''; } map[id] = value; }); return map; }; const markChanged = () => { if (!state.changed) { state.changed = true; setStatus('Cambios sin guardar', 'dirty'); } }; const bindListeners = () => { listContainer.addEventListener('change', markChanged, true); listContainer.addEventListener('input', markChanged, true); if (readOnly) { if (saveBtn) { saveBtn.disabled = true; saveBtn.classList.add('is-disabled'); } return; } form.addEventListener('submit', (event) => { event.preventDefault(); if (state.loading) return; const payload = readFormState(); setLoading(true); setStatus('Guardando...', 'saving'); request(saveUrl, { method: 'POST', body: JSON.stringify(payload) }) .then((response) => { const message = response && response.message ? response.message : 'Respuestas guardadas.'; setStatus(message, 'saved'); state.changed = false; showFlash(message, 'success'); if (summaryRefresh && typeof summaryRefresh === 'function') { summaryRefresh(response && response.data ? response.data.progress : null); } }) .catch((error) => { setStatus('Error al guardar.', 'error'); showError(error.payload ? error.payload.message : error.message, error.payload ? error.payload.errors : null); }) .finally(() => setLoading(false)); }); }; const fetchData = () => { setLoading(true); request(fetchUrl) .then((payload) => { const data = payload && payload.data ? payload.data : {}; state.questions = Array.isArray(data.questions) ? data.questions : []; state.answers = data.answers && typeof data.answers === 'object' ? data.answers : {}; renderQuestions(); state.changed = false; if (!state.questions.length) { setStatus('No hay preguntas configuradas.', 'info'); } else { setStatus('', 'idle'); } if (summaryRefresh && typeof summaryRefresh === 'function') { summaryRefresh(state.questions); } }) .catch((error) => { setStatus('No se pudieron cargar las preguntas.', 'error'); showError(error.payload ? error.payload.message : error.message, error.payload ? error.payload.errors : null); }) .finally(() => setLoading(false)); }; bindListeners(); fetchData(); return { refresh: fetchData, }; }; document.addEventListener('DOMContentLoaded', () => { // Sidebar toggle (mobile) const sidebar = qs('.sidebar'); const toggleBtn = qs('#sidebar-toggle'); if (toggleBtn && sidebar) { const body = document.body; const openSidebar = () => { sidebar.classList.add('is-open'); body.classList.add('sidebar-open'); }; const closeSidebar = () => { sidebar.classList.remove('is-open'); body.classList.remove('sidebar-open'); }; const toggleSidebar = () => { if (sidebar.classList.contains('is-open')) { closeSidebar(); } else { openSidebar(); } }; toggleBtn.addEventListener('click', (event) => { event.stopPropagation(); toggleSidebar(); }); document.addEventListener('click', (event) => { if (!sidebar.classList.contains('is-open')) return; const insideSidebar = sidebar.contains(event.target); const triggeredByToggle = toggleBtn.contains(event.target); if (!insideSidebar && !triggeredByToggle) { closeSidebar(); } }); document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && sidebar.classList.contains('is-open')) { closeSidebar(); } }); // Top-right hamburger close button const closeBtn = qs('#sidebar-close'); if (closeBtn) { closeBtn.addEventListener('click', (event) => { event.stopPropagation(); closeSidebar(); }); } } // User dropdown const userBtn = qs('#user-menu-toggle'); const dropdown = qs('#user-dropdown'); if (userBtn && dropdown) { const close = () => { dropdown.hidden = true; userBtn.setAttribute('aria-expanded', 'false'); } const open = () => { dropdown.hidden = false; userBtn.setAttribute('aria-expanded', 'true'); } userBtn.addEventListener('click', (e) => { e.stopPropagation(); dropdown.hidden ? open() : close(); }); document.addEventListener('click', (e) => { if (!dropdown.hidden && !dropdown.contains(e.target) && e.target !== userBtn) close(); }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') close(); }); } // Confirmación de eliminación de servicios const initServiceDeletion = () => { const forms = qsa('.js-delete-servicio'); const modal = qs('[data-servicio-delete-modal]'); if (!modal) { forms.forEach((form) => { if (form.dataset.bindDelete === '1') return; form.dataset.bindDelete = '1'; form.addEventListener('submit', (event) => { const name = form.dataset.servicioName || 'este servicio'; const confirmed = window.confirm(`¿Deseas eliminar ${name}? Esta acción no se puede deshacer.`); if (!confirmed) { event.preventDefault(); } }); }); return; } const nameEl = modal.querySelector('[data-servicio-delete-name]'); const confirmBtn = modal.querySelector('[data-servicio-delete-confirm]'); const cancelEls = qsa('[data-servicio-delete-cancel]', modal); const formActionField = modal.querySelector('[data-servicio-delete-action]'); let activeForm = null; const closeModal = () => { modal.classList.remove('is-open'); const onEnd = () => { modal.hidden = true; modal.removeEventListener('transitionend', onEnd); }; modal.addEventListener('transitionend', onEnd); setTimeout(() => { if (!modal.classList.contains('is-open')) { modal.hidden = true; } }, 260); activeForm = null; }; const openModal = (form) => { activeForm = form; const serviceName = form.dataset.servicioName || 'este servicio'; const action = form.getAttribute('action') || '#'; if (nameEl) { nameEl.textContent = serviceName; } if (formActionField) { formActionField.value = action; } modal.hidden = false; requestAnimationFrame(() => modal.classList.add('is-open')); }; cancelEls.forEach((el) => { el.addEventListener('click', (event) => { event.preventDefault(); closeModal(); }); }); modal.addEventListener('click', (event) => { if (event.target === modal) { closeModal(); } }); document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && !modal.hidden) { closeModal(); } }); if (confirmBtn) { confirmBtn.addEventListener('click', () => { if (activeForm) { const action = formActionField?.value || activeForm.getAttribute('action') || '#'; if (action && activeForm.getAttribute('action') !== action) { activeForm.setAttribute('action', action); } activeForm.dataset.deleteConfirmed = '1'; activeForm.submit(); } closeModal(); }); } forms.forEach((form) => { if (form.dataset.bindDelete === '1') return; form.dataset.bindDelete = '1'; form.addEventListener('submit', (event) => { if (form.dataset.deleteConfirmed === '1') { form.dataset.deleteConfirmed = ''; return; } event.preventDefault(); openModal(form); }); }); }; initServiceDeletion(); const initUserDeletion = () => { const root = qs('[data-users-root]'); if (!root) return; const forms = qsa('.js-delete-user', root); if (!forms.length) return; const modal = root.querySelector('[data-user-delete-modal]'); if (!modal) { forms.forEach((form) => { if (form.dataset.bindDelete === '1') return; form.dataset.bindDelete = '1'; form.addEventListener('submit', (event) => { const name = form.dataset.userName || 'este usuario'; const confirmed = window.confirm(`¿Deseas eliminar a ${name}? Esta acción no se puede deshacer.`); if (!confirmed) { event.preventDefault(); } }); }); return; } const nameEl = modal.querySelector('[data-user-delete-name]'); const messageEl = modal.querySelector('[data-user-delete-message]'); const confirmBtn = modal.querySelector('[data-user-delete-confirm]'); const cancelEls = qsa('[data-user-delete-cancel]', modal); let activeForm = null; const closeModal = () => { modal.classList.remove('is-open'); const onEnd = () => { modal.hidden = true; modal.removeEventListener('transitionend', onEnd); }; modal.addEventListener('transitionend', onEnd); window.setTimeout(() => { if (!modal.classList.contains('is-open')) { modal.hidden = true; } }, 260); activeForm = null; }; const openModal = (form) => { activeForm = form; if (nameEl) { nameEl.textContent = form.dataset.userName || 'este usuario'; } if (messageEl) { messageEl.textContent = 'Esta operación eliminará el acceso del usuario y no se puede deshacer.'; } modal.hidden = false; requestAnimationFrame(() => modal.classList.add('is-open')); }; cancelEls.forEach((btn) => { btn.addEventListener('click', (event) => { event.preventDefault(); closeModal(); }); }); modal.addEventListener('click', (event) => { if (event.target === modal) { closeModal(); } }); document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && !modal.hidden) { closeModal(); } }); if (confirmBtn) { confirmBtn.addEventListener('click', () => { if (!activeForm) { closeModal(); return; } activeForm.dataset.deleteConfirmed = '1'; activeForm.submit(); closeModal(); }); } forms.forEach((form) => { if (form.dataset.bindDelete === '1') return; form.dataset.bindDelete = '1'; form.addEventListener('submit', (event) => { if (form.dataset.deleteConfirmed === '1') { form.dataset.deleteConfirmed = ''; return; } event.preventDefault(); openModal(form); }); }); }; initUserDeletion(); let entrevistaApi = null; const initDiagnosticoTabs = () => { const detailRoot = qs('[data-diagnostico-detail]'); if (!detailRoot) return; const tabs = qsa('.diagnostico-tab', detailRoot); const panels = qsa('.diagnostico-tab-panel', detailRoot); const activate = (target) => { tabs.forEach((tab) => { const isActive = tab.dataset.tabTarget === target; tab.classList.toggle('is-active', isActive); tab.setAttribute('aria-selected', String(isActive)); }); panels.forEach((panel) => { const show = panel.dataset.tabPanel === target; if (show) { panel.hidden = false; panel.removeAttribute('hidden'); panel.style.display = ''; panel.classList.add('is-active'); } else { panel.hidden = true; panel.setAttribute('hidden', ''); panel.style.display = 'none'; panel.classList.remove('is-active'); } if (show && panel.dataset.tabPanel === 'entrevista' && entrevistaApi) { entrevistaApi.refresh(); } }); }; tabs.forEach((tab) => { tab.addEventListener('click', () => activate(tab.dataset.tabTarget)); }); const defaultTab = tabs.find((tab) => tab.classList.contains('is-active')) || tabs[0]; if (defaultTab) activate(defaultTab.dataset.tabTarget); }; initDiagnosticoTabs(); entrevistaApi = initDiagnosticoEntrevista(); const initDiagnosticoEstado = () => { const root = qs('[data-diagnostico-estado]'); if (!root) return; const toggle = root.querySelector('[data-diagnostico-estado-toggle]'); const menu = root.querySelector('[data-diagnostico-estado-menu]'); const labelEl = root.querySelector('[data-diagnostico-estado-label]'); const options = qsa('[data-estado-value]', menu); const updateUrl = root.dataset.updateUrl || ''; if (!toggle || !menu || !options.length || !updateUrl) { return; } const stateClasses = ['badge--pendiente', 'badge--en_progreso', 'badge--logrado', 'badge--cancelado']; let current = (root.dataset.current || '').toLowerCase(); let isOpen = false; let isLoading = false; const setLoading = (value) => { isLoading = value; toggle.disabled = value; root.classList.toggle('is-loading', value); }; const updateActiveOption = (value) => { options.forEach((option) => { option.classList.toggle('is-active', option.dataset.estadoValue === value); }); }; const updateBadgeState = (value, label) => { stateClasses.forEach((className) => toggle.classList.remove(className)); toggle.classList.add(`badge--${value}`); if (labelEl) { labelEl.textContent = label; } }; const closeMenu = () => { if (!isOpen) return; isOpen = false; menu.hidden = true; root.classList.remove('is-open'); toggle.setAttribute('aria-expanded', 'false'); document.removeEventListener('click', handleOutsideClick, true); document.removeEventListener('keydown', handleKeyDown, true); }; const openMenu = () => { if (isOpen || isLoading) return; isOpen = true; menu.hidden = false; root.classList.add('is-open'); toggle.setAttribute('aria-expanded', 'true'); document.addEventListener('click', handleOutsideClick, true); document.addEventListener('keydown', handleKeyDown, true); }; const toggleMenu = () => { if (isOpen) { closeMenu(); } else { openMenu(); } }; const handleOutsideClick = (event) => { if (!root.contains(event.target)) { closeMenu(); } }; const handleKeyDown = (event) => { if (event.key === 'Escape') { closeMenu(); } }; const requestEstado = (estado) => { return fetch(updateUrl, { method: 'POST', credentials: 'same-origin', headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ estado }), }).then(async (response) => { let payload = null; try { payload = await response.json(); } catch (error) { payload = null; } if (!response.ok || (payload && payload.status === 'error')) { const message = payload && payload.message ? payload.message : 'No se pudo actualizar el estado.'; const errors = payload && payload.errors ? payload.errors : {}; const err = new Error(message); err.payload = { message, errors }; throw err; } return payload; }); }; toggle.addEventListener('click', (event) => { event.preventDefault(); event.stopPropagation(); toggleMenu(); }); options.forEach((option) => { option.addEventListener('click', (event) => { event.preventDefault(); event.stopPropagation(); if (isLoading) return; const value = (option.dataset.estadoValue || '').toLowerCase(); if (!value || value === current) { closeMenu(); return; } setLoading(true); requestEstado(value) .then((payload) => { const data = payload && payload.data ? payload.data : {}; const nextEstado = (data.estado || value).toLowerCase(); const nextLabel = data.label || option.textContent || value; current = nextEstado; root.dataset.current = nextEstado; updateBadgeState(nextEstado, nextLabel); updateActiveOption(nextEstado); closeMenu(); }) .catch((error) => { const message = error && error.payload ? error.payload.message : error.message; window.alert(message || 'Ocurrió un error al actualizar el estado.'); }) .finally(() => { setLoading(false); }); }); }); updateActiveOption(current); }; initDiagnosticoEstado(); const bindConfirmForms = () => { qsa('form[data-confirm-message]').forEach((form) => { if (form.dataset.bindConfirm === '1') return; form.dataset.bindConfirm = '1'; form.addEventListener('submit', (event) => { const message = form.dataset.confirmMessage || '¿Confirmas esta acción?'; const confirmed = window.confirm(message); if (!confirmed) { event.preventDefault(); } }); }); }; bindConfirmForms(); document.addEventListener('htmx:afterSettle', bindConfirmForms); const initDiagnosticoDelete = () => { const deleteButton = qs('[data-diagnostico-delete-trigger]'); const modal = qs('[data-diagnostico-delete-modal]'); if (!deleteButton || !modal) return; const nameEl = modal.querySelector('[data-diagnostico-delete-name]'); const messageEl = modal.querySelector('[data-diagnostico-delete-message]'); const confirmBtn = modal.querySelector('[data-diagnostico-delete-confirm]'); const cancelEls = qsa('[data-diagnostico-delete-cancel]', modal); const hiddenForm = qs('#diagnostico-delete-form'); const idInput = hiddenForm ? hiddenForm.querySelector('input[name="id"]') : null; const setModalState = (diagnosticoId, diagnosticoName) => { if (nameEl) { nameEl.textContent = diagnosticoName || 'este diagnóstico'; } if (messageEl) { messageEl.textContent = 'El diagnóstico será eliminado permanentemente.'; } if (idInput) { idInput.value = diagnosticoId || ''; } }; const openModal = () => { modal.hidden = false; requestAnimationFrame(() => modal.classList.add('is-open')); }; const closeModal = () => { modal.classList.remove('is-open'); const onEnd = () => { modal.hidden = true; modal.removeEventListener('transitionend', onEnd); }; modal.addEventListener('transitionend', onEnd); setTimeout(() => { if (!modal.classList.contains('is-open')) { modal.hidden = true; } }, 240); }; deleteButton.addEventListener('click', () => { const diagnosticoId = deleteButton.dataset.diagnosticoId || ''; const diagnosticoName = deleteButton.dataset.diagnosticoName || ''; if (!diagnosticoId) { window.alert('No se pudo identificar el diagnóstico.'); return; } setModalState(diagnosticoId, diagnosticoName); openModal(); }); cancelEls.forEach((el) => { el.addEventListener('click', (event) => { event.preventDefault(); closeModal(); }); }); if (confirmBtn && hiddenForm) { confirmBtn.addEventListener('click', () => { hiddenForm.submit(); closeModal(); }); } }; initDiagnosticoDelete(); const initDiagnosticoInforme = () => { const root = qs('[data-diagnostico-informe]'); if (!root) return null; const fetchUrl = root.dataset.fetch || ''; const saveUrl = root.dataset.save || ''; const editor = root.querySelector('[data-informe-editor]'); if (!fetchUrl || !saveUrl || !editor) { return null; } const contentEl = editor.querySelector('[data-informe-content]'); const statusEl = editor.querySelector('[data-informe-status]'); const saveBtn = editor.querySelector('[data-informe-save]'); const buttons = Array.from(editor.querySelectorAll('[data-informe-btn]')); const state = { loading: false, dirty: false, lastSavedHtml: '', }; const request = (url, options = {}) => { const config = { credentials: 'same-origin', headers: { Accept: 'application/json', }, ...options, }; if (config.body && !(config.body instanceof FormData)) { config.headers['Content-Type'] = 'application/json'; } return fetch(url, config).then(async (response) => { let payload = null; try { payload = await response.json(); } catch (error) { payload = null; } if (!response.ok || (payload && payload.status === 'error')) { const message = payload && payload.message ? payload.message : 'Solicitud fallida.'; const errors = payload && payload.errors ? payload.errors : {}; const err = new Error(message); err.payload = { message, errors, status: payload ? payload.status : null }; throw err; } return payload; }); }; const setStatus = (text, type = 'idle') => { if (!statusEl) return; statusEl.dataset.status = type; statusEl.textContent = text || ''; statusEl.hidden = !text; }; const setLoading = (value) => { state.loading = value; editor.classList.toggle('is-loading', value); if (saveBtn) { saveBtn.disabled = value; } }; const execCommand = (command, value = null) => { if (!contentEl) return; contentEl.focus(); document.execCommand(command, false, value); }; const applyFormat = (type) => { switch (type) { case 'bold': execCommand('bold'); break; case 'italic': execCommand('italic'); break; case 'h1': execCommand('formatBlock', '<h1>'); break; case 'h2': execCommand('formatBlock', '<h2>'); break; case 'paragraph': execCommand('formatBlock', '<p>'); break; case 'ul': execCommand('insertUnorderedList'); break; case 'align-left': execCommand('justifyLeft'); break; case 'align-center': execCommand('justifyCenter'); break; case 'align-right': execCommand('justifyRight'); break; default: break; } }; const markDirty = () => { if (!state.dirty) { state.dirty = true; setStatus('Cambios sin guardar.', 'dirty'); } }; const loadContent = () => { if (!fetchUrl) return; setLoading(true); request(fetchUrl) .then((payload) => { const data = payload && payload.data ? payload.data : {}; const html = data.contenido_html || ''; state.lastSavedHtml = html; state.dirty = false; if (contentEl) { contentEl.innerHTML = html; } if (data.updated_at) { setStatus(`Guardado por última vez: ${data.updated_at}`, 'saved'); } else { setStatus('', 'idle'); } }) .catch((error) => { setStatus(error.payload ? error.payload.message : error.message, 'error'); }) .finally(() => setLoading(false)); }; const saveContent = () => { if (!saveUrl || !contentEl || state.loading) return; const html = contentEl.innerHTML.trim(); if (html === state.lastSavedHtml) { setStatus('No hay cambios por guardar.', 'info'); return; } setLoading(true); setStatus('Guardando…', 'saving'); request(saveUrl, { method: 'POST', body: JSON.stringify({ contenido_html: html }), }) .then((payload) => { const data = payload && payload.data ? payload.data : {}; state.lastSavedHtml = data.contenido_html || html; state.dirty = false; setStatus(payload && payload.message ? payload.message : 'Informe guardado correctamente.', 'saved'); }) .catch((error) => { setStatus(error.payload ? error.payload.message : error.message, 'error'); }) .finally(() => setLoading(false)); }; if (buttons.length) { buttons.forEach((btn) => { btn.addEventListener('click', () => { applyFormat(btn.dataset.informeBtn); markDirty(); }); }); } if (contentEl) { contentEl.addEventListener('input', markDirty); contentEl.addEventListener('blur', () => { if (state.dirty) { setStatus('Cambios sin guardar.', 'dirty'); } }); } if (saveBtn) { saveBtn.addEventListener('click', saveContent); } loadContent(); return { reload: loadContent, save: saveContent, }; }; const informesApi = initDiagnosticoInforme(); const initClienteDocumentos = () => { const root = qs('[data-cliente-documentos]'); if (!root) return null; const fetchUrl = root.dataset.fetch || ''; const saveUrl = root.dataset.save || ''; const listEl = root.querySelector('[data-documentos-list]'); const emptyEl = root.querySelector('[data-documentos-empty]'); const statusEl = root.querySelector('[data-documentos-status]'); const template = root.querySelector('[data-documento-template]'); const progressCountEl = document.querySelector('[data-document-progress-count]'); const progressBarWrapper = document.querySelector('[data-document-progress-bar-wrapper]'); const progressBarEl = document.querySelector('[data-document-progress-bar]'); const summaryRefresh = window.diagnosticoSummaryRefresh; if (!fetchUrl || !saveUrl || !listEl || !template) { return null; } const state = { loading: false, documents: [], progress: { total: 0, uploaded: 0, percent: 0, }, }; const escapeHtml = (value) => String(value ?? '') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); const setStatus = (text, type = 'idle') => { if (!statusEl) return; statusEl.dataset.status = type; statusEl.textContent = text || ''; statusEl.hidden = !text; }; const setLoading = (value) => { state.loading = value; root.classList.toggle('is-loading', value); if (value) { root.setAttribute('aria-busy', 'true'); } else { root.removeAttribute('aria-busy'); } }; const updateProgress = (progress) => { if (progress && typeof progress === 'object') { state.progress = { total: progress.total ?? 0, uploaded: progress.uploaded ?? 0, percent: progress.percent ?? 0, }; } const { total, uploaded, percent } = state.progress; if (progressCountEl) { progressCountEl.textContent = `${uploaded} / ${total}`; } if (progressBarWrapper) { progressBarWrapper.setAttribute('aria-valuenow', String(percent)); } if (progressBarEl) { progressBarEl.style.width = `${percent}%`; } if (summaryRefresh && typeof summaryRefresh === 'function') { summaryRefresh(state.progress); } }; const request = (url, options = {}) => { const config = { credentials: 'same-origin', headers: { Accept: 'application/json', }, ...options, }; return fetch(url, config).then(async (response) => { let payload = null; try { payload = await response.json(); } catch (error) { payload = null; } if (!response.ok || (payload && payload.status === 'error')) { const message = payload && payload.message ? payload.message : 'Solicitud fallida.'; const errors = payload && payload.errors ? payload.errors : {}; const err = new Error(message); err.payload = { message, errors, status: payload ? payload.status : null }; throw err; } return payload; }); }; const renderPreview = (wrapper, mediaEl, nameEl, emptyEl, source) => { if (!wrapper || !mediaEl || !nameEl || !emptyEl) return; const hasSource = Boolean(source && source.url); wrapper.hidden = !hasSource; if (!hasSource) { mediaEl.innerHTML = ''; nameEl.textContent = ''; emptyEl.hidden = false; return; } emptyEl.hidden = true; nameEl.textContent = source.name || source.url.split('/').pop() || ''; const extension = (source.name || '').split('.').pop()?.toLowerCase(); const isImage = ['png', 'jpg', 'jpeg', 'webp', 'gif'].includes(extension); const isPdf = extension === 'pdf'; if (isImage) { mediaEl.innerHTML = `<img src="${escapeHtml(source.url)}" alt="${escapeHtml(nameEl.textContent || 'Vista previa')}" loading="lazy" />`; } else if (isPdf) { mediaEl.innerHTML = '<i class="fa-solid fa-file-pdf"></i>'; } else { mediaEl.innerHTML = '<i class="fa-solid fa-file"></i>'; } }; const renderDocument = (documento) => { const fragment = document.importNode(template.content, true); const article = fragment.querySelector('[data-documento]'); if (!article) return null; const titleEl = fragment.querySelector('[data-documento-titulo]'); const descripcionEl = fragment.querySelector('[data-documento-descripcion]'); const obligatorioEl = fragment.querySelector('[data-documento-obligatorio]'); const formatosEl = fragment.querySelector('[data-documento-formatos]'); const estadoLabel = fragment.querySelector('[data-documento-estado-label]'); const form = fragment.querySelector('[data-documento-form]'); const configInput = fragment.querySelector('[data-documento-config]'); const fileInput = fragment.querySelector('[data-documento-archivo]'); const fileInfo = fragment.querySelector('[data-documento-archivo-info]'); const hintEl = fragment.querySelector('[data-documento-hint]'); const previewWrapper = fragment.querySelector('[data-documento-preview]'); const previewMedia = fragment.querySelector('[data-documento-preview-media]'); const previewName = fragment.querySelector('[data-documento-preview-name]'); const previewEmpty = fragment.querySelector('[data-documento-preview-empty]'); const comentariosWrapper = fragment.querySelector('[data-documento-comentarios]'); const comentariosText = fragment.querySelector('[data-documento-comentarios-text]'); const submitBtn = fragment.querySelector('[data-documento-submit]'); const verBtn = fragment.querySelector('[data-documento-ver]'); const setDisabled = (value) => { if (!submitBtn) return; submitBtn.disabled = value; submitBtn.classList.toggle('is-loading', value); }; if (titleEl) { titleEl.textContent = documento.titulo || 'Documento'; } if (descripcionEl) { const text = documento.descripcion || ''; descripcionEl.hidden = text === ''; if (text !== '') { descripcionEl.textContent = text; } } if (obligatorioEl) { obligatorioEl.textContent = documento.es_obligatorio ? 'Obligatorio' : 'Opcional'; } if (formatosEl) { const formatos = Array.isArray(documento.formatos) ? documento.formatos : []; if (formatos.length) { formatosEl.hidden = false; formatosEl.textContent = `Formatos permitidos: ${formatos.map((ext) => `.${String(ext).toUpperCase()}`).join(', ')}`; } else { formatosEl.hidden = true; } } if (estadoLabel) { estadoLabel.textContent = documento.estado_label || documento.estado || 'Pendiente'; } if (configInput) { configInput.value = documento.config_id || ''; } const existingUrl = documento.url || ''; if (fileInfo) { if (existingUrl) { fileInfo.hidden = false; fileInfo.innerHTML = `<span class="diagnostico-documento__archivo-label">Actual:</span> <span>${escapeHtml(documento.nombre_original || existingUrl.split('/').pop() || 'Archivo adjunto')}</span>`; } else { fileInfo.hidden = true; fileInfo.innerHTML = ''; } } if (hintEl) { hintEl.textContent = documento.es_obligatorio ? 'Adjunta el archivo requerido para continuar con tu proceso.' : 'Puedes adjuntar este documento si lo tienes disponible.'; } renderPreview(previewWrapper, previewMedia, previewName, previewEmpty, existingUrl ? { url: existingUrl, name: documento.nombre_original || '', } : null); if (comentariosWrapper) { const hasComments = documento.comentarios_revision && documento.comentarios_revision.trim() !== ''; comentariosWrapper.hidden = !hasComments; if (hasComments && comentariosText) { comentariosText.textContent = documento.comentarios_revision; } } if (verBtn) { if (existingUrl) { verBtn.hidden = false; verBtn.href = existingUrl; } else { verBtn.hidden = true; verBtn.removeAttribute('href'); } } if (fileInput) { fileInput.addEventListener('change', () => { const files = fileInput.files; if (files && files.length && fileInfo) { fileInfo.hidden = false; fileInfo.innerHTML = `<span class="diagnostico-documento__archivo-label">Seleccionado:</span> <span>${escapeHtml(files[0].name)}</span>`; renderPreview(previewWrapper, previewMedia, previewName, previewEmpty, { url: URL.createObjectURL(files[0]), name: files[0].name, }); } else if (existingUrl && fileInfo) { fileInfo.hidden = false; fileInfo.innerHTML = `<span class="diagnostico-documento__archivo-label">Actual:</span> <span>${escapeHtml(documento.nombre_original || existingUrl.split('/').pop() || 'Archivo adjunto')}</span>`; renderPreview(previewWrapper, previewMedia, previewName, previewEmpty, { url: existingUrl, name: documento.nombre_original || '', }); } else if (fileInfo) { fileInfo.hidden = true; fileInfo.innerHTML = ''; renderPreview(previewWrapper, previewMedia, previewName, previewEmpty, null); } }); } if (form) { form.addEventListener('submit', (event) => { event.preventDefault(); if (state.loading) return; if (!configInput || !configInput.value) { window.alert('Documento no identificado.'); return; } const formData = new FormData(form); formData.set('config_id', configInput.value); setDisabled(true); setStatus('Subiendo documento…', 'saving'); request(saveUrl, { method: 'POST', body: formData, }) .then((payload) => { const data = payload && payload.data ? payload.data : {}; if (payload && payload.message) { showFlash(payload.message, 'success'); } if (data.document) { replaceDocument(data.document); } else { fetchDocuments(); } if (data.progress) { updateProgress(data.progress); } setStatus('Documento enviado para revisión.', 'success'); }) .catch((error) => { const message = error && error.payload ? error.payload.message : error.message; const errors = error && error.payload ? error.payload.errors : null; let detail = ''; if (errors && typeof errors === 'object') { detail = Object.values(errors) .flat() .map((item) => (Array.isArray(item) ? item.join(' ') : String(item))) .filter(Boolean) .join(' '); } setStatus(`${message}${detail ? ` ${detail}` : ''}`, 'error'); window.alert(message || 'No se pudo subir el documento.'); }) .finally(() => { setDisabled(false); }); }); } return fragment; }; const renderDocuments = () => { listEl.innerHTML = ''; if (!Array.isArray(state.documents) || !state.documents.length) { if (emptyEl) { emptyEl.hidden = false; } return; } if (emptyEl) { emptyEl.hidden = true; } state.documents.forEach((doc) => { const node = renderDocument(doc); if (node) { listEl.appendChild(node); } }); }; const replaceDocument = (doc) => { state.documents = state.documents.map((item) => ( (item && item.config_id === doc.config_id) ? doc : item )); renderDocuments(); }; const fetchDocuments = () => { setLoading(true); setStatus('Cargando documentos…', 'loading'); request(fetchUrl) .then((payload) => { const data = payload && payload.data ? payload.data : {}; state.documents = Array.isArray(data.documents) ? data.documents : []; updateProgress(data.progress); setStatus('', 'idle'); renderDocuments(); }) .catch((error) => { const message = error.payload ? error.payload.message : error.message; setStatus(message || 'No se pudieron cargar los documentos.', 'error'); }) .finally(() => setLoading(false)); }; fetchDocuments(); return { refresh: fetchDocuments, }; }; const clienteDocumentosApi = initClienteDocumentos(); const initDiagnosticoDocumentos = () => { const root = qs('[data-diagnostico-documentos]'); if (!root) return null; const fetchUrl = root.dataset.fetch || ''; const saveUrl = root.dataset.save || ''; const listEl = root.querySelector('[data-documentos-list]'); const emptyEl = root.querySelector('[data-documentos-empty]'); const statusEl = root.querySelector('[data-documentos-status]'); const template = root.querySelector('[data-documentos-template]'); if (!fetchUrl || !saveUrl || !listEl || !template) { return null; } const state = { loading: false, documents: [], statuses: [], statusLabels: {}, }; const escapeHtml = (value) => String(value ?? '') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); const setStatus = (text, type = 'idle') => { if (!statusEl) return; statusEl.dataset.status = type; statusEl.textContent = text || ''; statusEl.hidden = !text; }; const setLoading = (value) => { state.loading = value; root.classList.toggle('is-loading', value); if (value) { root.setAttribute('aria-busy', 'true'); } else { root.removeAttribute('aria-busy'); } }; const request = (url, options = {}) => { const config = { credentials: 'same-origin', headers: { Accept: 'application/json', }, ...options, }; return fetch(url, config).then(async (response) => { let payload = null; try { payload = await response.json(); } catch (error) { payload = null; } if (!response.ok || (payload && payload.status === 'error')) { const message = payload && payload.message ? payload.message : 'Solicitud fallida.'; const errors = payload && payload.errors ? payload.errors : {}; const err = new Error(message); err.payload = { message, errors, status: payload ? payload.status : null }; throw err; } return payload; }); }; const createDocumentRow = (documento) => { const clone = document.importNode(template.content, true); const article = clone.querySelector('[data-documento]'); if (!article) return null; const titleEl = clone.querySelector('[data-documento-titulo]'); const descripcionEl = clone.querySelector('[data-documento-descripcion]'); const obligatorioEl = clone.querySelector('[data-documento-obligatorio]'); const formatoEl = clone.querySelector('[data-documento-formato]'); const estadoLabel = clone.querySelector('[data-documento-estado-label]'); const form = clone.querySelector('[data-documento-form]'); const configInput = clone.querySelector('[data-documento-config]'); const estadoSelect = clone.querySelector('[data-documento-estado]'); const comentariosInput = clone.querySelector('[data-documento-comentarios]'); const fileInput = clone.querySelector('[data-documento-archivo]'); const fileInfo = clone.querySelector('[data-documento-archivo-actual]'); const verBtn = clone.querySelector('[data-documento-ver]'); const previewWrapper = clone.querySelector('[data-documento-preview]'); const previewMedia = clone.querySelector('[data-documento-preview-media]'); const previewName = clone.querySelector('[data-documento-preview-name]'); const previewEmpty = clone.querySelector('[data-documento-preview-empty]'); const renderEstadoOptions = () => { if (!estadoSelect) return; estadoSelect.innerHTML = ''; state.statuses.forEach((status) => { const option = document.createElement('option'); option.value = status; option.textContent = state.statusLabels[status] || status; if (String(documento.estado) === status) { option.selected = true; } estadoSelect.appendChild(option); }); }; if (titleEl) { titleEl.textContent = documento.titulo || 'Documento'; } if (descripcionEl) { const text = documento.descripcion || ''; if (text !== '') { descripcionEl.hidden = false; descripcionEl.textContent = text; } else { descripcionEl.hidden = true; } } if (obligatorioEl) { obligatorioEl.textContent = documento.es_obligatorio ? 'Obligatorio' : 'Opcional'; } if (formatoEl) { const formatos = documento.formatos || documento.tipos_archivo || null; if (Array.isArray(formatos) && formatos.length) { formatoEl.hidden = false; formatoEl.textContent = `Formatos: ${formatos.map((item) => `.${item.toString().toUpperCase()}`).join(', ')}`; } else { formatoEl.hidden = true; } } if (estadoLabel) { estadoLabel.textContent = state.statusLabels[documento.estado] || documento.estado; } if (configInput) { configInput.value = documento.config_id || ''; } if (comentariosInput) { comentariosInput.value = documento.comentarios_revision || ''; } if (fileInfo) { const link = documento.url || ''; if (link) { fileInfo.hidden = false; fileInfo.innerHTML = `<span class="diagnostico-documento__archivo-label">Actual:</span> <span>${escapeHtml(documento.nombre_original || link.split('/').pop() || 'Archivo adjunto')}</span>`; } else { fileInfo.hidden = true; fileInfo.textContent = ''; } } if (verBtn) { const link = documento.url || ''; if (link) { verBtn.hidden = false; verBtn.href = link; } else { verBtn.hidden = true; verBtn.removeAttribute('href'); } } const renderPreview = (source) => { if (!previewWrapper || !previewMedia || !previewName || !previewEmpty) return; const hasSource = Boolean(source && source.url); previewWrapper.hidden = !hasSource; if (!hasSource) { previewMedia.innerHTML = ''; previewName.textContent = ''; previewEmpty.hidden = false; return; } previewEmpty.hidden = true; previewName.textContent = source.name || source.url.split('/').pop() || ''; const extension = (source.name || '').split('.').pop()?.toLowerCase(); const isImage = ['png', 'jpg', 'jpeg', 'webp', 'gif'].includes(extension); const isPdf = extension === 'pdf'; if (isImage) { previewMedia.innerHTML = `<img src="${escapeHtml(source.url)}" alt="${escapeHtml(previewName.textContent || 'Vista previa')}" loading="lazy" />`; } else if (isPdf) { previewMedia.innerHTML = `<i class="fa-solid fa-file-pdf"></i>`; } else { previewMedia.innerHTML = `<i class="fa-solid fa-file"></i>`; } }; renderPreview({ url: documento.url || '', name: documento.nombre_original || '', }); renderEstadoOptions(); const updateEstadoLabel = () => { if (!estadoLabel || !estadoSelect) return; const status = estadoSelect.value; estadoLabel.textContent = state.statusLabels[status] || status; }; if (estadoSelect) { estadoSelect.addEventListener('change', updateEstadoLabel); } if (form) { form.addEventListener('submit', (event) => { event.preventDefault(); if (state.loading) return; if (!configInput || !configInput.value) { window.alert('Documento no identificado.'); return; } const formData = new FormData(form); formData.set('config_id', configInput.value); setLoading(true); setStatus('Guardando documento…', 'saving'); request(saveUrl, { method: 'POST', body: formData, }) .then((payload) => { const saved = payload && payload.data ? payload.data : null; setStatus('Documento actualizado correctamente.', 'success'); if (saved) { replaceDocument(saved); } else { fetchDocuments(); } }) .catch((error) => { const message = error.payload ? error.payload.message : error.message; const errors = error.payload ? error.payload.errors : null; let detail = ''; if (errors && typeof errors === 'object') { detail = Object.values(errors) .flat() .map((item) => (Array.isArray(item) ? item.join(' ') : String(item))) .filter(Boolean) .join(' '); } setStatus(`${message}${detail ? ` ${detail}` : ''}`, 'error'); }) .finally(() => { setLoading(false); }); }); } if (fileInput) { fileInput.addEventListener('change', () => { if (!fileInfo) return; const files = fileInput.files; if (files && files.length) { fileInfo.hidden = false; fileInfo.innerHTML = `<span class="diagnostico-documento__archivo-label">Seleccionado:</span> <span>${escapeHtml(files[0].name)}</span>`; renderPreview({ url: URL.createObjectURL(files[0]), name: files[0].name, }); } else if (documento.url) { fileInfo.hidden = false; fileInfo.innerHTML = `<span class="diagnostico-documento__archivo-label">Actual:</span> <span>${escapeHtml(documento.nombre_original || documento.url.split('/').pop() || 'Archivo adjunto')}</span>`; renderPreview({ url: documento.url, name: documento.nombre_original || '', }); } else { fileInfo.hidden = true; fileInfo.textContent = ''; renderPreview(null); } }); } return clone; }; const replaceDocument = (doc) => { state.documents = state.documents.map((item) => ( (item && item.config_id === doc.config_id) ? doc : item )); renderDocuments(); }; const renderDocuments = () => { if (!listEl) return; listEl.innerHTML = ''; const docs = state.documents; if (!Array.isArray(docs) || !docs.length) { if (emptyEl) { emptyEl.hidden = false; emptyEl.removeAttribute('hidden'); } return; } if (emptyEl) { emptyEl.hidden = true; emptyEl.setAttribute('hidden', ''); } docs.forEach((documento) => { const node = createDocumentRow(documento); if (node) { listEl.appendChild(node); } }); }; const fetchDocuments = () => { setLoading(true); setStatus('Cargando documentos…', 'loading'); request(fetchUrl) .then((payload) => { const data = payload && payload.data ? payload.data : {}; state.documents = Array.isArray(data.documents) ? data.documents : []; state.statuses = Array.isArray(data.statuses) ? data.statuses : []; state.statusLabels = data.status_labels && typeof data.status_labels === 'object' ? data.status_labels : {}; setStatus('', 'idle'); renderDocuments(); }) .catch((error) => { const message = error.payload ? error.payload.message : error.message; setStatus(message || 'No se pudieron cargar los documentos.', 'error'); }) .finally(() => { setLoading(false); }); }; fetchDocuments(); return { refresh: fetchDocuments, }; }; const documentosApi = initDiagnosticoDocumentos(); const initDiagnosticoSettings = () => { const root = qs('[data-diagnostico-settings]'); if (!root) return; const tabs = qsa('[data-settings-tab]', root); const panels = qsa('[data-settings-panel]', root); const activateTab = (name) => { tabs.forEach((tab) => { const match = tab.dataset.settingsTab === name; tab.classList.toggle('is-active', match); tab.setAttribute('aria-selected', String(match)); }); panels.forEach((panel) => { const match = panel.dataset.settingsPanel === name; panel.classList.toggle('is-active', match); if (match) { panel.removeAttribute('hidden'); } else { panel.setAttribute('hidden', ''); } }); }; tabs.forEach((tab) => { if (tab.dataset.bindSettings === '1') return; tab.dataset.bindSettings = '1'; tab.addEventListener('click', () => { const name = tab.dataset.settingsTab; if (!name) return; activateTab(name); }); }); const questionsRoot = root.querySelector('[data-questions-root]'); const documentsRoot = root.querySelector('[data-documents-root]'); const initDocuments = () => { if (!documentsRoot) return; const listEl = documentsRoot.querySelector('[data-documents-list]'); const emptyEl = documentsRoot.querySelector('[data-documents-empty]'); const editor = documentsRoot.querySelector('[data-document-editor]'); const editorTitle = documentsRoot.querySelector('[data-document-editor-title]'); const form = documentsRoot.querySelector('[data-document-form]'); const newBtn = documentsRoot.closest('[data-diagnostico-settings]')?.querySelector('[data-document-new]'); const cancelBtn = documentsRoot.querySelector('[data-document-cancel]'); const typesWrapper = documentsRoot.querySelector('[data-document-types]'); const typeCheckboxes = typesWrapper ? Array.from(typesWrapper.querySelectorAll('input[type="checkbox"][name="tipos_archivo[]"]')) : []; const quantityField = documentsRoot.querySelector('[data-document-quantity] input[name="maximo_archivos"]'); const qtyMinusBtn = documentsRoot.querySelector('[data-document-qty="minus"]'); const qtyPlusBtn = documentsRoot.querySelector('[data-document-qty="plus"]'); const endpoints = { list: 'index.php?r=diagnosticos/documents', store: 'index.php?r=diagnosticos/documentsStore', update: (id) => `index.php?r=diagnosticos/documentsUpdate&id=${encodeURIComponent(String(id))}`, delete: (id) => `index.php?r=diagnosticos/documentsDelete&id=${encodeURIComponent(String(id))}`, move: (id, direction) => `index.php?r=diagnosticos/documentsMove&id=${encodeURIComponent(String(id))}&direction=${encodeURIComponent(direction)}`, }; const state = { loading: false, editingId: null, documents: [], }; const escapeHtml = (value) => String(value ?? '') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); const setLoading = (value) => { state.loading = value; documentsRoot.classList.toggle('is-loading', value); if (value) { documentsRoot.setAttribute('aria-busy', 'true'); } else { documentsRoot.removeAttribute('aria-busy'); } }; const showError = (message, errors) => { let text = message || 'Ocurrió un error inesperado.'; if (errors && typeof errors === 'object') { const extra = Object.values(errors) .flat() .map((item) => (Array.isArray(item) ? item.join(' ') : String(item))) .filter(Boolean) .join(' '); if (extra) { text = `${text} ${extra}`; } } window.alert(text); }; const request = (url, options = {}) => { const config = { credentials: 'same-origin', headers: { Accept: 'application/json', }, ...options, }; if (config.body && !(config.body instanceof FormData)) { config.headers['Content-Type'] = 'application/json'; } return fetch(url, config).then(async (response) => { let payload = null; try { payload = await response.json(); } catch (error) { payload = null; } if (!response.ok || (payload && payload.status === 'error')) { const message = payload && payload.message ? payload.message : 'Solicitud fallida.'; const errors = payload && payload.errors ? payload.errors : {}; const err = new Error(message); err.payload = { message, errors, status: payload ? payload.status : null }; throw err; } return payload; }); }; const renderList = () => { if (!listEl) return; listEl.innerHTML = ''; const items = state.documents; if (!items.length) { if (emptyEl) { emptyEl.hidden = false; emptyEl.removeAttribute('hidden'); } return; } if (emptyEl) { emptyEl.hidden = true; emptyEl.setAttribute('hidden', ''); } items.forEach((doc, index) => { const article = document.createElement('article'); article.className = 'settings-document'; article.dataset.documentId = String(doc.id); const resumen = doc.descripcion ? `<p>${escapeHtml(doc.descripcion)}</p>` : ''; const tipos = Array.isArray(doc.tipos_archivo) && doc.tipos_archivo.length ? `<div class="settings-document__chips">${doc.tipos_archivo.map((type) => `<span class="chip chip--outline">.${escapeHtml(String(type).toUpperCase())}</span>`).join('')}</div>` : '<div class="settings-document__chips"><span class="chip chip--outline">Formato libre</span></div>'; const obligatoriedad = doc.es_obligatorio ? 'Obligatorio' : 'Opcional'; const estado = doc.activo ? 'Activo' : 'Inactivo'; const maxFiles = doc.maximo_archivos && doc.maximo_archivos > 1 ? `${escapeHtml(doc.maximo_archivos)} archivos` : '1 archivo'; const badges = ` <div class="settings-document__badges"> <span class="settings-document__badge"> <span class="settings-document__badge-value">${obligatoriedad}</span> </span> <span class="settings-document__badge"> <span class="settings-document__badge-value">${maxFiles}</span> </span> <span class="settings-document__badge ${doc.activo ? 'is-active' : 'is-inactive'}"> <span class="settings-document__badge-value">${estado}</span> </span> </div>`; const icon = '<span class="settings-document__icon" aria-hidden="true"><i class="fa-solid fa-file-arrow-up"></i></span>'; article.innerHTML = ` <div class="settings-document__head"> <div class="settings-document__title"> ${icon} <div class="settings-document__info"> <h4>${escapeHtml(doc.titulo)}</h4> ${resumen} ${tipos} </div> </div> ${badges} </div> <div class="settings-document__actions"> <button type="button" class="btn btn--ghost" data-document-edit> <i class="fa-solid fa-pen-to-square"></i> <span>Editar</span> </button> <div class="settings-document__order"> <button type="button" class="icon-btn" data-document-move="up" ${index === 0 ? 'disabled' : ''} title="Mover arriba"> <i class="fa-solid fa-arrow-up"></i> </button> <button type="button" class="icon-btn" data-document-move="down" ${index === items.length - 1 ? 'disabled' : ''} title="Mover abajo"> <i class="fa-solid fa-arrow-down"></i> </button> </div> <button type="button" class="btn btn--ghost is-danger" data-document-delete> <i class="fa-solid fa-trash"></i> <span>Eliminar</span> </button> </div> `; const editBtn = article.querySelector('[data-document-edit]'); const deleteBtn = article.querySelector('[data-document-delete]'); const upBtn = article.querySelector('[data-document-move="up"]'); const downBtn = article.querySelector('[data-document-move="down"]'); if (editBtn) { editBtn.addEventListener('click', () => showEditor(doc)); } if (deleteBtn) { deleteBtn.addEventListener('click', () => { if (state.loading) return; const confirmed = window.confirm(`¿Eliminar el documento "${doc.titulo}"?`); if (!confirmed) return; setLoading(true); request(endpoints.delete(doc.id), { method: 'POST' }) .then(() => { setLoading(false); return fetchDocuments(true); }) .catch((error) => { showError(error.payload ? error.payload.message : error.message, error.payload ? error.payload.errors : null); }) .finally(() => setLoading(false)); }); } const handleMove = (direction) => { if (state.loading) return; setLoading(true); request(endpoints.move(doc.id, direction), { method: 'POST', body: JSON.stringify({ direction }) }) .then(() => { setLoading(false); return fetchDocuments(true); }) .catch((error) => { showError(error.payload ? error.payload.message : error.message, error.payload ? error.payload.errors : null); }) .finally(() => setLoading(false)); }; if (upBtn) { upBtn.addEventListener('click', () => handleMove('up')); } if (downBtn) { downBtn.addEventListener('click', () => handleMove('down')); } listEl.appendChild(article); }); }; const hideEditor = () => { if (!editor || !form) return; form.reset(); if (typeCheckboxes.length) { typeCheckboxes.forEach((checkbox) => { checkbox.checked = false; }); } if (quantityField) { quantityField.value = '1'; } if (form.activo) form.activo.checked = true; state.editingId = null; editor.hidden = true; }; const populateTypes = (selected) => { if (!typeCheckboxes.length) return; const values = Array.isArray(selected) ? selected.map((item) => item.toLowerCase()) : []; typeCheckboxes.forEach((checkbox) => { checkbox.checked = values.includes(checkbox.value.toLowerCase()); }); }; const collectTypes = () => { if (!typeCheckboxes.length) return null; const values = typeCheckboxes .filter((checkbox) => checkbox.checked) .map((checkbox) => checkbox.value.trim().toLowerCase()) .filter(Boolean); return values.length ? values : null; }; const showEditor = (doc) => { if (!editor || !form) return; if (doc) { if (editorTitle) editorTitle.textContent = 'Editar documento'; form.titulo.value = doc.titulo || ''; form.descripcion.value = doc.descripcion || ''; form.maximo_archivos.value = doc.maximo_archivos || 1; if (form.es_obligatorio) form.es_obligatorio.checked = Boolean(doc.es_obligatorio); if (form.activo) form.activo.checked = Boolean(doc.activo); populateTypes(doc.tipos_archivo || []); state.editingId = doc.id; } else { if (editorTitle) editorTitle.textContent = 'Nuevo documento'; form.reset(); form.maximo_archivos.value = 1; populateTypes([]); if (form.activo) form.activo.checked = true; state.editingId = null; } editor.hidden = false; const field = form.querySelector('input[name="titulo"]'); if (field) field.focus(); }; const fetchDocuments = (skipGuard = false) => { const manageLoading = !skipGuard; if (manageLoading) { setLoading(true); } return request(endpoints.list) .then((payload) => { const data = payload && Array.isArray(payload.data) ? payload.data : []; state.documents = data; renderList(); }) .catch((error) => { showError(error.payload ? error.payload.message : error.message, error.payload ? error.payload.errors : null); }) .finally(() => { if (manageLoading) { setLoading(false); } }); }; if (form && !form.dataset.bindDocuments) { form.dataset.bindDocuments = '1'; form.addEventListener('submit', (event) => { event.preventDefault(); if (state.loading) return; const payload = { titulo: form.titulo.value, descripcion: form.descripcion.value, tipos_archivo: collectTypes(), maximo_archivos: form.maximo_archivos.value, es_obligatorio: form.es_obligatorio ? form.es_obligatorio.checked : false, activo: form.activo ? form.activo.checked : true, }; const url = state.editingId ? endpoints.update(state.editingId) : endpoints.store; setLoading(true); request(url, { method: 'POST', body: JSON.stringify(payload) }) .then(() => { hideEditor(); setLoading(false); return fetchDocuments(true); }) .catch((error) => { showError(error.payload ? error.payload.message : error.message, error.payload ? error.payload.errors : null); }) .finally(() => setLoading(false)); }); } if (cancelBtn && !cancelBtn.dataset.bindDocuments) { cancelBtn.dataset.bindDocuments = '1'; cancelBtn.addEventListener('click', hideEditor); } if (newBtn && !newBtn.dataset.bindDocuments) { newBtn.dataset.bindDocuments = '1'; newBtn.addEventListener('click', () => showEditor(null)); } const adjustQuantity = (delta) => { if (!quantityField) return; const current = parseInt(quantityField.value, 10) || 1; const next = Math.max(1, current + delta); quantityField.value = String(next); }; if (qtyMinusBtn && !qtyMinusBtn.dataset.bindDocuments) { qtyMinusBtn.dataset.bindDocuments = '1'; qtyMinusBtn.addEventListener('click', () => adjustQuantity(-1)); } if (qtyPlusBtn && !qtyPlusBtn.dataset.bindDocuments) { qtyPlusBtn.dataset.bindDocuments = '1'; qtyPlusBtn.addEventListener('click', () => adjustQuantity(1)); } hideEditor(); if (emptyEl) { emptyEl.hidden = true; emptyEl.setAttribute('hidden', ''); } fetchDocuments(); }; initDocuments(); if (!questionsRoot) return; const endpoints = { list: 'index.php?r=diagnosticos/questions', store: 'index.php?r=diagnosticos/questionsStore', update: (id) => `index.php?r=diagnosticos/questionsUpdate&id=${encodeURIComponent(String(id))}`, delete: (id) => `index.php?r=diagnosticos/questionsDelete&id=${encodeURIComponent(String(id))}`, move: (id, direction) => `index.php?r=diagnosticos/questionsMove&id=${encodeURIComponent(String(id))}&direction=${encodeURIComponent(direction)}`, }; const state = { loading: false, editingId: null, questions: [], }; const listEl = questionsRoot.querySelector('[data-questions-list]'); const emptyEl = questionsRoot.querySelector('[data-questions-empty]'); const editor = questionsRoot.querySelector('[data-question-editor]'); const editorTitle = questionsRoot.querySelector('[data-question-editor-title]'); const form = questionsRoot.querySelector('[data-question-form]'); const newBtn = root.querySelector('[data-question-new]'); const cancelBtn = questionsRoot.querySelector('[data-question-cancel]'); const optionsField = questionsRoot.querySelector('[data-question-options-field]'); const optionsTextarea = optionsField ? optionsField.querySelector('textarea') : null; const escapeHtml = (value) => { return String(value ?? '') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }; const setLoading = (value) => { state.loading = value; questionsRoot.classList.toggle('is-loading', value); if (value) { questionsRoot.setAttribute('aria-busy', 'true'); } else { questionsRoot.removeAttribute('aria-busy'); } }; const showError = (message, errors) => { let text = message || 'Ocurrió un error inesperado.'; if (errors && typeof errors === 'object') { const extra = Object.values(errors) .flat() .map((item) => (Array.isArray(item) ? item.join(' ') : String(item))) .filter(Boolean) .join(' '); if (extra) { text = `${text} ${extra}`; } } window.alert(text); }; const request = (url, options = {}) => { const config = { credentials: 'same-origin', headers: { Accept: 'application/json', }, ...options, }; if (config.body && !(config.body instanceof FormData)) { config.headers['Content-Type'] = 'application/json'; } return fetch(url, config).then(async (response) => { let payload = null; try { payload = await response.json(); } catch (error) { payload = null; } if (!response.ok || (payload && payload.status === 'error')) { const message = payload && payload.message ? payload.message : 'Solicitud fallida.'; const errors = payload && payload.errors ? payload.errors : {}; const err = new Error(message); err.payload = { message, errors, status: payload ? payload.status : null }; throw err; } return payload; }); }; const renderList = () => { if (!listEl) return; listEl.innerHTML = ''; const items = state.questions; const typeLabels = { booleano: 'Sí / No', texto: 'Texto corto', textarea: 'Texto largo', numero: 'Número', fecha: 'Fecha', opciones: 'Selección única', multiple: 'Selección múltiple', }; if (!items.length) { if (emptyEl) { emptyEl.hidden = false; emptyEl.removeAttribute('hidden'); } return; } if (emptyEl) { emptyEl.hidden = true; emptyEl.setAttribute('hidden', ''); } items.forEach((question, index) => { const article = document.createElement('article'); article.className = 'settings-question'; article.dataset.questionId = String(question.id); const resumen = question.descripcion ? `<p>${escapeHtml(question.descripcion)}</p>` : ''; const ayuda = question.ayuda_contextual ? `<p class="settings-question__help">${escapeHtml(question.ayuda_contextual)}</p>` : ''; const opciones = Array.isArray(question.opciones) && question.opciones.length ? `<p class="settings-question__options">Opciones: ${escapeHtml(question.opciones.map((opt) => (typeof opt === 'string' ? opt : opt.label || opt.value || '')).filter(Boolean).join(', '))}</p>` : ''; const typeLabel = typeLabels[String(question.tipo_respuesta || '').toLowerCase()] || question.tipo_respuesta; article.innerHTML = ` <div class="settings-question__head"> <div> <h4>${escapeHtml(question.titulo)}</h4> ${resumen} ${ayuda} </div> <div class="settings-question__meta"> <span class="chip chip--outline">${escapeHtml(typeLabel)}</span> <span class="chip chip--outline">${question.es_obligatoria ? 'Obligatoria' : 'Opcional'}</span> <span class="chip chip--outline">${question.activo ? 'Activa' : 'Inactiva'}</span> </div> </div> ${opciones} <div class="settings-question__actions"> <button type="button" class="btn btn--ghost" data-question-edit> <i class="fa-solid fa-pen-to-square"></i> <span>Editar</span> </button> <div class="settings-question__order"> <button type="button" class="icon-btn" data-question-move="up" ${index === 0 ? 'disabled' : ''} title="Mover arriba"> <i class="fa-solid fa-arrow-up"></i> </button> <button type="button" class="icon-btn" data-question-move="down" ${index === items.length - 1 ? 'disabled' : ''} title="Mover abajo"> <i class="fa-solid fa-arrow-down"></i> </button> </div> <button type="button" class="btn btn--ghost is-danger" data-question-delete> <i class="fa-solid fa-trash"></i> <span>Eliminar</span> </button> </div> `; const editBtn = article.querySelector('[data-question-edit]'); const deleteBtn = article.querySelector('[data-question-delete]'); const upBtn = article.querySelector('[data-question-move="up"]'); const downBtn = article.querySelector('[data-question-move="down"]'); if (editBtn) { editBtn.addEventListener('click', () => { showEditor(question); }); } if (deleteBtn) { deleteBtn.addEventListener('click', () => { if (state.loading) return; const confirmed = window.confirm(`¿Eliminar la pregunta "${question.titulo}"?`); if (!confirmed) return; setLoading(true); request(endpoints.delete(question.id), { method: 'POST' }) .then(() => fetchQuestions()) .catch((error) => { showError(error.payload ? error.payload.message : error.message, error.payload ? error.payload.errors : null); }) .finally(() => setLoading(false)); }); } const handleMove = (direction) => { if (state.loading) return; setLoading(true); let shouldRefresh = false; request(endpoints.move(question.id, direction), { method: 'POST', body: JSON.stringify({ direction }) }) .then(() => { shouldRefresh = true; }) .catch((error) => { showError(error.payload ? error.payload.message : error.message, error.payload ? error.payload.errors : null); }) .finally(() => { setLoading(false); if (shouldRefresh) { fetchQuestions(); } }); }; if (upBtn) { upBtn.addEventListener('click', () => { handleMove('up'); }); } if (downBtn) { downBtn.addEventListener('click', () => { handleMove('down'); }); } listEl.appendChild(article); }); }; const toggleOptionsField = () => { if (!optionsField) return; const type = form ? form.tipo_respuesta.value : ''; const needsOptions = type === 'opciones' || type === 'multiple'; optionsField.hidden = !needsOptions; if (needsOptions) { optionsField.removeAttribute('hidden'); optionsField.style.display = ''; } else { optionsField.setAttribute('hidden', ''); optionsField.style.display = 'none'; } if (!needsOptions && optionsTextarea) { optionsTextarea.value = ''; } }; const hideEditor = () => { if (!editor || !form) return; form.reset(); if (form.activo) { form.activo.checked = true; } state.editingId = null; editor.hidden = true; toggleOptionsField(); }; const showEditor = (question) => { if (!editor || !form) return; if (question) { if (editorTitle) editorTitle.textContent = 'Editar pregunta'; form.titulo.value = question.titulo || ''; form.descripcion.value = question.descripcion || ''; form.tipo_respuesta.value = question.tipo_respuesta || ''; form.ayuda_contextual.value = question.ayuda_contextual || ''; if (form.es_obligatoria) { form.es_obligatoria.checked = Boolean(question.es_obligatoria); } if (form.activo) { form.activo.checked = Boolean(question.activo); } if (optionsTextarea) { if (Array.isArray(question.opciones) && question.opciones.length) { optionsTextarea.value = question.opciones .map((opt) => { if (typeof opt === 'string') return opt; if (opt && typeof opt === 'object') return opt.label || opt.value || ''; return ''; }) .filter(Boolean) .join('\n'); } else { optionsTextarea.value = ''; } } state.editingId = question.id; } else { if (editorTitle) editorTitle.textContent = 'Nueva pregunta'; form.reset(); if (form.activo) { form.activo.checked = true; } state.editingId = null; } editor.hidden = false; toggleOptionsField(); const field = form.querySelector('input[name="titulo"]'); if (field) { field.focus(); } }; const fetchQuestions = () => { if (state.loading) return Promise.resolve(); setLoading(true); return request(endpoints.list) .then((payload) => { let items = []; if (payload && Array.isArray(payload.data)) { items = payload.data; } else if (payload && payload.data && Array.isArray(payload.data.questions)) { items = payload.data.questions; } state.questions = items; renderList(); }) .catch((error) => { showError(error.payload ? error.payload.message : error.message, error.payload ? error.payload.errors : null); }) .finally(() => setLoading(false)); }; if (form && !form.dataset.bindQuestions) { form.dataset.bindQuestions = '1'; form.addEventListener('submit', (event) => { event.preventDefault(); if (state.loading) return; const payload = { titulo: form.titulo.value, descripcion: form.descripcion.value, tipo_respuesta: form.tipo_respuesta.value, opciones: optionsTextarea ? optionsTextarea.value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean) : null, ayuda_contextual: form.ayuda_contextual.value, es_obligatoria: form.es_obligatoria ? form.es_obligatoria.checked : false, activo: form.activo ? form.activo.checked : true, }; const url = state.editingId ? endpoints.update(state.editingId) : endpoints.store; setLoading(true); request(url, { method: 'POST', body: JSON.stringify(payload) }) .then((response) => { if (response && response.data) { if (state.editingId) { const index = state.questions.findIndex((q) => Number(q.id) === Number(state.editingId)); if (index >= 0) { state.questions[index] = response.data; } } else { state.questions.push(response.data); } state.questions.sort((a, b) => Number(a.orden) - Number(b.orden)); renderList(); } else { fetchQuestions(); } hideEditor(); }) .catch((error) => { showError(error.payload ? error.payload.message : error.message, error.payload ? error.payload.errors : null); }) .finally(() => setLoading(false)); }); form.tipo_respuesta.addEventListener('change', toggleOptionsField); } if (cancelBtn && !cancelBtn.dataset.bindQuestions) { cancelBtn.dataset.bindQuestions = '1'; cancelBtn.addEventListener('click', () => { hideEditor(); }); } if (newBtn && !newBtn.dataset.bindQuestions) { newBtn.dataset.bindQuestions = '1'; newBtn.addEventListener('click', () => { showEditor(null); }); } hideEditor(); if (emptyEl) { emptyEl.hidden = true; emptyEl.setAttribute('hidden', ''); } fetchQuestions(); }; initDiagnosticoSettings(); // Filtros de servicios const serviciosRoot = qs('[data-servicios-root]'); if (serviciosRoot) { const form = serviciosRoot.querySelector('.servicios-filters'); const grid = serviciosRoot.querySelector('[data-servicios-grid]'); const cards = qsa('[data-servicio-card]', grid); const emptyState = serviciosRoot.querySelector('[data-servicios-empty]'); const filterEmpty = serviciosRoot.querySelector('.servicios-filter-empty'); const resetBtn = serviciosRoot.querySelector('[data-servicios-reset]'); const searchInput = form?.querySelector('#servicios-search'); const areaSelect = form?.querySelector('#servicios-area'); const estadoSelect = form?.querySelector('#servicios-estado'); const statusBanner = serviciosRoot.querySelector('[data-servicios-status]'); const norm = (value) => (value || '') .toString() .toLowerCase() .normalize('NFD') .replace(/[^\p{Letter}\p{Number}\s]/gu, '') .replace(/\s+/g, ' ') .trim(); const toggleCard = (card, show) => { card.hidden = !show; card.style.display = show ? '' : 'none'; }; let serviciosFilterTimer = null; const setServiciosFiltering = (value) => { serviciosRoot.classList.toggle('is-filtering', Boolean(value)); if (statusBanner) { statusBanner.hidden = !value; } }; const applyFiltersServicios = () => { const term = norm(searchInput?.value || ''); const area = norm(areaSelect?.value || ''); const estado = norm(estadoSelect?.value || ''); let visible = 0; cards.forEach((card) => { const hayTermino = term !== ''; const hayArea = area !== ''; const hayEstado = estado !== ''; const content = card.dataset.search ? norm(card.dataset.search) : ''; const cardArea = card.dataset.area ? norm(card.dataset.area) : ''; const cardEstado = card.dataset.estado ? norm(card.dataset.estado) : ''; const matchBusqueda = !hayTermino || content.includes(term); const matchArea = !hayArea || cardArea === area; const matchEstado = !hayEstado || cardEstado === estado; const show = matchBusqueda && matchArea && matchEstado; toggleCard(card, show); if (show) visible += 1; }); if (emptyState) { emptyState.hidden = visible !== 0; } if (filterEmpty) filterEmpty.hidden = visible > 0; setServiciosFiltering(false); }; const scheduleServiciosFilters = (delay = 200) => { setServiciosFiltering(true); if (serviciosFilterTimer) { window.clearTimeout(serviciosFilterTimer); } serviciosFilterTimer = window.setTimeout(() => { applyFiltersServicios(); }, delay); }; if (form) { form.addEventListener('submit', (event) => { event.preventDefault(); setServiciosFiltering(true); applyFiltersServicios(); }); } if (searchInput) { ['input', 'change', 'search'].forEach((ev) => searchInput.addEventListener(ev, () => scheduleServiciosFilters(220))); } if (areaSelect) { areaSelect.addEventListener('change', () => scheduleServiciosFilters(160)); } if (estadoSelect) { estadoSelect.addEventListener('change', () => scheduleServiciosFilters(160)); } if (resetBtn && form) { resetBtn.addEventListener('click', () => { form.reset(); scheduleServiciosFilters(0); }); } // Aplicar filtros iniciales si llegan por query string setServiciosFiltering(true); applyFiltersServicios(); const serviciosDataScript = qs('#servicios-data', serviciosRoot) || qs('#servicios-data'); let serviciosDataset = []; if (serviciosDataScript) { try { serviciosDataset = JSON.parse(serviciosDataScript.textContent || '[]'); } catch (error) { console.warn('No se pudo parsear el dataset de servicios', error); serviciosDataset = []; } } const datasetMap = Array.isArray(serviciosDataset) ? serviciosDataset.reduce((acc, item, index) => { const key = String(index); acc[key] = item; if (item && typeof item === 'object' && (item.id || item.id === 0)) { acc[`id:${item.id}`] = item; } return acc; }, {}) : {}; const drawer = serviciosRoot.querySelector('[data-servicio-drawer]'); if (!drawer) { return; } const drawerMedia = drawer.querySelector('[data-servicio-media]'); const drawerTitle = drawer.querySelector('[data-servicio-title]'); const drawerSubtitle = drawer.querySelector('[data-servicio-subtitle]'); const drawerChips = drawer.querySelector('[data-servicio-chips]'); const drawerContent = drawer.querySelector('[data-servicio-drawer-content]'); const drawerEdit = drawer.querySelector('[data-servicio-edit]'); const drawerDeleteForm = drawer.querySelector('[data-servicio-delete]'); const closeEls = qsa('[data-servicio-drawer-close]', drawer); const pageBody = document.body; const escapeHtmlSvc = (str = '') => String(str) .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); const escapeAttrSvc = (str = '') => escapeHtmlSvc(str).replace(/`/g, '`'); const cleanText = (value) => String(value ?? '').trim(); const valueOrDash = (value) => { const text = cleanText(value); return text !== '' ? escapeHtmlSvc(text) : '-'; }; const listFrom = (value) => { if (!value) return '<li>-</li>'; const items = Array.isArray(value) ? value : String(value) .split(/\r?\n|;|,/) .map((item) => item.trim()) .filter(Boolean); if (!items.length) return '<li>-</li>'; return items.map((item) => `<li>${escapeHtmlSvc(item)}</li>`).join(''); }; const estadoBadgeClass = (estado) => { const norm = cleanText(estado).toLowerCase(); if (norm === 'activo') return 'is-success'; if (norm === 'inactivo' || norm === 'en pausa' || norm === 'pausado') return 'is-warning'; return 'is-info'; }; const formatCurrency = (value) => { const text = cleanText(value); if (text === '') return '-'; const numeric = Number(text.replace(/[^0-9.-]/g, '')); if (Number.isNaN(numeric)) { return escapeHtmlSvc(text); } return escapeHtmlSvc(new Intl.NumberFormat('es-CO', { style: 'currency', currency: 'COP', maximumFractionDigits: 0, }).format(numeric)); }; const normalizeImagePath = (rawPath, fallbackCard) => { let path = cleanText(rawPath); if (!path && fallbackCard) { const img = fallbackCard.querySelector('img'); if (img) { path = img.getAttribute('src') || ''; } } if (!path) return ''; if (/^(https?:)?\/\//i.test(path) || path.startsWith('../') || path.startsWith('data:')) { return path; } if (path.startsWith('admin/')) { return `../${path}`; } return path; }; const mergeServiceData = (card) => { if (!card) return null; const key = card.dataset.servicioKey; const idKey = card.dataset.servicioId ? `id:${card.dataset.servicioId}` : null; const datasetItem = (key && datasetMap[key]) || (idKey && datasetMap[idKey]) || null; const fallback = (() => { const titleEl = card.querySelector('.servicio-card__title'); const descriptionEl = card.querySelector('.servicio-card__description'); const meta = {}; qsa('.servicio-card__meta li', card).forEach((li) => { const raw = li.textContent || ''; const [label, ...rest] = raw.split(':'); const value = rest.join(':').trim(); const labelNorm = label ? label.trim().toLowerCase() : ''; if (labelNorm.includes('unidad')) meta.unidad_metrica = value; if (labelNorm.includes('tiempo')) meta.tiempo_entrega = value; if (labelNorm.includes('periodicidad')) meta.periodicidad = value; }); return { id: card.dataset.servicioId || '', servicio: titleEl ? titleEl.textContent.trim() : '', descripcion: descriptionEl ? descriptionEl.textContent.trim() : '', area: card.dataset.area || '', estado: card.dataset.estado || '', ...meta, imagen: normalizeImagePath('', card), }; })(); if (!datasetItem) { return fallback; } return Object.assign({}, fallback, datasetItem); }; const renderDrawerChips = (service) => { if (!drawerChips) return; const chips = []; if (service.area) { chips.push(`<span class="chip chip--area"><i class="fa-solid fa-diagram-project"></i> ${escapeHtmlSvc(service.area)}</span>`); } if (service.unidad_metrica) { chips.push(`<span class="chip"><i class="fa-solid fa-scale-balanced"></i> ${escapeHtmlSvc(service.unidad_metrica)}</span>`); } if (service.tiempo_entrega) { chips.push(`<span class="chip"><i class="fa-solid fa-clock"></i> ${escapeHtmlSvc(service.tiempo_entrega)}</span>`); } if (service.periodicidad) { chips.push(`<span class="chip"><i class="fa-solid fa-rotate"></i> ${escapeHtmlSvc(service.periodicidad)}</span>`); } if (service.estado) { chips.push(`<span class="badge ${estadoBadgeClass(service.estado)}">${escapeHtmlSvc(service.estado)}</span>`); } drawerChips.innerHTML = chips.join(''); }; const renderDrawerContent = (service) => { if (!drawerContent) return; const docsObligatorios = listFrom(service.docs_obligatorios); const docsOpcionales = listFrom(service.docs_opcionales); const resumenList = ` <li><strong>Código:</strong> ${valueOrDash(service.id)}</li> <li><strong>Área:</strong> ${valueOrDash(service.area)}</li> <li><strong>Estado:</strong> ${valueOrDash(service.estado)}</li> <li><strong>Unidad métrica:</strong> ${valueOrDash(service.unidad_metrica)}</li> <li><strong>Tiempo de entrega:</strong> ${valueOrDash(service.tiempo_entrega)}</li> <li><strong>Periodicidad:</strong> ${valueOrDash(service.periodicidad)}</li> `; const valoresList = ` <li><strong>Valor base:</strong> ${formatCurrency(service.valor_base)}</li> <li><strong>Precio empresa pequeña:</strong> ${formatCurrency(service.precio_pequena)}</li> <li><strong>Precio empresa mediana:</strong> ${formatCurrency(service.precio_mediana)}</li> <li><strong>Precio empresa grande:</strong> ${formatCurrency(service.precio_grande)}</li> `; const entregaList = ` <li><strong>Descripción:</strong> ${valueOrDash(service.descripcion)}</li> <li><strong>Observaciones:</strong> ${valueOrDash(service.observaciones || service.notas || service.detalles)}</li> `; drawerContent.innerHTML = ` <section class="team-ficha servicio-ficha"> <article class="team-ficha__grid"> <div> <h4>Resumen</h4> <ul>${resumenList}</ul> </div> <div> <h4>Valores de referencia</h4> <ul>${valoresList}</ul> </div> <div> <h4>Entrega y notas</h4> <ul>${entregaList}</ul> </div> </article> <article class="team-ficha__lists"> <div> <h4>Documentos obligatorios</h4> <ul>${docsObligatorios}</ul> </div> <div> <h4>Documentos opcionales</h4> <ul>${docsOpcionales}</ul> </div> </article> </section> `; }; const openDrawer = () => { drawer.hidden = false; requestAnimationFrame(() => drawer.classList.add('is-open')); pageBody.classList.add('has-drawer-open'); }; const closeDrawer = () => { drawer.classList.remove('is-open'); const onEnd = () => { drawer.hidden = true; pageBody.classList.remove('has-drawer-open'); drawer.removeEventListener('transitionend', onEnd); }; drawer.addEventListener('transitionend', onEnd); setTimeout(() => { if (!drawer.classList.contains('is-open')) { drawer.hidden = true; pageBody.classList.remove('has-drawer-open'); } }, 320); }; closeEls.forEach((el) => el.addEventListener('click', closeDrawer)); document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && !drawer.hidden) { closeDrawer(); } }); drawer.addEventListener('click', (event) => { const backdrop = drawer.querySelector('.team-drawer__backdrop'); if (backdrop && event.target === backdrop) { closeDrawer(); } }); const renderDrawer = (service, card) => { if (!service) return; const imageUrl = normalizeImagePath(service.imagen, card); if (drawerMedia) { if (imageUrl) { drawerMedia.classList.remove('is-empty'); drawerMedia.innerHTML = `<img src="${escapeAttrSvc(imageUrl)}" alt="${escapeHtmlSvc(service.servicio || 'Imagen del servicio')}" />`; } else { drawerMedia.classList.add('is-empty'); drawerMedia.textContent = (service.servicio || '?').charAt(0).toUpperCase(); } } if (drawerTitle) { drawerTitle.textContent = cleanText(service.servicio) || 'Detalle del servicio'; } if (drawerSubtitle) { drawerSubtitle.textContent = cleanText(service.descripcion) || 'Sin descripción disponible.'; } renderDrawerChips(service); renderDrawerContent(service); if (drawerEdit) { if (service.id) { drawerEdit.href = `index.php?r=servicios/edit&id=${encodeURIComponent(service.id)}`; drawerEdit.classList.remove('is-disabled'); } else { drawerEdit.href = '#'; drawerEdit.classList.add('is-disabled'); } } if (drawerDeleteForm) { const deleteButton = drawerDeleteForm.querySelector('button'); if (service.id) { drawerDeleteForm.action = `index.php?r=servicios/delete&id=${encodeURIComponent(service.id)}`; deleteButton?.classList.remove('is-disabled'); deleteButton?.removeAttribute('disabled'); } else { drawerDeleteForm.action = '#'; if (deleteButton) { deleteButton.classList.add('is-disabled'); deleteButton.setAttribute('disabled', 'disabled'); } } drawerDeleteForm.dataset.servicioName = service.servicio || ''; if (deleteButton) { deleteButton.dataset.servicioDeleteTrigger = '1'; } } }; cards.forEach((card) => { card.style.cursor = 'pointer'; card.addEventListener('click', (event) => { if (event.target.closest('.servicio-card__actions')) { return; } const serviceData = mergeServiceData(card); renderDrawer(serviceData, card); openDrawer(); }); }); } // Filtros de usuarios const searchInput = qs('#filter-search'); const roleSelect = qs('#filter-role'); const statusSelect = qs('#filter-status'); const cards = qsa('.user-card'); const userCards = cards; const emptyState = qs('.empty-state'); const filterEmpty = qs('.js-filter-empty'); const countDescriptor = null; const chipsWrap = null; const norm = (s) => (s || '') .toString() .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .trim(); const escapeHtml = (str = '') => str.replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', })[char] || char); const escapeAttribute = (str = '') => escapeHtml(String(str)).replace(/`/g, '`'); const popBadge = () => {}; const clearExitState = (card) => { if (card._hideTimeout) { clearTimeout(card._hideTimeout); card._hideTimeout = null; } if (card._exitHandler) { card.removeEventListener('animationend', card._exitHandler); card._exitHandler = null; } card.classList.remove('is-exiting'); }; const toggleCardVisibility = (card, shouldShow) => { if (shouldShow) { if (!card.hidden && card.style.display !== 'none') { return; } clearExitState(card); card.hidden = false; card.style.removeProperty('display'); void card.offsetWidth; card.classList.add('is-entering'); const onEnterEnd = (event) => { if (event.target !== card) return; card.classList.remove('is-entering'); card.removeEventListener('animationend', onEnterEnd); }; card.addEventListener('animationend', onEnterEnd); } else { if ((card.hidden || card.style.display === 'none') && !card.classList.contains('is-entering')) { return; } card.classList.remove('is-entering'); card.classList.add('is-exiting'); const onExitEnd = (event) => { if (event.target !== card) return; card.hidden = true; card.style.display = 'none'; card.classList.remove('is-exiting'); card.removeEventListener('animationend', onExitEnd); card._exitHandler = null; }; card._exitHandler = onExitEnd; card.addEventListener('animationend', onExitEnd); card._hideTimeout = setTimeout(() => { if (!card.hidden) { clearExitState(card); card.hidden = true; card.style.display = 'none'; } }, 260); } }; const renderChips = () => {}; const updateCounter = () => {}; const initPasswordToggles = () => { qsa('[data-toggle="password"]').forEach((button) => { const wrapper = button.closest('.password-field'); if (!wrapper) return; const input = wrapper.querySelector('input[type="password"], input[type="text"]'); if (!input) return; button.addEventListener('click', () => { const isHidden = input.type === 'password'; input.type = isHidden ? 'text' : 'password'; button.classList.toggle('is-active', isHidden); const icon = button.querySelector('i'); if (icon) { icon.classList.toggle('fa-eye', !isHidden); icon.classList.toggle('fa-eye-slash', isHidden); } button.setAttribute('aria-label', isHidden ? 'Ocultar contraseña' : 'Mostrar contraseña'); }); }); }; const initUserPhotoField = () => { const field = qs('[data-photo-field]'); if (!field) return; const preview = field.querySelector('.photo-preview'); const img = preview ? preview.querySelector('.photo-preview__img') : null; const initials = preview ? preview.querySelector('.photo-preview__initials') : null; const input = field.querySelector('[data-photo-input]'); const currentInput = field.querySelector('[data-photo-current]'); const removeFlag = field.querySelector('[data-photo-remove-flag]'); const removeBtn = field.querySelector('[data-photo-remove]'); const toggleState = (hasImage) => { preview?.classList.toggle('has-image', hasImage); if (img) { img.hidden = !hasImage; if (!hasImage) { img.removeAttribute('src'); } } if (initials) { initials.hidden = hasImage; } if (removeBtn) { removeBtn.hidden = !hasImage; } }; const updatePreviewFromFile = (file) => { if (!file) return; const reader = new FileReader(); reader.onload = (event) => { if (img && typeof event.target?.result === 'string') { img.src = event.target.result; toggleState(true); } }; reader.readAsDataURL(file); }; if (input) { input.addEventListener('change', () => { const file = input.files && input.files[0]; if (file) { updatePreviewFromFile(file); if (removeFlag) removeFlag.value = '0'; if (currentInput) currentInput.value = ''; } }); } if (removeBtn) { removeBtn.addEventListener('click', () => { toggleState(false); if (input) { input.value = ''; } if (currentInput) { currentInput.value = ''; } if (removeFlag) { removeFlag.value = '1'; } }); } const hasInitialImage = currentInput && currentInput.value !== ''; toggleState(hasInitialImage); }; const applyFilters = () => { const termRaw = searchInput?.value || ''; const roleValue = roleSelect?.value || 'todos'; const statusValue = statusSelect?.value || 'todos'; const term = norm(termRaw); const role = norm(roleValue || 'todos'); const status = norm(statusValue || 'todos'); const filters = { term: termRaw.trim(), roleValue, statusValue, roleLabel: roleSelect ? roleSelect.options[roleSelect.selectedIndex]?.text.trim() || '' : '', statusLabel: statusSelect ? statusSelect.options[statusSelect.selectedIndex]?.text.trim() || '' : '', }; let visibleCount = 0; const restoreAll = term === '' && role === 'todos' && status === 'todos'; userCards.forEach((card) => { const keyword = norm(card.dataset.search || card.dataset.name || ''); const cardRole = norm(card.dataset.role || ''); const cardStatus = norm(card.dataset.status || ''); const matchName = term === '' || keyword.includes(term); const matchRole = role === 'todos' || cardRole === role; const matchStatus = status === 'todos' || cardStatus === status; const shouldShow = restoreAll || (matchName && matchRole && matchStatus); toggleCardVisibility(card, shouldShow); if (shouldShow) { visibleCount++; } }); if (restoreAll) { visibleCount = userCards.length; } if (emptyState) { emptyState.hidden = userCards.length === 0 || visibleCount > 0; } if (filterEmpty) { filterEmpty.hidden = visibleCount > 0; } updateCounter(visibleCount); renderChips(filters); }; if (searchInput || roleSelect || statusSelect) { if (searchInput) { ['input','keyup','search','change'].forEach(evt => searchInput.addEventListener(evt, applyFilters)); } if (roleSelect) roleSelect.addEventListener('change', applyFilters); if (statusSelect) statusSelect.addEventListener('change', applyFilters); applyFilters(); // Mejor UX: enfocar el buscador al cargar if (searchInput) { setTimeout(() => { try { searchInput.focus(); } catch(_){ } }, 0); } } if (chipsWrap) {} initPasswordToggles(); const initEquipoModule = (root) => { const dataScript = qs('#equipo-data', root) || qs('#equipo-data'); let dataset = []; if (dataScript) { try { dataset = JSON.parse(dataScript.textContent || '[]'); } catch (error) { console.warn('No se pudo parsear el dataset de equipo', error); } } const searchField = qs('#equipo-filter-search', root); const areaSelect = qs('#equipo-filter-area', root); const contractSelect = qs('#equipo-filter-contract', root); const statusSelectEq = qs('#equipo-filter-status', root); const cards = qsa('.team-card', root); const emptyState = qs('.team-empty-state', root); const filterEmpty = qs('.team-filter-empty', root); const statusBanner = qs('[data-equipo-status]', root); const pendingSection = qs('.equipo-pending', root); const pageContainer = qs('[data-equipo-root]', root); const drawer = qs('[data-team-drawer]', root); const pageBody = document.body; const drawerContent = drawer ? drawer.querySelector('[data-team-drawer-content]') : null; const drawerName = drawer ? drawer.querySelector('[data-team-name]') : null; const drawerRole = drawer ? drawer.querySelector('[data-team-role]') : null; const drawerAvatar = drawer ? drawer.querySelector('[data-team-avatar]') : null; const drawerBadges = drawer ? drawer.querySelector('[data-team-badges]') : null; const drawerEditLink = drawer ? drawer.querySelector('[data-team-edit]') : null; const drawerDeleteBtn = drawer ? drawer.querySelector('[data-team-delete-trigger]') : null; const closeEls = drawer ? qsa('[data-team-drawer-close]', drawer) : []; const modal = qs('[data-team-modal]', root); const modalPanel = modal ? modal.querySelector('[data-team-form]') : null; const modalCloseEls = modal ? qsa('[data-team-modal-close]', modal) : []; const modalTitle = modal ? modal.querySelector('[data-team-form-title]') : null; const modalSubtitle = modal ? modal.querySelector('[data-team-form-subtitle]') : null; const form = modal ? modal.querySelector('[data-team-form]') : null; const formFields = form ? qsa('[data-team-field]', form) : []; const formSubmitBtn = modal ? modal.querySelector('[data-team-form-submit]') : null; const formatLabel = (value = '') => { return String(value) .replace(/[\-_]+/g, ' ') .replace(/\s+/g, ' ') .trim() .toLowerCase() .replace(/^(.)|\s+(.)/g, (match) => match.toUpperCase()); }; const initialsFromName = (name = '') => { const parts = String(name).trim().split(/\s+/).filter(Boolean).slice(0, 2); if (!parts.length) return 'E'; return parts.map((part) => part.charAt(0).toUpperCase()).join('').slice(0, 2); }; let currentMember = null; const datasetMap = Array.isArray(dataset) ? dataset.reduce((acc, item) => { if (item && typeof item === 'object' && (item.id || item.id === 0)) { acc[String(item.id)] = item; } return acc; }, {}) : {}; const renderListItems = (value) => { if (!value || (Array.isArray(value) && value.length === 0)) { return '<li>-</li>'; } const items = Array.isArray(value) ? value : String(value).split(/[;,]/).map((item) => item.trim()).filter(Boolean); if (!items.length) { return '<li>-</li>'; } return items.map((item) => `<li>${escapeHtml(String(item))}</li>`).join(''); }; const renderFicha = (member) => { if (!member) return; if (drawerName) { drawerName.textContent = member.nombre || 'Ficha miembro'; } if (drawerRole) { const memberRole = member.rol_cargo || member.rol || 'Cargo no definido'; drawerRole.textContent = memberRole; } if (drawerAvatar) { if (member.avatar_url) { const safeUrl = escapeAttribute(member.avatar_url); const safeName = escapeAttribute(member.nombre || 'Miembro'); drawerAvatar.innerHTML = `<img src="${safeUrl}" alt="Avatar de ${safeName}" />`; } else { drawerAvatar.textContent = initialsFromName(member.nombre || ''); } } if (drawerBadges) { drawerBadges.innerHTML = ` <span class="chip chip--area"><i class="fa-solid fa-diagram-project"></i> ${escapeHtml(formatLabel(member.area || ''))}</span> <span class="chip chip--contract"><i class="fa-solid fa-briefcase"></i> ${escapeHtml(formatLabel(member.tipo_contrato || ''))}</span> <span class="badge ${escapeHtml(member.estado === 'activo' ? 'is-success' : (member.estado === 'inactivo' ? 'is-warning' : 'is-info'))}">${escapeHtml(formatLabel(member.estado || ''))}</span> `; } if (drawerEditLink) { drawerEditLink.href = member.id ? `index.php?r=equipo/edit&id=${encodeURIComponent(member.id)}` : '#'; } if (drawerDeleteBtn) { drawerDeleteBtn.disabled = !member.id; drawerDeleteBtn.dataset.memberId = member.id || ''; drawerDeleteBtn.dataset.memberName = member.nombre || ''; } if (!drawerContent) return; const permisos = (member.permisos_sistema || '').split(',').map((p) => p.trim()).filter(Boolean); const responsabilidades = (member.responsabilidades || '').split(';').map((r) => r.trim()).filter(Boolean); const listOrFallback = (items) => { if (!items || items.length === 0) { return '<li>-</li>'; } return items.map((item) => `<li>${escapeHtml(item)}</li>`).join(''); }; const rawDocuments = Array.isArray(member.documentos) ? member.documentos : Array.isArray(member.documentos_expediente) ? member.documentos_expediente : []; const docItems = rawDocuments.map((doc) => { if (!doc) return null; if (typeof doc === 'string') { const trimmed = doc.trim(); if (!trimmed) return null; const label = trimmed.split('/').pop(); const safeHref = escapeAttribute(trimmed); return `<li><a href="${safeHref}" target="_blank" rel="noopener">${escapeHtml(label || trimmed)}</a></li>`; } const name = doc.tipo_documento || doc.nombre || doc.label || doc.archivo || ''; const link = doc.archivo || doc.url || ''; if (!name && !link) return null; const safeName = escapeHtml(name || 'Documento'); if (link) { const safeHref = escapeAttribute(link); return `<li><a href="${safeHref}" target="_blank" rel="noopener">${safeName}</a></li>`; } return `<li>${safeName}</li>`; }).filter(Boolean).join(''); const formatCurrency = (value) => { if (value === null || value === undefined || value === '') return null; const numeric = Number(value); if (Number.isNaN(numeric)) return String(value); return new Intl.NumberFormat('es-CO', { style: 'currency', currency: 'COP', maximumFractionDigits: 0 }).format(numeric); }; const salarioEstimado = formatCurrency(member.estimado_mensual); drawerContent.innerHTML = ` <section class="team-ficha"> <article class="team-ficha__grid"> <div> <h4>Contacto</h4> <ul> <li><strong>Email:</strong> ${escapeHtml(member.email_corporativo || '-')}</li> <li><strong>Teléfono:</strong> ${escapeHtml(member.telefono_movil || '-')}</li> <li><strong>Extensión:</strong> ${escapeHtml(member.extension_oficina || '-')}</li> <li><strong>WhatsApp:</strong> ${member.acceso_whatsapp ? 'Sí' : 'No'}</li> </ul> </div> <div> <h4>Organización</h4> <ul> <li><strong>Cargo:</strong> ${escapeHtml(member.rol_cargo || member.rol || '-')}</li> <li><strong>Área:</strong> ${escapeHtml(member.area || '-')}</li> <li><strong>Tipo de contrato:</strong> ${escapeHtml(formatLabel(member.tipo_contrato || '-'))}</li> <li><strong>Horario:</strong> ${escapeHtml(member.horario || '-')}</li> <li><strong>Jefe directo:</strong> ${escapeHtml(member.jefe_directo || '-')}</li> <li><strong>Estado:</strong> ${escapeHtml(formatLabel(member.estado || '-'))}</li> </ul> </div> <div> <h4>Expediente</h4> <ul> <li><strong>Documento:</strong> ${escapeHtml(member.tipo_documento || '')} ${escapeHtml(member.num_documento || '')}</li> <li><strong>Ingreso:</strong> ${escapeHtml(member.fecha_ingreso || '-')}</li> <li><strong>Dirección:</strong> ${escapeHtml(member.direccion || '-')}</li> <li><strong>Ubicación:</strong> ${escapeHtml(member.ubicacion || '-')}</li> <li><strong>Acceso WhatsApp:</strong> ${member.acceso_whatsapp ? 'Sí' : 'No'}</li> </ul> </div> <div> <h4>Compensación</h4> <ul> <li><strong>Tipo de salario:</strong> ${escapeHtml(formatLabel(member.tipo_salario || '-'))}</li> <li><strong>Estimado mensual:</strong> ${escapeHtml(salarioEstimado || '-')}</li> <li><strong>Observaciones:</strong> ${escapeHtml(member.observaciones || '-')}</li> </ul> </div> </article> <article class="team-ficha__lists"> <div> <h4>Permisos</h4> <ul>${listOrFallback(permisos)}</ul> </div> <div> <h4>Responsabilidades</h4> <ul>${listOrFallback(responsabilidades)}</ul> </div> <div> <h4>Documentos</h4> <ul>${docItems || '<li>-</li>'}</ul> </div> <div> <h4>Certificaciones</h4> <ul>${renderListItems(member.certificaciones)}</ul> </div> </article> </section> `; }; const openDrawer = () => { if (!drawer) return; drawer.hidden = false; if (pageBody) { pageBody.classList.add('has-drawer-open'); } requestAnimationFrame(() => drawer.classList.add('is-open')); }; const closeDrawer = () => { if (!drawer) return; drawer.classList.remove('is-open'); const onEnd = () => { drawer.hidden = true; if (pageBody) { pageBody.classList.remove('has-drawer-open'); } drawer.removeEventListener('transitionend', onEnd); }; drawer.addEventListener('transitionend', onEnd); setTimeout(() => { if (!drawer.classList.contains('is-open')) { drawer.hidden = true; if (pageBody) { pageBody.classList.remove('has-drawer-open'); } } }, 320); }; closeEls.forEach((el) => el.addEventListener('click', closeDrawer)); if (drawerDeleteBtn) { drawerDeleteBtn.addEventListener('click', () => { if (!currentMember || !currentMember.id) { alert('Genera la ficha primero.'); return; } const deleteButton = root.querySelector(`[data-member-id="${currentMember.id}"][data-member-name]`); if (deleteButton) { deleteButton.dispatchEvent(new Event('click', { bubbles: true })); } }); } let teamFilterTimer = null; const setTeamFiltering = (value) => { if (pageContainer) { pageContainer.classList.toggle('is-filtering', Boolean(value)); } if (statusBanner) { statusBanner.hidden = !value; } }; const applyTeamFilters = () => { const term = norm(searchField?.value || ''); const area = norm(areaSelect?.value || 'todas'); const contract = norm(contractSelect?.value || 'todos'); const status = norm(statusSelectEq?.value || 'todos'); const filterActive = term !== '' || area !== 'todas' || contract !== 'todos' || status !== 'todos'; let visible = 0; cards.forEach((card) => { const matchesTerm = term === '' || norm(card.dataset.search || '').includes(term); const matchesArea = area === 'todas' || norm(card.dataset.area || '') === area; const matchesContract = contract === 'todos' || norm(card.dataset.contract || '') === contract; const matchesStatus = status === 'todos' || norm(card.dataset.status || '') === status; const shouldShow = matchesTerm && matchesArea && matchesContract && matchesStatus; toggleCardVisibility(card, shouldShow); if (shouldShow) { visible++; } }); if (emptyState) { emptyState.hidden = cards.length === 0 ? false : visible > 0; } if (filterEmpty) { filterEmpty.hidden = cards.length === 0 ? true : visible > 0; } if (pendingSection) { pendingSection.hidden = filterActive; } setTeamFiltering(false); }; const scheduleTeamFilters = (immediate = false) => { if (immediate) { setTeamFiltering(true); applyTeamFilters(); return; } setTeamFiltering(true); if (teamFilterTimer) { window.clearTimeout(teamFilterTimer); } teamFilterTimer = window.setTimeout(() => { applyTeamFilters(); }, 200); }; if (searchField) { ['input','keyup','change','search'].forEach((evt) => searchField.addEventListener(evt, () => scheduleTeamFilters(false))); } if (areaSelect) areaSelect.addEventListener('change', () => scheduleTeamFilters(false)); if (contractSelect) contractSelect.addEventListener('change', () => scheduleTeamFilters(false)); if (statusSelectEq) statusSelectEq.addEventListener('change', () => scheduleTeamFilters(false)); scheduleTeamFilters(true); const handleCardClick = (event) => { const card = event.currentTarget; const memberId = card?.dataset.memberId; if (!memberId) return; const memberData = datasetMap[memberId]; if (!memberData) { alert('No se encontró información para este miembro.'); return; } currentMember = memberData; renderFicha(memberData); openDrawer(); }; qsa('[data-team-card]', root).forEach((card) => { card.addEventListener('click', handleCardClick); card.style.cursor = 'pointer'; }); const deleteModal = qs('[data-team-delete-modal]'); const deleteNameEl = qs('[data-team-delete-name]', deleteModal); const deleteMessageEl = qs('[data-team-delete-message]', deleteModal); const deleteConfirmBtn = qs('[data-team-delete-confirm]', deleteModal); const deleteCancelEls = qsa('[data-team-delete-cancel]', deleteModal); let deletePayload = { memberId: null, memberName: '' }; const openDeleteModal = ({ memberId, memberName }) => { if (!deleteModal || !memberId) { return; } deletePayload = { memberId, memberName }; if (deleteNameEl) { deleteNameEl.textContent = memberName || 'este miembro'; } if (deleteMessageEl) { deleteMessageEl.textContent = 'Esta operación eliminará la ficha y el usuario asociado. No se puede deshacer.'; } deleteModal.hidden = false; requestAnimationFrame(() => deleteModal.classList.add('is-open')); }; const closeDeleteModal = () => { if (!deleteModal) return; deleteModal.classList.remove('is-open'); const onEnd = () => { deleteModal.hidden = true; deleteModal.removeEventListener('transitionend', onEnd); }; deleteModal.addEventListener('transitionend', onEnd); setTimeout(() => { if (!deleteModal.classList.contains('is-open')) { deleteModal.hidden = true; } }, 220); }; if (deleteConfirmBtn) { deleteConfirmBtn.addEventListener('click', () => { const memberId = deletePayload.memberId; if (!memberId) { closeDeleteModal(); return; } let form = document.querySelector('#team-delete-form'); if (!form) { form = document.createElement('form'); form.method = 'POST'; form.action = 'index.php?r=equipo/delete'; form.id = 'team-delete-form'; form.style.display = 'none'; const input = document.createElement('input'); input.type = 'hidden'; input.name = 'id'; form.appendChild(input); document.body.appendChild(form); } const inputId = form.querySelector('input[name="id"]'); if (inputId) { inputId.value = memberId; } closeDeleteModal(); form.submit(); }); } deleteCancelEls.forEach((btn) => btn.addEventListener('click', closeDeleteModal)); qsa('.js-team-delete', root).forEach((button) => { button.addEventListener('click', () => { const memberId = button.dataset.memberId; const memberName = button.dataset.memberName || ''; if (!memberId) { alert('No se pudo identificar al miembro a eliminar.'); return; } openDeleteModal({ memberId, memberName }); }); }); const openModal = (mode = 'create', memberId = null) => { if (!modal || !form) return; modal.hidden = false; requestAnimationFrame(() => modal.classList.add('is-open')); const defaults = { id: '', mode, nombre: '', rol: '', email_corporativo: '', telefono_movil: '', area: '', tipo_contrato: '', estado: '', horario: '', responsabilidades: '', permisos_sistema: '', }; let fill = { ...defaults }; if (mode === 'edit' && memberId && datasetMap[memberId]) { const member = datasetMap[memberId]; fill = { ...fill, ...member, responsabilidades: member.responsabilidades || '', permisos_sistema: member.permisos_sistema || '', }; } formFields.forEach((field) => { const key = field.dataset.teamField; if (!key) return; const value = fill[key] ?? ''; if (field.tagName === 'SELECT') { field.value = value; } else if (field.tagName === 'TEXTAREA') { field.value = value; } else { field.value = value; } }); if (modalTitle) { modalTitle.textContent = mode === 'edit' ? 'Editar miembro' : 'Registrar miembro'; } if (modalSubtitle) { modalSubtitle.textContent = mode === 'edit' ? 'Ajusta los datos del colaborador seleccionado.' : 'Completa los datos para el nuevo colaborador.'; } if (formSubmitBtn) { formSubmitBtn.textContent = mode === 'edit' ? 'Actualizar (simulado)' : 'Guardar (simulado)'; } }; const closeModal = () => { if (!modal) return; modal.classList.remove('is-open'); const end = () => { modal.hidden = true; modal.removeEventListener('transitionend', end); }; modal.addEventListener('transitionend', end); setTimeout(() => { if (!modal.classList.contains('is-open')) { modal.hidden = true; } }, 240); }; modalCloseEls.forEach((item) => item.addEventListener('click', closeModal)); if (form) { form.addEventListener('submit', (event) => { event.preventDefault(); const formData = new FormData(form); const payload = {}; formData.forEach((value, key) => { payload[key] = value; }); const mode = payload.mode || 'create'; const label = mode === 'edit' ? 'actualizado' : 'registrado'; alert(`Formulario ${label} (simulado). Guarda estos datos cuando conectes la base de datos.`); closeModal(); }); } qsa('.js-team-edit', root).forEach((button) => { button.addEventListener('click', () => { const memberId = button.dataset.memberId ? String(button.dataset.memberId) : null; if (!memberId || !datasetMap[memberId]) { alert('No se encontró el miembro para editar.'); return; } openModal('edit', memberId); }); }); qsa('[data-equipo-create]', root).forEach((button) => { button.addEventListener('click', () => { openModal('create'); }); }); document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && drawer && !drawer.hidden) { closeDrawer(); } if (event.key === 'Escape' && modal && !modal.hidden) { closeModal(); } }); root.addEventListener('click', (event) => { if (!drawer || drawer.hidden) return; if (event.target === drawer.querySelector('.team-drawer__backdrop')) { closeDrawer(); } }); if (modal) { modal.addEventListener('click', (event) => { if (event.target === modal.querySelector('.team-modal__backdrop')) { closeModal(); } }); } }; const initEquipoFormTopbar = () => { const form = qs('#equipo-form'); if (!form) return; const submitBtn = qs('button[form="equipo-form"]'); const cancelLinks = qsa(`a[href="${form.getAttribute('action') === 'index.php?r=equipo/store' ? 'index.php?r=equipo/index' : 'index.php?r=equipo/index'}"]`); if (submitBtn) { submitBtn.addEventListener('click', () => { form.requestSubmit(); }); } cancelLinks.forEach((link) => { link.addEventListener('click', (event) => { const href = link.getAttribute('href'); if (href && href === '#') { event.preventDefault(); form.reset(); } }); }); }; const equipoRoot = qs('[data-equipo-root]'); if (equipoRoot) { initEquipoModule(document); } initEquipoFormTopbar(); initUserPhotoField(); initEquipoDocuments(); }); })(); function initEquipoDocuments() { const section = document.querySelector('[data-documents-section]'); if (!section) return; const list = section.querySelector('[data-documents-list]'); const template = section.querySelector('[data-document-template]'); const addBtn = section.querySelector('[data-document-add]'); const getNextIndex = () => { const raw = Number(section.dataset.nextIndex || 0); section.dataset.nextIndex = String(raw + 1); return raw; }; const ensureAtLeastOneRow = () => { if (!list || list.querySelector('[data-document-row]')) { return; } addRow(); }; const toggleRemovalState = (row, forceState) => { const existing = row.dataset.existing === '1'; const removeFlag = row.querySelector('[data-document-remove-flag]'); const statusLabel = row.querySelector('[data-document-status]'); const removeButton = row.querySelector('[data-document-remove]'); const isMarked = typeof forceState === 'boolean' ? forceState : !row.classList.contains('is-marked-for-removal'); if (!existing) { row.remove(); ensureAtLeastOneRow(); return; } row.classList.toggle('is-marked-for-removal', isMarked); if (removeFlag) { removeFlag.value = isMarked ? '1' : '0'; } if (statusLabel) { statusLabel.hidden = !isMarked; } if (removeButton) { removeButton.textContent = isMarked ? 'Deshacer eliminación' : 'Marcar para eliminar'; } }; const bindRow = (row) => { if (!row) return; const removeButton = row.querySelector('[data-document-remove]'); if (removeButton) { removeButton.addEventListener('click', () => toggleRemovalState(row)); } const uploadInput = row.querySelector('[data-document-upload]'); if (uploadInput) { uploadInput.addEventListener('change', () => { const removeFlag = row.querySelector('[data-document-remove-flag]'); if (removeFlag) { removeFlag.value = '0'; } row.classList.remove('is-marked-for-removal'); const statusLabel = row.querySelector('[data-document-status]'); if (statusLabel) { statusLabel.hidden = true; } const removeBtn = row.querySelector('[data-document-remove]'); if (removeBtn && row.dataset.existing === '1') { removeBtn.textContent = 'Marcar para eliminar'; } const filenameLabel = row.querySelector('[data-document-filename]'); if (filenameLabel) { const files = uploadInput.files; if (files && files.length > 0) { filenameLabel.textContent = files[0].name; } else { filenameLabel.textContent = filenameLabel.dataset.defaultText || 'Ningún archivo seleccionado'; } } }); } }; const addRow = () => { if (!template || !list) return; const index = getNextIndex(); const html = template.innerHTML.replace(/__INDEX__/g, index); const container = document.createElement('div'); container.innerHTML = html.trim(); const newRow = container.firstElementChild; if (!newRow) return; list.appendChild(newRow); bindRow(newRow); }; const existingRows = list ? Array.from(list.querySelectorAll('[data-document-row]')) : []; existingRows.forEach((row) => { bindRow(row); const removeFlag = row.querySelector('[data-document-remove-flag]'); if (removeFlag && removeFlag.value === '1') { toggleRemovalState(row, true); } }); if (addBtn) { addBtn.addEventListener('click', addRow); } ensureAtLeastOneRow(); }
Coded With 💗 by
0x6ick