mi-proyecto/app/static/js/app.js

879 lines
37 KiB
JavaScript

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 = '<option value="">Seleccionar</option>';
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 = '<option value="">Seleccionar</option>';
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 = '<option value="">Sin disponibilidad</option>';
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 = '<div class="small text-muted">Sin coincidencias.</div>';
return;
}
response.items.forEach(function (item) {
const card = document.createElement('button');
card.type = 'button';
card.className = 'btn btn-light text-start border';
card.innerHTML = `<strong>${item.display_name || 'Sin nombre'}</strong><br><span class="small text-muted">${item.profession_name || '—'} · ${item.specialty || 'Sin especialidad'} · ${item.matricula || 'Sin matrícula'}</span>`;
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 = '<option value="P">Párrafo</option><option value="H2">Título</option><option value="H3">Subtítulo</option><option value="BLOCKQUOTE">Cita</option><option value="PRE">Código/monoespacio</option>';
blockSelect.addEventListener('change', function () { cmd('formatBlock', blockSelect.value); });
toolbar.appendChild(blockSelect);
addButton('<strong>B</strong>', 'Negrita', function () { cmd('bold'); });
addButton('<em>I</em>', 'Cursiva', function () { cmd('italic'); });
addButton('<u>U</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', '<table class="table table-bordered"><tbody><tr><td>Dato</td><td>Valor</td></tr><tr><td></td><td></td></tr></tbody></table><p></p>'); });
addButton('✓ Check', 'Insertar checklist', function () { cmd('insertHTML', '<ul><li>☐ Pendiente</li><li>☐ Control</li></ul>'); });
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 ({'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'}[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 = `
<div class="toast-header">
<i class="bi bi-chat-square-text me-2 text-primary"></i>
<strong class="me-auto">${escapeToastText(title || 'Nuevo mensaje')}</strong>
<small>ahora</small>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">${escapeToastText(bodyText || 'Tenés un nuevo mensaje.')}</div>`;
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' ? '<i class="bi bi-sun"></i>' : '<i class="bi bi-moon-stars"></i>';
}
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);
});
});
})();