feat: 6 nuevas funcionalidades - notas obligatorias turno, reporte enfermero, plantilla WhatsApp, adjuntar orden modal, roles solo lectura, fix valores copago

This commit is contained in:
Lizandro Guarnizo
2026-04-02 09:41:49 -05:00
parent 052a263659
commit d8ceb3f822
14 changed files with 2794 additions and 49 deletions
+104 -13
View File
@@ -10,9 +10,10 @@ if (!isUserLoggedIn()) {
header('Location: login.php'); exit;
}
$usuario = $_SESSION['admin_user'];
$rol = $usuario['role'] ?? 'admin';
$enfId = (int)($usuario['enfermera_id'] ?? 0);
$usuario = $_SESSION['admin_user'];
$rol = $usuario['role'] ?? 'admin';
$enfId = (int)($usuario['enfermera_id'] ?? 0);
$enfermeraNombre = $usuario['full_name'] ?? $usuario['username'] ?? 'El profesional de salud';
// Si es admin puede simular ver la agenda de otra enfermera vía ?eid=X
if ($rol === 'admin' && isset($_GET['eid'])) {
@@ -386,8 +387,9 @@ $hoy = date('Y-m-d');
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
const ENFERMERA_ID = <?= (int)$enfId ?>;
const ROL = '<?= $rol ?>';
const ENFERMERA_ID = <?= (int)$enfId ?>;
const ROL = '<?= $rol ?>';
const ENFERMERO_NOMBRE = <?= json_encode($enfermeraNombre) ?>;
// Etiquetas descriptivas
const ESTADOS_LABEL = {
@@ -662,10 +664,18 @@ const portal = {
<i class="fas fa-plus me-1"></i>Servicio extra
</button>` : ''}
${item.paciente_telefono
? `<a class="btn btn-outline-success btn-accion"
href="https://wa.me/${item.paciente_telefono.replace(/\D/g,'')}" target="_blank">
<i class="fab fa-whatsapp"></i>
</a>` : ''}
? (()=>{
const tel = item.paciente_telefono.replace(/\D/g,'');
const txt = encodeURIComponent(
'Hola, soy ' + ENFERMERO_NOMBRE + ', el profesional de salud asignado a su atenci\u00f3n. '+
'Me comunico con ' + (item.paciente_nombre||'usted') + ' para confirmar la visita programada. \u00bfTiene alguna pregunta?'
);
return `<a class="btn btn-outline-success btn-accion"
href="https://wa.me/${tel}?text=${txt}" target="_blank"
title="Enviar mensaje WhatsApp">
<i class="fab fa-whatsapp"></i>
</a>`;
})() : ''}
<button class="btn btn-outline-secondary btn-accion btn-form-envio"
style="color:#6f42c1;border-color:#6f42c1"
data-dom-id="${item.domicilio_id}"
@@ -686,6 +696,43 @@ const portal = {
// ── Cambiar estado ─────────────────────────────────────────────────────
async actualizarEstado(domId, nuevoEstado, notas = '') {
// ── BLOQUEO: para completar es obligatorio tener al menos una nota ──
if (nuevoEstado === 'completado') {
// Verificar en caché o cargar desde API
let cache = notasManager._cache[domId];
if (!cache) {
try {
const r = await fetch(`api/lab/get_notas_domicilio.php?domicilio_id=${domId}`);
const d = await r.json();
const notas = d.success ? (d.data || []) : [];
cache = {
clinica: notas.find(n => n.tipo === 'clinica') || null,
libres: notas.filter(n => n.tipo === 'libre')
};
notasManager._cache[domId] = cache;
} catch (e) {
cache = { clinica: null, libres: [] };
}
}
const tieneNota = cache.clinica || cache.libres.length > 0;
if (!tieneNota) {
// Abrir el sheet de notas para que registre una antes de completar
// Guardamos el intento para retomarlo al cerrar la nota
notasManager._pendingCompleteId = domId;
// Mostrar aviso visual dentro del sheet
notasManager.abrirLibre(domId, null);
// Colocar banner de advertencia en la parte superior del sheet
const inner = document.getElementById('nota-sheet-inner');
if (inner) {
const banner = document.createElement('div');
banner.id = 'nota-required-banner';
banner.style.cssText = 'background:#fff3cd;border:1px solid #ffc107;border-radius:8px;padding:10px 12px;margin-bottom:12px;font-size:.82rem;color:#856404;';
banner.innerHTML = '<i class="fas fa-exclamation-triangle me-1"></i><strong>Nota obligatoria</strong> — Debes registrar al menos una nota antes de marcar este domicilio como completado.';
inner.prepend(banner);
}
return;
}
}
try {
const r = await fetch('api/lab/update_domicilio_enfermero.php', {
method: 'POST',
@@ -1100,6 +1147,16 @@ const formVer = {
placeholder="Instrucciones adicionales para el servicio…"></textarea>
</div>
<!-- Orden médica / Archivo -->
<div class="mb-2">
<label class="form-label small fw-semibold">
Orden médica / Archivo <span class="text-muted fw-normal">(opcional)</span>
</label>
<input type="file" id="na-orden-file" accept=".pdf,.jpg,.jpeg,.png"
class="form-control form-control-sm">
<div id="na-orden-preview" class="mt-1 small text-muted"></div>
</div>
<div id="na-error" class="alert alert-danger py-2 d-none small"></div>
</div>
<div class="modal-footer py-2">
@@ -1386,6 +1443,23 @@ const agendaNueva = {
});
const d = await r.json();
if (!d.success) throw new Error(d.error || 'Error al guardar');
// Subir archivo adjunto si el usuario seleccionó uno
const archivoInput = document.getElementById('na-orden-file');
if (archivoInput?.files?.length && d.id) {
const fd = new FormData();
fd.append('file', archivoInput.files[0]);
fd.append('domicilio_id', d.id);
try {
const rFile = await fetch('api/lab/upload_orden.php', { method: 'POST', body: fd });
// Si falla el upload no bloqueamos; el domicilio ya fue creado
const dFile = await rFile.json();
if (!dFile.success) console.warn('No se pudo adjuntar el archivo:', dFile.error);
} catch (eFile) {
console.warn('Error al subir archivo:', eFile);
}
}
this._modal.hide();
portal.cargar();
} catch (e) {
@@ -1620,6 +1694,7 @@ const notasManager = {
_domId: null, // domicilio actualmente en el sheet
_notaId: null, // null = nueva nota libre, número = editar
_imagen: null, // File pendiente de subir
_pendingCompleteId: null, // domicilio_id pendiente de marcar completado tras guardar nota
// ── Abrir/cerrar sección de notas en la tarjeta ────────────────────────
async toggle(domId, btn) {
@@ -1847,9 +1922,17 @@ const notasManager = {
});
const d = await r.json();
if (!d.success) throw new Error(d.error || 'Error al guardar');
delete this._cache[this._domId];
const savedDomId = this._domId;
delete this._cache[savedDomId];
this._cerrarSheet();
await this._renderSeccion(this._domId);
await this._renderSeccion(savedDomId);
// Si había un completado pendiente para este domicilio, retomarlo
if (this._pendingCompleteId && this._pendingCompleteId == savedDomId) {
this._pendingCompleteId = null;
if (confirm('✅ Nota guardada. ¿Marcar el domicilio como completado ahora?')) {
portal.actualizarEstado(savedDomId, 'completado');
}
}
} catch(e) { alert('Error: ' + e.message); }
},
@@ -1886,9 +1969,17 @@ const notasManager = {
});
const d = await r.json();
if (!d.success) throw new Error(d.error || 'Error al guardar');
delete this._cache[this._domId];
const savedDomId = this._domId;
delete this._cache[savedDomId];
this._cerrarSheet();
await this._renderSeccion(this._domId);
await this._renderSeccion(savedDomId);
// Si había un completado pendiente para este domicilio, retomarlo
if (this._pendingCompleteId && this._pendingCompleteId == savedDomId) {
this._pendingCompleteId = null;
if (confirm('✅ Nota guardada. ¿Marcar el domicilio como completado ahora?')) {
portal.actualizarEstado(savedDomId, 'completado');
}
}
} catch(e) { alert('Error: ' + e.message); }
},