879 lines
37 KiB
JavaScript
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 ({'&': '&', '<': '<', '>': '>', '"': '"', "'": '''}[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);
|
|
});
|
|
});
|
|
})();
|