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' ? ` ` + : ''} +
` + ).join(''); + this._actualizarTotal(); + }, + + _actualizarTotal() { + const sum = this._filas.reduce((s, f) => s + (parseFloat(f.monto) || 0), 0); + const lbl = document.getElementById('pago-total-label'); + if (lbl) lbl.textContent = '$' + sum.toLocaleString('es-CO'); + }, + + agregarMetodo() { + this._filas.push({ modo: 'efectivo', monto: '', notas: '' }); + this._renderFilas(); + }, + + eliminarFila(i) { + this._filas.splice(i, 1); + this._renderFilas(); + }, + + // Mostrar/ocultar sección de métodos según estado toggleModo() { const est = document.querySelector('input[name="pago-estado"]:checked')?.value; document.getElementById('pago-campos-pago').style.display = @@ -1932,23 +1988,26 @@ const pagoModal = { // Guardar pago vía API async guardar() { - const domId = document.getElementById('pago-dom-id').value; - const estado = document.querySelector('input[name="pago-estado"]:checked')?.value; - const modo = document.getElementById('pago-modo').value; - const monto = parseFloat(document.getElementById('pago-monto').value) || 0; - const notas = document.getElementById('pago-notas').value.trim(); + const domId = document.getElementById('pago-dom-id').value; + const estado = document.querySelector('input[name="pago-estado"]:checked')?.value; + const notas = document.getElementById('pago-notas').value.trim(); if (!estado) { alert('Selecciona el estado del pago.'); return; } - if (estado === 'pagado' && (!modo || monto <= 0)) { - alert('Para marcar como pagado debes ingresar el modo y el monto cobrado.'); - return; + + let pagos = []; + if (estado === 'pagado') { + pagos = this._filas.map(f => ({ + modo: f.modo, + monto: parseFloat(f.monto) || 0, + notas: f.notas || null, + })); + if (!pagos.length || pagos.some(p => p.monto <= 0)) { + alert('Ingresa al menos un método de pago con monto mayor a 0.'); + return; + } } - const payload = { domicilio_id: domId, pago_estado: estado }; - if (estado === 'pagado') { - payload.pago_modo = modo; - payload.pago_monto = monto; - } + const payload = { domicilio_id: domId, pago_estado: estado, pagos }; if (notas) payload.pago_notas = notas; try { @@ -1960,16 +2019,16 @@ const pagoModal = { const res = await r.json(); if (!res.success) throw new Error(res.error || 'Error al guardar'); - // Actualizar item en la agenda local para evitar recarga completa + // Actualizar item en la agenda local const idx = (portal._agenda || []).findIndex(d => d.domicilio_id == domId); if (idx >= 0) { - portal._agenda[idx].pago_estado = estado; - portal._agenda[idx].pago_modo = estado === 'pagado' ? modo : null; - portal._agenda[idx].pago_monto = estado === 'pagado' ? monto : null; - portal._agenda[idx].pago_notas = notas || null; + portal._agenda[idx].pago_estado = estado; + portal._agenda[idx].pago_monto = estado === 'pagado' ? res.pago_total : null; + portal._agenda[idx].pago_modo = estado === 'pagado' && pagos.length ? pagos[0].modo : null; + portal._agenda[idx].pago_notas = notas || null; + portal._agenda[idx].pagos_detalle = estado === 'pagado' ? pagos : []; } - // Cerrar modal y re-renderizar if (this._bsModal) this._bsModal.hide(); portal._renderLista(); } catch(e) { diff --git a/lab_domicilios.php b/lab_domicilios.php index 17498a3..ed5e19c 100644 --- a/lab_domicilios.php +++ b/lab_domicilios.php @@ -894,12 +894,23 @@ async function verDomicilio(id) { ${dom.valor_copago ? `Copago: $${Number(dom.valor_copago).toLocaleString('es-CO')}` : ''} ${totalC > 0 ? `Total: $${totalC.toLocaleString('es-CO')}` : ''} - ${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 + );