document.addEventListener('DOMContentLoaded', function () { const csrfToken = document.body.dataset.csrfToken || ''; const nativeFetch = window.fetch.bind(window); window.fetch = function (resource, options) { const opts = options ? { ...options } : {}; const method = String(opts.method || 'GET').toUpperCase(); if (csrfToken && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) { const headers = new Headers(opts.headers || {}); if (!headers.has('X-CSRF-Token')) headers.set('X-CSRF-Token', csrfToken); opts.headers = headers; } return nativeFetch(resource, opts); }; const shell = document.getElementById('adminShell'); const sidebarToggle = document.getElementById('sidebarToggle'); const sidebarOverlay = document.getElementById('sidebarOverlay'); const DESKTOP_BREAKPOINT = 992; function isMobile() { return window.innerWidth < DESKTOP_BREAKPOINT; } function setDesktopState(collapsed) { if (!shell) return; shell.classList.toggle('sidebar-collapsed', collapsed); localStorage.setItem('sidebarCollapsed', collapsed ? '1' : '0'); } function openMobileSidebar() { if (!shell) return; shell.classList.add('sidebar-open'); document.body.classList.add('sidebar-lock'); } function closeMobileSidebar() { if (!shell) return; shell.classList.remove('sidebar-open'); document.body.classList.remove('sidebar-lock'); } function applySidebarMode() { if (!shell) return; if (isMobile()) { shell.classList.remove('sidebar-collapsed'); closeMobileSidebar(); } else { closeMobileSidebar(); setDesktopState(localStorage.getItem('sidebarCollapsed') === '1'); } } function toggleSidebar() { if (!shell) return; if (isMobile()) { shell.classList.contains('sidebar-open') ? closeMobileSidebar() : openMobileSidebar(); } else { setDesktopState(!shell.classList.contains('sidebar-collapsed')); } } applySidebarMode(); if (sidebarToggle) sidebarToggle.addEventListener('click', toggleSidebar); if (sidebarOverlay) sidebarOverlay.addEventListener('click', closeMobileSidebar); window.addEventListener('resize', applySidebarMode); document.querySelectorAll('.copy-btn').forEach(function (btn) { btn.addEventListener('click', async function () { const targetId = btn.getAttribute('data-copy-target'); const el = document.getElementById(targetId); if (!el) return; try { await navigator.clipboard.writeText(el.textContent.trim()); const original = btn.textContent; btn.textContent = 'Copiado'; setTimeout(function () { btn.textContent = original; }, 1400); } catch (e) { console.error('No se pudo copiar', e); } }); }); function ensureCsrfFields() { if (!csrfToken) return; document.querySelectorAll('form').forEach(function (form) { if (form.querySelector('input[name="csrf_token"]')) return; const input = document.createElement('input'); input.type = 'hidden'; input.name = 'csrf_token'; input.value = csrfToken; form.appendChild(input); }); } function applyImplicitPlaceholders() { document.querySelectorAll('form').forEach(function (form) { form.querySelectorAll('input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]):not([type="file"]), textarea, select').forEach(function (field) { if (field.placeholder || field.tagName === 'SELECT') return; const wrapper = field.closest('.col, [class*="col-"], .mb-3, .section-card, .soft-block, div'); const label = wrapper ? wrapper.querySelector('label.form-label, label') : null; const labelText = label ? label.textContent.replace(/\*/g, '').trim() : ''; if (labelText) field.placeholder = 'Ingresá ' + labelText.toLowerCase(); }); }); } function initActionConfirmations() { const modalEl = document.getElementById('confirmActionModal'); const titleEl = document.getElementById('confirmActionTitle'); const msgEl = document.getElementById('confirmActionMessage'); const acceptBtn = document.getElementById('confirmActionAccept'); if (!modalEl || !window.bootstrap || !acceptBtn) return; const modal = bootstrap.Modal.getOrCreateInstance(modalEl); let pending = null; function openConfirm(element, onAccept) { pending = onAccept; titleEl.textContent = element.getAttribute('data-confirm-title') || 'Confirmar acción'; msgEl.textContent = element.getAttribute('data-confirm') || '¿Deseás continuar con esta acción?'; acceptBtn.textContent = element.getAttribute('data-confirm-accept') || 'Continuar'; modal.show(); } acceptBtn.addEventListener('click', function () { if (typeof pending === 'function') pending(); pending = null; modal.hide(); }); document.addEventListener('click', function (ev) { const link = ev.target.closest('a[data-confirm]'); if (!link) return; ev.preventDefault(); openConfirm(link, function () { window.location.href = link.href; }); }); document.addEventListener('submit', function (ev) { const form = ev.target; if (!(form instanceof HTMLFormElement) || form.dataset.confirmed === '1') return; const submitter = ev.submitter || document.activeElement; const source = submitter && submitter.hasAttribute('data-confirm') ? submitter : (form.hasAttribute('data-confirm') ? form : null); if (!source) return; ev.preventDefault(); openConfirm(source, function () { form.dataset.confirmed = '1'; if (form.requestSubmit && submitter && submitter.form === form) form.requestSubmit(submitter); else form.submit(); setTimeout(function () { delete form.dataset.confirmed; }, 500); }); }, true); } function jsonFetch(url) { return fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }).then(async function (res) { let data; try { data = await res.json(); } catch (e) { throw new Error('La respuesta del servidor no es JSON válido.'); } if (!res.ok) { throw new Error((data && (data.error || data.message)) || ('Error HTTP ' + res.status)); } return data; }); } function sortItems(items) { return [...items].sort(function (a, b) { return String(a.nombre || a.text || '').localeCompare(String(b.nombre || b.text || ''), 'es', { sensitivity: 'base' }); }); } function hasSelect2(select) { return !!(window.jQuery && window.jQuery(select).hasClass('select2-hidden-accessible')); } function destroySearchableSelect(select) { if (!select || !window.jQuery || !window.jQuery.fn.select2) return; if (hasSelect2(select)) { window.jQuery(select).off('.select2Bridge'); window.jQuery(select).select2('destroy'); } } function initSearchableSelect(select, forceVisibleInit) { if (!select || !window.jQuery || !window.jQuery.fn.select2) return; if (select.classList.contains('geo-native-input')) return; const modal = select.closest('.modal'); if (!forceVisibleInit && modal && !modal.classList.contains('show')) { return; } destroySearchableSelect(select); const $select = window.jQuery(select); const $modal = $select.closest('.modal'); const placeholder = select.dataset.placeholder || (select.options[0] ? select.options[0].textContent : 'Seleccionar'); $select.select2({ theme: 'bootstrap-5', width: '100%', placeholder: placeholder, allowClear: !select.required, dropdownAutoWidth: false, dropdownParent: $modal.length ? $modal : window.jQuery(document.body), language: { noResults: function () { return 'Sin resultados'; }, searching: function () { return 'Buscando...'; }, inputTooShort: function () { return 'Escribí para buscar'; } }, matcher: function (params, data) { if (!params.term || !params.term.trim()) return data; if (typeof data.text === 'undefined') return null; const term = params.term.toLowerCase().trim(); const text = String(data.text || '').toLowerCase(); const sub = String((data.element && data.element.dataset && data.element.dataset.subtext) || '').toLowerCase(); return (text.includes(term) || sub.includes(term)) ? data : null; }, templateResult: function (data) { if (!data.id) return data.text; const subtext = data.element && data.element.dataset ? data.element.dataset.subtext : ''; if (!subtext) return data.text; const wrapper = document.createElement('div'); const title = document.createElement('div'); title.textContent = data.text; title.className = 'select2-main-label'; const sub = document.createElement('div'); sub.textContent = subtext; sub.className = 'select2-sub-label'; wrapper.appendChild(title); wrapper.appendChild(sub); return wrapper; }, templateSelection: function (data) { return data.text || placeholder; }, escapeMarkup: function (markup) { return markup; } }); $select.on('select2:open.select2Bridge', function () { const searchField = document.querySelector('.select2-container--open .select2-search__field'); if (searchField) searchField.placeholder = 'Escribí para buscar...'; }); } function refreshSearchableSelect(select) { if (!select) return; initSearchableSelect(select, true); if (window.jQuery && hasSelect2(select)) window.jQuery(select).trigger('change.select2'); } function fillSelect(select, items, selectedValue) { const placeholder = select.dataset.placeholder || 'Seleccionar'; destroySearchableSelect(select); select.innerHTML = ''; const first = document.createElement('option'); first.value = ''; first.textContent = placeholder; select.appendChild(first); sortItems(items).forEach(function (item) { const option = document.createElement('option'); option.value = item.id || item.value || item.nombre; option.textContent = item.nombre || item.text; option.dataset.name = item.nombre || item.text; if (item.subtext) option.dataset.subtext = item.subtext; if (selectedValue && (String(option.value) === String(selectedValue) || option.dataset.name === selectedValue)) { option.selected = true; } select.appendChild(option); }); initSearchableSelect(select, !select.closest('.modal') || select.closest('.modal').classList.contains('show')); } document.querySelectorAll('select.searchable-select').forEach(function (select) { if (!select.closest('.modal')) initSearchableSelect(select); }); // stacked modals support document.addEventListener('show.bs.modal', function (event) { const visible = document.querySelectorAll('.modal.show').length; const modal = event.target; const zIndex = 1055 + (10 * visible); modal.style.zIndex = zIndex; setTimeout(function () { const backdrops = document.querySelectorAll('.modal-backdrop:not(.modal-stack)'); const backdrop = backdrops[backdrops.length - 1]; if (backdrop) { backdrop.style.zIndex = zIndex - 1; backdrop.classList.add('modal-stack'); } }, 0); }); document.addEventListener('hidden.bs.modal', function () { if (document.querySelectorAll('.modal.show').length) { document.body.classList.add('modal-open'); } }); document.querySelectorAll('.modal').forEach(function (modalEl) { modalEl.addEventListener('shown.bs.modal', function () { modalEl.querySelectorAll('select.searchable-select').forEach(function (select) { initSearchableSelect(select, true); }); modalEl.querySelectorAll('.geo-form').forEach(function (form) { setTimeout(function () { initGeoForm(form); }, 30); }); }); }); async function hydratePatientIntoForm(form, patientId) { if (!form || !patientId) return; const email = form.querySelector('.patient-email'); const phone = form.querySelector('.patient-phone'); try { const payload = await jsonFetch('/api/patients/' + encodeURIComponent(patientId)); if (email) email.value = payload.email || ''; if (phone) phone.value = payload.telefono || ''; } catch (e) { const select = form.querySelector('.patient-select'); const opt = select && select.selectedOptions ? select.selectedOptions[0] : null; if (opt) { if (email) email.value = opt.dataset.email || ''; if (phone) phone.value = opt.dataset.phone || ''; } } } function bindEnhancedLookup(selectSelector, hiddenSelector) { document.querySelectorAll(selectSelector).forEach(function (select) { const form = select.closest('form, .card-body, .modal-body, body'); const hidden = form ? form.querySelector(hiddenSelector) : null; const sync = async function () { if (hidden) hidden.value = select.value || ''; if (select.classList.contains('patient-select')) { await hydratePatientIntoForm(form, select.value || ''); } }; select.addEventListener('change', sync); if (window.jQuery) { window.jQuery(select).on('select2:select.select2Bridge select2:clear.select2Bridge', function () { sync(); }); } sync(); }); } bindEnhancedLookup('.patient-select', '.patient-id'); bindEnhancedLookup('.obra-social-select', '.obra-social-id'); ensureCsrfFields(); applyImplicitPlaceholders(); initActionConfirmations(); let provincesPromise = null; const municipiosCache = new Map(); const localidadesCache = new Map(); function getProvincesOnce() { if (!provincesPromise) { provincesPromise = jsonFetch('/api/georef/provinces').then(function (data) { if (!data.ok) throw new Error(data.error || 'No se pudieron cargar las provincias'); return data.items || []; }).catch(function (error) { provincesPromise = null; throw error; }); } return provincesPromise; } function getMunicipiosOnce(provinciaId) { const key = String(provinciaId || '').trim(); if (!key) return Promise.resolve([]); if (!municipiosCache.has(key)) { municipiosCache.set(key, jsonFetch('/api/georef/municipios?provincia_id=' + encodeURIComponent(key)).then(function (data) { if (!data.ok) throw new Error(data.error || 'No se pudieron cargar los municipios'); return data.items || []; }).catch(function (error) { municipiosCache.delete(key); throw error; })); } return municipiosCache.get(key); } function getLocalidadesOnce(provinciaId, municipioId) { const key = String(provinciaId || '').trim() + '::' + String(municipioId || '').trim(); if (!localidadesCache.has(key)) { localidadesCache.set(key, jsonFetch('/api/georef/localidades?provincia_id=' + encodeURIComponent(provinciaId || '') + '&municipio_id=' + encodeURIComponent(municipioId || '')).then(function (data) { if (!data.ok) throw new Error(data.error || 'No se pudieron cargar las localidades'); return data.items || []; }).catch(function (error) { localidadesCache.delete(key); throw error; })); } return localidadesCache.get(key); } function setDatalistOptions(listEl, items) { if (!listEl) return; listEl.innerHTML = ''; sortItems(items).forEach(function (item) { const option = document.createElement('option'); option.value = item.nombre || item.text; listEl.appendChild(option); }); } function findGeoItem(items, value) { const term = String(value || '').trim().toLowerCase(); if (!term) return null; return (items || []).find(function (item) { return String(item.nombre || '').trim().toLowerCase() === term; }) || null; } function clearGeoInput(input, hiddenName, hiddenId) { if (input) input.value = ''; if (hiddenName) hiddenName.value = ''; if (hiddenId) hiddenId.value = ''; } function attachGeoAutocomplete(form) { if (!form || form.dataset.geoInitialized === '1') return; form.dataset.geoInitialized = '1'; const provinceInput = form.querySelector('.geo-province-input'); const municipalityInput = form.querySelector('.geo-municipality-input'); const cityInput = form.querySelector('.geo-city-input'); const provinceList = form.querySelector('.geo-province-list'); const municipalityList = form.querySelector('.geo-municipality-list'); const cityList = form.querySelector('.geo-city-list'); const provinceName = form.querySelector('.geo-province-name'); const municipalityName = form.querySelector('.geo-municipality-name'); const cityName = form.querySelector('.geo-city-name'); const provinceId = form.querySelector('.geo-province-id'); const municipalityId = form.querySelector('.geo-municipality-id'); const cityId = form.querySelector('.geo-city-id'); if (!provinceInput || !municipalityInput || !cityInput) return; async function loadProvinces() { const items = await getProvincesOnce(); provinceInput._geoItems = items; setDatalistOptions(provinceList, items); if (!provinceInput.value && provinceName && provinceName.value) provinceInput.value = provinceName.value; } async function loadMunicipiosByProvince(selectedName) { const provinceItem = findGeoItem(provinceInput._geoItems, provinceInput.value); provinceName.value = provinceItem ? provinceItem.nombre : (provinceInput.value || ''); provinceId.value = provinceItem ? provinceItem.id || '' : ''; clearGeoInput(municipalityInput, municipalityName, municipalityId); clearGeoInput(cityInput, cityName, cityId); municipalityInput._geoItems = []; cityInput._geoItems = []; setDatalistOptions(municipalityList, []); setDatalistOptions(cityList, []); if (!provinceItem || !provinceItem.id) return; const items = await getMunicipiosOnce(provinceItem.id); municipalityInput._geoItems = items; setDatalistOptions(municipalityList, items); if (selectedName) { municipalityInput.value = selectedName; municipalityName.value = selectedName; } } async function loadLocalidadesByMunicipio(selectedName) { const provinceItem = findGeoItem(provinceInput._geoItems, provinceInput.value); const municipioItem = findGeoItem(municipalityInput._geoItems, municipalityInput.value); municipalityName.value = municipioItem ? municipioItem.nombre : (municipalityInput.value || ''); municipalityId.value = municipioItem ? municipioItem.id || '' : ''; clearGeoInput(cityInput, cityName, cityId); cityInput._geoItems = []; setDatalistOptions(cityList, []); if (!provinceItem || !provinceItem.id) return; const items = await getLocalidadesOnce(provinceItem.id, municipioItem ? municipioItem.id : ''); cityInput._geoItems = items; setDatalistOptions(cityList, items); if (selectedName) { cityInput.value = selectedName; cityName.value = selectedName; } } async function syncProvince() { try { await loadMunicipiosByProvince(municipalityName.value || municipalityInput.value || ''); if ((municipalityName.value || municipalityInput.value) && municipalityInput._geoItems && municipalityInput._geoItems.length) { await loadLocalidadesByMunicipio(cityName.value || cityInput.value || ''); } } catch (e) { console.error(e); } } async function syncMunicipality() { try { await loadLocalidadesByMunicipio(cityName.value || cityInput.value || ''); } catch (e) { console.error(e); } } function syncCityValue() { const cityItem = findGeoItem(cityInput._geoItems, cityInput.value); cityName.value = cityItem ? cityItem.nombre : (cityInput.value || ''); cityId.value = cityItem ? cityItem.id || '' : ''; } ['change', 'blur'].forEach(function (evt) { provinceInput.addEventListener(evt, syncProvince); municipalityInput.addEventListener(evt, syncMunicipality); cityInput.addEventListener(evt, syncCityValue); }); provinceInput.addEventListener('input', function () { if (findGeoItem(provinceInput._geoItems, provinceInput.value)) syncProvince(); }); municipalityInput.addEventListener('input', function () { if (findGeoItem(municipalityInput._geoItems, municipalityInput.value)) syncMunicipality(); }); cityInput.addEventListener('input', function () { if (findGeoItem(cityInput._geoItems, cityInput.value)) syncCityValue(); }); loadProvinces().then(function () { if (provinceName && provinceName.value) { provinceInput.value = provinceName.value; syncProvince(); } }).catch(function (e) { console.error(e); provinceInput.placeholder = 'No se pudo cargar GeoRef'; }); } document.querySelectorAll('.geo-form').forEach(function (form) { attachGeoAutocomplete(form); }); document.querySelectorAll('[data-table-filter-target]').forEach(function (input) { input.addEventListener('input', function () { const target = document.querySelector(input.dataset.tableFilterTarget); if (!target) return; const term = (input.value || '').toLowerCase().trim(); target.querySelectorAll('tbody tr').forEach(function (row) { const txt = (row.textContent || '').toLowerCase(); row.style.display = !term || txt.includes(term) ? '' : 'none'; }); }); }); function syncRecipePatientState() { const filterSelect = document.getElementById('recipesFilterPatientSelect'); const filterHidden = document.getElementById('recipesFilterPatientId'); const newBtn = document.getElementById('newRecipeBtn'); const modalSelect = document.getElementById('recipeModalPatientSelect'); const modalHidden = document.getElementById('recipeModalPatientId'); const selectedValue = (filterHidden && filterHidden.value) || (filterSelect && filterSelect.value) || ''; if (newBtn) newBtn.disabled = !selectedValue; if (modalSelect && selectedValue && String(modalSelect.value) !== String(selectedValue)) { modalSelect.value = selectedValue; if (window.jQuery && hasSelect2(modalSelect)) window.jQuery(modalSelect).trigger('change'); modalSelect.dispatchEvent(new Event('change', { bubbles: true })); } if (modalHidden) modalHidden.value = selectedValue; } const recipesFilterPatientSelect = document.getElementById('recipesFilterPatientSelect'); if (recipesFilterPatientSelect) { recipesFilterPatientSelect.addEventListener('change', syncRecipePatientState); if (window.jQuery) { window.jQuery(recipesFilterPatientSelect).on('select2:select.recipeFilter select2:clear.recipeFilter', syncRecipePatientState); } syncRecipePatientState(); } function setupAppointmentSlots() { const form = document.getElementById('appointmentForm'); if (!form) return; const serviceSelect = document.getElementById('appointmentServiceSelect'); const professionalSelect = document.getElementById('appointmentProfessionalSelect'); const dateInput = document.getElementById('appointmentDateInput'); const slotSelect = document.getElementById('appointmentSlotSelect'); const timeInput = document.getElementById('appointmentTimeInput'); const slotHelp = document.getElementById('appointmentSlotHelp'); if (!serviceSelect || !professionalSelect || !dateInput || !slotSelect || !timeInput) return; async function loadSlots() { const serviceId = serviceSelect.value; const professionalId = professionalSelect.value; const selectedDate = dateInput.value; slotSelect.innerHTML = ''; slotSelect.disabled = true; if (slotHelp) slotHelp.textContent = 'Elegí servicio, profesional y fecha para ver los horarios disponibles.'; if (!serviceId || !professionalId || !selectedDate) return; try { const payload = await jsonFetch('/api/appointments/slots?service_id=' + encodeURIComponent(serviceId) + '&professional_id=' + encodeURIComponent(professionalId) + '&date=' + encodeURIComponent(selectedDate)); const slots = payload.items || []; const currentValue = timeInput.value || ''; slotSelect.innerHTML = ''; slots.forEach(function (slot) { const option = document.createElement('option'); option.value = slot.start; option.textContent = slot.label; if (currentValue && currentValue === slot.start) option.selected = true; slotSelect.appendChild(option); }); if (currentValue && !slots.some(function (slot) { return slot.start === currentValue; })) { const option = document.createElement('option'); option.value = currentValue; option.textContent = currentValue + ' (actual)'; option.selected = true; slotSelect.appendChild(option); } slotSelect.disabled = false; if (slotHelp) slotHelp.textContent = slots.length ? 'Seleccioná uno de los horarios calculados automáticamente.' : 'No hay turnos disponibles para esta combinación.'; refreshSearchableSelect(slotSelect); } catch (e) { slotSelect.innerHTML = ''; slotSelect.disabled = true; if (slotHelp) slotHelp.textContent = e.message; } } slotSelect.addEventListener('change', function () { timeInput.value = slotSelect.value || timeInput.value || ''; }); [serviceSelect, professionalSelect, dateInput].forEach(function (el) { el.addEventListener('change', loadSlots); if (window.jQuery && el.tagName === 'SELECT') { window.jQuery(el).on('select2:select.slotLoader select2:clear.slotLoader', loadSlots); } }); loadSlots(); } setupAppointmentSlots(); const sisaBtn = document.getElementById('sisaSearchBtn'); if (sisaBtn) { sisaBtn.addEventListener('click', async function () { const dni = document.getElementById('sisaDni').value.trim(); const query = document.getElementById('sisaQuery').value.trim(); const matricula = document.getElementById('sisaMatricula').value.trim(); const status = document.getElementById('sisaSearchStatus'); const resultsBox = document.getElementById('sisaSearchResults'); status.textContent = 'Consultando SISA...'; resultsBox.innerHTML = ''; try { const response = await jsonFetch('/admin/config-sisa/search?dni=' + encodeURIComponent(dni) + '&query=' + encodeURIComponent(query) + '&matricula=' + encodeURIComponent(matricula)); if (!response.ok) throw new Error(response.error || 'No se pudo consultar SISA'); status.textContent = response.items.length + ' resultado(s) encontrado(s).'; if (!response.items.length) { resultsBox.innerHTML = '
Sin coincidencias.
'; return; } response.items.forEach(function (item) { const card = document.createElement('button'); card.type = 'button'; card.className = 'btn btn-light text-start border'; card.innerHTML = `${item.display_name || 'Sin nombre'}
${item.profession_name || '—'} · ${item.specialty || 'Sin especialidad'} · ${item.matricula || 'Sin matrícula'}`; card.addEventListener('click', function () { const setValue = function (id, value) { const el = document.getElementById(id); if (el) el.value = value || ''; }; setValue('professional_display_name', item.display_name); setValue('professional_matricula', item.matricula); setValue('professional_profession_name', item.profession_name); setValue('professional_specialty', item.specialty); setValue('professional_jurisdiction_name', item.jurisdiction_name); setValue('professional_state_name', item.state_name); status.textContent = 'Profesional cargado desde SISA: ' + item.display_name; }); resultsBox.appendChild(card); }); } catch (e) { status.textContent = e.message; } }); } function initHtmlEditor(textarea) { if (!textarea || textarea.dataset.editorReady === '1') return; textarea.dataset.editorReady = '1'; const wrapper = document.createElement('div'); wrapper.className = 'apex-rte-wrapper html-editor-wrapper rounded-4 overflow-hidden'; const toolbar = document.createElement('div'); toolbar.className = 'apex-rte-toolbar d-flex flex-wrap gap-2 p-2 border-bottom'; const editor = document.createElement('div'); editor.className = 'apex-rte-surface html-editor-surface p-3'; editor.contentEditable = 'true'; editor.setAttribute('role', 'textbox'); editor.setAttribute('aria-multiline', 'true'); editor.dataset.targetName = textarea.name || ''; editor.style.minHeight = Math.max(180, (parseInt(textarea.getAttribute('rows') || '8', 10) * 22)) + 'px'; editor.innerHTML = textarea.value || ''; function sync() { textarea.value = editor.innerHTML.trim(); } function cmd(command, value) { document.execCommand(command, false, value || null); editor.focus(); sync(); } function addButton(label, title, handler, extraClass) { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'btn btn-sm ' + (extraClass || 'btn-outline-secondary'); btn.title = title || label; btn.innerHTML = label; btn.addEventListener('click', function () { handler(); }); toolbar.appendChild(btn); return btn; } const blockSelect = document.createElement('select'); blockSelect.className = 'form-select form-select-sm apex-rte-format'; blockSelect.innerHTML = ''; blockSelect.addEventListener('change', function () { cmd('formatBlock', blockSelect.value); }); toolbar.appendChild(blockSelect); addButton('B', 'Negrita', function () { cmd('bold'); }); addButton('I', 'Cursiva', function () { cmd('italic'); }); addButton('U', 'Subrayado', function () { cmd('underline'); }); addButton('S', 'Tachado', function () { cmd('strikeThrough'); }); addButton('• Lista', 'Lista con viñetas', function () { cmd('insertUnorderedList'); }); addButton('1. Lista', 'Lista numerada', function () { cmd('insertOrderedList'); }); addButton('↤', 'Disminuir sangría', function () { cmd('outdent'); }); addButton('↦', 'Aumentar sangría', function () { cmd('indent'); }); addButton('Link', 'Insertar enlace', function () { const url = prompt('URL del enlace:'); if (url) cmd('createLink', url); }); addButton('Tabla', 'Insertar tabla simple', function () { cmd('insertHTML', '
DatoValor

'); }); addButton('✓ Check', 'Insertar checklist', function () { cmd('insertHTML', ''); }); addButton('Limpiar', 'Quitar formato', function () { cmd('removeFormat'); }); addButton('HTML', 'Ver/editar HTML', function () { const raw = textarea.style.display === 'none'; if (raw) { sync(); textarea.style.display = ''; editor.style.display = 'none'; } else { editor.innerHTML = textarea.value || ''; textarea.style.display = 'none'; editor.style.display = ''; } }, 'btn-outline-primary'); editor.addEventListener('input', sync); editor.addEventListener('blur', sync); textarea.addEventListener('change', function () { editor.innerHTML = textarea.value || ''; }); textarea.style.display = 'none'; textarea.parentNode.insertBefore(wrapper, textarea); wrapper.appendChild(toolbar); wrapper.appendChild(editor); textarea.__apexEditor = { wrapper, editor, sync, setValue: function (value) { textarea.value = value || ''; editor.innerHTML = value || ''; } }; } window.ApexRichText = window.ApexRichText || { syncAll: function () { document.querySelectorAll('textarea.html-editor').forEach(function (textarea) { if (textarea.__apexEditor) textarea.__apexEditor.sync(); }); }, setValue: function (textareaOrSelector, value) { const textarea = typeof textareaOrSelector === 'string' ? document.querySelector(textareaOrSelector) : textareaOrSelector; if (!textarea) return; if (textarea.__apexEditor) textarea.__apexEditor.setValue(value || ''); else textarea.value = value || ''; }, refresh: function () { document.querySelectorAll('textarea.html-editor').forEach(function (textarea) { initHtmlEditor(textarea); }); } }; document.querySelectorAll('textarea.html-editor').forEach(function (textarea) { initHtmlEditor(textarea); }); document.addEventListener('submit', function () { if (window.ApexRichText) window.ApexRichText.syncAll(); }, true); }); document.addEventListener('DOMContentLoaded', function () { const csrfToken = document.body.dataset.csrfToken || ''; const nativeFetch = window.fetch.bind(window); window.fetch = function (resource, options) { const opts = options ? { ...options } : {}; const method = String(opts.method || 'GET').toUpperCase(); if (csrfToken && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) { const headers = new Headers(opts.headers || {}); if (!headers.has('X-CSRF-Token')) headers.set('X-CSRF-Token', csrfToken); opts.headers = headers; } return nativeFetch(resource, opts); }; const body = document.body; if (!body || body.dataset.chatEnabled !== '1' || typeof io === 'undefined') return; const userId = String(body.dataset.chatUserId || ''); const chatUrl = body.dataset.chatUrl || '/chat'; const topbarBadge = document.getElementById('topbarChatBadge'); const sidebarBadge = document.getElementById('sidebarChatBadge'); const toastContainer = document.getElementById('chatToastContainer'); function setBadge(el, value) { if (!el) return; const n = Number(value || 0); el.textContent = String(n); el.classList.toggle('d-none', !n); } function syncBadges(total) { setBadge(topbarBadge, total); setBadge(sidebarBadge, total); } function escapeToastText(value) { return String(value || '').replace(/[&<>"']/g, function (ch) { return ({'&': '&', '<': '<', '>': '>', '"': '"', "'": '''}[ch]); }); } function showChatToast(title, bodyText, conversationId) { if (!toastContainer || typeof bootstrap === 'undefined') return; const toastEl = document.createElement('div'); toastEl.className = 'toast border-0 shadow-sm'; toastEl.role = 'alert'; toastEl.ariaLive = 'assertive'; toastEl.ariaAtomic = 'true'; toastEl.innerHTML = `
${escapeToastText(title || 'Nuevo mensaje')} ahora
${escapeToastText(bodyText || 'Tenés un nuevo mensaje.')}
`; toastContainer.appendChild(toastEl); toastEl.addEventListener('click', function () { window.location.href = `${chatUrl}?conversation_id=${conversationId}`; }); toastEl.style.cursor = 'pointer'; const toast = new bootstrap.Toast(toastEl, { delay: 4500 }); toast.show(); toastEl.addEventListener('hidden.bs.toast', function () { toastEl.remove(); }); } const socket = io(); window.AppChat = window.AppChat || {}; window.AppChat.socket = socket; window.AppChat.syncBadges = syncBadges; window.AppChat.showChatToast = showChatToast; socket.on('connect', function () { if (userId) socket.emit('join', { room: `user_${userId}` }); }); socket.on('chat_notify', function (payload) { if (!payload) return; syncBadges(payload.unread_total || 0); if (payload.show_toast && payload.conversation && payload.sender_id && String(payload.sender_id) !== userId) { showChatToast(payload.toast_title, payload.toast_body, payload.conversation.id); } document.dispatchEvent(new CustomEvent('appchat:notify', { detail: payload })); }); }); // Admin dark mode toggle (function(){ function applyTheme(theme){ document.body.dataset.adminTheme = theme; document.body.classList.toggle('admin-dark', theme === 'dark'); var btn = document.getElementById('adminThemeToggle'); if (btn) btn.innerHTML = theme === 'dark' ? '' : ''; } document.addEventListener('DOMContentLoaded', function(){ var saved = localStorage.getItem('adminTheme') || (document.documentElement.dataset.adminTheme === 'dark' ? 'dark' : 'light'); applyTheme(saved); document.documentElement.classList.toggle('admin-dark-preload', saved === 'dark'); var btn = document.getElementById('adminThemeToggle'); if (btn) btn.addEventListener('click', function(){ var next = (document.body.dataset.adminTheme === 'dark') ? 'light' : 'dark'; localStorage.setItem('adminTheme', next); applyTheme(next); }); }); })();