up
This commit is contained in:
@@ -90,7 +90,24 @@ $filas = $db->fetchAll(
|
|||||||
$params
|
$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 = [
|
$resumen = [
|
||||||
'total' => count($filas),
|
'total' => count($filas),
|
||||||
'pagados' => 0,
|
'pagados' => 0,
|
||||||
@@ -104,9 +121,21 @@ foreach ($filas as $f) {
|
|||||||
$est = $f['pago_estado'] ?? 'pendiente';
|
$est = $f['pago_estado'] ?? 'pendiente';
|
||||||
if ($est === 'pagado') {
|
if ($est === 'pagado') {
|
||||||
$resumen['pagados']++;
|
$resumen['pagados']++;
|
||||||
$resumen['monto_total'] += (float)$f['pago_monto'];
|
if (!empty($f['pagos_detalle'])) {
|
||||||
$key = $f['pago_modo'] ?: 'sin_registrar';
|
// Sumar desde parcialidades
|
||||||
$resumen['por_modo'][$key] = ($resumen['por_modo'][$key] ?? 0.0) + (float)$f['pago_monto'];
|
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') {
|
} elseif ($est === 'exento') {
|
||||||
$resumen['exentos']++;
|
$resumen['exentos']++;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -32,12 +32,18 @@ try {
|
|||||||
// Agenda del día con pleno detalle
|
// Agenda del día con pleno detalle
|
||||||
$agenda = $enf->agenda($eid, $fecha);
|
$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();
|
$db = Database::getInstance();
|
||||||
foreach ($agenda as &$item) {
|
foreach ($agenda as &$item) {
|
||||||
|
$domId = (int)$item['domicilio_id'];
|
||||||
$item['servicios_extra'] = $db->fetchAll(
|
$item['servicios_extra'] = $db->fetchAll(
|
||||||
"SELECT * FROM lab_servicios_extra WHERE domicilio_id = ? ORDER BY created_at ASC",
|
"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);
|
unset($item);
|
||||||
|
|||||||
+53
-18
@@ -2,15 +2,17 @@
|
|||||||
/**
|
/**
|
||||||
* POST /api/lab/registrar_pago.php
|
* POST /api/lab/registrar_pago.php
|
||||||
* Registra el pago de un domicilio por parte del cliente.
|
* 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:
|
* Body JSON:
|
||||||
* {
|
* {
|
||||||
* domicilio_id: int,
|
* domicilio_id: int,
|
||||||
* pago_estado: 'pagado' | 'exento' | 'pendiente',
|
* pago_estado: 'pagado' | 'exento' | 'pendiente',
|
||||||
* pago_modo: 'efectivo' | 'transferencia' | 'otro', (requerido si pagado)
|
* pagos: [ (requerido si pagado)
|
||||||
* pago_monto: float, (requerido si pagado)
|
* { modo: 'efectivo'|'transferencia'|'otro', monto: float, notas: string|null },
|
||||||
* pago_notas: string|null
|
* ...
|
||||||
|
* ],
|
||||||
|
* pago_notas: string|null (nota general, opcional)
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
require_once __DIR__ . '/_helpers.php';
|
require_once __DIR__ . '/_helpers.php';
|
||||||
@@ -22,33 +24,64 @@ try {
|
|||||||
$domId = (int)($datos['domicilio_id'] ?? 0);
|
$domId = (int)($datos['domicilio_id'] ?? 0);
|
||||||
$estado = $datos['pago_estado'] ?? '';
|
$estado = $datos['pago_estado'] ?? '';
|
||||||
|
|
||||||
if (!$domId) jsonError('domicilio_id requerido');
|
if (!$domId) jsonError('domicilio_id requerido');
|
||||||
if (!in_array($estado, ['pagado', 'exento', 'pendiente'], true))
|
if (!in_array($estado, ['pagado', 'exento', 'pendiente'], true))
|
||||||
jsonError('pago_estado inválido (pagado|exento|pendiente)');
|
jsonError('pago_estado inválido (pagado|exento|pendiente)');
|
||||||
|
|
||||||
|
$pagos = [];
|
||||||
if ($estado === 'pagado') {
|
if ($estado === 'pagado') {
|
||||||
if (empty($datos['pago_modo']) || !in_array($datos['pago_modo'], ['efectivo','transferencia','otro'], true))
|
$raw = $datos['pagos'] ?? [];
|
||||||
jsonError('pago_modo inválido (efectivo|transferencia|otro)');
|
if (!is_array($raw) || empty($raw)) jsonError('Se requiere al menos un método de pago');
|
||||||
if (!isset($datos['pago_monto']) || (float)$datos['pago_monto'] <= 0)
|
$modosValidos = ['efectivo', 'transferencia', 'otro'];
|
||||||
jsonError('pago_monto debe ser mayor a 0');
|
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();
|
$adminId = adminId();
|
||||||
$dom = new Domicilio();
|
$dom = new Domicilio();
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
// Verificar que existe
|
// Verificar que existe
|
||||||
$exist = Database::getInstance()->fetch(
|
$exist = $db->fetch('SELECT id, pago_estado FROM lab_domicilios WHERE id = ?', [$domId]);
|
||||||
'SELECT id, pago_estado FROM lab_domicilios WHERE id = ?', [$domId]
|
|
||||||
);
|
|
||||||
if (!$exist) jsonError("Domicilio #$domId no encontrado");
|
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 = [
|
$campos = [
|
||||||
'pago_estado' => $estado,
|
'pago_estado' => $estado,
|
||||||
'pago_modo' => ($estado === 'pagado') ? ($datos['pago_modo'] ?? null) : null,
|
'pago_modo' => ($estado === 'pagado') ? $primerModo : null,
|
||||||
'pago_monto' => ($estado === 'pagado') ? ((float)$datos['pago_monto']) : null,
|
'pago_monto' => ($estado === 'pagado') ? $totalMonto : null,
|
||||||
'pago_fecha' => ($estado === 'pagado') ? date('Y-m-d H:i:s') : 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_notas' => $notasGeneral,
|
||||||
'pago_registrado_por' => $adminId,
|
'pago_registrado_por' => $adminId,
|
||||||
];
|
];
|
||||||
|
|
||||||
$dom->actualizar($domId, $campos, $adminId);
|
$dom->actualizar($domId, $campos, $adminId);
|
||||||
@@ -56,6 +89,8 @@ try {
|
|||||||
jsonOk([
|
jsonOk([
|
||||||
'domicilio_id' => $domId,
|
'domicilio_id' => $domId,
|
||||||
'pago_estado' => $estado,
|
'pago_estado' => $estado,
|
||||||
|
'pago_total' => $totalMonto,
|
||||||
|
'pagos' => $pagos,
|
||||||
], 'Pago registrado correctamente');
|
], 'Pago registrado correctamente');
|
||||||
|
|
||||||
} catch (InvalidArgumentException $e) {
|
} catch (InvalidArgumentException $e) {
|
||||||
|
|||||||
@@ -163,6 +163,12 @@ class Domicilio {
|
|||||||
[$id]
|
[$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)
|
// Vínculo: domicilio origen (este fue reprogramado desde otro)
|
||||||
if (!empty($dom['domicilio_origen_id'])) {
|
if (!empty($dom['domicilio_origen_id'])) {
|
||||||
$dom['origen_info'] = $this->db->fetch(
|
$dom['origen_info'] = $this->db->fetch(
|
||||||
|
|||||||
+104
-45
@@ -422,28 +422,25 @@ $hoy = date('Y-m-d');
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Métodos de pago (múltiples) -->
|
||||||
<div id="pago-campos-pago">
|
<div id="pago-campos-pago">
|
||||||
<div class="mb-3">
|
<label class="form-label small fw-semibold">Métodos de pago <span class="text-danger">*</span></label>
|
||||||
<label class="form-label small fw-semibold">Modo de pago <span class="text-danger">*</span></label>
|
<div id="pago-metodos-lista"></div>
|
||||||
<select class="form-select form-select-sm" id="pago-modo">
|
<button type="button" class="btn btn-outline-success btn-sm mt-1 w-100"
|
||||||
<option value="efectivo">💵 Efectivo</option>
|
onclick="pagoModal.agregarMetodo()">
|
||||||
<option value="transferencia">📲 Transferencia</option>
|
<i class="fas fa-plus me-1"></i>Agregar método de pago
|
||||||
<option value="otro">📋 Otro</option>
|
</button>
|
||||||
</select>
|
<div class="d-flex justify-content-between align-items-center mt-2 pt-2 border-top">
|
||||||
</div>
|
<span class="small fw-semibold text-muted">Total:</span>
|
||||||
<div class="mb-3">
|
<span class="fw-bold text-success" id="pago-total-label">$0</span>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</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"
|
<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>
|
</div>
|
||||||
<div class="modal-footer py-2">
|
<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>` : ''}
|
${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>
|
<span class="fw-bold text-success">Total: <strong>$${totalCobro.toLocaleString('es-CO')}</strong></span>
|
||||||
</div>
|
</div>
|
||||||
${item.pago_estado === 'pagado' && item.pago_modo ? `
|
${item.pago_estado === 'pagado' && (item.pagos_detalle || []).length ? `
|
||||||
<div class="small text-success">
|
<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}
|
${MODO_LABEL[item.pago_modo] || item.pago_modo}
|
||||||
${item.pago_monto ? ` · $${Number(item.pago_monto).toLocaleString('es-CO')}` : ''}
|
${item.pago_monto ? ` · $${Number(item.pago_monto).toLocaleString('es-CO')}` : ''}
|
||||||
${item.pago_notas ? ` · <em>${esc(item.pago_notas)}</em>` : ''}
|
${item.pago_notas ? ` · <em>${esc(item.pago_notas)}</em>` : ''}
|
||||||
</div>` : ''}
|
</div>` : '')}
|
||||||
${item.pago_estado !== 'pagado' && item.pago_estado !== 'exento' ? `
|
${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"
|
<button class="btn btn-sm mt-2 text-white w-100" style="background:#065f46;font-size:.78rem"
|
||||||
onclick="pagoModal.abrir(${item.domicilio_id}, ${totalCobro})">
|
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 = {
|
const pagoModal = {
|
||||||
_bsModal: null,
|
_bsModal: null,
|
||||||
|
_filas: [], // [{ modo, monto, notas }]
|
||||||
|
|
||||||
// Abrir modal pre-cargado con domicilio y monto sugerido
|
// Abrir modal pre-cargado con domicilio y monto sugerido
|
||||||
abrir(domId, totalSugerido = 0) {
|
abrir(domId, totalSugerido = 0) {
|
||||||
document.getElementById('pago-dom-id').value = domId;
|
document.getElementById('pago-dom-id').value = domId;
|
||||||
document.getElementById('pago-monto').value = totalSugerido > 0 ? totalSugerido : '';
|
|
||||||
document.getElementById('pago-notas').value = '';
|
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 item = (portal._agenda || []).find(d => d.domicilio_id == domId);
|
||||||
const est = item ? (item.pago_estado || 'pendiente') : 'pendiente';
|
const est = item ? (item.pago_estado || 'pendiente') : 'pendiente';
|
||||||
document.querySelector(`input[name="pago-estado"][value="${est}"]`).checked = true;
|
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();
|
this.toggleModo();
|
||||||
|
|
||||||
if (!this._bsModal) {
|
if (!this._bsModal) {
|
||||||
@@ -1923,7 +1932,54 @@ const pagoModal = {
|
|||||||
this._bsModal.show();
|
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() {
|
toggleModo() {
|
||||||
const est = document.querySelector('input[name="pago-estado"]:checked')?.value;
|
const est = document.querySelector('input[name="pago-estado"]:checked')?.value;
|
||||||
document.getElementById('pago-campos-pago').style.display =
|
document.getElementById('pago-campos-pago').style.display =
|
||||||
@@ -1932,23 +1988,26 @@ const pagoModal = {
|
|||||||
|
|
||||||
// Guardar pago vía API
|
// Guardar pago vía API
|
||||||
async guardar() {
|
async guardar() {
|
||||||
const domId = document.getElementById('pago-dom-id').value;
|
const domId = document.getElementById('pago-dom-id').value;
|
||||||
const estado = document.querySelector('input[name="pago-estado"]:checked')?.value;
|
const estado = document.querySelector('input[name="pago-estado"]:checked')?.value;
|
||||||
const modo = document.getElementById('pago-modo').value;
|
const notas = document.getElementById('pago-notas').value.trim();
|
||||||
const monto = parseFloat(document.getElementById('pago-monto').value) || 0;
|
|
||||||
const notas = document.getElementById('pago-notas').value.trim();
|
|
||||||
|
|
||||||
if (!estado) { alert('Selecciona el estado del pago.'); return; }
|
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.');
|
let pagos = [];
|
||||||
return;
|
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 };
|
const payload = { domicilio_id: domId, pago_estado: estado, pagos };
|
||||||
if (estado === 'pagado') {
|
|
||||||
payload.pago_modo = modo;
|
|
||||||
payload.pago_monto = monto;
|
|
||||||
}
|
|
||||||
if (notas) payload.pago_notas = notas;
|
if (notas) payload.pago_notas = notas;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1960,16 +2019,16 @@ const pagoModal = {
|
|||||||
const res = await r.json();
|
const res = await r.json();
|
||||||
if (!res.success) throw new Error(res.error || 'Error al guardar');
|
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);
|
const idx = (portal._agenda || []).findIndex(d => d.domicilio_id == domId);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
portal._agenda[idx].pago_estado = estado;
|
portal._agenda[idx].pago_estado = estado;
|
||||||
portal._agenda[idx].pago_modo = estado === 'pagado' ? modo : null;
|
portal._agenda[idx].pago_monto = estado === 'pagado' ? res.pago_total : null;
|
||||||
portal._agenda[idx].pago_monto = estado === 'pagado' ? monto : null;
|
portal._agenda[idx].pago_modo = estado === 'pagado' && pagos.length ? pagos[0].modo : null;
|
||||||
portal._agenda[idx].pago_notas = notas || 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();
|
if (this._bsModal) this._bsModal.hide();
|
||||||
portal._renderLista();
|
portal._renderLista();
|
||||||
} catch(e) {
|
} 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>` : ''}
|
${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>` : ''}
|
${totalC > 0 ? `<span class="small fw-bold text-success">Total: $${totalC.toLocaleString('es-CO')}</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
${dom.pago_estado === 'pagado' ? `
|
${dom.pago_estado === 'pagado' ? (() => {
|
||||||
<div class="small mt-1 text-success">
|
// Usar pagos_detalle (multi-método) si existen
|
||||||
${MODO_LABEL[dom.pago_modo] || (dom.pago_modo||'')}
|
const detalle = dom.pagos_detalle || [];
|
||||||
${dom.pago_monto ? ` · $${Number(dom.pago_monto).toLocaleString('es-CO')} cobrado` : ''}
|
if (detalle.length) {
|
||||||
${dom.pago_notas ? ` · <em>${esc(dom.pago_notas)}</em>` : ''}
|
return `<div class="small mt-1 text-success">
|
||||||
</div>` : ''}
|
${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>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|||||||
+18
-2
@@ -812,6 +812,22 @@ async function cargarReportePagos() {
|
|||||||
const cliente = f.tipo_cliente === 'seguro' && f.seguro_nombre
|
const cliente = f.tipo_cliente === 'seguro' && f.seguro_nombre
|
||||||
? `Seguro: ${esc(f.seguro_nombre)}`
|
? `Seguro: ${esc(f.seguro_nombre)}`
|
||||||
: esc(f.tipo_cliente || 'particular');
|
: 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'}">
|
return `<tr class="${pagoEst === 'pagado' ? '' : 'opacity-75'}">
|
||||||
<td class="ps-3 text-muted">${f.id}</td>
|
<td class="ps-3 text-muted">${f.id}</td>
|
||||||
<td class="text-nowrap">${esc(f.fecha_programada?.slice(0,10) || '—')}</td>
|
<td class="text-nowrap">${esc(f.fecha_programada?.slice(0,10) || '—')}</td>
|
||||||
@@ -823,9 +839,9 @@ async function cargarReportePagos() {
|
|||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<span class="badge bg-${PAGO_BADGE[pagoEst] || 'secondary'}">${esc(pagoEst)}</span>
|
<span class="badge bg-${PAGO_BADGE[pagoEst] || 'secondary'}">${esc(pagoEst)}</span>
|
||||||
</td>
|
</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'}">
|
<td class="text-end fw-semibold ${pagoEst === 'pagado' ? 'text-success' : 'text-muted'}">
|
||||||
${pagoEst === 'pagado' ? fmtMoney(f.pago_monto) : '—'}
|
${pagoEst === 'pagado' ? fmtMoney(montoTotal) : '—'}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-nowrap text-muted small">${f.pago_fecha ? esc(f.pago_fecha.slice(0,16)) : '—'}</td>
|
<td class="text-nowrap text-muted small">${f.pago_fecha ? esc(f.pago_fecha.slice(0,16)) : '—'}</td>
|
||||||
<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