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
+134
View File
@@ -243,6 +243,55 @@ $adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════ -->
<!-- Reporte domicilios por enfermero -->
<!-- ══════════════════════════════════════════════════════════════ -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white border-0 d-flex flex-wrap gap-2 align-items-end">
<h6 class="fw-semibold mb-0 me-auto">
<i class="fas fa-user-nurse me-2 text-success"></i>Domicilios por enfermero
</h6>
<div class="d-flex flex-wrap gap-2 align-items-end">
<div>
<label class="form-label small text-muted text-uppercase mb-1">Año</label>
<input type="number" id="enf-anio" class="form-control form-control-sm"
value="<?= date('Y') ?>" min="2020" max="2099" style="width:90px">
</div>
<div>
<label class="form-label small text-muted text-uppercase mb-1">Mes</label>
<select id="enf-mes" class="form-select form-select-sm" style="width:100px">
<option value="">Todos</option>
<option value="1">Enero</option><option value="2">Febrero</option>
<option value="3">Marzo</option><option value="4">Abril</option>
<option value="5">Mayo</option><option value="6">Junio</option>
<option value="7">Julio</option><option value="8">Agosto</option>
<option value="9">Septiembre</option><option value="10">Octubre</option>
<option value="11">Noviembre</option><option value="12">Diciembre</option>
</select>
</div>
<div>
<label class="form-label small text-muted text-uppercase mb-1">Enfermero/a</label>
<select id="enf-selector" class="form-select form-select-sm" style="min-width:160px">
<option value="">Todos</option>
</select>
</div>
<button class="btn btn-primary btn-sm" onclick="cargarReporteEnfermero()">
<i class="fas fa-sync-alt me-1"></i>Filtrar
</button>
<button class="btn btn-sm btn-outline-success" onclick="exportarReporteEnfermero()">
<i class="fas fa-file-excel me-1"></i>Excel
</button>
</div>
</div>
<div class="card-body p-0">
<div id="enf-tabla-wrap" style="overflow-x:auto">
<div class="text-center py-4 text-muted">
<i class="fas fa-spinner fa-spin"></i> Cargando…
</div>
</div>
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════ -->
<!-- KPIs principales -->
<!-- ══════════════════════════════════════════════════════════════ -->
@@ -429,6 +478,90 @@ const COLOR_EST = {
const MODULO_COLOR = { ordenes:'warning', domicilios:'success', pacientes:'primary', enfermeras:'info', asignaciones:'secondary' };
let logPag = 1;
// ── Reporte por enfermero ───────────────────────────────────────────────────
async function cargarReporteEnfermero() {
const wrap = document.getElementById('enf-tabla-wrap');
wrap.innerHTML = '<div class="text-center py-4 text-muted"><i class="fas fa-spinner fa-spin"></i></div>';
const anio = document.getElementById('enf-anio').value;
const mes = document.getElementById('enf-mes').value;
const enf = document.getElementById('enf-selector').value;
const params = new URLSearchParams();
if (anio) params.set('anio', anio);
if (mes) params.set('mes', mes);
if (enf) params.set('enfermera_id', enf);
try {
const r = await fetch(`api/lab/get_reporte_enfermero.php?${params}`);
const d = await r.json();
if (!d.success) throw new Error(d.error || 'Error');
// Poblar selector de enfermeras (sólo la primera vez)
const sel = document.getElementById('enf-selector');
if (sel.options.length === 1 && d.enfermeras?.length) {
d.enfermeras.forEach(e => {
const opt = document.createElement('option');
opt.value = e.id;
opt.textContent = e.nombre_completo;
sel.appendChild(opt);
});
}
if (!d.filas?.length) {
wrap.innerHTML = '<p class="text-muted text-center py-3 small">Sin datos para los filtros seleccionados.</p>';
return;
}
wrap.innerHTML = `
<table class="table table-sm table-hover mb-0" style="font-size:.85rem">
<thead class="table-light">
<tr>
<th>Enfermero/a</th>
<th class="text-center">Período</th>
<th class="text-center">Total</th>
<th class="text-center text-success">Completados</th>
<th class="text-center text-danger">Cancelados</th>
<th class="text-center text-warning">En proceso</th>
<th class="text-center">Tasa éxito</th>
</tr>
</thead>
<tbody>
${d.filas.map(f => {
const color = f.tasa_pct >= 80 ? '#43a047' : f.tasa_pct >= 50 ? '#fb8c00' : '#e53935';
return `<tr>
<td class="fw-semibold">${esc(f.enfermera)}</td>
<td class="text-center text-muted">${esc(f.mes_nombre)} ${f.anio}</td>
<td class="text-center">${f.total}</td>
<td class="text-center text-success">${f.completados}</td>
<td class="text-center text-danger">${f.cancelados}</td>
<td class="text-center text-warning">${f.en_proceso}</td>
<td class="text-center">
<span class="fw-bold" style="color:${color}">${f.tasa_pct}%</span>
<div style="width:60px;height:5px;background:#f0f0f0;border-radius:3px;margin:2px auto 0">
<div style="width:${f.tasa_pct}%;height:100%;background:${color};border-radius:3px"></div>
</div>
</td>
</tr>`;
}).join('')}
</tbody>
</table>`;
} catch (e) {
wrap.innerHTML = `<div class="alert alert-danger m-3 small">${esc(e.message)}</div>`;
}
}
function exportarReporteEnfermero() {
const anio = document.getElementById('enf-anio').value;
const mes = document.getElementById('enf-mes').value;
const enf = document.getElementById('enf-selector').value;
const params = new URLSearchParams({ export: 'domicilios', desde: `${anio || '2020'}-${mes ? String(mes).padStart(2,'0') : '01'}-01` });
if (mes) params.set('hasta', `${anio || new Date().getFullYear()}-${String(mes).padStart(2,'0')}-31`);
else params.set('hasta', `${anio || new Date().getFullYear()}-12-31`);
if (enf) params.set('enfermera_id', enf);
window.open(`lab_reportes.php?${params}`, '_blank');
}
// ── Carga todo en paralelo ──────────────────────────────────────────────────
async function cargarTodo() {
const desde = fecha('rpt-desde');
@@ -437,6 +570,7 @@ async function cargarTodo() {
cargarMetricas(desde, hasta),
cargarResumenOrdenes(desde, hasta),
cargarActividad(1),
cargarReporteEnfermero(),
]);
}