From c8bef9d9c4646bfca5c6a38b8e11a75c408033d2 Mon Sep 17 00:00:00 2001
From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com>
Date: Fri, 10 Apr 2026 23:04:28 -0500
Subject: [PATCH] up
---
api/lab/get_reporte_pagos.php | 37 ++++-
api/lab/my_agenda.php | 10 +-
api/lab/registrar_pago.php | 71 ++++++---
classes/lab/Domicilio.php | 6 +
enfermero_portal.php | 149 ++++++++++++------
lab_domicilios.php | 23 ++-
lab_reportes.php | 20 ++-
...20260410_lab_domicilio_pagos_parciales.sql | 34 ++++
8 files changed, 273 insertions(+), 77 deletions(-)
create mode 100644 migrations/20260410_lab_domicilio_pagos_parciales.sql
diff --git a/api/lab/get_reporte_pagos.php b/api/lab/get_reporte_pagos.php
index ad2a141..7b08782 100644
--- a/api/lab/get_reporte_pagos.php
+++ b/api/lab/get_reporte_pagos.php
@@ -90,7 +90,24 @@ $filas = $db->fetchAll(
$params
);
-// Resumen financiero
+// Enriquecer con parcialidades de pago
+$domIds = array_column($filas, 'id');
+$parciales = [];
+if ($domIds) {
+ $in = implode(',', array_map('intval', $domIds));
+ $rows = $db->fetchAll(
+ "SELECT domicilio_id, modo, monto, notas FROM lab_domicilio_pagos WHERE domicilio_id IN ($in) ORDER BY id ASC"
+ );
+ foreach ($rows as $row) {
+ $parciales[(int)$row['domicilio_id']][] = $row;
+ }
+}
+foreach ($filas as &$f) {
+ $f['pagos_detalle'] = $parciales[(int)$f['id']] ?? [];
+}
+unset($f);
+
+// Resumen financiero — sumar desde parcialidades cuando existen
$resumen = [
'total' => count($filas),
'pagados' => 0,
@@ -104,9 +121,21 @@ 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'];
+ if (!empty($f['pagos_detalle'])) {
+ // Sumar desde parcialidades
+ foreach ($f['pagos_detalle'] as $p) {
+ $monto = (float)$p['monto'];
+ $key = $p['modo'] ?: 'sin_registrar';
+ $resumen['monto_total'] += $monto;
+ $resumen['por_modo'][$key] = ($resumen['por_modo'][$key] ?? 0.0) + $monto;
+ }
+ } else {
+ // Fallback registro legacy
+ $monto = (float)$f['pago_monto'];
+ $key = $f['pago_modo'] ?: 'sin_registrar';
+ $resumen['monto_total'] += $monto;
+ $resumen['por_modo'][$key] = ($resumen['por_modo'][$key] ?? 0.0) + $monto;
+ }
} elseif ($est === 'exento') {
$resumen['exentos']++;
} else {
diff --git a/api/lab/my_agenda.php b/api/lab/my_agenda.php
index f3536bc..9a9de5d 100644
--- a/api/lab/my_agenda.php
+++ b/api/lab/my_agenda.php
@@ -32,12 +32,18 @@ try {
// Agenda del día con pleno detalle
$agenda = $enf->agenda($eid, $fecha);
- // Enriquecer con servicios extra (el resto ya viene de la query agenda())
+ // Enriquecer con servicios extra y pagos parciales
$db = Database::getInstance();
foreach ($agenda as &$item) {
+ $domId = (int)$item['domicilio_id'];
$item['servicios_extra'] = $db->fetchAll(
"SELECT * FROM lab_servicios_extra WHERE domicilio_id = ? ORDER BY created_at ASC",
- [(int)$item['domicilio_id']]
+ [$domId]
+ );
+ // Parcialidades de pago (tabla nueva); vacío si no hay pagos registrados
+ $item['pagos_detalle'] = $db->fetchAll(
+ "SELECT modo, monto, notas FROM lab_domicilio_pagos WHERE domicilio_id = ? ORDER BY id ASC",
+ [$domId]
);
}
unset($item);
diff --git a/api/lab/registrar_pago.php b/api/lab/registrar_pago.php
index efbc945..6ad1431 100644
--- a/api/lab/registrar_pago.php
+++ b/api/lab/registrar_pago.php
@@ -2,15 +2,17 @@
/**
* POST /api/lab/registrar_pago.php
* Registra el pago de un domicilio por parte del cliente.
- * Solo puede llamarse desde el portal del enfermero o por un admin.
+ * Soporta múltiples métodos de pago (pagos parciales).
*
* Body JSON:
* {
* domicilio_id: int,
* pago_estado: 'pagado' | 'exento' | 'pendiente',
- * pago_modo: 'efectivo' | 'transferencia' | 'otro', (requerido si pagado)
- * pago_monto: float, (requerido si pagado)
- * pago_notas: string|null
+ * pagos: [ (requerido si pagado)
+ * { modo: 'efectivo'|'transferencia'|'otro', monto: float, notas: string|null },
+ * ...
+ * ],
+ * pago_notas: string|null (nota general, opcional)
* }
*/
require_once __DIR__ . '/_helpers.php';
@@ -22,33 +24,64 @@ try {
$domId = (int)($datos['domicilio_id'] ?? 0);
$estado = $datos['pago_estado'] ?? '';
- if (!$domId) jsonError('domicilio_id requerido');
+ if (!$domId) jsonError('domicilio_id requerido');
if (!in_array($estado, ['pagado', 'exento', 'pendiente'], true))
jsonError('pago_estado inválido (pagado|exento|pendiente)');
+ $pagos = [];
if ($estado === 'pagado') {
- if (empty($datos['pago_modo']) || !in_array($datos['pago_modo'], ['efectivo','transferencia','otro'], true))
- jsonError('pago_modo inválido (efectivo|transferencia|otro)');
- if (!isset($datos['pago_monto']) || (float)$datos['pago_monto'] <= 0)
- jsonError('pago_monto debe ser mayor a 0');
+ $raw = $datos['pagos'] ?? [];
+ if (!is_array($raw) || empty($raw)) jsonError('Se requiere al menos un método de pago');
+ $modosValidos = ['efectivo', 'transferencia', 'otro'];
+ foreach ($raw as $i => $p) {
+ $modo = $p['modo'] ?? '';
+ $monto = (float)($p['monto'] ?? 0);
+ if (!in_array($modo, $modosValidos, true)) jsonError("Método de pago inválido en fila " . ($i + 1));
+ if ($monto <= 0) jsonError("El monto debe ser mayor a 0 en fila " . ($i + 1));
+ $pagos[] = [
+ 'modo' => $modo,
+ 'monto' => $monto,
+ 'notas' => isset($p['notas']) && $p['notas'] !== '' ? trim($p['notas']) : null,
+ ];
+ }
}
$adminId = adminId();
$dom = new Domicilio();
+ $db = Database::getInstance();
// Verificar que existe
- $exist = Database::getInstance()->fetch(
- 'SELECT id, pago_estado FROM lab_domicilios WHERE id = ?', [$domId]
- );
+ $exist = $db->fetch('SELECT id, pago_estado FROM lab_domicilios WHERE id = ?', [$domId]);
if (!$exist) jsonError("Domicilio #$domId no encontrado");
+ // ── Persistir parcialidades ──────────────────────────────────────────
+ // Eliminar registros previos y reinsertar (permite editar el pago)
+ $db->execute('DELETE FROM lab_domicilio_pagos WHERE domicilio_id = ?', [$domId]);
+
+ $totalMonto = 0;
+ $primerModo = null;
+ $notasGeneral = !empty($datos['pago_notas']) ? trim($datos['pago_notas']) : null;
+
+ if ($estado === 'pagado') {
+ foreach ($pagos as $p) {
+ $db->execute(
+ 'INSERT INTO lab_domicilio_pagos (domicilio_id, modo, monto, notas) VALUES (?, ?, ?, ?)',
+ [$domId, $p['modo'], $p['monto'], $p['notas']]
+ );
+ $totalMonto += $p['monto'];
+ if ($primerModo === null) $primerModo = $p['modo'];
+ }
+ }
+
+ // ── Actualizar campos resumen en lab_domicilios ─────────────────────
+ // pago_modo guarda el primer método (o el único); pago_monto guarda el total.
$campos = [
- 'pago_estado' => $estado,
- 'pago_modo' => ($estado === 'pagado') ? ($datos['pago_modo'] ?? null) : null,
- 'pago_monto' => ($estado === 'pagado') ? ((float)$datos['pago_monto']) : null,
- 'pago_fecha' => ($estado === 'pagado') ? date('Y-m-d H:i:s') : null,
- 'pago_notas' => !empty($datos['pago_notas']) ? trim($datos['pago_notas']) : null,
- 'pago_registrado_por' => $adminId,
+ 'pago_estado' => $estado,
+ 'pago_modo' => ($estado === 'pagado') ? $primerModo : null,
+ 'pago_monto' => ($estado === 'pagado') ? $totalMonto : null,
+ 'pago_fecha' => ($estado === 'pagado') ? date('Y-m-d H:i:s') : null,
+ 'pago_notas' => $notasGeneral,
+ 'pago_registrado_por' => $adminId,
];
$dom->actualizar($domId, $campos, $adminId);
@@ -56,6 +89,8 @@ try {
jsonOk([
'domicilio_id' => $domId,
'pago_estado' => $estado,
+ 'pago_total' => $totalMonto,
+ 'pagos' => $pagos,
], 'Pago registrado correctamente');
} catch (InvalidArgumentException $e) {
diff --git a/classes/lab/Domicilio.php b/classes/lab/Domicilio.php
index 39d7240..d132273 100644
--- a/classes/lab/Domicilio.php
+++ b/classes/lab/Domicilio.php
@@ -163,6 +163,12 @@ class Domicilio {
[$id]
);
+ // Pagos parciales por método de pago
+ $dom['pagos_detalle'] = $this->db->fetchAll(
+ "SELECT modo, monto, notas FROM lab_domicilio_pagos WHERE domicilio_id = ? ORDER BY id ASC",
+ [$id]
+ );
+
// Vínculo: domicilio origen (este fue reprogramado desde otro)
if (!empty($dom['domicilio_origen_id'])) {
$dom['origen_info'] = $this->db->fetch(
diff --git a/enfermero_portal.php b/enfermero_portal.php
index d8aacc0..1ca2271 100644
--- a/enfermero_portal.php
+++ b/enfermero_portal.php
@@ -422,28 +422,25 @@ $hoy = date('Y-m-d');
+
+
-
-
-
-
-
-
-
- $
-
-
+
+
+
+
+ Total:
+ $0
-
-
+
+
+
+ placeholder="Ej: incluye copago Nequi">
- ${item.pago_estado === 'pagado' && item.pago_modo ? `
-
+ ${item.pago_estado === 'pagado' && (item.pagos_detalle || []).length ? `
+
+ ${item.pagos_detalle.map(p =>
+ `
${MODO_LABEL[p.modo] || p.modo}: $${Number(p.monto).toLocaleString('es-CO')}${p.notas ? ` · ${esc(p.notas)}` : ''}
`
+ ).join('')}
+
` :
+ (item.pago_estado === 'pagado' && item.pago_modo ? `
+
${MODO_LABEL[item.pago_modo] || item.pago_modo}
${item.pago_monto ? ` · $${Number(item.pago_monto).toLocaleString('es-CO')}` : ''}
${item.pago_notas ? ` · ${esc(item.pago_notas)}` : ''}
-
` : ''}
+
` : '')}
${item.pago_estado !== 'pagado' && item.pago_estado !== 'exento' ? `
- ${dom.pago_estado === 'pagado' ? `
-
- ${MODO_LABEL[dom.pago_modo] || (dom.pago_modo||'')}
- ${dom.pago_monto ? ` · $${Number(dom.pago_monto).toLocaleString('es-CO')} cobrado` : ''}
- ${dom.pago_notas ? ` · ${esc(dom.pago_notas)}` : ''}
-
` : ''}
+ ${dom.pago_estado === 'pagado' ? (() => {
+ // Usar pagos_detalle (multi-método) si existen
+ const detalle = dom.pagos_detalle || [];
+ if (detalle.length) {
+ return `
+ ${detalle.map(p =>
+ `
${MODO_LABEL[p.modo] || p.modo}: $${Number(p.monto).toLocaleString('es-CO')}${p.notas ? ` · ${esc(p.notas)}` : ''}
`
+ ).join('')}
+
`;
+ }
+ // Fallback registro legacy (un solo método)
+ return `
+ ${MODO_LABEL[dom.pago_modo] || (dom.pago_modo||'')}
+ ${dom.pago_monto ? ` · $${Number(dom.pago_monto).toLocaleString('es-CO')} cobrado` : ''}
+ ${dom.pago_notas ? ` · ${esc(dom.pago_notas)}` : ''}
+
`;
+ })() : ''}
`;
diff --git a/lab_reportes.php b/lab_reportes.php
index fcf2e00..806c224 100644
--- a/lab_reportes.php
+++ b/lab_reportes.php
@@ -812,6 +812,22 @@ async function cargarReportePagos() {
const cliente = f.tipo_cliente === 'seguro' && f.seguro_nombre
? `Seguro: ${esc(f.seguro_nombre)}`
: esc(f.tipo_cliente || 'particular');
+
+ // Columna modo: usar parcialidades si existen, sino legacy
+ const detalle = f.pagos_detalle || [];
+ const modoHtml = pagoEst === 'pagado'
+ ? (detalle.length
+ ? detalle.map(p => `${esc(MODO_LABEL[p.modo] || p.modo)}: $${Number(p.monto).toLocaleString('es-CO')}`).join('
')
+ : esc(MODO_LABEL[f.pago_modo] || f.pago_modo || '—'))
+ : '—';
+
+ // Monto total recibido
+ const montoTotal = pagoEst === 'pagado'
+ ? (detalle.length
+ ? detalle.reduce((s, p) => s + parseFloat(p.monto || 0), 0)
+ : parseFloat(f.pago_monto || 0))
+ : 0;
+
return `
| ${f.id} |
${esc(f.fecha_programada?.slice(0,10) || '—')} |
@@ -823,9 +839,9 @@ async function cargarReportePagos() {
${esc(pagoEst)}
|
- ${esc(f.pago_modo) || '—'} |
+ ${modoHtml} |
- ${pagoEst === 'pagado' ? fmtMoney(f.pago_monto) : '—'}
+ ${pagoEst === 'pagado' ? fmtMoney(montoTotal) : '—'}
|
${f.pago_fecha ? esc(f.pago_fecha.slice(0,16)) : '—'} |
diff --git a/migrations/20260410_lab_domicilio_pagos_parciales.sql b/migrations/20260410_lab_domicilio_pagos_parciales.sql
new file mode 100644
index 0000000..4aec842
--- /dev/null
+++ b/migrations/20260410_lab_domicilio_pagos_parciales.sql
@@ -0,0 +1,34 @@
+-- ============================================================
+-- Migration: 20260410_lab_domicilio_pagos_parciales
+-- Soporta múltiples métodos de pago por domicilio.
+-- ============================================================
+
+CREATE TABLE IF NOT EXISTS `lab_domicilio_pagos` (
+ `id` INT(11) NOT NULL AUTO_INCREMENT,
+ `domicilio_id` INT(11) NOT NULL COMMENT 'FK lab_domicilios.id',
+ `modo` ENUM('efectivo','transferencia','otro') NOT NULL DEFAULT 'efectivo'
+ COMMENT 'Método de pago',
+ `monto` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT 'Monto recibido por este método',
+ `notas` VARCHAR(255) DEFAULT NULL COMMENT 'Referencia / observación del pago',
+ `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ INDEX `idx_domicilio` (`domicilio_id`),
+ CONSTRAINT `fk_ldp_domicilio`
+ FOREIGN KEY (`domicilio_id`) REFERENCES `lab_domicilios` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ COMMENT='Pagos parciales por método de pago para cada domicilio';
+
+-- Migrar registros existentes que ya tienen pago_modo + pago_monto registrados
+INSERT INTO `lab_domicilio_pagos` (`domicilio_id`, `modo`, `monto`, `notas`, `created_at`)
+SELECT id,
+ pago_modo,
+ pago_monto,
+ pago_notas,
+ COALESCE(pago_fecha, created_at)
+FROM `lab_domicilios`
+WHERE pago_estado = 'pagado'
+ AND pago_modo IS NOT NULL
+ AND pago_monto > 0
+ AND NOT EXISTS (
+ SELECT 1 FROM `lab_domicilio_pagos` ldp WHERE ldp.domicilio_id = `lab_domicilios`.id
+ );
|