up
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
+53
-18
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
+104
-45
@@ -422,28 +422,25 @@ $hoy = date('Y-m-d');
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Métodos de pago (múltiples) -->
|
||||
<div id="pago-campos-pago">
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Modo de pago <span class="text-danger">*</span></label>
|
||||
<select class="form-select form-select-sm" id="pago-modo">
|
||||
<option value="efectivo">💵 Efectivo</option>
|
||||
<option value="transferencia">📲 Transferencia</option>
|
||||
<option value="otro">📋 Otro</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Monto cobrado <span class="text-danger">*</span></label>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" class="form-control" id="pago-monto"
|
||||
placeholder="0" min="0" step="1000">
|
||||
</div>
|
||||
<label class="form-label small fw-semibold">Métodos de pago <span class="text-danger">*</span></label>
|
||||
<div id="pago-metodos-lista"></div>
|
||||
<button type="button" class="btn btn-outline-success btn-sm mt-1 w-100"
|
||||
onclick="pagoModal.agregarMetodo()">
|
||||
<i class="fas fa-plus me-1"></i>Agregar método de pago
|
||||
</button>
|
||||
<div class="d-flex justify-content-between align-items-center mt-2 pt-2 border-top">
|
||||
<span class="small fw-semibold text-muted">Total:</span>
|
||||
<span class="fw-bold text-success" id="pago-total-label">$0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Notas / referencia</label>
|
||||
|
||||
<div class="mb-2 mt-2">
|
||||
<label class="form-label small">Notas generales</label>
|
||||
<input type="text" class="form-control form-control-sm" id="pago-notas"
|
||||
placeholder="Ej: nequi ref #123456">
|
||||
placeholder="Ej: incluye copago Nequi">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer py-2">
|
||||
@@ -604,12 +601,18 @@ const portal = {
|
||||
${item.valor_copago ? `<span>Copago: <strong>$${Number(item.valor_copago).toLocaleString('es-CO')}</strong></span>` : ''}
|
||||
<span class="fw-bold text-success">Total: <strong>$${totalCobro.toLocaleString('es-CO')}</strong></span>
|
||||
</div>
|
||||
${item.pago_estado === 'pagado' && item.pago_modo ? `
|
||||
<div class="small text-success">
|
||||
${item.pago_estado === 'pagado' && (item.pagos_detalle || []).length ? `
|
||||
<div class="small text-success mt-1">
|
||||
${item.pagos_detalle.map(p =>
|
||||
`<div>${MODO_LABEL[p.modo] || p.modo}: <strong>$${Number(p.monto).toLocaleString('es-CO')}</strong>${p.notas ? ` · <em>${esc(p.notas)}</em>` : ''}</div>`
|
||||
).join('')}
|
||||
</div>` :
|
||||
(item.pago_estado === 'pagado' && item.pago_modo ? `
|
||||
<div class="small text-success mt-1">
|
||||
${MODO_LABEL[item.pago_modo] || item.pago_modo}
|
||||
${item.pago_monto ? ` · $${Number(item.pago_monto).toLocaleString('es-CO')}` : ''}
|
||||
${item.pago_notas ? ` · <em>${esc(item.pago_notas)}</em>` : ''}
|
||||
</div>` : ''}
|
||||
</div>` : '')}
|
||||
${item.pago_estado !== 'pagado' && item.pago_estado !== 'exento' ? `
|
||||
<button class="btn btn-sm mt-2 text-white w-100" style="background:#065f46;font-size:.78rem"
|
||||
onclick="pagoModal.abrir(${item.domicilio_id}, ${totalCobro})">
|
||||
@@ -1898,23 +1901,29 @@ const formEnvio = {
|
||||
};
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// OBJETO: pagoModal — Gestión del modal de registro de pago
|
||||
// OBJETO: pagoModal — Gestión del modal de registro de pago (multi-método)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
const pagoModal = {
|
||||
_bsModal: null,
|
||||
_filas: [], // [{ modo, monto, notas }]
|
||||
|
||||
// Abrir modal pre-cargado con domicilio y monto sugerido
|
||||
abrir(domId, totalSugerido = 0) {
|
||||
document.getElementById('pago-dom-id').value = domId;
|
||||
document.getElementById('pago-monto').value = totalSugerido > 0 ? totalSugerido : '';
|
||||
document.getElementById('pago-notas').value = '';
|
||||
document.getElementById('pago-modo').value = 'efectivo';
|
||||
|
||||
// Recuperar estado actual desde la agenda cargada
|
||||
// Recuperar estado y pagos actuales desde la agenda cargada
|
||||
const item = (portal._agenda || []).find(d => d.domicilio_id == domId);
|
||||
const est = item ? (item.pago_estado || 'pendiente') : 'pendiente';
|
||||
document.querySelector(`input[name="pago-estado"][value="${est}"]`).checked = true;
|
||||
|
||||
// Cargar filas existentes o inicializar con una vacía
|
||||
const existentes = (item && item.pagos_detalle && item.pagos_detalle.length)
|
||||
? item.pagos_detalle
|
||||
: [{ modo: 'efectivo', monto: totalSugerido > 0 ? totalSugerido : '', notas: '' }];
|
||||
|
||||
this._filas = existentes.map(p => ({ modo: p.modo || 'efectivo', monto: p.monto || '', notas: p.notas || '' }));
|
||||
this._renderFilas();
|
||||
this.toggleModo();
|
||||
|
||||
if (!this._bsModal) {
|
||||
@@ -1923,7 +1932,54 @@ const pagoModal = {
|
||||
this._bsModal.show();
|
||||
},
|
||||
|
||||
// Mostrar/ocultar campos de modo y monto según estado
|
||||
// Renderiza la lista de filas de métodos de pago
|
||||
_renderFilas() {
|
||||
const lista = document.getElementById('pago-metodos-lista');
|
||||
if (!lista) return;
|
||||
lista.innerHTML = this._filas.map((f, i) => `
|
||||
<div class="d-flex gap-1 align-items-center mb-2" id="pago-fila-${i}">
|
||||
<select class="form-select form-select-sm" style="max-width:130px"
|
||||
onchange="pagoModal._filas[${i}].modo=this.value">
|
||||
<option value="efectivo" ${f.modo==='efectivo' ? 'selected':''}>💵 Efectivo</option>
|
||||
<option value="transferencia" ${f.modo==='transferencia' ? 'selected':''}>📲 Transferencia</option>
|
||||
<option value="otro" ${f.modo==='otro' ? 'selected':''}>📋 Otro</option>
|
||||
</select>
|
||||
<div class="input-group input-group-sm" style="flex:1">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" class="form-control" placeholder="0" min="0" step="1000"
|
||||
value="${f.monto !== '' ? f.monto : ''}"
|
||||
oninput="pagoModal._filas[${i}].monto=parseFloat(this.value)||0;pagoModal._actualizarTotal()"
|
||||
>
|
||||
</div>
|
||||
<input type="text" class="form-control form-control-sm" style="flex:1;min-width:60px"
|
||||
placeholder="Ref." value="${esc(String(f.notas||''))}"
|
||||
oninput="pagoModal._filas[${i}].notas=this.value">
|
||||
${this._filas.length > 1
|
||||
? `<button type="button" class="btn btn-sm btn-outline-danger px-2"
|
||||
onclick="pagoModal.eliminarFila(${i})"><i class="fas fa-times"></i></button>`
|
||||
: ''}
|
||||
</div>`
|
||||
).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) {
|
||||
|
||||
+17
-6
@@ -894,12 +894,23 @@ async function verDomicilio(id) {
|
||||
${dom.valor_copago ? `<span class="small">Copago: <strong>$${Number(dom.valor_copago).toLocaleString('es-CO')}</strong></span>` : ''}
|
||||
${totalC > 0 ? `<span class="small fw-bold text-success">Total: $${totalC.toLocaleString('es-CO')}</span>` : ''}
|
||||
</div>
|
||||
${dom.pago_estado === 'pagado' ? `
|
||||
<div class="small mt-1 text-success">
|
||||
${MODO_LABEL[dom.pago_modo] || (dom.pago_modo||'')}
|
||||
${dom.pago_monto ? ` · $${Number(dom.pago_monto).toLocaleString('es-CO')} cobrado` : ''}
|
||||
${dom.pago_notas ? ` · <em>${esc(dom.pago_notas)}</em>` : ''}
|
||||
</div>` : ''}
|
||||
${dom.pago_estado === 'pagado' ? (() => {
|
||||
// Usar pagos_detalle (multi-método) si existen
|
||||
const detalle = dom.pagos_detalle || [];
|
||||
if (detalle.length) {
|
||||
return `<div class="small mt-1 text-success">
|
||||
${detalle.map(p =>
|
||||
`<div>${MODO_LABEL[p.modo] || p.modo}: <strong>$${Number(p.monto).toLocaleString('es-CO')}</strong>${p.notas ? ` · <em>${esc(p.notas)}</em>` : ''}</div>`
|
||||
).join('')}
|
||||
</div>`;
|
||||
}
|
||||
// Fallback registro legacy (un solo método)
|
||||
return `<div class="small mt-1 text-success">
|
||||
${MODO_LABEL[dom.pago_modo] || (dom.pago_modo||'')}
|
||||
${dom.pago_monto ? ` · $${Number(dom.pago_monto).toLocaleString('es-CO')} cobrado` : ''}
|
||||
${dom.pago_notas ? ` · <em>${esc(dom.pago_notas)}</em>` : ''}
|
||||
</div>`;
|
||||
})() : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
+18
-2
@@ -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('<br>')
|
||||
: 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 `<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>
|
||||
@@ -823,9 +839,9 @@ async function cargarReportePagos() {
|
||||
<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-start small" style="white-space:pre-line">${modoHtml}</td>
|
||||
<td class="text-end fw-semibold ${pagoEst === 'pagado' ? 'text-success' : 'text-muted'}">
|
||||
${pagoEst === 'pagado' ? fmtMoney(f.pago_monto) : '—'}
|
||||
${pagoEst === 'pagado' ? fmtMoney(montoTotal) : '—'}
|
||||
</td>
|
||||
<td class="text-nowrap text-muted small">${f.pago_fecha ? esc(f.pago_fecha.slice(0,16)) : '—'}</td>
|
||||
<td>
|
||||
|
||||
@@ -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
|
||||
);
|
||||
Reference in New Issue
Block a user