This commit is contained in:
Lizandro Guarnizo
2026-04-09 16:38:03 -05:00
parent 6a8b82b8a2
commit 5e58b14eba
2 changed files with 438 additions and 0 deletions
+127
View File
@@ -0,0 +1,127 @@
<?php
/**
* API: Reporte de pagos recibidos en domicilios.
*
* GET params:
* modo 'dia' | 'mes' | 'rango' (default: 'dia')
* fecha YYYY-MM-DD (para modo=dia)
* anio año 4 dígitos (para modo=mes)
* mes 1-12 (para modo=mes)
* desde YYYY-MM-DD (para modo=rango)
* hasta YYYY-MM-DD (para modo=rango)
* enfermera_id INT (opcional)
*/
require_once __DIR__ . '/../../config/config.php';
require_once __DIR__ . '/../../classes/Database.php';
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
requireAuthentication();
$db = Database::getInstance();
$modo = in_array($_GET['modo'] ?? '', ['dia','mes','rango']) ? $_GET['modo'] : 'dia';
$wheres = [];
$params = [];
switch ($modo) {
case 'mes':
$anio = isset($_GET['anio']) && ctype_digit($_GET['anio']) ? (int)$_GET['anio'] : (int)date('Y');
$mes = isset($_GET['mes']) && ctype_digit($_GET['mes']) ? (int)$_GET['mes'] : (int)date('n');
if ($mes < 1 || $mes > 12) $mes = (int)date('n');
$wheres[] = 'YEAR(d.fecha_programada) = ?';
$params[] = $anio;
$wheres[] = 'MONTH(d.fecha_programada) = ?';
$params[] = $mes;
break;
case 'rango':
$desde = $_GET['desde'] ?? date('Y-m-01');
$hasta = $_GET['hasta'] ?? date('Y-m-d');
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $desde)) $desde = date('Y-m-01');
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $hasta)) $hasta = date('Y-m-d');
$wheres[] = 'DATE(d.fecha_programada) BETWEEN ? AND ?';
$params[] = $desde;
$params[] = $hasta;
break;
default: // dia
$fecha = $_GET['fecha'] ?? date('Y-m-d');
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $fecha)) $fecha = date('Y-m-d');
$wheres[] = 'DATE(d.fecha_programada) = ?';
$params[] = $fecha;
break;
}
// Filtro por enfermera
$enfermeraId = null;
if (!empty($_GET['enfermera_id']) && ctype_digit($_GET['enfermera_id'])) {
$enfermeraId = (int)$_GET['enfermera_id'];
$wheres[] = 'a.enfermera_id = ?';
$params[] = $enfermeraId;
}
$where = $wheres ? 'WHERE ' . implode(' AND ', $wheres) : '';
$filas = $db->fetchAll(
"SELECT
d.id,
d.fecha_programada,
d.pago_fecha,
p.nombre_completo AS paciente,
p.numero_documento,
COALESCE(e.nombre_completo,'Sin asignar') AS enfermera,
d.tipo_cliente,
COALESCE(d.seguro_nombre,'') AS seguro_nombre,
d.estado AS dom_estado,
COALESCE(d.valor_domicilio, 0) AS valor_domicilio,
d.pago_estado,
COALESCE(d.pago_modo,'') AS pago_modo,
COALESCE(d.pago_monto, 0) AS pago_monto,
COALESCE(d.pago_notas,'') AS pago_notas
FROM lab_domicilios d
JOIN lab_pacientes p ON p.id = d.paciente_id
LEFT JOIN lab_asignaciones a ON a.domicilio_id = d.id AND a.estado NOT IN ('liberada')
LEFT JOIN lab_enfermeras e ON e.id = a.enfermera_id
$where
ORDER BY d.fecha_programada ASC, d.id ASC",
$params
);
// Resumen financiero
$resumen = [
'total' => count($filas),
'pagados' => 0,
'pendientes' => 0,
'exentos' => 0,
'monto_total' => 0.0,
'por_modo' => [],
];
foreach ($filas as $f) {
$est = $f['pago_estado'] ?? 'pendiente';
if ($est === 'pagado') {
$resumen['pagados']++;
$resumen['monto_total'] += (float)$f['pago_monto'];
$key = $f['pago_modo'] ?: 'sin_registrar';
$resumen['por_modo'][$key] = ($resumen['por_modo'][$key] ?? 0.0) + (float)$f['pago_monto'];
} elseif ($est === 'exento') {
$resumen['exentos']++;
} else {
$resumen['pendientes']++;
}
}
// Lista de enfermeras para poblar el selector
$enfermeras = $db->fetchAll(
"SELECT id, nombre_completo FROM lab_enfermeras WHERE is_active = 1 ORDER BY nombre_completo"
);
echo json_encode([
'success' => true,
'filas' => $filas,
'resumen' => $resumen,
'enfermeras' => $enfermeras,
], JSON_UNESCAPED_UNICODE);
+311
View File
@@ -136,6 +136,67 @@ if ($tipo) {
$headers = ['ID','Nombre','Tipo Doc.','Documento','Teléfono','Email','EPS','Ciudad','Barrio','Activo','Total Órdenes','Registrado'];
break;
case 'pagos':
$pModo = in_array($_GET['modo'] ?? '', ['dia','mes','rango']) ? $_GET['modo'] : 'dia';
$pWheres = [];
$pParams = [];
switch ($pModo) {
case 'mes':
$pAnio = isset($_GET['anio']) && ctype_digit($_GET['anio']) ? (int)$_GET['anio'] : (int)date('Y');
$pMes = isset($_GET['mes']) && ctype_digit($_GET['mes']) ? (int)$_GET['mes'] : (int)date('n');
if ($pMes < 1 || $pMes > 12) $pMes = (int)date('n');
$pWheres[] = 'YEAR(d.fecha_programada) = ?'; $pParams[] = $pAnio;
$pWheres[] = 'MONTH(d.fecha_programada) = ?'; $pParams[] = $pMes;
$rangoNombre = sprintf('%04d-%02d', $pAnio, $pMes);
break;
case 'rango':
$pDesde = $_GET['desde'] ?? date('Y-m-01');
$pHasta = $_GET['hasta'] ?? date('Y-m-d');
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $pDesde)) $pDesde = date('Y-m-01');
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $pHasta)) $pHasta = date('Y-m-d');
$pWheres[] = 'DATE(d.fecha_programada) BETWEEN ? AND ?';
$pParams[] = $pDesde; $pParams[] = $pHasta;
$rangoNombre = "{$pDesde}_{$pHasta}";
break;
default: // dia
$pFecha = $_GET['fecha'] ?? date('Y-m-d');
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $pFecha)) $pFecha = date('Y-m-d');
$pWheres[] = 'DATE(d.fecha_programada) = ?';
$pParams[] = $pFecha;
$rangoNombre = $pFecha;
}
if (!empty($_GET['enfermera_id']) && ctype_digit($_GET['enfermera_id'])) {
$pWheres[] = 'a.enfermera_id = ?';
$pParams[] = (int)$_GET['enfermera_id'];
}
$pWhere = $pWheres ? 'WHERE ' . implode(' AND ', $pWheres) : '';
$rows = $db->fetchAll(
"SELECT d.id, d.fecha_programada, d.pago_fecha,
p.nombre_completo AS paciente, p.numero_documento,
COALESCE(e.nombre_completo,'Sin asignar') AS enfermera,
d.tipo_cliente, COALESCE(d.seguro_nombre,'') AS seguro,
d.estado AS dom_estado,
COALESCE(d.valor_domicilio,0) AS valor_domicilio,
COALESCE(d.pago_estado,'pendiente') AS pago_estado,
COALESCE(d.pago_modo,'') AS pago_modo,
COALESCE(d.pago_monto,0) AS pago_monto,
COALESCE(d.pago_notas,'') AS pago_notas
FROM lab_domicilios d
JOIN lab_pacientes p ON p.id = d.paciente_id
LEFT JOIN lab_asignaciones a ON a.domicilio_id = d.id AND a.estado NOT IN ('liberada')
LEFT JOIN lab_enfermeras e ON e.id = a.enfermera_id
$pWhere
ORDER BY d.fecha_programada ASC, d.id ASC",
$pParams
);
$filename = "reporte_pagos_{$rangoNombre}.csv";
$headers = [
'ID','Fecha programada','Fecha pago','Paciente','Documento',
'Enfermero/a','Tipo cliente','Seguro','Estado domicilio',
'Valor domicilio','Estado pago','Modo pago','Monto recibido','Notas pago',
];
break;
default:
http_response_code(400);
exit('Tipo de exportación no válido');
@@ -244,6 +305,100 @@ $adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════ -->
<!-- Reporte de Pagos recibidos -->
<!-- ══════════════════════════════════════════════════════════════ -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white border-0 pb-0">
<h6 class="fw-semibold mb-2">
<i class="fas fa-money-bill-wave me-2 text-success"></i>Informe de pagos recibidos en domicilios
</h6>
<div class="d-flex flex-wrap gap-2 align-items-end pb-2">
<div>
<label class="form-label small text-muted text-uppercase mb-1">Período</label>
<select id="pago-modo" class="form-select form-select-sm" onchange="pagoToggleModo()" style="min-width:110px">
<option value="dia">Por día</option>
<option value="mes">Por mes</option>
<option value="rango">Rango fechas</option>
</select>
</div>
<!-- Día -->
<div id="pago-wrap-dia">
<label class="form-label small text-muted text-uppercase mb-1">Fecha</label>
<input type="date" id="pago-fecha" class="form-control form-control-sm" value="<?= date('Y-m-d') ?>">
</div>
<!-- Mes -->
<div id="pago-wrap-anio" style="display:none">
<label class="form-label small text-muted text-uppercase mb-1">Año</label>
<input type="number" id="pago-anio" class="form-control form-control-sm" value="<?= date('Y') ?>" min="2020" max="2099" style="width:88px">
</div>
<div id="pago-wrap-mes" style="display:none">
<label class="form-label small text-muted text-uppercase mb-1">Mes</label>
<select id="pago-mes" class="form-select form-select-sm" style="width:120px">
<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>
<!-- Rango -->
<div id="pago-wrap-desde" style="display:none">
<label class="form-label small text-muted text-uppercase mb-1">Desde</label>
<input type="date" id="pago-desde" class="form-control form-control-sm" value="<?= date('Y-m-01') ?>">
</div>
<div id="pago-wrap-hasta" style="display:none">
<label class="form-label small text-muted text-uppercase mb-1">Hasta</label>
<input type="date" id="pago-hasta" class="form-control form-control-sm" value="<?= date('Y-m-d') ?>">
</div>
<!-- Enfermero -->
<div>
<label class="form-label small text-muted text-uppercase mb-1">Enfermero/a</label>
<select id="pago-enfermera" class="form-select form-select-sm" style="min-width:160px">
<option value="">Todos</option>
</select>
</div>
<button class="btn btn-primary btn-sm" onclick="cargarReportePagos()">
<i class="fas fa-sync-alt me-1"></i>Ver informe
</button>
<button class="btn btn-sm btn-outline-success" onclick="exportarReportePagos()">
<i class="fas fa-file-excel me-1"></i>Excel
</button>
</div>
</div>
<!-- Resumen KPIs -->
<div id="pago-resumen" class="px-3 py-2 border-bottom bg-light" style="display:none">
<div class="row g-2 text-center">
<div class="col-6 col-md-3">
<div class="fw-bold fs-5 text-secondary" id="pr-total">—</div>
<div class="small text-muted">Total domicilios</div>
</div>
<div class="col-6 col-md-3">
<div class="fw-bold fs-5 text-success" id="pr-pagados">—</div>
<div class="small text-muted">Pagados</div>
</div>
<div class="col-6 col-md-3">
<div class="fw-bold fs-5 text-warning" id="pr-pendientes">—</div>
<div class="small text-muted">Pendientes</div>
</div>
<div class="col-6 col-md-3">
<div class="fw-bold fs-5" style="color:#7b1fa2" id="pr-monto">—</div>
<div class="small text-muted">Total recibido</div>
</div>
</div>
<div id="pr-modos" class="d-flex flex-wrap gap-3 justify-content-center mt-2" style="font-size:.82rem"></div>
</div>
<!-- Tabla -->
<div class="card-body p-0">
<div id="pago-tabla-wrap" style="overflow-x:auto">
<div class="text-center py-4 text-muted small">
<i class="fas fa-info-circle me-1"></i>Selecciona los filtros y haz clic en <strong>Ver informe</strong>
</div>
</div>
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════ -->
<!-- Reporte domicilios por enfermero -->
<!-- ══════════════════════════════════════════════════════════════ -->
@@ -563,6 +718,161 @@ function exportarReporteEnfermero() {
window.open(`lab_reportes.php?${params}`, '_blank');
}
// ── Reporte de Pagos ───────────────────────────────────────────────────────
function pagoToggleModo() {
const modo = document.getElementById('pago-modo').value;
document.getElementById('pago-wrap-dia').style.display = modo === 'dia' ? '' : 'none';
document.getElementById('pago-wrap-anio').style.display = modo === 'mes' ? '' : 'none';
document.getElementById('pago-wrap-mes').style.display = modo === 'mes' ? '' : 'none';
document.getElementById('pago-wrap-desde').style.display = modo === 'rango' ? '' : 'none';
document.getElementById('pago-wrap-hasta').style.display = modo === 'rango' ? '' : 'none';
}
// Seleccionar el mes actual al cargar
(function initPagoMes() {
const mes = new Date().getMonth() + 1;
const sel = document.getElementById('pago-mes');
if (sel) sel.value = mes;
})();
async function cargarReportePagos() {
const wrap = document.getElementById('pago-tabla-wrap');
wrap.innerHTML = '<div class="text-center py-4 text-muted"><i class="fas fa-spinner fa-spin"></i> Cargando…</div>';
document.getElementById('pago-resumen').style.display = 'none';
const params = _pagoParams();
try {
const r = await fetch(`api/lab/get_reporte_pagos.php?${params}`);
const d = await r.json();
if (!d.success) throw new Error(d.error || 'Error al cargar');
// Poblar selector de enfermeras (solo la primera vez)
const sel = document.getElementById('pago-enfermera');
if (sel.options.length === 1 && d.enfermeras?.length) {
d.enfermeras.forEach(e => {
const o = document.createElement('option');
o.value = e.id;
o.textContent = e.nombre_completo;
sel.appendChild(o);
});
}
// Resumen KPIs
const rs = d.resumen || {};
document.getElementById('pr-total').textContent = rs.total ?? 0;
document.getElementById('pr-pagados').textContent = rs.pagados ?? 0;
document.getElementById('pr-pendientes').textContent = rs.pendientes ?? 0;
document.getElementById('pr-monto').textContent = fmtMoney(rs.monto_total);
const modosEl = document.getElementById('pr-modos');
const MODO_LABEL = { efectivo:'Efectivo', transferencia:'Transferencia', otro:'Otro', sin_registrar:'Sin modo' };
const modos = Object.entries(rs.por_modo || {});
modosEl.innerHTML = modos.length
? modos.map(([k, v]) =>
`<span class="badge bg-secondary">${esc(MODO_LABEL[k] || k)}: ${fmtMoney(v)}</span>`
).join('')
: '';
document.getElementById('pago-resumen').style.display = '';
// Tabla de detalle
if (!d.filas?.length) {
wrap.innerHTML = '<p class="text-muted text-center py-3 small">Sin domicilios para los filtros seleccionados.</p>';
return;
}
const PAGO_BADGE = { pagado:'success', pendiente:'warning', exento:'secondary' };
const DOM_BADGE = { completado:'success', cancelado:'danger', programado:'secondary',
confirmado:'info', en_camino:'primary', reprogramado:'dark' };
wrap.innerHTML = `
<table class="table table-sm table-hover mb-0" style="font-size:.83rem">
<thead class="table-light sticky-top">
<tr>
<th class="ps-3">#</th>
<th>Fecha prog.</th>
<th>Paciente</th>
<th>Documento</th>
<th>Enfermero/a</th>
<th>Tipo cliente</th>
<th class="text-end">Valor domicilio</th>
<th class="text-center">Estado pago</th>
<th class="text-center">Modo pago</th>
<th class="text-end">Monto recibido</th>
<th>Fecha pago</th>
<th>Estado domicilio</th>
<th>Notas pago</th>
</tr>
</thead>
<tbody>
${d.filas.map(f => {
const pagoEst = f.pago_estado || 'pendiente';
const domEst = f.dom_estado || '';
const cliente = f.tipo_cliente === 'seguro' && f.seguro_nombre
? `Seguro: ${esc(f.seguro_nombre)}`
: esc(f.tipo_cliente || 'particular');
return `<tr class="${pagoEst === 'pagado' ? '' : 'opacity-75'}">
<td class="ps-3 text-muted">${f.id}</td>
<td class="text-nowrap">${esc(f.fecha_programada?.slice(0,10) || '—')}</td>
<td class="fw-semibold text-nowrap">${esc(f.paciente)}</td>
<td class="text-muted">${esc(f.numero_documento || '—')}</td>
<td class="text-nowrap">${esc(f.enfermera)}</td>
<td class="small">${cliente}</td>
<td class="text-end">${fmtMoney(f.valor_domicilio)}</td>
<td class="text-center">
<span class="badge bg-${PAGO_BADGE[pagoEst] || 'secondary'}">${esc(pagoEst)}</span>
</td>
<td class="text-center text-muted small">${esc(f.pago_modo) || '—'}</td>
<td class="text-end fw-semibold ${pagoEst === 'pagado' ? 'text-success' : 'text-muted'}">
${pagoEst === 'pagado' ? fmtMoney(f.pago_monto) : '—'}
</td>
<td class="text-nowrap text-muted small">${f.pago_fecha ? esc(f.pago_fecha.slice(0,16)) : '—'}</td>
<td>
<span class="badge bg-${DOM_BADGE[domEst] || 'secondary'}">${esc(domEst)}</span>
</td>
<td class="text-muted small" style="max-width:160px;white-space:normal">${esc(f.pago_notas) || ''}</td>
</tr>`;
}).join('')}
</tbody>
<tfoot class="table-light fw-semibold">
<tr>
<td colspan="6" class="ps-3">Total</td>
<td class="text-end">${fmtMoney(d.filas.reduce((s,f) => s + parseFloat(f.valor_domicilio||0), 0))}</td>
<td></td><td></td>
<td class="text-end text-success">${fmtMoney(rs.monto_total)}</td>
<td colspan="3"></td>
</tr>
</tfoot>
</table>`;
} catch (e) {
wrap.innerHTML = `<div class="alert alert-danger m-3 small">${esc(e.message)}</div>`;
}
}
function exportarReportePagos() {
const p = _pagoParams();
p.set('export', 'pagos');
window.open(`lab_reportes.php?${p}`, '_blank');
}
function _pagoParams() {
const modo = document.getElementById('pago-modo').value;
const enf = document.getElementById('pago-enfermera').value;
const p = new URLSearchParams({ modo });
if (modo === 'dia') {
p.set('fecha', document.getElementById('pago-fecha').value || '<?= date('Y-m-d') ?>');
} else if (modo === 'mes') {
p.set('anio', document.getElementById('pago-anio').value || '<?= date('Y') ?>');
p.set('mes', document.getElementById('pago-mes').value || '<?= date('n') ?>');
} else {
p.set('desde', document.getElementById('pago-desde').value || '<?= date('Y-m-01') ?>');
p.set('hasta', document.getElementById('pago-hasta').value || '<?= date('Y-m-d') ?>');
}
if (enf) p.set('enfermera_id', enf);
return p;
}
// ── Carga todo en paralelo ──────────────────────────────────────────────────
async function cargarTodo() {
const desde = fecha('rpt-desde');
@@ -572,6 +882,7 @@ async function cargarTodo() {
cargarResumenOrdenes(desde, hasta),
cargarActividad(1),
cargarReporteEnfermero(),
cargarReportePagos(),
]);
}