feat: sistema de pagos domicilios — cobro enfermero y estado pago admin

- database/07_add_pago_domicilio.sql: 6 columnas nuevas en lab_domicilios
  (pago_estado ENUM, pago_modo, pago_monto, pago_fecha, pago_notas, pago_registrado_por)
- Domicilio::filtrarCampos(): permite guardar campos de pago vía actualizar()
- Enfermera::agenda(): incluye valor_domicilio, valor_copago, pago_estado, pago_modo, pago_monto, pago_notas
- api/lab/registrar_pago.php: endpoint POST para registrar/actualizar pago del domicilio
- enfermero_portal.php: bloque cobro en card, modal registrar pago (efectivo/transferencia/exento), objeto pagoModal JS
- lab_domicilios.php: badge pago //🔵 en fila tabla, sección pago detallada en panel lateral
This commit is contained in:
Lizandro Guarnizo
2026-03-12 22:53:24 -05:00
parent c42e0976d7
commit beb1bbe1d4
6 changed files with 353 additions and 11 deletions
+202
View File
@@ -110,6 +110,13 @@ $hoy = date('Y-m-d');
border-bottom:1px solid #f0f0f0; }
.sugerencias-pac .item:hover { background:#f0f4ff; }
.sugerencias-pac .item:last-child { border-bottom:none; }
/* ── Pago ── */
.pago-badge-pendiente { background:#fff3cd; color:#664d03; border:1px solid #ffc107; }
.pago-badge-pagado { background:#d1fae5; color:#065f46; border:1px solid #34d399; }
.pago-badge-exento { background:#e0e7ff; color:#3730a3; border:1px solid #818cf8; }
.cobro-box { background:#f0fdf4; border:1px solid #86efac; border-radius:8px; padding:.6rem .9rem; }
.cobro-box.sin-cobro { background:#f9fafb; border-color:#e5e7eb; color:#9ca3af; }
</style>
</head>
<body>
@@ -242,6 +249,74 @@ $hoy = date('Y-m-d');
</div>
</div>
<!-- ═══════════════════════ MODAL: Registrar Pago ══════════════════ -->
<div class="modal fade" id="modalPago" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header py-2" style="background:#065f46;color:#fff">
<h6 class="modal-title mb-0"><i class="fas fa-money-bill-wave me-1"></i>Registrar Pago</h6>
<button class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="pago-dom-id">
<div class="mb-3">
<label class="form-label small fw-semibold">Estado del pago</label>
<div class="d-flex gap-2 flex-wrap">
<div class="form-check">
<input class="form-check-input" type="radio" name="pago-estado"
id="pago-pagado" value="pagado" checked
onchange="pagoModal.toggleModo()">
<label class="form-check-label small" for="pago-pagado">💵 Pagado</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="pago-estado"
id="pago-exento" value="exento"
onchange="pagoModal.toggleModo()">
<label class="form-check-label small" for="pago-exento">🔵 Exento</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="pago-estado"
id="pago-pendiente" value="pendiente"
onchange="pagoModal.toggleModo()">
<label class="form-check-label small" for="pago-pendiente">⏳ Pendiente</label>
</div>
</div>
</div>
<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>
</div>
</div>
<div class="mb-2">
<label class="form-label small">Notas / referencia</label>
<input type="text" class="form-control form-control-sm" id="pago-notas"
placeholder="Ej: nequi ref #123456">
</div>
</div>
<div class="modal-footer py-2">
<button class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancelar</button>
<button class="btn btn-sm text-white" style="background:#065f46"
onclick="pagoModal.guardar()">
<i class="fas fa-save me-1"></i>Guardar pago
</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
const ENFERMERA_ID = <?= (int)$enfId ?>;
@@ -341,6 +416,49 @@ const portal = {
`<span class="se-chip">${esc(se.descripcion)}</span>`
).join(' ');
// ── Bloque de cobro ─────────────────────────────────────────────
const totalCobro = (parseFloat(item.valor_domicilio)||0) + (parseFloat(item.valor_copago)||0);
const hayCobro = totalCobro > 0;
const PAGO_LABEL = {
pendiente: { cls:'pago-badge-pendiente', icon:'⏳', txt:'Pago pendiente' },
pagado: { cls:'pago-badge-pagado', icon:'✅', txt:'Pagado' },
exento: { cls:'pago-badge-exento', icon:'🔵', txt:'Exento de pago' },
};
const pagoEst = item.pago_estado || 'pendiente';
const pagoInfo = PAGO_LABEL[pagoEst] || PAGO_LABEL.pendiente;
const MODO_LABEL = { efectivo:'💵 Efectivo', transferencia:'📲 Transferencia', otro:'📋 Otro' };
const cobroHtml = `
<div class="cobro-box ${hayCobro ? '' : 'sin-cobro'} mt-2 mb-2">
${hayCobro ? `
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="fw-semibold small"><i class="fas fa-cash-register me-1 text-success"></i>Cobro al cliente</span>
<span class="badge rounded-pill ${pagoInfo.cls} small px-2">${pagoInfo.icon} ${pagoInfo.txt}</span>
</div>
<div class="d-flex flex-wrap gap-2 small mb-1">
${item.valor_domicilio ? `<span>Domicilio: <strong>$${Number(item.valor_domicilio).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>
</div>
${item.pago_estado === 'pagado' && item.pago_modo ? `
<div class="small text-success">
${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>` : ''}
${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})">
<i class="fas fa-money-bill-wave me-1"></i>Registrar pago
</button>` : `
<button class="btn btn-sm btn-outline-secondary mt-2 w-100" style="font-size:.75rem"
onclick="pagoModal.abrir(${item.domicilio_id}, ${totalCobro})">
<i class="fas fa-edit me-1"></i>Editar pago
</button>`}
` : `<span class="small"><i class="fas fa-info-circle me-1"></i>Sin cobro registrado para este servicio</span>`}
</div>`;
// ────────────────────────────────────────────────────────────────
const btnsAccion = trans.map(t =>
`<button class="btn btn-${t.color} btn-accion"
onclick="portal.actualizarEstado(${item.domicilio_id},'${t.estado}')">
@@ -412,6 +530,9 @@ const portal = {
? `<div class="small mb-2"><span class="badge bg-info text-dark"><i class="fas fa-shield-alt me-1"></i>Seguro${item.seguro_nombre ? ': ' + esc(item.seguro_nombre) : ''}</span></div>`
: ''}
<!-- Cobro y pago -->
${cobroHtml}
<!-- Orden médica (imagen) -->
${item.orden_local_file ? `
<div class="mt-2 mb-2">
@@ -1030,6 +1151,87 @@ const formEnvio = {
} catch(e) { alert('No se pudo copiar'); }
},
};
// ══════════════════════════════════════════════════════════
// OBJETO: pagoModal — Gestión del modal de registro de pago
// ══════════════════════════════════════════════════════════
const pagoModal = {
_bsModal: null,
// 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
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;
this.toggleModo();
if (!this._bsModal) {
this._bsModal = new bootstrap.Modal(document.getElementById('modalPago'));
}
this._bsModal.show();
},
// Mostrar/ocultar campos de modo y monto según estado
toggleModo() {
const est = document.querySelector('input[name="pago-estado"]:checked')?.value;
document.getElementById('pago-campos-pago').style.display =
est === 'pagado' ? '' : 'none';
},
// 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();
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;
}
const payload = { domicilio_id: domId, pago_estado: estado };
if (estado === 'pagado') {
payload.pago_modo = modo;
payload.pago_monto = monto;
}
if (notas) payload.pago_notas = notas;
try {
const r = await fetch('api/lab/registrar_pago.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
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
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;
}
// Cerrar modal y re-renderizar
if (this._bsModal) this._bsModal.hide();
portal.renderAgenda();
} catch(e) {
alert('Error: ' + e.message);
}
},
};
</script>
</body>
</html>