From d69562c8674bad1ccf16f6aaee02e1bc17cd344d Mon Sep 17 00:00:00 2001 From: Keiko Date: Tue, 30 Jun 2026 00:39:14 +0000 Subject: [PATCH] =?UTF-8?q?Migraci=C3=B3n=20a=20la=20versi=C3=B3n=20m?= =?UTF-8?q?=C3=A1s=20estable=20de=20Gitea=20en=20Del=20Austral?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Keiko --- assets/js/app.js | 3096 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3096 insertions(+) create mode 100644 assets/js/app.js diff --git a/assets/js/app.js b/assets/js/app.js new file mode 100644 index 0000000..5798db8 --- /dev/null +++ b/assets/js/app.js @@ -0,0 +1,3096 @@ +/* ============================================================ + DEL AUSTRAL · Lógica principal de la aplicación + ============================================================ */ + +const API = { + auth: 'api/auth.php', + pacientes: 'api/pacientes.php', + obrasSociales: 'api/obras_sociales.php', + citas: 'api/citas.php', + adjuntos: 'api/adjuntos.php', + plantillas: 'api/plantillas.php', + admin: 'api/admin.php', +}; + +let CACHE_OBRAS_SOCIALES = []; +let CACHE_PLANTILLAS = []; +let TIPO_BUSQUEDA_ACTUAL = 'dni'; +let VISTA_BUSQUEDA_ORIGEN = 'acceder'; // 'acceder' | 'borrar', para volver bien desde el detalle +let MODO_FORM_LEGAJO = 'crear'; // 'crear' | 'editar' +let MES_AGENDA_ACTUAL = new Date(); // mes que se muestra en el calendario +let DIA_SELECCIONADO_AGENDA = null; // 'YYYY-MM-DD' +let ARCHIVO_PENDIENTE_SUBIR = null; +let PACIENTE_ID_ACTUAL_DETALLE = null; +let ROL_ACTUAL = null; // 'profesional' | 'administrativa' +let NOMBRE_USUARIO_ACTUAL = null; + +document.addEventListener('DOMContentLoaded', () => { + inicializarAcceso(); + vincularBotonesGlobales(); + registrarServiceWorker(); +}); + +/** + * Registra el service worker mínimo que permite "instalar" el + * sitio como app. Si el navegador no lo soporta (algunos viejos + * o ciertos modos privados), simplemente no pasa nada — el + * sistema sigue funcionando igual, solo no se podría instalar. + */ +function registrarServiceWorker() { + if (!('serviceWorker' in navigator)) return; + navigator.serviceWorker.register('sw.js').catch(() => { + // Si falla el registro (ej. el sitio no está en HTTPS), + // no rompemos nada: la app sigue funcionando como sitio web normal. + }); +} + +/* ============================================================ + TOASTS + ============================================================ */ +function mostrarToast(mensaje, tipo = 'info') { + const cont = document.getElementById('toast-contenedor'); + const toast = document.createElement('div'); + toast.className = `toast ${tipo}`; + toast.textContent = mensaje; + cont.appendChild(toast); + setTimeout(() => toast.remove(), 4200); +} + +/** + * Descarga un archivo desde una URL del propio backend (que + * responde con Content-Disposition: attachment), sin abrir + * pestañas nuevas. Usamos fetch + blob en vez de window.open + * porque los navegadores suelen bloquear popups que disparan + * una descarga inmediata, fallando en silencio sin avisar nada. + * Esta forma también nos deja mostrar un error legible si la + * descarga falla (por ejemplo, sesión vencida o error del server). + */ +async function descargarArchivoDesdeUrl(url, nombrePorDefecto = 'descarga.json') { + try { + const respuesta = await fetch(url, { method: 'GET' }); + if (!respuesta.ok) { + let mensaje = 'No se pudo generar la descarga.'; + try { + const datos = await respuesta.json(); + if (datos && datos.error) mensaje = datos.error; + } catch (_) { + // La respuesta de error no era JSON; nos quedamos con el mensaje genérico. + } + throw new Error(mensaje); + } + + const disposicion = respuesta.headers.get('Content-Disposition') || ''; + const coincidencia = disposicion.match(/filename="?([^"]+)"?/); + const nombreArchivo = coincidencia ? coincidencia[1] : nombrePorDefecto; + + const blob = await respuesta.blob(); + const urlObjeto = URL.createObjectURL(blob); + const enlace = document.createElement('a'); + enlace.href = urlObjeto; + enlace.download = nombreArchivo; + document.body.appendChild(enlace); + enlace.click(); + enlace.remove(); + URL.revokeObjectURL(urlObjeto); + } catch (e) { + mostrarToast(e.message || 'No se pudo descargar el archivo.', 'error'); + } +} + +/* ============================================================ + PETICIONES A LA API + ============================================================ */ +async function llamarApi(url, opciones = {}) { + try { + const respuesta = await fetch(url, { + headers: { 'Content-Type': 'application/json' }, + ...opciones, + }); + const datos = await respuesta.json(); + if (!respuesta.ok || !datos.ok) { + throw new Error(datos.error || 'Ocurrió un error inesperado.'); + } + return datos; + } catch (err) { + throw err; + } +} + +/* ============================================================ + ACCESO CON PIN — flujo multi-paso + ============================================================ */ +const LARGO_PIN = 4; +let ETAPA_SISTEMA = 'listo'; // 'sin_desarrollador' | 'sin_sedes_o_usuarios' | 'listo' +let SEDE_ELEGIDA_ID = null; +let SEDE_ELEGIDA_NOMBRE = null; +let USUARIO_ELEGIDO_ID = null; +let USUARIO_ELEGIDO_ROL = null; +let PROFESIONAL_ACTIVO_ELEGIDO_ID = null; + +/** + * Motor genérico de captura de PIN: conecta un oculto con + * sus puntitos indicadores, y llama el callback al completar los + * 4 dígitos. Se reutiliza para los 3 inputs de PIN distintos que + * hay en el flujo de acceso. + */ +function crearCapturadorPin(idInput, idIndicadores, alCompletar) { + let valorActual = ''; + const input = document.getElementById(idInput); + + function actualizarIndicadores() { + const puntos = document.querySelectorAll(`#${idIndicadores} .punto-pin`); + puntos.forEach((p, i) => { + p.classList.toggle('relleno', i < valorActual.length); + p.classList.remove('error'); + }); + } + + function marcarError() { + document.querySelectorAll(`#${idIndicadores} .punto-pin`).forEach(p => p.classList.add('error')); + } + + function limpiar() { + valorActual = ''; + input.value = ''; + input.focus(); + actualizarIndicadores(); + } + + input.addEventListener('input', () => { + valorActual = input.value.replace(/\D/g, '').slice(0, LARGO_PIN); + input.value = valorActual; + actualizarIndicadores(); + if (valorActual.length === LARGO_PIN) { + alCompletar(valorActual); + } + }); + + return { limpiar, marcarError, focus: () => input.focus() }; +} + +let CAPTURADOR_PIN_LOGIN = null; +let CAPTURADOR_PIN_DEV_CREAR = null; +let CAPTURADOR_CLAVE_DEV = null; + +async function inicializarAcceso() { + try { + const res = await llamarApi(`${API.auth}?accion=estado`, { + method: 'POST', + body: JSON.stringify({ accion: 'estado' }), + }); + ETAPA_SISTEMA = res.etapa; + } catch (e) { + const t = document.getElementById('texto-instruccion'); + t.classList.remove('oculto'); + t.textContent = 'No se pudo conectar con el servidor. Revisá config.php.'; + t.classList.add('error'); + return; + } + + vincularPasosLogin(); + + if (ETAPA_SISTEMA === 'sin_desarrollador') { + mostrarPasoLogin('paso-crear-desarrollador'); + } else { + // Tanto si faltan sedes/usuarios como si está todo listo, el + // punto de partida visible es "elegir sede" — desde ahí hay un + // link para entrar como Desarrollador si hace falta configurar. + cargarSedesLogin(); + mostrarPasoLogin('paso-elegir-sede'); + } +} + +function mostrarPasoLogin(idPaso) { + document.querySelectorAll('.paso-login').forEach(p => p.classList.add('oculto')); + document.getElementById(idPaso).classList.remove('oculto'); +} + +function vincularPasosLogin() { + // --- Crear clave de desarrollador (primera vez) --- + CAPTURADOR_PIN_DEV_CREAR = crearCapturadorPin('input-pin-dev-crear', 'indicadores-pin-dev-crear', async (clave) => { + try { + await llamarApi(API.auth, { method: 'POST', body: JSON.stringify({ accion: 'crear_desarrollador', clave }) }); + mostrarVistaSetupInicial(); + } catch (e) { + mostrarToast(e.message, 'error'); + CAPTURADOR_PIN_DEV_CREAR.marcarError(); + setTimeout(() => CAPTURADOR_PIN_DEV_CREAR.limpiar(), 600); + } + }); + + // --- Elegir sede --- + document.getElementById('btn-soy-desarrollador').addEventListener('click', () => { + mostrarPasoLogin('paso-clave-desarrollador'); + CAPTURADOR_CLAVE_DEV.focus(); + }); + + // --- Elegir usuario dentro de la sede --- + document.getElementById('btn-volver-sede').addEventListener('click', () => mostrarPasoLogin('paso-elegir-sede')); + + // --- Elegir profesional activo (administrativa) --- + document.getElementById('btn-volver-usuario-desde-prof').addEventListener('click', () => mostrarPasoLogin('paso-elegir-usuario')); + + // --- Ingresar PIN normal --- + document.getElementById('btn-volver-usuario').addEventListener('click', () => { + mostrarPasoLogin(USUARIO_ELEGIDO_ROL === 'administrativa' ? 'paso-elegir-profesional-activo' : 'paso-elegir-usuario'); + }); + document.getElementById('btn-reintentar-patron').addEventListener('click', () => { + document.getElementById('btn-reintentar-patron').classList.add('oculto'); + CAPTURADOR_PIN_LOGIN.limpiar(); + }); + CAPTURADOR_PIN_LOGIN = crearCapturadorPin('input-pin-oculto', 'indicadores-pin', manejarPinLoginCompletado); + + // --- Clave de desarrollador (login normal) --- + document.getElementById('btn-volver-sede-desde-dev').addEventListener('click', () => mostrarPasoLogin('paso-elegir-sede')); + CAPTURADOR_CLAVE_DEV = crearCapturadorPin('input-clave-dev', 'indicadores-pin-dev', async (clave) => { + try { + const res = await llamarApi(API.auth, { method: 'POST', body: JSON.stringify({ accion: 'verificar_desarrollador', clave }) }); + entrarAlApp(res.nombre_usuario, res.rol); + } catch (e) { + mostrarToast(e.message, 'error'); + CAPTURADOR_CLAVE_DEV.marcarError(); + setTimeout(() => CAPTURADOR_CLAVE_DEV.limpiar(), 600); + } + }); + + // --- Setup inicial (sede + profesional, justo después de crear desarrollador) --- + document.getElementById('btn-crear-setup-inicial').addEventListener('click', crearSetupInicial); + document.getElementById('btn-ir-panel-dev').addEventListener('click', () => entrarAlApp('Desarrollador', 'desarrollador')); +} + +function mostrarVistaSetupInicial() { + document.getElementById('vista-acceso').classList.add('oculto'); + document.getElementById('vista-setup-inicial').classList.remove('oculto'); +} + +async function crearSetupInicial() { + const nombreSede = document.getElementById('input-primera-sede').value.trim(); + const nombreProfesional = document.getElementById('input-primer-profesional').value.trim(); + const pin = document.getElementById('input-pin-primer-profesional').value.trim(); + const btn = document.getElementById('btn-crear-setup-inicial'); + + if (!nombreSede || !nombreProfesional) { + mostrarToast('Completá el nombre de la sede y del profesional.', 'error'); + return; + } + if (!/^\d{4}$/.test(pin)) { + mostrarToast('El PIN del profesional debe tener 4 números.', 'error'); + return; + } + + btn.disabled = true; + btn.innerHTML = ' Creando...'; + try { + await llamarApi(API.auth, { + method: 'POST', + body: JSON.stringify({ accion: 'crear_setup_inicial', nombre_sede: nombreSede, nombre_profesional: nombreProfesional, pin }), + }); + mostrarToast('Sede y profesional creados. Ya podés entrar al panel.', 'exito'); + entrarAlApp('Desarrollador', 'desarrollador'); + } catch (e) { + mostrarToast(e.message, 'error'); + btn.disabled = false; + btn.textContent = 'Crear sede y profesional'; + } +} + +async function cargarSedesLogin() { + const cont = document.getElementById('lista-sedes-login'); + cont.innerHTML = '
'; + try { + const res = await llamarApi(`${API.auth}?accion=listar_sedes_login`, { method: 'POST', body: JSON.stringify({ accion: 'listar_sedes_login' }) }); + cont.innerHTML = ''; + if (!res.datos.length) { + cont.innerHTML = ''; + return; + } + res.datos.forEach(s => { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'opcion-login'; + btn.textContent = s.nombre; + btn.addEventListener('click', () => { + SEDE_ELEGIDA_ID = s.id; + SEDE_ELEGIDA_NOMBRE = s.nombre; + cargarUsuariosLogin(s.id); + mostrarPasoLogin('paso-elegir-usuario'); + }); + cont.appendChild(btn); + }); + } catch (e) { + cont.innerHTML = ''; + mostrarToast(e.message, 'error'); + } +} + +async function cargarUsuariosLogin(sedeId) { + const cont = document.getElementById('lista-usuarios-login'); + cont.innerHTML = '
'; + try { + const res = await llamarApi(`${API.auth}?accion=listar_usuarios_sede_login`, { + method: 'POST', + body: JSON.stringify({ accion: 'listar_usuarios_sede_login', sede_id: sedeId }), + }); + cont.innerHTML = ''; + if (!res.datos.length) { + cont.innerHTML = ''; + return; + } + res.datos.forEach(u => { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'opcion-login'; + const etiquetaRol = u.rol === 'profesional' ? 'Profesional' : 'Administrativa'; + btn.innerHTML = `${escaparHtml(u.nombre_completo)} `; + btn.addEventListener('click', () => { + USUARIO_ELEGIDO_ID = u.id; + USUARIO_ELEGIDO_ROL = u.rol; + if (u.rol === 'administrativa') { + cargarProfesionalesLogin(sedeId); + mostrarPasoLogin('paso-elegir-profesional-activo'); + } else { + PROFESIONAL_ACTIVO_ELEGIDO_ID = null; + irAPasoPin(u.nombre_completo); + } + }); + cont.appendChild(btn); + }); + } catch (e) { + cont.innerHTML = ''; + mostrarToast(e.message, 'error'); + } +} + +async function cargarProfesionalesLogin(sedeId) { + const cont = document.getElementById('lista-profesionales-login'); + cont.innerHTML = '
'; + try { + const res = await llamarApi(`${API.auth}?accion=listar_profesionales_sede_login`, { + method: 'POST', + body: JSON.stringify({ accion: 'listar_profesionales_sede_login', sede_id: sedeId }), + }); + cont.innerHTML = ''; + if (!res.datos.length) { + cont.innerHTML = ''; + return; + } + res.datos.forEach(p => { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'opcion-login'; + btn.textContent = p.nombre_completo; + btn.addEventListener('click', () => { + PROFESIONAL_ACTIVO_ELEGIDO_ID = p.id; + irAPasoPin(`Administrativa de ${p.nombre_completo}`); + }); + cont.appendChild(btn); + }); + } catch (e) { + cont.innerHTML = ''; + mostrarToast(e.message, 'error'); + } +} + +function irAPasoPin(textoContexto) { + document.getElementById('texto-instruccion-pin').textContent = `Ingresá tu PIN — ${textoContexto}`; + mostrarPasoLogin('paso-ingresar-pin'); + setTimeout(() => CAPTURADOR_PIN_LOGIN.focus(), 50); +} + +async function manejarPinLoginCompletado(pin) { + try { + const res = await llamarApi(API.auth, { + method: 'POST', + body: JSON.stringify({ + accion: 'verificar', + sede_id: SEDE_ELEGIDA_ID, + usuario_id: USUARIO_ELEGIDO_ID, + pin, + profesional_activo_id: PROFESIONAL_ACTIVO_ELEGIDO_ID, + }), + }); + entrarAlApp(res.nombre_usuario, res.rol); + } catch (e) { + mostrarToast(e.message, 'error'); + CAPTURADOR_PIN_LOGIN.marcarError(); + document.getElementById('btn-reintentar-patron').classList.remove('oculto'); + setTimeout(() => CAPTURADOR_PIN_LOGIN.limpiar(), 600); + } +} + +function entrarAlApp(nombreUsuario, rol) { + ROL_ACTUAL = rol; + NOMBRE_USUARIO_ACTUAL = nombreUsuario; + document.getElementById('vista-acceso').classList.add('oculto'); + document.getElementById('vista-setup-inicial').classList.add('oculto'); + document.getElementById('vista-app').classList.remove('oculto'); + irAVista(rol === 'desarrollador' ? 'configuracion' : 'menu'); + mostrarCartelBienvenida(nombreUsuario); +} + +function mostrarCartelBienvenida(nombreUsuario) { + const modal = document.getElementById('modal-bienvenida'); + const texto = document.getElementById('texto-bienvenida'); + texto.textContent = nombreUsuario + ? `Bienvenido/a, ${nombreUsuario}` + : '¡Bienvenido/a!'; + modal.classList.remove('oculto'); + + const cerrar = () => modal.classList.add('oculto'); + document.getElementById('btn-cerrar-bienvenida').onclick = cerrar; +} + +/** + * Oculta del DOM cualquier elemento marcado con data-rol="profesional" + * cuando el usuario logueado es administrativa. Se llama después de + * montar cada vista que pueda tener este tipo de elementos. + */ +function aplicarVisibilidadPorRol(contenedor) { + if (ROL_ACTUAL === 'profesional') return; + contenedor.querySelectorAll('[data-rol="profesional"]').forEach(el => el.remove()); +} + +function vincularBotonesGlobales() { + document.getElementById('btn-inicio').addEventListener('click', () => irAVista(ROL_ACTUAL === 'desarrollador' ? 'configuracion' : 'menu')); + document.getElementById('btn-cerrar-sesion').addEventListener('click', async () => { + await llamarApi(`${API.auth}`, { method: 'POST', body: JSON.stringify({ accion: 'cerrar_sesion' }) }); + location.reload(); + }); +} + +/* ============================================================ + NAVEGACIÓN ENTRE VISTAS + ============================================================ */ +function irAVista(nombre, datos = {}) { + const contenido = document.getElementById('contenido'); + contenido.innerHTML = ''; + + if (nombre === 'menu') { + contenido.appendChild(clonarPlantilla('tpl-menu')); + aplicarVisibilidadPorRol(contenido); + adaptarTextosMenuSegunRol(contenido); + contenido.querySelectorAll('.tarjeta-menu').forEach(btn => { + btn.addEventListener('click', () => irAVista(btn.dataset.vista)); + }); + cargarResumenMenu(); + } else if (nombre === 'crear') { + MODO_FORM_LEGAJO = 'crear'; + montarVistaCrear(contenido); + } else if (nombre === 'editar') { + MODO_FORM_LEGAJO = 'editar'; + montarVistaCrear(contenido, datos.id); + } else if (nombre === 'acceder') { + VISTA_BUSQUEDA_ORIGEN = 'acceder'; + montarVistaBusqueda(contenido, 'acceder'); + } else if (nombre === 'borrar') { + VISTA_BUSQUEDA_ORIGEN = 'borrar'; + montarVistaBusqueda(contenido, 'borrar'); + } else if (nombre === 'detalle') { + montarVistaDetalle(contenido, datos.id); + } else if (nombre === 'agenda') { + montarVistaAgenda(contenido); + } else if (nombre === 'dashboard') { + montarVistaDashboard(contenido); + } else if (nombre === 'mi-legajo') { + montarVistaMiLegajo(contenido); + } else if (nombre === 'configuracion') { + montarVistaConfiguracion(contenido); + } + + contenido.querySelectorAll('[data-volver]').forEach(b => b.addEventListener('click', () => irAVista(ROL_ACTUAL === 'desarrollador' ? 'configuracion' : 'menu'))); + window.scrollTo({ top: 0, behavior: 'smooth' }); +} + +/** + * Ajusta textos del menú principal y de algunas tarjetas para que + * tengan sentido cuando quien entra es la administrativa (no ve + * contenido clínico, así que el copy no debe prometerlo). + */ +function adaptarTextosMenuSegunRol(contenido) { + if (ROL_ACTUAL === 'profesional') return; + + const titulo = contenido.querySelector('#titulo-menu'); + const desc = contenido.querySelector('#desc-menu'); + if (titulo) titulo.textContent = `¿Qué necesitás hacer, ${NOMBRE_USUARIO_ACTUAL || ''}?`; + if (desc) desc.textContent = 'Gestioná turnos y datos de contacto de los pacientes.'; + + const descCrear = contenido.querySelector('#desc-crear-legajo'); + if (descCrear) descCrear.textContent = 'Registrá los datos de contacto de un paciente nuevo para poder agendarlo.'; + + const descAcceder = contenido.querySelector('#desc-acceder-legajos'); + if (descAcceder) descAcceder.textContent = 'Buscá por DNI, nombre, fecha de atención u obra social para ver sus datos de contacto.'; +} + +function clonarPlantilla(idPlantilla) { + const tpl = document.getElementById(idPlantilla); + const clon = tpl.content.cloneNode(true); + const envoltorio = document.createElement('div'); + envoltorio.appendChild(clon); + return envoltorio; +} + +/* ============================================================ + UTILIDADES DE FECHA + ============================================================ */ +function formatearFechaCorta(fechaIso) { + if (!fechaIso) return '—'; + return new Date(fechaIso + 'T00:00:00').toLocaleDateString('es-AR', { day: '2-digit', month: '2-digit', year: 'numeric' }); +} + +function formatearFechaLarga(fechaIso) { + if (!fechaIso) return '—'; + return new Date(fechaIso + 'T00:00:00').toLocaleDateString('es-AR', { day: '2-digit', month: 'long', year: 'numeric' }); +} + +function aFechaIso(date) { + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, '0'); + const d = String(date.getDate()).padStart(2, '0'); + return `${y}-${m}-${d}`; +} + +/* ============================================================ + RESUMEN DEL MENÚ PRINCIPAL (agenda + inactivos) + ============================================================ */ +async function cargarResumenMenu() { + cargarResumenCitas(); + cargarResumenCumpleanios(); + if (ROL_ACTUAL === 'profesional') { + cargarResumenInactivos(); + cargarResumenHoy(); + cargarAvisosTurnos(); + } +} + +async function cargarResumenHoy() { + const franja = document.getElementById('franja-resumen-hoy'); + if (!franja) return; + try { + const res = await llamarApi(`${API.citas}?accion=resumen_hoy`, { method: 'GET' }); + if (res.total_hoy === 0) { + franja.classList.add('oculto'); + return; + } + const horaTexto = res.proxima_hora ? ` — la próxima es a las ${res.proxima_hora.slice(0, 5)}` : ''; + if (res.restantes_hoy === 0) { + franja.innerHTML = `✅ Ya atendiste todas tus consultas de hoy (${res.total_hoy} en total).`; + } else { + franja.innerHTML = `Hoy tenés ${res.restantes_hoy} consulta${res.restantes_hoy === 1 ? '' : 's'} por delante de ${res.total_hoy}${horaTexto}.`; + } + franja.classList.remove('oculto'); + } catch (e) { + franja.classList.add('oculto'); + } +} + +async function cargarAvisosTurnos() { + const boton = document.getElementById('franja-avisos-turnos'); + if (!boton) return; + try { + const res = await llamarApi(`${API.citas}?accion=avisos_pendientes`, { method: 'GET' }); + if (res.total === 0) { + boton.classList.add('oculto'); + return; + } + boton.innerHTML = `🔔 Tenés ${res.total} novedad${res.total === 1 ? '' : 'es'} de turnos (confirmaciones o cancelaciones) — tocá para ver`; + boton.classList.remove('oculto'); + boton.onclick = abrirModalAvisosTurnos; + } catch (e) { + boton.classList.add('oculto'); + } +} + +async function abrirModalAvisosTurnos() { + const modalEnv = clonarPlantilla('tpl-modal-avisos-turnos'); + document.body.appendChild(modalEnv); + const cont = document.getElementById('lista-avisos-turnos'); + cont.innerHTML = '
'; + + try { + const res = await llamarApi(`${API.citas}?accion=listar_avisos`, { method: 'GET' }); + cont.innerHTML = ''; + if (!res.datos.length) { + cont.innerHTML = '

No hay novedades.

'; + } else { + res.datos.forEach(c => { + const item = document.createElement('div'); + item.className = 'item-resumen'; + const fechaTexto = `${formatearFechaCorta(c.fecha)}${c.hora ? ' · ' + c.hora.slice(0, 5) : ''}`; + let estadoTexto; + if (c.estado === 'cancelada') { + estadoTexto = 'Canceló el turno'; + } else if (c.confirmada_por_paciente === 1) { + estadoTexto = 'Confirmó el turno'; + } else { + estadoTexto = 'Sin cambios de estado'; + } + item.innerHTML = ` + ${fechaTexto} + ${escaparHtml(c.nombre + ' ' + c.apellido)} + ${estadoTexto} + `; + cont.appendChild(item); + }); + } + } catch (e) { + cont.innerHTML = '

No se pudieron cargar las novedades.

'; + } + + function cerrar() { modalEnv.remove(); } + document.getElementById('btn-marcar-avisos-vistos').addEventListener('click', async () => { + try { + await llamarApi(`${API.citas}?accion=marcar_avisos_vistos`, { method: 'POST', body: JSON.stringify({}) }); + cerrar(); + document.getElementById('franja-avisos-turnos').classList.add('oculto'); + } catch (e) { + mostrarToast(e.message, 'error'); + } + }); +} + +async function cargarResumenCitas() { + const cont = document.getElementById('lista-resumen-citas'); + if (!cont) return; + try { + const res = await llamarApi(`${API.citas}?accion=proximas&dias=7`, { method: 'GET' }); + if (!res.datos.length) { + cont.innerHTML = '

No hay citas agendadas para los próximos 7 días.

'; + return; + } + cont.innerHTML = ''; + res.datos.slice(0, 6).forEach(c => { + const item = document.createElement('button'); + item.className = 'item-resumen'; + item.type = 'button'; + const esHoy = c.fecha === aFechaIso(new Date()); + item.innerHTML = ` + ${esHoy ? 'Hoy' : formatearFechaCorta(c.fecha)}${c.hora ? ' · ' + c.hora.slice(0,5) : ''} + ${escaparHtml(c.nombre + ' ' + c.apellido)} + `; + item.addEventListener('click', () => irAVista('detalle', { id: c.paciente_id })); + cont.appendChild(item); + }); + } catch (e) { + cont.innerHTML = '

No se pudo cargar la agenda.

'; + } +} + +async function cargarResumenCumpleanios() { + const cont = document.getElementById('lista-resumen-cumple'); + if (!cont) return; + try { + const res = await llamarApi(`${API.citas}?accion=cumpleanios&dias=14`, { method: 'GET' }); + if (!res.datos.length) { + cont.innerHTML = '

No hay cumpleaños en los próximos 14 días.

'; + return; + } + cont.innerHTML = ''; + res.datos.slice(0, 6).forEach(p => { + const item = document.createElement('button'); + item.className = 'item-resumen'; + item.type = 'button'; + const fechaIso = `${new Date().getFullYear()}-${p.mes_dia}`; + const esHoy = aFechaIso(new Date()).slice(5) === p.mes_dia; + item.innerHTML = ` + ${esHoy ? 'Hoy' : formatearFechaCorta(fechaIso)} · ${p.edad_que_cumple} años + ${escaparHtml(p.nombre + ' ' + p.apellido)} + `; + item.addEventListener('click', () => irAVista('detalle', { id: p.id })); + cont.appendChild(item); + }); + } catch (e) { + cont.innerHTML = '

No se pudo cargar la información.

'; + } +} + +async function cargarResumenInactivos() { + const cont = document.getElementById('lista-resumen-inactivos'); + if (!cont) return; + try { + const res = await llamarApi(`${API.citas}?accion=inactivos&dias=30`, { method: 'GET' }); + if (!res.datos.length) { + cont.innerHTML = '

Todos tus pacientes tuvieron sesiones en los últimos 30 días.

'; + return; + } + cont.innerHTML = ''; + res.datos.slice(0, 6).forEach(p => { + const item = document.createElement('button'); + item.className = 'item-resumen'; + item.type = 'button'; + const texto = p.ultima_sesion + ? `Última sesión: ${formatearFechaCorta(p.ultima_sesion)}` + : 'Sin sesiones registradas'; + item.innerHTML = ` + ${texto} + ${escaparHtml(p.nombre + ' ' + p.apellido)} + `; + item.addEventListener('click', () => irAVista('detalle', { id: p.id })); + cont.appendChild(item); + }); + } catch (e) { + cont.innerHTML = '

No se pudo cargar la información.

'; + } +} + +/* ============================================================ + OBRAS SOCIALES (carga + selector + alta rápida) + ============================================================ */ +async function cargarObrasSociales() { + const res = await llamarApi(API.obrasSociales, { method: 'GET' }); + CACHE_OBRAS_SOCIALES = res.datos; + return CACHE_OBRAS_SOCIALES; +} + +function poblarSelectObrasSociales(select, valorSeleccionado = null) { + select.innerHTML = ''; + CACHE_OBRAS_SOCIALES.forEach(o => { + const opt = document.createElement('option'); + opt.value = o.id; + opt.textContent = o.nombre; + if (valorSeleccionado && Number(valorSeleccionado) === Number(o.id)) opt.selected = true; + select.appendChild(opt); + }); +} + +function abrirModalAgregarObra(callbackAlAgregar) { + const modalEnv = clonarPlantilla('tpl-modal-obra-social'); + document.body.appendChild(modalEnv); + const input = document.getElementById('input-nueva-obra'); + input.focus(); + + function cerrar() { modalEnv.remove(); } + + document.getElementById('btn-cancelar-obra').addEventListener('click', cerrar); + document.getElementById('btn-confirmar-obra').addEventListener('click', async () => { + const nombre = input.value.trim(); + if (!nombre) { mostrarToast('Escribí un nombre para la obra social.', 'error'); return; } + try { + const res = await llamarApi(API.obrasSociales, { + method: 'POST', + body: JSON.stringify({ nombre }), + }); + await cargarObrasSociales(); + mostrarToast(res.ya_existia ? 'Esa obra social ya existía, la seleccionamos.' : 'Obra social agregada.', 'exito'); + cerrar(); + if (callbackAlAgregar) callbackAlAgregar(res.id); + } catch (e) { + mostrarToast(e.message, 'error'); + } + }); + input.addEventListener('keydown', (ev) => { + if (ev.key === 'Enter') document.getElementById('btn-confirmar-obra').click(); + }); +} + +/* ============================================================ + VISTA: CREAR / EDITAR LEGAJO (comparten el mismo formulario) + ============================================================ */ +async function montarVistaCrear(contenido, idEditar = null) { + contenido.appendChild(clonarPlantilla('tpl-crear')); + + const esEdicion = MODO_FORM_LEGAJO === 'editar'; + + document.getElementById('titulo-form-crear').textContent = esEdicion ? 'Editar legajo' : 'Crear legajo nuevo'; + document.getElementById('desc-form-crear').textContent = esEdicion + ? 'Actualizá los datos del paciente. Las sesiones se gestionan desde su ficha.' + : 'Completá los datos del paciente. Podés agregar sesiones más adelante desde su ficha.'; + document.getElementById('btn-guardar-legajo').textContent = esEdicion ? 'Guardar cambios' : 'Guardar legajo'; + + if (esEdicion) { + document.getElementById('panel-sesiones-iniciales').classList.add('oculto'); + } else { + document.getElementById('btn-abrir-aviso-legal').addEventListener('click', abrirModalAvisoLegal); + if (!sessionStorage.getItem('aviso_legal_visto')) { + abrirModalAvisoLegal(); + sessionStorage.setItem('aviso_legal_visto', '1'); + } + } + + if (ROL_ACTUAL !== 'profesional') { + document.getElementById('panel-cuadro-clinico').classList.add('oculto'); + document.getElementById('panel-sesiones-iniciales').classList.add('oculto'); + } + + await cargarObrasSociales(); + poblarSelectObrasSociales(document.getElementById('select-obra-social')); + + document.getElementById('btn-agregar-obra').addEventListener('click', () => { + abrirModalAgregarObra((nuevoId) => { + poblarSelectObrasSociales(document.getElementById('select-obra-social'), nuevoId); + }); + }); + + document.getElementById('input-fecha-nac').addEventListener('change', (e) => { + const edad = calcularEdadDesde(e.target.value); + document.getElementById('input-edad-calculada').value = edad !== null ? `${edad} años` : ''; + }); + + const listaSesiones = document.getElementById('lista-sesiones-form'); + document.getElementById('btn-agregar-dia').addEventListener('click', () => { + const item = clonarPlantilla('tpl-sesion-form-item').firstElementChild; + listaSesiones.appendChild(item); + item.querySelector('.quitar-sesion').addEventListener('click', () => item.remove()); + }); + + // Si es edición, precargamos los datos del paciente + if (esEdicion && idEditar) { + const form = document.getElementById('form-crear'); + try { + const res = await llamarApi(`${API.pacientes}?accion=detalle&id=${idEditar}`, { method: 'GET' }); + const p = res.datos; + form.querySelector('[name="id"]').value = p.id; + form.querySelector('[name="nombre"]').value = p.nombre || ''; + form.querySelector('[name="apellido"]').value = p.apellido || ''; + form.querySelector('[name="dni"]').value = p.dni || ''; + form.querySelector('[name="sexo"]').value = p.sexo || ''; + form.querySelector('[name="fecha_nacimiento"]').value = (p.fecha_nacimiento || '').slice(0, 10); + document.getElementById('input-edad-calculada').value = p.edad !== null ? `${p.edad} años` : ''; + form.querySelector('[name="telefono"]').value = p.telefono || ''; + form.querySelector('[name="email"]').value = p.email || ''; + form.querySelector('[name="direccion"]').value = p.direccion || ''; + poblarSelectObrasSociales(document.getElementById('select-obra-social'), p.obra_social_id); + form.querySelector('[name="numero_afiliado"]').value = p.numero_afiliado || ''; + form.querySelector('[name="motivo_consulta"]').value = p.motivo_consulta || ''; + form.querySelector('[name="patologia"]').value = p.patologia || ''; + form.querySelector('[name="sintomas"]').value = p.sintomas || ''; + form.querySelector('[name="observaciones_generales"]').value = p.observaciones_generales || ''; + } catch (e) { + mostrarToast(e.message, 'error'); + irAVista('menu'); + return; + } + } + + document.getElementById('form-crear').addEventListener('submit', async (ev) => { + ev.preventDefault(); + const btnGuardar = document.getElementById('btn-guardar-legajo'); + const form = ev.target; + const datos = Object.fromEntries(new FormData(form).entries()); + + if (!esEdicion) { + const sesiones = []; + listaSesiones.querySelectorAll('.sesion-form-item').forEach(item => { + sesiones.push({ + fecha_sesion: item.querySelector('.campo-fecha-sesion').value, + proxima_cita: item.querySelector('.campo-proxima-cita').value || null, + descripcion: item.querySelector('.campo-descripcion-sesion').value, + evolucion: item.querySelector('.campo-evolucion-sesion').value || null, + }); + }); + datos.sesiones = sesiones; + } + + btnGuardar.disabled = true; + btnGuardar.innerHTML = ' Guardando...'; + try { + if (esEdicion) { + await llamarApi(`${API.pacientes}?accion=actualizar`, { + method: 'POST', + body: JSON.stringify(datos), + }); + mostrarToast('Legajo actualizado correctamente.', 'exito'); + irAVista('detalle', { id: datos.id }); + } else { + await llamarApi(`${API.pacientes}?accion=crear`, { + method: 'POST', + body: JSON.stringify(datos), + }); + mostrarToast('Legajo creado correctamente.', 'exito'); + irAVista('menu'); + } + } catch (e) { + mostrarToast(e.message, 'error'); + btnGuardar.disabled = false; + btnGuardar.textContent = esEdicion ? 'Guardar cambios' : 'Guardar legajo'; + } + }); +} + +function abrirModalAvisoLegal() { + const modalEnv = clonarPlantilla('tpl-modal-aviso-legal'); + document.body.appendChild(modalEnv); + function cerrar() { modalEnv.remove(); } + document.getElementById('btn-cerrar-aviso-legal').addEventListener('click', cerrar); + document.getElementById('btn-cerrar-aviso-legal-x').addEventListener('click', cerrar); + modalEnv.querySelector('.overlay-modal').addEventListener('click', (ev) => { + if (ev.target.classList.contains('overlay-modal')) cerrar(); + }); +} + +function calcularEdadDesde(fechaIso) { + if (!fechaIso) return null; + const nacimiento = new Date(fechaIso); + const hoy = new Date(); + let edad = hoy.getFullYear() - nacimiento.getFullYear(); + const aunNoCumple = (hoy.getMonth() < nacimiento.getMonth()) || + (hoy.getMonth() === nacimiento.getMonth() && hoy.getDate() < nacimiento.getDate()); + if (aunNoCumple) edad--; + return edad; +} + +/* ============================================================ + VISTA: BUSCAR (acceder / borrar comparten estructura) + ============================================================ */ +async function montarVistaBusqueda(contenido, modo) { + const idPlantilla = modo === 'acceder' ? 'tpl-acceder' : 'tpl-borrar'; + contenido.appendChild(clonarPlantilla(idPlantilla)); + TIPO_BUSQUEDA_ACTUAL = 'dni'; + + if (modo === 'borrar') { + document.getElementById('btn-ver-papelera').addEventListener('click', abrirPapelera); + } + + await cargarObrasSociales(); + + const tabs = contenido.parentElement.querySelectorAll('.tab-busqueda'); + tabs.forEach(tab => { + tab.addEventListener('click', () => { + tabs.forEach(t => t.classList.remove('activo')); + tab.classList.add('activo'); + TIPO_BUSQUEDA_ACTUAL = tab.dataset.tipo; + renderizarFilaBuscador(modo); + document.getElementById('resultados-busqueda').innerHTML = ''; + }); + }); + + renderizarFilaBuscador(modo); +} + +function renderizarFilaBuscador(modo) { + const fila = document.getElementById('fila-buscador'); + fila.innerHTML = ''; + + if (TIPO_BUSQUEDA_ACTUAL === 'dni') { + fila.innerHTML = ` + `; + } else if (TIPO_BUSQUEDA_ACTUAL === 'nombre') { + fila.innerHTML = ` + `; + } else if (TIPO_BUSQUEDA_ACTUAL === 'fecha') { + fila.innerHTML = ` + + + `; + } else if (TIPO_BUSQUEDA_ACTUAL === 'obra_social') { + fila.innerHTML = ` + `; + poblarSelectObrasSociales(document.getElementById('input-busqueda-obra')); + } else if (TIPO_BUSQUEDA_ACTUAL === 'sede') { + fila.innerHTML = ` + `; + poblarSelectSedesBusqueda(document.getElementById('input-busqueda-sede')); + } + + document.getElementById('btn-buscar').addEventListener('click', () => ejecutarBusqueda(modo)); + + fila.querySelectorAll('input').forEach(inp => { + inp.addEventListener('keydown', (ev) => { if (ev.key === 'Enter') ejecutarBusqueda(modo); }); + }); +} + +async function poblarSelectSedesBusqueda(select) { + select.innerHTML = ''; + try { + const res = await llamarApi(`${API.pacientes}?accion=sedes_disponibles`, { method: 'GET' }); + select.innerHTML = ''; + res.datos.forEach(s => { + const opt = document.createElement('option'); + opt.value = s.id; + opt.textContent = s.nombre; + select.appendChild(opt); + }); + } catch (e) { + select.innerHTML = ''; + mostrarToast(e.message, 'error'); + } +} + +async function ejecutarBusqueda(modo) { + const params = new URLSearchParams({ accion: 'buscar', tipo: TIPO_BUSQUEDA_ACTUAL }); + + if (TIPO_BUSQUEDA_ACTUAL === 'dni') { + params.set('valor', document.getElementById('input-busqueda-dni').value.trim()); + } else if (TIPO_BUSQUEDA_ACTUAL === 'nombre') { + params.set('valor', document.getElementById('input-busqueda-nombre').value.trim()); + } else if (TIPO_BUSQUEDA_ACTUAL === 'fecha') { + const desde = document.getElementById('input-busqueda-desde').value; + const hasta = document.getElementById('input-busqueda-hasta').value; + if (desde) params.set('desde', desde); + if (hasta) params.set('hasta', hasta); + } else if (TIPO_BUSQUEDA_ACTUAL === 'obra_social') { + params.set('obra_social_id', document.getElementById('input-busqueda-obra').value); + } else if (TIPO_BUSQUEDA_ACTUAL === 'sede') { + params.set('sede_id', document.getElementById('input-busqueda-sede').value); + } + + const contRes = document.getElementById('resultados-busqueda'); + contRes.innerHTML = '
Buscando...
'; + + try { + const res = await llamarApi(`${API.pacientes}?${params.toString()}`, { method: 'GET' }); + renderizarResultados(res.datos, modo); + } catch (e) { + contRes.innerHTML = ''; + mostrarToast(e.message, 'error'); + } +} + +function renderizarResultados(lista, modo) { + const contRes = document.getElementById('resultados-busqueda'); + contRes.innerHTML = ''; + + if (!lista.length) { + const vacio = document.createElement('div'); + vacio.className = 'estado-vacio'; + vacio.innerHTML = ` + +

No encontramos resultados

+

Probá con otro criterio de búsqueda o revisá que el dato esté bien escrito.

`; + contRes.appendChild(vacio); + return; + } + + const contenedorLista = document.createElement('div'); + contenedorLista.className = 'lista-resultados'; + + lista.forEach(p => { + const tarjeta = clonarPlantilla('tpl-tarjeta-paciente').firstElementChild; + tarjeta.dataset.id = p.id; + tarjeta.querySelector('.avatar-iniciales').textContent = (p.nombre[0] + p.apellido[0]).toUpperCase(); + tarjeta.querySelector('.nombre').textContent = `${p.apellido}, ${p.nombre}`; + tarjeta.querySelector('.meta').innerHTML = `DNI ${p.dni} · ${p.edad} años${p.sede_nombre ? ' · ' + escaparHtml(p.sede_nombre) : ''} · `; + + const etiqueta = document.createElement('span'); + etiqueta.className = 'etiqueta' + (p.obra_social_nombre && p.obra_social_nombre.includes('Particular') ? ' particular' : ''); + etiqueta.textContent = p.obra_social_nombre || 'Sin obra social'; + tarjeta.querySelector('.meta').appendChild(etiqueta); + + const acciones = tarjeta.querySelector('.acciones-tarjeta'); + + if (modo === 'acceder') { + const btnVer = document.createElement('button'); + btnVer.className = 'btn-icono'; + btnVer.title = 'Ver legajo'; + btnVer.innerHTML = ``; + btnVer.addEventListener('click', () => irAVista('detalle', { id: p.id })); + acciones.appendChild(btnVer); + } else if (modo === 'borrar') { + const btnBorrar = document.createElement('button'); + btnBorrar.className = 'btn-icono peligro'; + btnBorrar.title = 'Eliminar legajo'; + btnBorrar.innerHTML = ``; + btnBorrar.addEventListener('click', () => confirmarEliminarLegajo(p.id, `${p.nombre} ${p.apellido}`, modo)); + acciones.appendChild(btnBorrar); + } + + contenedorLista.appendChild(tarjeta); + }); + + contRes.appendChild(contenedorLista); +} + +function confirmarEliminarLegajo(id, nombreCompleto) { + const modalEnv = clonarPlantilla('tpl-modal-confirmar-borrado'); + document.body.appendChild(modalEnv); + document.getElementById('texto-confirmar-borrado').textContent = + `Vas a eliminar el legajo de ${nombreCompleto} del sistema activo. Quedará guardado en la base histórica, pero no aparecerá más en las búsquedas normales.`; + + const inputConfirmar = document.getElementById('input-confirmar-borrado'); + const btnConfirmar = document.getElementById('btn-confirmar-borrado'); + inputConfirmar.focus(); + + inputConfirmar.addEventListener('input', () => { + btnConfirmar.disabled = inputConfirmar.value.trim().toUpperCase() !== 'ELIMINAR'; + }); + + function cerrar() { modalEnv.remove(); } + document.getElementById('btn-cancelar-borrado').addEventListener('click', cerrar); + document.getElementById('btn-confirmar-borrado').addEventListener('click', async () => { + if (inputConfirmar.value.trim().toUpperCase() !== 'ELIMINAR') return; + btnConfirmar.disabled = true; + btnConfirmar.innerHTML = ' Eliminando...'; + try { + await llamarApi(`${API.pacientes}?accion=eliminar`, { + method: 'POST', + body: JSON.stringify({ id }), + }); + mostrarToast('Legajo eliminado del sistema activo.', 'exito'); + cerrar(); + ejecutarBusqueda('borrar'); + } catch (e) { + mostrarToast(e.message, 'error'); + btnConfirmar.disabled = false; + btnConfirmar.textContent = 'Sí, eliminar definitivamente'; + } + }); +} + +/* ============================================================ + BASE HISTÓRICA (papelera) + ============================================================ */ +async function abrirPapelera() { + const modalEnv = clonarPlantilla('tpl-papelera'); + document.body.appendChild(modalEnv); + document.getElementById('btn-cerrar-papelera').addEventListener('click', () => modalEnv.remove()); + + const lista = document.getElementById('lista-papelera'); + lista.innerHTML = '
Cargando...
'; + + try { + const res = await llamarApi(`${API.pacientes}?accion=papelera`, { method: 'GET' }); + lista.innerHTML = ''; + if (!res.datos.length) { + lista.innerHTML = '

Todavía no eliminaste ningún legajo.

'; + return; + } + res.datos.forEach(item => { + const fecha = new Date(item.eliminado_en).toLocaleDateString('es-AR', { day: '2-digit', month: '2-digit', year: 'numeric' }); + const fila = document.createElement('div'); + fila.className = 'tarjeta-paciente'; + fila.innerHTML = ` +
+
${item.nombre_completo.split(' ').map(p => p[0]).slice(0,2).join('').toUpperCase()}
+
+
${item.nombre_completo}
+
DNI ${item.dni} · Eliminado el ${fecha}
+
+
`; + lista.appendChild(fila); + }); + } catch (e) { + lista.innerHTML = ''; + mostrarToast(e.message, 'error'); + } +} + +/* ============================================================ + VISTA: DETALLE DE PACIENTE + ============================================================ */ +async function montarVistaDetalle(contenido, id) { + contenido.innerHTML = '
Cargando legajo...
'; + PACIENTE_ID_ACTUAL_DETALLE = id; + + let p; + try { + const res = await llamarApi(`${API.pacientes}?accion=detalle&id=${id}`, { method: 'GET' }); + p = res.datos; + } catch (e) { + contenido.innerHTML = ''; + mostrarToast(e.message, 'error'); + irAVista('menu'); + return; + } + + contenido.innerHTML = ''; + contenido.appendChild(clonarPlantilla('tpl-detalle-paciente')); + + document.querySelector('[data-volver-busqueda]').addEventListener('click', () => irAVista(VISTA_BUSQUEDA_ORIGEN)); + + document.getElementById('detalle-avatar').textContent = (p.nombre[0] + p.apellido[0]).toUpperCase(); + document.getElementById('detalle-nombre').textContent = `${p.nombre} ${p.apellido}`; + document.getElementById('detalle-meta').textContent = `Paciente desde ${new Date(p.creado_en).toLocaleDateString('es-AR')}`; + + document.getElementById('dato-dni').textContent = p.dni; + document.getElementById('dato-edad').textContent = `${p.edad} años`; + document.getElementById('dato-sexo').textContent = p.sexo; + document.getElementById('dato-obra').textContent = p.obra_social_nombre || 'Sin especificar'; + document.getElementById('dato-sede').textContent = p.sede_nombre || 'Sin asignar'; + + const avisoRecuperado = document.getElementById('aviso-legajo-recuperado'); + if (p.recuperado_de_profesional) { + avisoRecuperado.innerHTML = `📋 Este legajo perteneció antes a ${escaparHtml(p.recuperado_de_profesional)} y fue recuperado de la papelera.`; + avisoRecuperado.classList.remove('oculto'); + } else { + avisoRecuperado.classList.add('oculto'); + } + + const esProfesionalActual = ROL_ACTUAL === 'profesional'; + + if (esProfesionalActual) { + rellenarTextoOVacio('dato-motivo', p.motivo_consulta); + rellenarTextoOVacio('dato-patologia', p.patologia); + rellenarTextoOVacio('dato-sintomas', p.sintomas); + rellenarTextoOVacio('dato-observaciones', p.observaciones_generales); + + const linea = document.getElementById('linea-tiempo'); + linea.innerHTML = ''; + if (!p.sesiones || !p.sesiones.length) { + linea.innerHTML = '

Todavía no se registraron sesiones para este paciente.

'; + } else { + p.sesiones.forEach(s => { + const item = document.createElement('div'); + item.className = 'sesion-item'; + const fecha = new Date(s.fecha_sesion + 'T00:00:00').toLocaleDateString('es-AR', { day: '2-digit', month: 'long', year: 'numeric' }); + item.innerHTML = ` +
+
${fecha}
+
+ + +
+
+
${escaparHtml(s.descripcion)}
+ ${s.evolucion ? `
${escaparHtml(s.evolucion)}
` : ''} + `; + item.querySelector('[data-accion="editar-sesion"]').addEventListener('click', () => abrirModalEditarSesion(s, p.id)); + item.querySelector('[data-accion="eliminar-sesion"]').addEventListener('click', () => confirmarEliminarSesion(s.id, fecha, p.id)); + linea.appendChild(item); + }); + } + + document.getElementById('btn-nueva-sesion').addEventListener('click', () => abrirModalNuevaSesion(p.id)); + document.getElementById('btn-editar-legajo').addEventListener('click', () => irAVista('editar', { id: p.id })); + document.getElementById('btn-exportar-pdf').addEventListener('click', () => { + window.open(`exportar.php?id=${p.id}`, '_blank'); + }); + document.getElementById('btn-migrar-sede').addEventListener('click', () => abrirModalMigrarSede(p.id, `${p.nombre} ${p.apellido}`, p.sede_nombre)); + cargarAdjuntosPaciente(p.id); + vincularSubidaAdjunto(p.id); + } else { + // La administrativa no ve contenido clínico: se ocultan los + // bloques de motivo/patología/síntomas/observaciones, el historial + // de sesiones y los adjuntos, junto con las acciones que los tocan. + document.querySelectorAll('.bloque-texto').forEach(el => el.remove()); + document.getElementById('btn-editar-legajo').remove(); + document.getElementById('btn-exportar-pdf').remove(); + document.getElementById('btn-migrar-sede').remove(); + const panelSesiones = document.getElementById('linea-tiempo').closest('.panel'); + if (panelSesiones) panelSesiones.remove(); + const panelAdjuntos = document.getElementById('lista-adjuntos').closest('.panel'); + if (panelAdjuntos) panelAdjuntos.remove(); + } + + document.getElementById('btn-agendar-cita-paciente').addEventListener('click', () => abrirModalNuevaCita(p.id, `${p.nombre} ${p.apellido}`)); + + cargarCitasPaciente(p.id, p.nombre, p.apellido, p.telefono); +} + +async function abrirModalMigrarSede(pacienteId, nombrePaciente, sedeActualNombre) { + const modalEnv = clonarPlantilla('tpl-modal-migrar-sede'); + document.body.appendChild(modalEnv); + document.getElementById('texto-paciente-migrar-sede').textContent = + `${nombrePaciente} está actualmente en "${sedeActualNombre || 'sin sede asignada'}". Elegí la nueva sede.`; + + const select = document.getElementById('select-nueva-sede'); + select.innerHTML = ''; + try { + const res = await llamarApi(`${API.pacientes}?accion=sedes_disponibles`, { method: 'GET' }); + select.innerHTML = ''; + res.datos.forEach(s => { + const opt = document.createElement('option'); + opt.value = s.id; + opt.textContent = s.nombre; + select.appendChild(opt); + }); + } catch (e) { + select.innerHTML = ''; + mostrarToast(e.message, 'error'); + } + + function cerrar() { modalEnv.remove(); } + document.getElementById('btn-cancelar-migrar-sede').addEventListener('click', cerrar); + document.getElementById('btn-confirmar-migrar-sede').addEventListener('click', async () => { + const sedeId = select.value; + if (!sedeId) { mostrarToast('Elegí una sede.', 'error'); return; } + try { + await llamarApi(`${API.pacientes}?accion=migrar_sede`, { + method: 'POST', + body: JSON.stringify({ id: pacienteId, sede_id: sedeId }), + }); + mostrarToast('Paciente migrado correctamente.', 'exito'); + cerrar(); + irAVista('detalle', { id: pacienteId }); + } catch (e) { + mostrarToast(e.message, 'error'); + } + }); +} + +function rellenarTextoOVacio(idElemento, texto) { + const el = document.getElementById(idElemento); + if (texto && texto.trim() !== '') { + el.textContent = texto; + el.classList.remove('vacio'); + } else { + el.textContent = 'No se registró información.'; + el.classList.add('vacio'); + } +} + +function escaparHtml(texto) { + const div = document.createElement('div'); + div.textContent = texto; + return div.innerHTML; +} + +/* ============================================================ + PLANTILLAS DE EVOLUCIÓN + ============================================================ */ +async function cargarPlantillas() { + const res = await llamarApi(API.plantillas, { method: 'GET' }); + CACHE_PLANTILLAS = res.datos; + return CACHE_PLANTILLAS; +} + +function poblarSelectPlantillas(select) { + select.innerHTML = ''; + CACHE_PLANTILLAS.forEach(pl => { + const opt = document.createElement('option'); + opt.value = pl.id; + opt.textContent = pl.nombre; + select.appendChild(opt); + }); +} + +function abrirModalGestionarPlantillas(alCerrarCallback) { + const modalEnv = clonarPlantilla('tpl-modal-gestionar-plantillas'); + document.body.appendChild(modalEnv); + + function cerrar() { + modalEnv.remove(); + if (alCerrarCallback) alCerrarCallback(); + } + + function renderizarLista() { + const lista = document.getElementById('lista-plantillas'); + lista.innerHTML = ''; + if (!CACHE_PLANTILLAS.length) { + lista.innerHTML = '

Todavía no creaste ninguna plantilla.

'; + return; + } + CACHE_PLANTILLAS.forEach(pl => { + const item = document.createElement('div'); + item.className = 'item-plantilla'; + item.innerHTML = ` +
+
${escaparHtml(pl.nombre)}
+
${escaparHtml(pl.contenido)}
+
+ + `; + item.querySelector('.btn-borrar-plantilla').addEventListener('click', async () => { + try { + await llamarApi(`${API.plantillas}?accion=eliminar`, { method: 'POST', body: JSON.stringify({ id: pl.id }) }); + await cargarPlantillas(); + renderizarLista(); + mostrarToast('Plantilla eliminada.', 'exito'); + } catch (e) { + mostrarToast(e.message, 'error'); + } + }); + lista.appendChild(item); + }); + } + + renderizarLista(); + + document.getElementById('btn-guardar-plantilla').addEventListener('click', async () => { + const nombre = document.getElementById('input-nombre-plantilla').value.trim(); + const texto = document.getElementById('input-contenido-plantilla').value.trim(); + if (!nombre || !texto) { + mostrarToast('Completá el nombre y el contenido de la plantilla.', 'error'); + return; + } + try { + await llamarApi(`${API.plantillas}?accion=crear`, { + method: 'POST', + body: JSON.stringify({ nombre, contenido: texto }), + }); + await cargarPlantillas(); + renderizarLista(); + document.getElementById('input-nombre-plantilla').value = ''; + document.getElementById('input-contenido-plantilla').value = ''; + mostrarToast('Plantilla guardada.', 'exito'); + } catch (e) { + mostrarToast(e.message, 'error'); + } + }); + + document.getElementById('btn-cerrar-gestionar-plantillas').addEventListener('click', cerrar); +} + +/* ============================================================ + SESIONES (agregar nueva, con plantillas) + ============================================================ */ +async function abrirModalNuevaSesion(pacienteId) { + const modalEnv = clonarPlantilla('tpl-modal-nueva-sesion'); + document.body.appendChild(modalEnv); + + await cargarPlantillas(); + const selectPlantilla = document.getElementById('select-plantilla-sesion'); + poblarSelectPlantillas(selectPlantilla); + + const inputDescripcion = document.getElementById('input-descripcion-nueva-sesion'); + selectPlantilla.addEventListener('change', () => { + const plantilla = CACHE_PLANTILLAS.find(pl => String(pl.id) === selectPlantilla.value); + if (plantilla) { + inputDescripcion.value = plantilla.contenido; + } + }); + + document.getElementById('btn-gestionar-plantillas').addEventListener('click', () => { + abrirModalGestionarPlantillas(() => { + poblarSelectPlantillas(selectPlantilla); + }); + }); + + function cerrar() { modalEnv.remove(); } + document.getElementById('btn-cancelar-nueva-sesion').addEventListener('click', cerrar); + document.getElementById('btn-confirmar-nueva-sesion').addEventListener('click', async () => { + const fecha = document.getElementById('input-fecha-nueva-sesion').value; + const descripcion = inputDescripcion.value.trim(); + const proxima = document.getElementById('input-proxima-nueva-sesion').value || null; + const evolucion = document.getElementById('input-evolucion-nueva-sesion').value.trim() || null; + + if (!fecha || !descripcion) { + mostrarToast('Completá la fecha y la descripción de la sesión.', 'error'); + return; + } + try { + await llamarApi(`${API.pacientes}?accion=agregar_sesion`, { + method: 'POST', + body: JSON.stringify({ paciente_id: pacienteId, fecha_sesion: fecha, descripcion, proxima_cita: proxima, evolucion }), + }); + mostrarToast('Sesión agregada correctamente.', 'exito'); + cerrar(); + irAVista('detalle', { id: pacienteId }); + } catch (e) { + mostrarToast(e.message, 'error'); + } + }); +} + +function abrirModalEditarSesion(sesion, pacienteId) { + const modalEnv = clonarPlantilla('tpl-modal-editar-sesion'); + document.body.appendChild(modalEnv); + + document.getElementById('input-fecha-editar-sesion').value = sesion.fecha_sesion.slice(0, 10); + document.getElementById('input-descripcion-editar-sesion').value = sesion.descripcion; + document.getElementById('input-evolucion-editar-sesion').value = sesion.evolucion || ''; + + function cerrar() { modalEnv.remove(); } + document.getElementById('btn-cancelar-editar-sesion').addEventListener('click', cerrar); + document.getElementById('btn-confirmar-editar-sesion').addEventListener('click', async () => { + const fecha = document.getElementById('input-fecha-editar-sesion').value; + const descripcion = document.getElementById('input-descripcion-editar-sesion').value.trim(); + const evolucion = document.getElementById('input-evolucion-editar-sesion').value.trim() || null; + + if (!fecha || !descripcion) { + mostrarToast('Completá la fecha y la descripción de la sesión.', 'error'); + return; + } + + const btn = document.getElementById('btn-confirmar-editar-sesion'); + btn.disabled = true; + btn.innerHTML = ' Guardando...'; + try { + await llamarApi(`${API.pacientes}?accion=editar_sesion`, { + method: 'POST', + body: JSON.stringify({ id: sesion.id, fecha_sesion: fecha, descripcion, evolucion }), + }); + mostrarToast('Sesión actualizada.', 'exito'); + cerrar(); + irAVista('detalle', { id: pacienteId }); + } catch (e) { + mostrarToast(e.message, 'error'); + btn.disabled = false; + btn.textContent = 'Guardar cambios'; + } + }); +} + +function confirmarEliminarSesion(sesionId, fechaTexto, pacienteId) { + if (!confirm(`¿Eliminar la sesión del ${fechaTexto}? Esta acción no se puede deshacer.`)) return; + llamarApi(`${API.pacientes}?accion=eliminar_sesion`, { + method: 'POST', + body: JSON.stringify({ id: sesionId }), + }).then(() => { + mostrarToast('Sesión eliminada.', 'exito'); + irAVista('detalle', { id: pacienteId }); + }).catch(e => { + mostrarToast(e.message, 'error'); + }); +} + +/* ============================================================ + CITAS DENTRO DE LA FICHA DEL PACIENTE + ============================================================ */ +async function cargarCitasPaciente(pacienteId, nombrePaciente, apellidoPaciente, telefonoPaciente) { + const cont = document.getElementById('lista-citas-paciente'); + cont.innerHTML = '
'; + try { + const res = await llamarApi(`${API.citas}?accion=por_paciente&paciente_id=${pacienteId}`, { method: 'GET' }); + cont.innerHTML = ''; + const pendientes = res.datos.filter(c => c.estado === 'pendiente'); + if (!pendientes.length) { + cont.innerHTML = '

No hay citas pendientes agendadas.

'; + return; + } + pendientes.forEach(c => cont.appendChild(crearFilaCita(c, pacienteId, nombrePaciente, apellidoPaciente, telefonoPaciente))); + } catch (e) { + cont.innerHTML = ''; + mostrarToast(e.message, 'error'); + } +} + +/** + * Genera un link de WhatsApp (wa.me) con un mensaje de recordatorio + * pre-armado. No envía nada automáticamente: abre WhatsApp con el + * texto listo para que el profesional/administrativa lo revise y + * lo mande con un toque. + */ +function generarLinkWhatsapp(telefono, nombrePaciente, fechaIso, hora, tokenConfirmacion) { + if (!telefono) return null; + const telefonoLimpio = telefono.replace(/[^\d+]/g, ''); + const fechaTexto = formatearFechaLarga(fechaIso); + const horaTexto = hora ? ` a las ${hora.slice(0, 5)}` : ''; + let mensaje = `Hola ${nombrePaciente}, te recuerdo tu turno del ${fechaTexto}${horaTexto}. ¡Te esperamos!`; + if (tokenConfirmacion) { + const linkConfirmacion = `${location.origin}${location.pathname.replace(/index\.html$/, '')}confirmar_turno.php?token=${tokenConfirmacion}`; + mensaje += `\n\nPodés confirmar o cancelar tu turno acá: ${linkConfirmacion}`; + } + return `https://wa.me/${telefonoLimpio}?text=${encodeURIComponent(mensaje)}`; +} + +function copiarLinkConfirmacion(token) { + const link = `${location.origin}${location.pathname.replace(/index\.html$/, '')}confirmar_turno.php?token=${token}`; + navigator.clipboard.writeText(link) + .then(() => mostrarToast('Link copiado al portapapeles.', 'exito')) + .catch(() => mostrarToast('No se pudo copiar el link. Copialo manualmente: ' + link, 'error')); +} + +function crearFilaCita(c, pacienteId, nombrePaciente, apellidoPaciente, telefonoPaciente) { + const fila = document.createElement('div'); + fila.className = 'tarjeta-paciente'; + fila.innerHTML = ` +
+
+ +
+
+
${formatearFechaLarga(c.fecha)}${c.hora ? ' · ' + c.hora.slice(0,5) : ''}
+
${escaparHtml(c.motivo || 'Sin motivo especificado')}
+
+
+
+ `; + const acciones = fila.querySelector('.acciones-tarjeta'); + + const linkWhatsapp = generarLinkWhatsapp(telefonoPaciente, nombrePaciente, c.fecha, c.hora, c.token_confirmacion); + if (linkWhatsapp) { + const btnWhatsapp = document.createElement('a'); + btnWhatsapp.className = 'btn-icono whatsapp'; + btnWhatsapp.title = 'Enviar recordatorio por WhatsApp'; + btnWhatsapp.href = linkWhatsapp; + btnWhatsapp.target = '_blank'; + btnWhatsapp.innerHTML = ``; + acciones.appendChild(btnWhatsapp); + } + + if (c.token_confirmacion) { + const btnCopiarLink = document.createElement('button'); + btnCopiarLink.className = 'btn-icono'; + btnCopiarLink.title = 'Copiar link de confirmación'; + btnCopiarLink.innerHTML = ``; + btnCopiarLink.addEventListener('click', () => copiarLinkConfirmacion(c.token_confirmacion)); + acciones.appendChild(btnCopiarLink); + } + + const btnAtendida = document.createElement('button'); + btnAtendida.className = 'btn-icono'; + btnAtendida.title = 'Marcar como atendida'; + btnAtendida.innerHTML = ``; + btnAtendida.addEventListener('click', () => cambiarEstadoCita(c.id, 'atendida', pacienteId)); + + const btnCancelar = document.createElement('button'); + btnCancelar.className = 'btn-icono peligro'; + btnCancelar.title = 'Cancelar cita'; + btnCancelar.innerHTML = ``; + btnCancelar.addEventListener('click', () => cambiarEstadoCita(c.id, 'cancelada', pacienteId)); + + acciones.appendChild(btnAtendida); + acciones.appendChild(btnCancelar); + return fila; +} + +async function cambiarEstadoCita(citaId, estado, pacienteId) { + try { + await llamarApi(`${API.citas}?accion=cambiar_estado`, { + method: 'POST', + body: JSON.stringify({ id: citaId, estado }), + }); + mostrarToast(estado === 'atendida' ? 'Cita marcada como atendida.' : 'Cita cancelada.', 'exito'); + if (pacienteId) cargarCitasPaciente(pacienteId); + } catch (e) { + mostrarToast(e.message, 'error'); + } +} + +function abrirModalNuevaCita(pacienteIdFijo = null, nombreFijo = null) { + const modalEnv = clonarPlantilla('tpl-modal-nueva-cita'); + document.body.appendChild(modalEnv); + + const inputBuscar = document.getElementById('input-buscar-paciente-cita'); + const inputPacienteId = document.getElementById('input-paciente-id-cita'); + const resultadosBox = document.getElementById('resultados-buscar-paciente-cita'); + + if (pacienteIdFijo) { + inputBuscar.value = nombreFijo; + inputBuscar.disabled = true; + inputPacienteId.value = pacienteIdFijo; + } else { + let timeoutBusqueda = null; + inputBuscar.addEventListener('input', () => { + clearTimeout(timeoutBusqueda); + const valor = inputBuscar.value.trim(); + inputPacienteId.value = ''; + if (valor.length < 2) { resultadosBox.innerHTML = ''; return; } + timeoutBusqueda = setTimeout(() => buscarPacientesParaCita(valor, resultadosBox, inputBuscar, inputPacienteId), 300); + }); + } + + function cerrar() { modalEnv.remove(); } + document.getElementById('btn-cancelar-nueva-cita').addEventListener('click', cerrar); + document.getElementById('btn-confirmar-nueva-cita').addEventListener('click', async () => { + const pacienteId = inputPacienteId.value; + const fecha = document.getElementById('input-fecha-cita').value; + const hora = document.getElementById('input-hora-cita').value || null; + const motivo = document.getElementById('input-motivo-cita').value.trim() || null; + + if (!pacienteId) { mostrarToast('Elegí un paciente de la lista.', 'error'); return; } + if (!fecha) { mostrarToast('Elegí una fecha para la cita.', 'error'); return; } + + try { + await llamarApi(`${API.citas}?accion=crear`, { + method: 'POST', + body: JSON.stringify({ paciente_id: pacienteId, fecha, hora, motivo }), + }); + mostrarToast('Cita agendada correctamente.', 'exito'); + cerrar(); + if (PACIENTE_ID_ACTUAL_DETALLE && String(PACIENTE_ID_ACTUAL_DETALLE) === String(pacienteId)) { + cargarCitasPaciente(pacienteId); + } + if (document.getElementById('calendario-grilla')) { + renderizarCalendario(); + } + } catch (e) { + mostrarToast(e.message, 'error'); + } + }); +} + +async function buscarPacientesParaCita(valor, resultadosBox, inputBuscar, inputPacienteId) { + try { + const esNumerico = /^\d+$/.test(valor); + const tipo = esNumerico ? 'dni' : 'nombre'; + const res = await llamarApi(`${API.pacientes}?accion=buscar&tipo=${tipo}&valor=${encodeURIComponent(valor)}`, { method: 'GET' }); + resultadosBox.innerHTML = ''; + if (!res.datos.length) { + resultadosBox.innerHTML = '
No se encontraron pacientes.
'; + return; + } + res.datos.slice(0, 6).forEach(p => { + const item = document.createElement('button'); + item.type = 'button'; + item.className = 'item-autocompletar'; + item.textContent = `${p.apellido}, ${p.nombre} — DNI ${p.dni}`; + item.addEventListener('click', () => { + inputBuscar.value = `${p.apellido}, ${p.nombre}`; + inputPacienteId.value = p.id; + resultadosBox.innerHTML = ''; + }); + resultadosBox.appendChild(item); + }); + } catch (e) { + resultadosBox.innerHTML = ''; + } +} + +/* ============================================================ + ARCHIVOS ADJUNTOS + ============================================================ */ +async function cargarAdjuntosPaciente(pacienteId) { + const cont = document.getElementById('lista-adjuntos'); + cont.innerHTML = '
'; + try { + const res = await llamarApi(`${API.adjuntos}?accion=listar&paciente_id=${pacienteId}`, { method: 'GET' }); + cont.innerHTML = ''; + if (!res.datos.length) { + cont.innerHTML = '

No hay archivos adjuntos todavía.

'; + return; + } + res.datos.forEach(a => cont.appendChild(crearItemAdjunto(a, pacienteId))); + } catch (e) { + cont.innerHTML = ''; + mostrarToast(e.message, 'error'); + } +} + +function crearItemAdjunto(a, pacienteId) { + const esImagen = a.tipo_mime.startsWith('image/'); + const item = document.createElement('div'); + item.className = 'item-adjunto'; + const pesoKb = Math.round(a.tamanio_bytes / 1024); + const pesoTexto = pesoKb > 1024 ? `${(pesoKb / 1024).toFixed(1)} MB` : `${pesoKb} KB`; + + item.innerHTML = ` +
+ ${esImagen + ? '' + : ''} +
+
+
${escaparHtml(a.descripcion || a.nombre_original)}
+
${escaparHtml(a.nombre_original)} · ${pesoTexto} · ${formatearFechaCorta(a.subido_en.slice(0,10))}
+
+
+ `; + const acciones = item.querySelector('.acciones-tarjeta'); + + const btnVer = document.createElement('a'); + btnVer.className = 'btn-icono'; + btnVer.title = 'Ver / descargar'; + btnVer.href = `${API.adjuntos}?accion=ver&id=${a.id}`; + btnVer.target = '_blank'; + btnVer.innerHTML = ``; + + const btnBorrar = document.createElement('button'); + btnBorrar.className = 'btn-icono peligro'; + btnBorrar.title = 'Eliminar archivo'; + btnBorrar.innerHTML = ``; + btnBorrar.addEventListener('click', async () => { + if (!confirm('¿Eliminar este archivo adjunto? Esta acción no se puede deshacer.')) return; + try { + await llamarApi(`${API.adjuntos}?accion=eliminar`, { method: 'POST', body: JSON.stringify({ id: a.id }) }); + mostrarToast('Archivo eliminado.', 'exito'); + cargarAdjuntosPaciente(pacienteId); + } catch (e) { + mostrarToast(e.message, 'error'); + } + }); + + acciones.appendChild(btnVer); + acciones.appendChild(btnBorrar); + return item; +} + +function vincularSubidaAdjunto(pacienteId) { + const input = document.getElementById('input-subir-adjunto'); + input.addEventListener('change', () => { + if (!input.files || !input.files[0]) return; + const archivo = input.files[0]; + const TAMANIO_MAXIMO_MB = 15; + if (archivo.size > TAMANIO_MAXIMO_MB * 1024 * 1024) { + mostrarToast(`El archivo pesa ${(archivo.size / (1024 * 1024)).toFixed(1)} MB. El máximo permitido es ${TAMANIO_MAXIMO_MB} MB.`, 'error'); + input.value = ''; + return; + } + ARCHIVO_PENDIENTE_SUBIR = archivo; + abrirModalDescripcionAdjunto(pacienteId); + input.value = ''; + }); +} + +function abrirModalDescripcionAdjunto(pacienteId) { + const modalEnv = clonarPlantilla('tpl-modal-descripcion-adjunto'); + document.body.appendChild(modalEnv); + document.getElementById('texto-nombre-archivo-subir').textContent = `Archivo: ${ARCHIVO_PENDIENTE_SUBIR.name}`; + + function cerrar() { modalEnv.remove(); ARCHIVO_PENDIENTE_SUBIR = null; } + document.getElementById('btn-cancelar-subir-adjunto').addEventListener('click', cerrar); + document.getElementById('btn-confirmar-subir-adjunto').addEventListener('click', async () => { + const descripcion = document.getElementById('input-descripcion-adjunto').value.trim(); + const btn = document.getElementById('btn-confirmar-subir-adjunto'); + btn.disabled = true; + btn.innerHTML = ' Subiendo...'; + + const formData = new FormData(); + formData.append('archivo', ARCHIVO_PENDIENTE_SUBIR); + formData.append('paciente_id', pacienteId); + formData.append('descripcion', descripcion); + + try { + const respuesta = await fetch(`${API.adjuntos}?accion=subir`, { method: 'POST', body: formData }); + let datos; + try { + datos = await respuesta.json(); + } catch (errorParseo) { + // El servidor devolvió algo que no es JSON (por ejemplo, una + // página de error de PHP/Apache cuando el archivo supera el + // límite de subida configurado en el hosting). + throw new Error( + respuesta.status === 413 || respuesta.status === 0 + ? 'El archivo es demasiado grande para este servidor. Probá con un archivo más chico.' + : 'No se pudo subir el archivo (el servidor devolvió una respuesta inesperada). Probá con un archivo más chico, o avisá a soporte si sigue pasando.' + ); + } + if (!respuesta.ok || !datos.ok) throw new Error(datos.error || 'No se pudo subir el archivo.'); + mostrarToast('Archivo subido correctamente.', 'exito'); + modalEnv.remove(); + ARCHIVO_PENDIENTE_SUBIR = null; + cargarAdjuntosPaciente(pacienteId); + } catch (e) { + mostrarToast(e.message, 'error'); + btn.disabled = false; + btn.textContent = 'Subir archivo'; + } + }); +} + +/* ============================================================ + VISTA: AGENDA (calendario completo) + ============================================================ */ +async function montarVistaAgenda(contenido) { + contenido.appendChild(clonarPlantilla('tpl-agenda')); + MES_AGENDA_ACTUAL = new Date(); + DIA_SELECCIONADO_AGENDA = null; + + document.getElementById('btn-mes-anterior').addEventListener('click', () => { + MES_AGENDA_ACTUAL.setMonth(MES_AGENDA_ACTUAL.getMonth() - 1); + renderizarCalendario(); + }); + document.getElementById('btn-mes-siguiente').addEventListener('click', () => { + MES_AGENDA_ACTUAL.setMonth(MES_AGENDA_ACTUAL.getMonth() + 1); + renderizarCalendario(); + }); + document.getElementById('btn-nueva-cita').addEventListener('click', () => abrirModalNuevaCita()); + + await renderizarCalendario(); +} + +async function renderizarCalendario() { + const titulo = document.getElementById('calendario-mes-titulo'); + const grilla = document.getElementById('calendario-grilla'); + if (!titulo || !grilla) return; + + const año = MES_AGENDA_ACTUAL.getFullYear(); + const mes = MES_AGENDA_ACTUAL.getMonth(); + + titulo.textContent = MES_AGENDA_ACTUAL.toLocaleDateString('es-AR', { month: 'long', year: 'numeric' }); + + const primerDiaMes = new Date(año, mes, 1); + const ultimoDiaMes = new Date(año, mes + 1, 0); + const desde = aFechaIso(primerDiaMes); + const hasta = aFechaIso(ultimoDiaMes); + + let citasDelMes = []; + try { + const res = await llamarApi(`${API.citas}?accion=rango&desde=${desde}&hasta=${hasta}`, { method: 'GET' }); + citasDelMes = res.datos; + } catch (e) { + mostrarToast('No se pudo cargar la agenda del mes.', 'error'); + } + + const citasPorDia = {}; + citasDelMes.forEach(c => { + if (!citasPorDia[c.fecha]) citasPorDia[c.fecha] = []; + citasPorDia[c.fecha].push(c); + }); + + grilla.innerHTML = ''; + const diaSemanaInicio = primerDiaMes.getDay(); // 0 = domingo + for (let i = 0; i < diaSemanaInicio; i++) { + const celdaVacia = document.createElement('div'); + celdaVacia.className = 'celda-calendario vacia'; + grilla.appendChild(celdaVacia); + } + + const hoyIso = aFechaIso(new Date()); + + for (let dia = 1; dia <= ultimoDiaMes.getDate(); dia++) { + const fechaIso = aFechaIso(new Date(año, mes, dia)); + const celda = document.createElement('button'); + celda.type = 'button'; + celda.className = 'celda-calendario'; + if (fechaIso === hoyIso) celda.classList.add('hoy'); + if (fechaIso === DIA_SELECCIONADO_AGENDA) celda.classList.add('seleccionada'); + + const citasDia = citasPorDia[fechaIso] || []; + const pendientesDia = citasDia.filter(c => c.estado === 'pendiente').length; + + celda.innerHTML = ` + ${dia} + ${pendientesDia ? `${pendientesDia}` : ''} + `; + celda.addEventListener('click', () => { + DIA_SELECCIONADO_AGENDA = fechaIso; + renderizarCalendario(); + mostrarCitasDelDia(fechaIso, citasDia); + }); + grilla.appendChild(celda); + } + + if (DIA_SELECCIONADO_AGENDA) { + mostrarCitasDelDia(DIA_SELECCIONADO_AGENDA, citasPorDia[DIA_SELECCIONADO_AGENDA] || []); + } else { + document.getElementById('titulo-citas-dia').textContent = 'Citas — elegí un día en el calendario'; + document.getElementById('lista-citas-dia').innerHTML = ''; + } +} + +function mostrarCitasDelDia(fechaIso, citas) { + document.getElementById('titulo-citas-dia').textContent = `Citas — ${formatearFechaLarga(fechaIso)}`; + const lista = document.getElementById('lista-citas-dia'); + lista.innerHTML = ''; + + if (!citas.length) { + lista.innerHTML = '

No hay citas agendadas este día.

'; + return; + } + + citas.sort((a, b) => (a.hora || '').localeCompare(b.hora || '')); + + citas.forEach(c => { + const fila = document.createElement('div'); + fila.className = 'tarjeta-paciente'; + const estadoEtiquetas = { + pendiente: 'Pendiente', + atendida: 'Atendida', + cancelada: 'Cancelada', + ausente: 'Ausente', + }; + fila.innerHTML = ` +
+
${(c.nombre[0] + c.apellido[0]).toUpperCase()}
+
+
${escaparHtml(c.nombre + ' ' + c.apellido)}
+
${c.hora ? c.hora.slice(0,5) + ' · ' : ''}${escaparHtml(c.motivo || 'Sin motivo')} ${estadoEtiquetas[c.estado] || ''}
+
+
+
+ `; + const acciones = fila.querySelector('.acciones-tarjeta'); + + const btnVerLegajo = document.createElement('button'); + btnVerLegajo.className = 'btn-icono'; + btnVerLegajo.title = 'Ver legajo'; + btnVerLegajo.innerHTML = ``; + btnVerLegajo.addEventListener('click', () => irAVista('detalle', { id: c.paciente_id })); + acciones.appendChild(btnVerLegajo); + + const linkWhatsappDia = generarLinkWhatsapp(c.telefono, c.nombre, c.fecha, c.hora, c.token_confirmacion); + if (linkWhatsappDia) { + const btnWhatsappDia = document.createElement('a'); + btnWhatsappDia.className = 'btn-icono whatsapp'; + btnWhatsappDia.title = 'Enviar recordatorio por WhatsApp'; + btnWhatsappDia.href = linkWhatsappDia; + btnWhatsappDia.target = '_blank'; + btnWhatsappDia.innerHTML = ``; + acciones.appendChild(btnWhatsappDia); + } + + if (c.estado === 'pendiente') { + const btnAtendida = document.createElement('button'); + btnAtendida.className = 'btn-icono'; + btnAtendida.title = 'Marcar como atendida'; + btnAtendida.innerHTML = ``; + btnAtendida.addEventListener('click', async () => { + await cambiarEstadoCita(c.id, 'atendida'); + renderizarCalendario(); + }); + acciones.appendChild(btnAtendida); + + const btnAusente = document.createElement('button'); + btnAusente.className = 'btn-icono'; + btnAusente.title = 'Marcar como ausente'; + btnAusente.innerHTML = ``; + btnAusente.addEventListener('click', async () => { + await cambiarEstadoCita(c.id, 'ausente'); + renderizarCalendario(); + }); + acciones.appendChild(btnAusente); + + const btnCancelar = document.createElement('button'); + btnCancelar.className = 'btn-icono peligro'; + btnCancelar.title = 'Cancelar cita'; + btnCancelar.innerHTML = ``; + btnCancelar.addEventListener('click', async () => { + await cambiarEstadoCita(c.id, 'cancelada'); + renderizarCalendario(); + }); + acciones.appendChild(btnCancelar); + } + + lista.appendChild(fila); + }); +} + +/* ============================================================ + VISTA: DASHBOARD DE ESTADÍSTICAS (solo profesional) + ============================================================ */ +async function montarVistaDashboard(contenido) { + contenido.appendChild(clonarPlantilla('tpl-dashboard')); + + document.getElementById('btn-exportar-todo').addEventListener('click', () => { + descargarArchivoDesdeUrl(`${API.pacientes}?accion=exportar_todo`); + }); + + const cont = document.getElementById('contenido-dashboard'); + try { + const res = await llamarApi(`${API.admin}?accion=estadisticas`, { method: 'GET' }); + renderizarDashboard(cont, res.datos); + } catch (e) { + cont.innerHTML = ''; + mostrarToast(e.message, 'error'); + } +} + +function renderizarDashboard(cont, d) { + cont.innerHTML = ''; + + // Fila de tarjetas numéricas + const filaStats = document.createElement('div'); + filaStats.className = 'grilla-stats'; + + const diferenciaSesiones = d.sesiones_este_mes - d.sesiones_mes_anterior; + const tendenciaTexto = diferenciaSesiones === 0 + ? 'Igual que el mes anterior' + : diferenciaSesiones > 0 + ? `+${diferenciaSesiones} respecto al mes anterior` + : `${diferenciaSesiones} respecto al mes anterior`; + + const stats = [ + { valor: d.total_pacientes, etiqueta: 'Pacientes totales', detalle: `+${d.pacientes_nuevos_mes} este mes` }, + { valor: d.sesiones_este_mes, etiqueta: 'Sesiones este mes', detalle: tendenciaTexto }, + ]; + + const estadosCita = { pendiente: 'Pendientes', atendida: 'Atendidas', cancelada: 'Canceladas', ausente: 'Ausencias' }; + d.citas_por_estado_mes.forEach(c => { + stats.push({ valor: c.total, etiqueta: estadosCita[c.estado] || c.estado, detalle: 'este mes' }); + }); + + stats.forEach(s => { + const tarjeta = clonarPlantilla('tpl-tarjeta-stat').firstElementChild; + tarjeta.querySelector('.tarjeta-stat-valor').textContent = s.valor; + tarjeta.querySelector('.tarjeta-stat-etiqueta').textContent = s.etiqueta; + tarjeta.querySelector('.tarjeta-stat-detalle').textContent = s.detalle; + filaStats.appendChild(tarjeta); + }); + + cont.appendChild(filaStats); + + // Panel de obras sociales + const panelObras = document.createElement('div'); + panelObras.className = 'panel'; + panelObras.innerHTML = ` +
Pacientes por obra social
+

Distribución de tu cartera de pacientes.

+
+ `; + cont.appendChild(panelObras); + + const contBarras = panelObras.querySelector('#barras-obras-sociales'); + const maximoObra = Math.max(...d.por_obra_social.map(o => o.total), 1); + d.por_obra_social.forEach(o => { + const fila = document.createElement('div'); + fila.className = 'fila-barra-stat'; + const porcentaje = Math.round((o.total / maximoObra) * 100); + fila.innerHTML = ` + ${escaparHtml(o.nombre)} +
+ ${o.total} + `; + contBarras.appendChild(fila); + }); + + // Panel de sesiones últimos 6 meses + const panelMeses = document.createElement('div'); + panelMeses.className = 'panel'; + panelMeses.innerHTML = ` +
Sesiones de los últimos 6 meses
+

Volumen de atención mes a mes.

+
+ `; + cont.appendChild(panelMeses); + + const contMeses = panelMeses.querySelector('#barras-meses'); + const maximoMes = Math.max(...d.sesiones_ultimos_6_meses.map(m => m.total), 1); + const nombresMeses = ['ene','feb','mar','abr','may','jun','jul','ago','sep','oct','nov','dic']; + d.sesiones_ultimos_6_meses.forEach(m => { + const [anio, mes] = m.mes.split('-'); + const alturaPx = Math.max(8, Math.round((m.total / maximoMes) * 120)); + const columna = document.createElement('div'); + columna.className = 'columna-mes'; + columna.innerHTML = ` +
${m.total}
+
+
${nombresMeses[parseInt(mes, 10) - 1]}
+ `; + contMeses.appendChild(columna); + }); +} + +/* ============================================================ + VISTA: CONFIGURACIÓN (usuarios + historial de cambios) + ============================================================ */ +async function montarVistaConfiguracion(contenido) { + contenido.appendChild(clonarPlantilla('tpl-configuracion')); + + // El Desarrollador ya está en su única pantalla: no tiene + // a dónde "volver al panel", así que ese botón no aplica. + if (ROL_ACTUAL === 'desarrollador') { + const btnVolver = contenido.querySelector('[data-volver]'); + if (btnVolver) btnVolver.remove(); + } + + const tabs = contenido.querySelectorAll('[data-tab-config]'); + const paneles = { sedes: 'panel-config-sedes', usuarios: 'panel-config-usuarios', historial: 'panel-config-historial', version: 'panel-config-version', reportes: 'panel-config-reportes', papelera: 'panel-config-papelera', huerfanos: 'panel-config-huerfanos' }; + tabs.forEach(tab => { + tab.addEventListener('click', () => { + tabs.forEach(t => t.classList.remove('activo')); + tab.classList.add('activo'); + const destino = tab.dataset.tabConfig; + Object.entries(paneles).forEach(([clave, idPanel]) => { + document.getElementById(idPanel).classList.toggle('oculto', clave !== destino); + }); + if (destino === 'historial') cargarHistorialCambios(1); + if (destino === 'usuarios') cargarListaUsuarios(); + if (destino === 'version') cargarVerificacionVersion(); + if (destino === 'reportes') cargarReportesSede(); + if (destino === 'papelera') inicializarPapeleraDev(); + if (destino === 'huerfanos') inicializarLegajosHuerfanos(); + }); + }); + + document.getElementById('btn-revisar-version').addEventListener('click', cargarVerificacionVersion); + document.getElementById('btn-agregar-sede').addEventListener('click', abrirModalNuevaSede); + document.getElementById('btn-agregar-usuario').addEventListener('click', abrirModalNuevoUsuario); + + await cargarListaSedes(); +} + +let SEDE_PAPELERA_DEV_ID = null; + +async function inicializarPapeleraDev() { + const selectSede = document.getElementById('select-sede-papelera-dev'); + const selectProf = document.getElementById('select-profesional-papelera-dev'); + const lista = document.getElementById('lista-papelera-dev'); + + // Evitamos volver a pedir las sedes si ya se cargaron antes en esta visita al panel. + if (!selectSede.dataset.cargado) { + try { + const res = await llamarApi(`${API.auth}?accion=listar_sedes`, { method: 'POST', body: JSON.stringify({ accion: 'listar_sedes' }) }); + const sedesActivas = res.datos.filter(s => s.activa); + sedesActivas.forEach(s => { + const opt = document.createElement('option'); + opt.value = s.id; + opt.textContent = s.nombre; + selectSede.appendChild(opt); + }); + selectSede.dataset.cargado = '1'; + } catch (e) { + mostrarToast(e.message, 'error'); + } + } + + selectSede.onchange = async () => { + SEDE_PAPELERA_DEV_ID = selectSede.value || null; + selectProf.innerHTML = ''; + selectProf.disabled = true; + lista.innerHTML = ''; + + if (!SEDE_PAPELERA_DEV_ID) { + selectProf.innerHTML = ''; + return; + } + + try { + const res = await llamarApi(`${API.auth}?accion=listar_profesionales_sede_dev`, { + method: 'POST', + body: JSON.stringify({ accion: 'listar_profesionales_sede_dev', sede_id: SEDE_PAPELERA_DEV_ID }), + }); + selectProf.innerHTML = ''; + res.datos.forEach(p => { + const opt = document.createElement('option'); + opt.value = p.id; + opt.textContent = p.nombre_completo; + selectProf.appendChild(opt); + }); + selectProf.disabled = false; + } catch (e) { + mostrarToast(e.message, 'error'); + } + }; + + selectProf.onchange = () => { + if (selectProf.value) cargarPapeleraDevDeProfesional(selectProf.value); + else lista.innerHTML = ''; + }; +} + +async function cargarPapeleraDevDeProfesional(profesionalId) { + const lista = document.getElementById('lista-papelera-dev'); + lista.innerHTML = '
'; + try { + const res = await llamarApi(`${API.auth}?accion=listar_papelera_dev`, { + method: 'POST', + body: JSON.stringify({ accion: 'listar_papelera_dev', profesional_id: profesionalId, sede_id: SEDE_PAPELERA_DEV_ID }), + }); + lista.innerHTML = ''; + if (!res.datos.length) { + lista.innerHTML = '

Este profesional no tiene legajos eliminados en esta sede.

'; + return; + } + res.datos.forEach(reg => lista.appendChild(crearFilaPapeleraDev(reg))); + } catch (e) { + lista.innerHTML = ''; + mostrarToast(e.message, 'error'); + } +} + +function crearFilaPapeleraDev(reg) { + const fila = document.createElement('div'); + fila.className = 'tarjeta-paciente'; + const fecha = new Date(reg.eliminado_en).toLocaleDateString('es-AR'); + fila.innerHTML = ` +
+
+ +
+
+
${escaparHtml(reg.nombre_completo)}
+
DNI ${escaparHtml(reg.dni)} · Eliminado el ${fecha}
+
+
+
+ `; + const acciones = fila.querySelector('.acciones-tarjeta'); + const btnRecuperar = document.createElement('button'); + btnRecuperar.className = 'btn btn-secundario btn-chico'; + btnRecuperar.textContent = 'Recuperar'; + btnRecuperar.addEventListener('click', () => abrirModalRecuperarLegajo(reg)); + acciones.appendChild(btnRecuperar); + return fila; +} + +async function abrirModalRecuperarLegajo(reg) { + const modalEnv = clonarPlantilla('tpl-modal-recuperar-legajo'); + document.body.appendChild(modalEnv); + document.getElementById('texto-paciente-recuperar').textContent = + `${reg.nombre_completo} (DNI ${reg.dni}) va a volver a aparecer como legajo activo, a nombre del profesional que elijas.`; + + const select = document.getElementById('select-nuevo-profesional-recuperar'); + select.innerHTML = ''; + try { + const res = await llamarApi(`${API.auth}?accion=listar_profesionales_sede_dev`, { + method: 'POST', + body: JSON.stringify({ accion: 'listar_profesionales_sede_dev', sede_id: reg.sede_id_original }), + }); + select.innerHTML = ''; + if (!res.datos.length) { + select.innerHTML = ''; + } else { + res.datos.forEach(p => { + const opt = document.createElement('option'); + opt.value = p.id; + opt.textContent = p.nombre_completo; + select.appendChild(opt); + }); + } + } catch (e) { + select.innerHTML = ''; + mostrarToast(e.message, 'error'); + } + + function cerrar() { modalEnv.remove(); } + document.getElementById('btn-cancelar-recuperar-legajo').addEventListener('click', cerrar); + document.getElementById('btn-confirmar-recuperar-legajo').addEventListener('click', async () => { + const nuevoProfesionalId = select.value; + if (!nuevoProfesionalId) { mostrarToast('Elegí a qué profesional asignarlo.', 'error'); return; } + + const btn = document.getElementById('btn-confirmar-recuperar-legajo'); + btn.disabled = true; + btn.innerHTML = ' Recuperando...'; + try { + await llamarApi(API.auth, { + method: 'POST', + body: JSON.stringify({ accion: 'recuperar_legajo_dev', id: reg.id, profesional_id: nuevoProfesionalId }), + }); + mostrarToast('Legajo recuperado correctamente.', 'exito'); + cerrar(); + const selectProf = document.getElementById('select-profesional-papelera-dev'); + if (selectProf.value) cargarPapeleraDevDeProfesional(selectProf.value); + } catch (e) { + mostrarToast(e.message, 'error'); + btn.disabled = false; + btn.textContent = 'Recuperar legajo'; + } + }); +} + +async function inicializarLegajosHuerfanos() { + const select = document.getElementById('select-profesional-huerfano'); + const lista = document.getElementById('lista-huerfanos-dev'); + lista.innerHTML = ''; + + select.innerHTML = ''; + try { + const res = await llamarApi(`${API.auth}?accion=listar_profesionales_desactivados`, { + method: 'POST', + body: JSON.stringify({ accion: 'listar_profesionales_desactivados' }), + }); + select.innerHTML = ''; + if (!res.datos.length) { + select.innerHTML = ''; + } else { + res.datos.forEach(p => { + const opt = document.createElement('option'); + opt.value = p.id; + opt.textContent = `${p.nombre_completo} (${p.total_pacientes} paciente${p.total_pacientes === 1 ? '' : 's'})`; + select.appendChild(opt); + }); + } + } catch (e) { + select.innerHTML = ''; + mostrarToast(e.message, 'error'); + } + + select.onchange = () => { + if (select.value) cargarLegajosHuerfanosDeProfesional(select.value); + else lista.innerHTML = ''; + }; +} + +async function cargarLegajosHuerfanosDeProfesional(profesionalId) { + const lista = document.getElementById('lista-huerfanos-dev'); + lista.innerHTML = '
'; + try { + const res = await llamarApi(`${API.auth}?accion=listar_legajos_huerfanos`, { + method: 'POST', + body: JSON.stringify({ accion: 'listar_legajos_huerfanos', profesional_id: profesionalId }), + }); + lista.innerHTML = ''; + if (!res.datos.length) { + lista.innerHTML = '

Este profesional no tiene pacientes activos.

'; + return; + } + res.datos.forEach(p => lista.appendChild(crearFilaLegajoHuerfano(p))); + } catch (e) { + lista.innerHTML = ''; + mostrarToast(e.message, 'error'); + } +} + +function crearFilaLegajoHuerfano(p) { + const fila = document.createElement('div'); + fila.className = 'tarjeta-paciente'; + fila.innerHTML = ` +
+
${(p.nombre[0] + p.apellido[0]).toUpperCase()}
+
+
${escaparHtml(p.nombre)} ${escaparHtml(p.apellido)}
+
DNI ${escaparHtml(p.dni)} · ${escaparHtml(p.sede_nombre || 'Sin sede')}
+
+
+
+ `; + const acciones = fila.querySelector('.acciones-tarjeta'); + const btnTransferir = document.createElement('button'); + btnTransferir.className = 'btn btn-secundario btn-chico'; + btnTransferir.textContent = 'Transferir'; + btnTransferir.addEventListener('click', () => abrirModalTransferirHuerfano(p)); + acciones.appendChild(btnTransferir); + return fila; +} + +async function abrirModalTransferirHuerfano(p) { + const modalEnv = clonarPlantilla('tpl-modal-transferir-huerfano'); + document.body.appendChild(modalEnv); + document.getElementById('texto-paciente-transferir').textContent = + `${p.nombre} ${p.apellido} (DNI ${p.dni}) va a pasar a estar a cargo del profesional que elijas.`; + + const select = document.getElementById('select-nuevo-profesional-transferir'); + select.innerHTML = ''; + try { + const res = await llamarApi(`${API.auth}?accion=listar_profesionales_sede_dev`, { + method: 'POST', + body: JSON.stringify({ accion: 'listar_profesionales_sede_dev', sede_id: p.sede_id }), + }); + select.innerHTML = ''; + if (!res.datos.length) { + select.innerHTML = ''; + } else { + res.datos.forEach(prof => { + const opt = document.createElement('option'); + opt.value = prof.id; + opt.textContent = prof.nombre_completo; + select.appendChild(opt); + }); + } + } catch (e) { + select.innerHTML = ''; + mostrarToast(e.message, 'error'); + } + + function cerrar() { modalEnv.remove(); } + document.getElementById('btn-cancelar-transferir-huerfano').addEventListener('click', cerrar); + document.getElementById('btn-confirmar-transferir-huerfano').addEventListener('click', async () => { + const nuevoProfesionalId = select.value; + if (!nuevoProfesionalId) { mostrarToast('Elegí a qué profesional asignarlo.', 'error'); return; } + + const btn = document.getElementById('btn-confirmar-transferir-huerfano'); + btn.disabled = true; + btn.innerHTML = ' Transfiriendo...'; + try { + await llamarApi(API.auth, { + method: 'POST', + body: JSON.stringify({ accion: 'reasignar_legajo_huerfano', paciente_id: p.id, profesional_id: nuevoProfesionalId }), + }); + mostrarToast('Legajo transferido correctamente.', 'exito'); + cerrar(); + const selectProf = document.getElementById('select-profesional-huerfano'); + if (selectProf.value) cargarLegajosHuerfanosDeProfesional(selectProf.value); + } catch (e) { + mostrarToast(e.message, 'error'); + btn.disabled = false; + btn.textContent = 'Transferir legajo'; + } + }); +} + +async function cargarReportesSede() { + const cont = document.getElementById('lista-reportes-sede'); + cont.innerHTML = '
'; + try { + const res = await llamarApi(`${API.admin}?accion=reporte_sedes`, { method: 'GET' }); + cont.innerHTML = ''; + if (!res.datos.length) { + cont.innerHTML = '

Todavía no hay sedes activas para reportar.

'; + return; + } + res.datos.forEach(s => cont.appendChild(crearTarjetaReporteSede(s))); + } catch (e) { + cont.innerHTML = ''; + mostrarToast(e.message, 'error'); + } +} + +function crearTarjetaReporteSede(s) { + const tarjeta = document.createElement('div'); + tarjeta.className = 'tarjeta-reporte-sede'; + + const etiquetasEstado = { pendiente: 'Pendientes', atendida: 'Atendidas', cancelada: 'Canceladas', ausente: 'Ausentes' }; + const citasTexto = (s.citas_por_estado_mes || []).length + ? s.citas_por_estado_mes.map(c => `${etiquetasEstado[c.estado] || c.estado}: ${c.total}`).join(' · ') + : 'Sin citas registradas este mes'; + + tarjeta.innerHTML = ` +
+ + ${escaparHtml(s.nombre)} +
+
+
${s.profesionales}Profesional${s.profesionales === 1 ? '' : 'es'}
+
${s.administrativas}Administrativa${s.administrativas === 1 ? '' : 's'}
+
${s.pacientes}Paciente${s.pacientes === 1 ? '' : 's'} en total
+
${s.sesiones_mes}Sesiones este mes
+
+
${escaparHtml(citasTexto)}
+ `; + return tarjeta; +} + +async function cargarVerificacionVersion() { + const resumen = document.getElementById('resumen-version'); + const lista = document.getElementById('lista-archivos-version'); + resumen.innerHTML = '
'; + lista.innerHTML = ''; + + try { + const res = await llamarApi(`${API.admin}?accion=verificar_version`, { method: 'GET' }); + + if (res.sin_version_json) { + resumen.innerHTML = `

No se encontró el archivo version.json en el servidor. Subilo junto con la próxima actualización para poder usar esta verificación.

`; + return; + } + + if (res.hay_desactualizados) { + resumen.innerHTML = ` +
+ ⚠ Hay archivos desactualizados. Versión esperada: ${escaparHtml(res.version)}${res.fecha ? ' (' + escaparHtml(res.fecha) + ')' : ''}. + ${res.descripcion ? `
${escaparHtml(res.descripcion)}
` : ''} +
`; + } else { + resumen.innerHTML = ` +
+ ✅ Todo está actualizado a la versión ${escaparHtml(res.version)}${res.fecha ? ' (' + escaparHtml(res.fecha) + ')' : ''}. + ${res.descripcion ? `
${escaparHtml(res.descripcion)}
` : ''} +
`; + } + + lista.innerHTML = ''; + res.archivos.forEach(a => { + const fila = document.createElement('div'); + fila.className = 'fila-archivo-version'; + let etiqueta, clase; + if (a.estado === 'actualizado') { + etiqueta = '✅ Actualizado'; clase = 'ok'; + } else if (a.estado === 'falta') { + etiqueta = '❌ Falta en el servidor'; clase = 'mal'; + } else { + etiqueta = '⚠ Versión vieja'; clase = 'mal'; + } + fila.innerHTML = ` + ${escaparHtml(a.archivo)} + ${etiqueta} + `; + lista.appendChild(fila); + }); + } catch (e) { + resumen.innerHTML = ''; + mostrarToast(e.message, 'error'); + } +} + +async function cargarListaSedes() { + const cont = document.getElementById('lista-sedes-config'); + cont.innerHTML = '
'; + try { + const res = await llamarApi(`${API.auth}?accion=listar_sedes`, { method: 'POST', body: JSON.stringify({ accion: 'listar_sedes' }) }); + cont.innerHTML = ''; + if (!res.datos.length) { + cont.innerHTML = '

Todavía no creaste ninguna sede.

'; + return; + } + res.datos.forEach(s => cont.appendChild(crearFilaSede(s))); + } catch (e) { + cont.innerHTML = ''; + mostrarToast(e.message, 'error'); + } +} + +function crearFilaSede(s) { + const fila = document.createElement('div'); + fila.className = 'tarjeta-paciente'; + const fecha = new Date(s.creado_en).toLocaleDateString('es-AR'); + fila.innerHTML = ` +
+
+ +
+
+
${escaparHtml(s.nombre)} ${s.activa ? '' : 'Desactivada'}
+
Desde ${fecha}
+
+
+
+ `; + if (s.activa) { + const acciones = fila.querySelector('.acciones-tarjeta'); + const btnDesactivar = document.createElement('button'); + btnDesactivar.className = 'btn-icono peligro'; + btnDesactivar.title = 'Desactivar sede'; + btnDesactivar.innerHTML = ``; + btnDesactivar.addEventListener('click', async () => { + if (!confirm(`¿Desactivar la sede "${s.nombre}"? Ya no va a aparecer en el login.`)) return; + try { + await llamarApi(API.auth, { method: 'POST', body: JSON.stringify({ accion: 'desactivar_sede', id: s.id }) }); + mostrarToast('Sede desactivada.', 'exito'); + cargarListaSedes(); + } catch (e) { + mostrarToast(e.message, 'error'); + } + }); + acciones.appendChild(btnDesactivar); + } + return fila; +} + +function abrirModalNuevaSede() { + const modalEnv = clonarPlantilla('tpl-modal-nueva-sede'); + document.body.appendChild(modalEnv); + const input = document.getElementById('input-nombre-nueva-sede'); + input.focus(); + + function cerrar() { modalEnv.remove(); } + document.getElementById('btn-cancelar-nueva-sede').addEventListener('click', cerrar); + document.getElementById('btn-confirmar-nueva-sede').addEventListener('click', async () => { + const nombre = input.value.trim(); + if (!nombre) { mostrarToast('Escribí el nombre de la sede.', 'error'); return; } + try { + await llamarApi(API.auth, { method: 'POST', body: JSON.stringify({ accion: 'crear_sede', nombre }) }); + mostrarToast('Sede creada.', 'exito'); + cerrar(); + cargarListaSedes(); + } catch (e) { + mostrarToast(e.message, 'error'); + } + }); + input.addEventListener('keydown', (ev) => { if (ev.key === 'Enter') document.getElementById('btn-confirmar-nueva-sede').click(); }); +} + +async function cargarListaUsuarios() { + const cont = document.getElementById('lista-usuarios'); + cont.innerHTML = '
'; + try { + const res = await llamarApi(`${API.auth}?accion=listar_usuarios`, { + method: 'POST', + body: JSON.stringify({ accion: 'listar_usuarios' }), + }); + cont.innerHTML = ''; + if (!res.datos.length) { + cont.innerHTML = '

Todavía no creaste ningún usuario.

'; + return; + } + res.datos.forEach(u => cont.appendChild(crearFilaUsuario(u))); + } catch (e) { + cont.innerHTML = ''; + mostrarToast(e.message, 'error'); + } +} + +function crearFilaUsuario(u) { + const fila = document.createElement('div'); + fila.className = 'tarjeta-paciente'; + const rolEtiqueta = u.rol === 'profesional' + ? 'Profesional' + : 'Administrativa'; + const fecha = new Date(u.creado_en).toLocaleDateString('es-AR'); + const nombresSedes = (u.sedes || []).map(s => s.nombre).join(', ') || 'Sin sede asignada'; + + let licenciaTexto = ''; + if (u.rol === 'profesional') { + const colorEstado = { activo: 'var(--salvia-oscuro)', suspendido: 'var(--coral)', pausado: 'var(--arena)', prohibido: 'var(--coral)' }; + const estado = u.estado_licencia || 'activo'; + const color = colorEstado[estado] || 'var(--salvia-oscuro)'; + let diasInfo = ''; + if (estado === 'activo' && u.licencia_vencimiento) { + diasInfo = ` · Vence ${formatearFechaCorta(u.licencia_vencimiento)}`; + } else if (estado === 'activo' && !u.licencia_dias) { + diasInfo = ' · Indeterminada'; + } + licenciaTexto = ` · ${estado}${diasInfo}`; + } + + fila.innerHTML = ` +
+
${u.nombre_completo.split(' ').map(p => p[0]).slice(0,2).join('').toUpperCase()}
+
+
${escaparHtml(u.nombre_completo)}${u.especialidad ? ' ' : ''}
+
${rolEtiqueta} · ${escaparHtml(nombresSedes)}${licenciaTexto} · Desde ${fecha}
+
+
+
+ `; + if (u.activo && u.id !== undefined) { + const acciones = fila.querySelector('.acciones-tarjeta'); + + if (u.rol === 'profesional') { + const btnLicencia = document.createElement('button'); + btnLicencia.className = 'btn-icono'; + btnLicencia.title = 'Gestionar licencia'; + btnLicencia.innerHTML = ``; + btnLicencia.addEventListener('click', () => abrirModalGestionarLicencia(u)); + acciones.appendChild(btnLicencia); + } + + const btnGestionarSedes = document.createElement('button'); + btnGestionarSedes.className = 'btn-icono'; + btnGestionarSedes.title = 'Gestionar sedes'; + btnGestionarSedes.innerHTML = ``; + btnGestionarSedes.addEventListener('click', () => abrirModalGestionarSedesUsuario(u)); + acciones.appendChild(btnGestionarSedes); + + const btnDesactivar = document.createElement('button'); + btnDesactivar.className = 'btn-icono peligro'; + btnDesactivar.title = 'Quitar acceso'; + btnDesactivar.innerHTML = ``; + btnDesactivar.addEventListener('click', async () => { + if (!confirm(`¿Quitar el acceso de ${u.nombre_completo}? Va a dejar de poder entrar al sistema.`)) return; + try { + await llamarApi(`${API.auth}`, { + method: 'POST', + body: JSON.stringify({ accion: 'desactivar_usuario', id: u.id }), + }); + mostrarToast('Acceso desactivado.', 'exito'); + cargarListaUsuarios(); + } catch (e) { + mostrarToast(e.message, 'error'); + } + }); + acciones.appendChild(btnDesactivar); + } + return fila; +} + +async function abrirModalNuevoUsuario() { + // Preguntamos qué tipo de persona se quiere agregar, + // para abrir el formulario correcto. + const tipo = await new Promise(resolve => { + const div = document.createElement('div'); + div.className = 'overlay-modal'; + div.innerHTML = ` + `; + document.body.appendChild(div); + div.querySelector('#elegir-profesional').onclick = () => { div.remove(); resolve('profesional'); }; + div.querySelector('#elegir-administrativa').onclick = () => { div.remove(); resolve('administrativa'); }; + div.querySelector('#elegir-cancelar').onclick = () => { div.remove(); resolve(null); }; + }); + + if (!tipo) return; + if (tipo === 'profesional') abrirModalNuevoProfesional(); + else abrirModalNuevaAdministrativa(); +} + +async function cargarSedesEnCheckboxes(contenedorId, claseCheckbox) { + const cont = document.getElementById(contenedorId); + if (!cont) return; + cont.innerHTML = '
'; + try { + const res = await llamarApi(`${API.auth}?accion=listar_sedes`, { method: 'POST', body: JSON.stringify({ accion: 'listar_sedes' }) }); + const sedesActivas = res.datos.filter(s => s.activa); + cont.innerHTML = ''; + if (!sedesActivas.length) { + cont.innerHTML = ''; + } else { + sedesActivas.forEach(s => { + const label = document.createElement('label'); + label.className = 'checkbox-sede-item'; + label.innerHTML = ` ${escaparHtml(s.nombre)}`; + cont.appendChild(label); + }); + } + } catch (e) { + cont.innerHTML = ''; + mostrarToast(e.message, 'error'); + } +} + +async function abrirModalNuevoProfesional() { + const modalEnv = clonarPlantilla('tpl-modal-nuevo-profesional'); + document.body.appendChild(modalEnv); + await cargarSedesEnCheckboxes('lista-checkboxes-sedes-prof', 'checkbox-sede-prof'); + + const inputPin = document.getElementById('input-pin-prof'); + inputPin.addEventListener('input', () => { inputPin.value = inputPin.value.replace(/\D/g, '').slice(0, 4); }); + + function cerrar() { modalEnv.remove(); } + document.getElementById('btn-cancelar-nuevo-profesional').addEventListener('click', cerrar); + + document.getElementById('btn-confirmar-nuevo-profesional').addEventListener('click', async () => { + const titulo = document.getElementById('input-titulo-prof').value; + const nombre = document.getElementById('input-nombre-prof').value.trim(); + const apellido = document.getElementById('input-apellido-prof').value.trim(); + const dni = document.getElementById('input-dni-prof').value.trim(); + const fechaNac = document.getElementById('input-fechanac-prof').value; + const lugarNac = document.getElementById('input-lugarnac-prof').value.trim(); + const especialidad = document.getElementById('input-especialidad-prof').value.trim(); + const email = document.getElementById('input-email-prof').value.trim(); + const tel = document.getElementById('input-tel-prof').value.trim(); + const pin = inputPin.value.trim(); + const licenciaDias = document.getElementById('input-licencia-dias-prof').value; + const sedeIds = Array.from(document.querySelectorAll('.checkbox-sede-prof:checked')).map(c => parseInt(c.value, 10)); + + if (!nombre || !apellido) { mostrarToast('Ingresá el nombre y apellido del profesional.', 'error'); return; } + if (!/^\d{4}$/.test(pin)) { mostrarToast('El PIN tiene que tener exactamente 4 números.', 'error'); return; } + if (!sedeIds.length) { mostrarToast('Elegí al menos una sede.', 'error'); return; } + + const btn = document.getElementById('btn-confirmar-nuevo-profesional'); + btn.disabled = true; + btn.innerHTML = ' Creando...'; + try { + await llamarApi(API.auth, { + method: 'POST', + body: JSON.stringify({ + accion: 'crear_usuario', rol: 'profesional', titulo, nombre, apellido, + dni, fecha_nacimiento: fechaNac, lugar_nacimiento: lugarNac, especialidad, + email, telefono: tel, pin, sede_ids: sedeIds, licencia_dias: licenciaDias, + }), + }); + mostrarToast('Legajo creado correctamente.', 'exito'); + cerrar(); + cargarListaUsuarios(); + } catch (e) { + mostrarToast(e.message, 'error'); + btn.disabled = false; + btn.textContent = 'Crear legajo'; + } + }); +} + +async function abrirModalNuevaAdministrativa() { + const modalEnv = clonarPlantilla('tpl-modal-nueva-administrativa'); + document.body.appendChild(modalEnv); + await cargarSedesEnCheckboxes('lista-checkboxes-sedes-admin', 'checkbox-sede-admin'); + + const inputPin = document.getElementById('input-pin-nueva-admin'); + inputPin.addEventListener('input', () => { inputPin.value = inputPin.value.replace(/\D/g, '').slice(0, 4); }); + + function cerrar() { modalEnv.remove(); } + document.getElementById('btn-cancelar-nueva-admin').addEventListener('click', cerrar); + + document.getElementById('btn-confirmar-nueva-admin').addEventListener('click', async () => { + const nombre = document.getElementById('input-nombre-nueva-admin').value.trim(); + const pin = inputPin.value.trim(); + const sedeIds = Array.from(document.querySelectorAll('.checkbox-sede-admin:checked')).map(c => parseInt(c.value, 10)); + + if (!nombre) { mostrarToast('Ingresá el nombre de la administrativa.', 'error'); return; } + if (!/^\d{4}$/.test(pin)) { mostrarToast('El PIN tiene que tener exactamente 4 números.', 'error'); return; } + if (!sedeIds.length) { mostrarToast('Elegí al menos una sede.', 'error'); return; } + + const btn = document.getElementById('btn-confirmar-nueva-admin'); + btn.disabled = true; + btn.innerHTML = ' Creando...'; + try { + await llamarApi(API.auth, { + method: 'POST', + body: JSON.stringify({ accion: 'crear_usuario', rol: 'administrativa', nombre_completo: nombre, pin, sede_ids: sedeIds }), + }); + mostrarToast('Acceso creado correctamente.', 'exito'); + cerrar(); + cargarListaUsuarios(); + } catch (e) { + mostrarToast(e.message, 'error'); + btn.disabled = false; + btn.textContent = 'Crear acceso'; + } + }); +} + +function abrirModalGestionarLicencia(usuario) { + const modalEnv = clonarPlantilla('tpl-modal-licencia'); + document.body.appendChild(modalEnv); + + document.getElementById('texto-prof-licencia').textContent = + `${usuario.nombre_completo} — estado actual: ${usuario.estado_licencia || 'activo'}.`; + + const selectEstado = document.getElementById('select-estado-licencia'); + const selectDias = document.getElementById('select-dias-licencia'); + const campoDias = document.getElementById('campo-dias-licencia'); + + selectEstado.value = usuario.estado_licencia || 'activo'; + if (usuario.licencia_dias) selectDias.value = String(usuario.licencia_dias); + + selectEstado.addEventListener('change', () => { + campoDias.style.opacity = selectEstado.value === 'activo' ? '1' : '0.4'; + campoDias.style.pointerEvents = selectEstado.value === 'activo' ? 'auto' : 'none'; + }); + if (selectEstado.value !== 'activo') { + campoDias.style.opacity = '0.4'; + campoDias.style.pointerEvents = 'none'; + } + + function cerrar() { modalEnv.remove(); } + document.getElementById('btn-cancelar-licencia').addEventListener('click', cerrar); + document.getElementById('btn-confirmar-licencia').addEventListener('click', async () => { + const estado = selectEstado.value; + const dias = selectDias.value; + const btn = document.getElementById('btn-confirmar-licencia'); + btn.disabled = true; + btn.innerHTML = ' Guardando...'; + try { + await llamarApi(API.auth, { + method: 'POST', + body: JSON.stringify({ accion: 'actualizar_licencia', usuario_id: usuario.id, estado, dias }), + }); + mostrarToast('Licencia actualizada.', 'exito'); + cerrar(); + cargarListaUsuarios(); + } catch (e) { + mostrarToast(e.message, 'error'); + btn.disabled = false; + btn.textContent = 'Guardar'; + } + }); +} + +async function montarVistaMiLegajo(contenido) { + contenido.appendChild(clonarPlantilla('tpl-mi-legajo')); + const cont = document.getElementById('contenido-mi-legajo'); + try { + const res = await llamarApi(`${API.auth}?accion=obtener_legajo_profesional`, { + method: 'POST', body: JSON.stringify({ accion: 'obtener_legajo_profesional' }), + }); + const l = res.datos; + const venc = l.licencia_vencimiento + ? `Vence el ${formatearFechaCorta(l.licencia_vencimiento)}` + : 'Sin vencimiento (indeterminada)'; + const estadoColor = { activo: 'var(--salvia-oscuro)', suspendido: 'var(--coral)', pausado: 'var(--arena)', prohibido: 'var(--coral)' }; + cont.innerHTML = ` +
+
+
+
${escaparHtml(l.titulo + ' ' + l.nombre + ' ' + l.apellido)}
+ ${l.especialidad ? `

${escaparHtml(l.especialidad)}

` : ''} +
+ ${l.estado_licencia || 'activo'} +
+
+
DNI
${escaparHtml(l.dni || '—')}
+
Fecha de nacimiento
${l.fecha_nacimiento ? formatearFechaCorta(l.fecha_nacimiento) : '—'}
+
Lugar de nacimiento
${escaparHtml(l.lugar_nacimiento || '—')}
+
Correo
${escaparHtml(l.email || '—')}
+
Celular
${escaparHtml(l.telefono || '—')}
+
Licencia
${venc}
+
+

Si necesitás corregir algún dato, comunicate con el administrador del sistema.

+
`; + } catch (e) { + cont.innerHTML = '

No se encontró tu legajo. Comunicate con el administrador del sistema.

'; + } +} + +async function abrirModalGestionarSedesUsuario(usuario) { + const modalEnv = clonarPlantilla('tpl-modal-gestionar-sedes-usuario'); + document.body.appendChild(modalEnv); + + document.getElementById('texto-nombre-usuario-gestionar-sedes').textContent = + `Marcá o destildá en qué sedes atiende ${usuario.nombre_completo}.`; + + const sedeIdsActuales = (usuario.sedes || []).map(s => s.id); + const contCheckboxes = document.getElementById('lista-checkboxes-sedes-editar'); + const avisoBorrado = document.getElementById('aviso-borrado-sedes'); + const bloqueConfirmar = document.getElementById('bloque-confirmar-borrado-sedes'); + const inputConfirmarTexto = document.getElementById('input-confirmar-borrado-sedes'); + const btnConfirmar = document.getElementById('btn-confirmar-gestionar-sedes'); + + contCheckboxes.innerHTML = '
'; + try { + const res = await llamarApi(`${API.auth}?accion=listar_sedes`, { method: 'POST', body: JSON.stringify({ accion: 'listar_sedes' }) }); + const sedesActivas = res.datos.filter(s => s.activa); + contCheckboxes.innerHTML = ''; + if (!sedesActivas.length) { + contCheckboxes.innerHTML = ''; + } else { + sedesActivas.forEach(s => { + const label = document.createElement('label'); + label.className = 'checkbox-sede-item'; + const marcado = sedeIdsActuales.includes(s.id) ? 'checked' : ''; + label.innerHTML = ` ${escaparHtml(s.nombre)}`; + contCheckboxes.appendChild(label); + }); + } + } catch (e) { + contCheckboxes.innerHTML = ''; + mostrarToast(e.message, 'error'); + } + + let totalPacientesABorrar = 0; + + async function revisarImpacto() { + const sedeIdsElegidas = Array.from(document.querySelectorAll('.checkbox-sede-editar:checked')).map(c => parseInt(c.value, 10)); + try { + const res = await llamarApi(`${API.auth}?accion=previsualizar_cambio_sedes`, { + method: 'POST', + body: JSON.stringify({ accion: 'previsualizar_cambio_sedes', usuario_id: usuario.id, sede_ids: sedeIdsElegidas }), + }); + totalPacientesABorrar = res.total_pacientes_a_borrar; + if (totalPacientesABorrar > 0) { + const detalle = res.sedes_que_se_quitan.map(s => `${s.pacientes} legajo(s) en "${s.nombre}"`).join(', '); + avisoBorrado.textContent = `⚠ Si guardás este cambio, se van a eliminar definitivamente ${totalPacientesABorrar} legajo(s): ${detalle}. Esta acción no se puede deshacer.`; + avisoBorrado.classList.remove('oculto'); + bloqueConfirmar.classList.remove('oculto'); + btnConfirmar.disabled = true; + btnConfirmar.textContent = 'Guardar cambios'; + } else { + avisoBorrado.classList.add('oculto'); + bloqueConfirmar.classList.add('oculto'); + inputConfirmarTexto.value = ''; + btnConfirmar.disabled = false; + btnConfirmar.textContent = 'Guardar cambios'; + } + } catch (e) { + mostrarToast(e.message, 'error'); + } + } + + contCheckboxes.addEventListener('change', revisarImpacto); + inputConfirmarTexto.addEventListener('input', () => { + if (totalPacientesABorrar > 0) { + btnConfirmar.disabled = inputConfirmarTexto.value.trim().toUpperCase() !== 'ELIMINAR'; + } + }); + + function cerrar() { modalEnv.remove(); } + document.getElementById('btn-cancelar-gestionar-sedes').addEventListener('click', cerrar); + + btnConfirmar.addEventListener('click', async () => { + const sedeIdsElegidas = Array.from(document.querySelectorAll('.checkbox-sede-editar:checked')).map(c => parseInt(c.value, 10)); + + if (!sedeIdsElegidas.length) { mostrarToast('Elegí al menos una sede.', 'error'); return; } + if (totalPacientesABorrar > 0 && inputConfirmarTexto.value.trim().toUpperCase() !== 'ELIMINAR') { + mostrarToast('Escribí ELIMINAR para confirmar el borrado de legajos.', 'error'); + return; + } + + btnConfirmar.disabled = true; + btnConfirmar.innerHTML = ' Guardando...'; + try { + const res = await llamarApi(`${API.auth}`, { + method: 'POST', + body: JSON.stringify({ accion: 'actualizar_sedes_usuario', usuario_id: usuario.id, sede_ids: sedeIdsElegidas }), + }); + mostrarToast( + res.pacientes_borrados > 0 + ? `Sedes actualizadas. Se eliminaron ${res.pacientes_borrados} legajo(s).` + : 'Sedes actualizadas correctamente.', + 'exito' + ); + cerrar(); + cargarListaUsuarios(); + } catch (e) { + mostrarToast(e.message, 'error'); + btnConfirmar.disabled = false; + btnConfirmar.textContent = 'Guardar cambios'; + } + }); +} + +async function cargarHistorialCambios(pagina) { + const cont = document.getElementById('lista-historial'); + cont.innerHTML = '
'; + try { + const res = await llamarApi(`${API.admin}?accion=historial&pagina=${pagina}`, { method: 'GET' }); + cont.innerHTML = ''; + if (!res.datos.length) { + cont.innerHTML = '

Todavía no hay cambios registrados.

'; + return; + } + const accionesTexto = { crear: 'Creó', editar: 'Editó', eliminar: 'Eliminó', desactivar: 'Desactivó' }; + res.datos.forEach(h => { + const fila = document.createElement('div'); + fila.className = 'item-historial'; + const fecha = new Date(h.creado_en).toLocaleString('es-AR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }); + fila.innerHTML = ` +
${fecha}
+
+ ${escaparHtml(h.usuario_nombre || 'Usuario eliminado')} + ${(accionesTexto[h.accion] || h.accion).toLowerCase()} + ${escaparHtml(h.descripcion || '')} +
+ `; + cont.appendChild(fila); + }); + + const totalPaginas = Math.max(1, Math.ceil(res.total / 40)); + const paginadoEl = document.getElementById('paginado-historial'); + paginadoEl.innerHTML = ''; + if (totalPaginas > 1) { + for (let i = 1; i <= totalPaginas; i++) { + const btnPagina = document.createElement('button'); + btnPagina.className = 'btn-pagina' + (i === pagina ? ' activo' : ''); + btnPagina.textContent = i; + btnPagina.addEventListener('click', () => cargarHistorialCambios(i)); + paginadoEl.appendChild(btnPagina); + } + } + } catch (e) { + cont.innerHTML = ''; + mostrarToast(e.message, 'error'); + } +}