2380 lines
120 KiB
PHP
2380 lines
120 KiB
PHP
<?php
|
|
/**
|
|
* enfermero_portal.php — Portal exclusivo para enfermeros.
|
|
* Muestra solo los domicilios asignados al enfermero autenticado.
|
|
*/
|
|
require_once 'config/config.php';
|
|
|
|
// Solo enfermeros (o admins visualizando el portal)
|
|
if (!isUserLoggedIn()) {
|
|
header('Location: login.php'); exit;
|
|
}
|
|
|
|
$usuario = $_SESSION['admin_user'];
|
|
$rol = $usuario['role'] ?? 'admin';
|
|
$enfId = (int)($usuario['enfermera_id'] ?? 0);
|
|
|
|
// Si es admin puede simular ver la agenda de otra enfermera vía ?eid=X
|
|
if ($rol === 'admin' && isset($_GET['eid'])) {
|
|
$enfId = (int)$_GET['eid'];
|
|
}
|
|
|
|
if ($rol !== 'admin' && $rol !== 'enfermero') {
|
|
header('Location: index.php'); exit;
|
|
}
|
|
|
|
$hoy = date('Y-m-d');
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
|
<title>Mi Agenda · Lab</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
|
<style>
|
|
body { background:#f4f6fb; font-size:.92rem; }
|
|
|
|
/* ── Top bar ── */
|
|
.topbar { background:linear-gradient(135deg,#0d6efd,#0a58ca);
|
|
color:#fff; padding:.75rem 1rem; position:sticky; top:0; z-index:100; }
|
|
.topbar .btn-logout { color:rgba(255,255,255,.8); }
|
|
|
|
/* ── Fecha selector ── */
|
|
.date-nav .btn { min-width:36px; }
|
|
input[type=date].date-input { max-width:160px; border-radius:20px;
|
|
font-weight:600; color:#0d6efd; }
|
|
|
|
/* ── Tarjeta domicilio ── */
|
|
.domcard { border-left:4px solid #dee2e6; border-radius:8px;
|
|
background:#fff; box-shadow:0 1px 4px rgba(0,0,0,.07);
|
|
transition:box-shadow .2s; overflow:hidden; }
|
|
.domcard:hover { box-shadow:0 3px 10px rgba(0,0,0,.12); }
|
|
.domcard.estado-programado { border-color:#6c757d; }
|
|
.domcard.estado-confirmado { border-color:#0d6efd; }
|
|
.domcard.estado-en_camino { border-color:#fd7e14; }
|
|
.domcard.estado-en_domicilio { border-color:#20c997; }
|
|
.domcard.estado-completado { border-color:#198754; opacity:.75; }
|
|
.domcard.estado-cancelado { border-color:#dc3545; opacity:.6; }
|
|
|
|
/* ── Badges estado ── */
|
|
.badge-programado { background:#6c757d; }
|
|
.badge-confirmado { background:#0d6efd; }
|
|
.badge-en_camino { background:#fd7e14; }
|
|
.badge-en_domicilio { background:#20c997; }
|
|
.badge-completado { background:#198754; }
|
|
.badge-cancelado { background:#dc3545; }
|
|
|
|
/* ── Botones acción ── */
|
|
.btn-accion { font-size:.78rem; padding:.28rem .65rem; border-radius:20px; }
|
|
|
|
/* ── Servicios extra ── */
|
|
.se-chip { font-size:.72rem; background:#f0f4ff; border:1px solid #c9d8ff;
|
|
border-radius:16px; padding:2px 8px; color:#0d6efd; white-space:nowrap; }
|
|
|
|
/* ── Empty state ── */
|
|
.empty-state { color:#adb5bd; text-align:center; padding:3rem 1rem; }
|
|
|
|
/* ── FAB Formulario ── */
|
|
.fab-form { position:fixed; right:1.1rem; bottom:1.4rem; z-index:200;
|
|
background:#6f42c1; color:#fff; width:52px; height:52px;
|
|
border-radius:50%; border:none; box-shadow:0 4px 14px rgba(111,66,193,.4);
|
|
font-size:1.3rem; display:flex; align-items:center; justify-content:center;
|
|
transition:transform .15s; }
|
|
.fab-form:hover { transform:scale(1.1); background:#5a32a3; }
|
|
|
|
/* ── Modal formulario (bottom-sheet en móvil) ── */
|
|
@media (max-width:576px) {
|
|
#modalEnvioForm .modal-dialog { margin:0; position:fixed;
|
|
bottom:0; left:0; right:0; max-width:100%; }
|
|
#modalEnvioForm .modal-content { border-radius:1rem 1rem 0 0; }
|
|
}
|
|
.form-result-box { background:#f0fff4; border:1px solid #b7ebc8;
|
|
border-radius:8px; padding:.75rem 1rem; font-size:.85rem; }
|
|
|
|
/* ── FAB Nueva Agenda ── */
|
|
.fab-agenda { position:fixed; right:1.1rem; bottom:4.8rem; z-index:200;
|
|
background:#0d6efd; color:#fff; width:52px; height:52px;
|
|
border-radius:50%; border:none; box-shadow:0 4px 14px rgba(13,110,253,.4);
|
|
font-size:1.3rem; display:flex; align-items:center; justify-content:center;
|
|
transition:transform .15s; }
|
|
.fab-agenda:hover { transform:scale(1.1); background:#0a58ca; }
|
|
|
|
/* ── Sugerencias paciente ── */
|
|
.sugerencias-pac { position:absolute; z-index:9999; background:#fff;
|
|
border:1px solid #dee2e6; border-radius:8px;
|
|
box-shadow:0 4px 12px rgba(0,0,0,.1); max-height:200px;
|
|
overflow-y:auto; width:100%; top:100%; left:0; }
|
|
.sugerencias-pac .item { padding:.5rem .75rem; cursor:pointer;
|
|
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; }
|
|
|
|
/* ── Notas clínicas ── */
|
|
.nota-toggle-btn { text-align:left; font-size:.78rem; font-weight:700;
|
|
background:#fef9c3; border:1px solid #fde68a; color:#92400e;
|
|
border-radius:8px; padding:7px 10px; width:100%; cursor:pointer;
|
|
display:flex; align-items:center; gap:6px; transition:.15s; }
|
|
.nota-toggle-btn:active { transform:scale(.98); }
|
|
.nota-chevron { margin-left:auto; transition:transform .2s; font-size:.7rem; }
|
|
.nota-chevron.open { transform:rotate(180deg); }
|
|
.notas-body { padding:0; margin-top:4px; }
|
|
.nota-libre-card { background:#fff; border:1px solid #e5e7eb; border-radius:8px;
|
|
padding:9px 11px; margin-bottom:6px; }
|
|
.nota-libre-card .nota-titulo { font-weight:700; font-size:.82rem; color:#374151; }
|
|
.nota-libre-card .nota-cuerpo { font-size:.78rem; color:#4b5563; margin-top:4px;
|
|
word-break:break-word; }
|
|
.nota-libre-card .nota-fecha { font-size:.69rem; color:#9ca3af; margin-top:4px; }
|
|
.nota-ficha-card { background:#fff7ed; border:1px solid #fdba74; border-radius:10px;
|
|
padding:9px 11px; margin-bottom:6px; }
|
|
.nota-ficha-card .ficha-header { display:flex; justify-content:space-between;
|
|
align-items:center; margin-bottom:6px; }
|
|
.nota-ficha-card .ficha-titulo { font-weight:700; font-size:.82rem; color:#9a3412; }
|
|
/* Bottom sheet */
|
|
#nota-backdrop { display:none; position:fixed; inset:0; background:rgba(0,0,0,.45); z-index:1050; }
|
|
#nota-sheet { display:none; position:fixed; bottom:0; left:0; right:0; z-index:1055;
|
|
background:#fff; border-radius:20px 20px 0 0; max-height:88vh;
|
|
overflow-y:auto; box-shadow:0 -4px 24px rgba(0,0,0,.18);
|
|
padding-bottom:env(safe-area-inset-bottom, 16px); }
|
|
#nota-sheet .sheet-handle { display:flex; justify-content:center; padding:10px 0 2px; }
|
|
#nota-sheet .sheet-handle span { width:40px; height:4px; background:#e0e0e0; border-radius:2px; display:block; }
|
|
#nota-sheet-inner { padding:8px 16px 28px; }
|
|
/* Toolbar formato */
|
|
.fmt-toolbar { display:flex; gap:4px; overflow-x:auto; padding-bottom:2px; margin-bottom:6px; }
|
|
.fmt-btn { min-width:34px; border:1px solid #e5e7eb; background:#fff; border-radius:5px;
|
|
font-size:.78rem; padding:4px 8px; cursor:pointer; white-space:nowrap; }
|
|
.fmt-btn:active { background:#f0f4ff; }
|
|
/* Campos de nota */
|
|
.nota-input { width:100%; border:1px solid #d1d5db; border-radius:8px;
|
|
padding:8px 12px; font-size:.88rem; font-family:inherit; resize:vertical; }
|
|
.nota-input:focus { outline:2px solid #0d6efd; border-color:transparent; }
|
|
.nota-editor { min-height:90px; border:1px solid #d1d5db; border-radius:8px;
|
|
padding:8px 12px; font-size:.85rem; outline:none; background:#fff;
|
|
word-break:break-word; -webkit-user-select:text; cursor:text; }
|
|
.nota-editor:focus { outline:2px solid #0d6efd; border-color:transparent; }
|
|
.nota-label { font-size:.78rem; font-weight:700; color:#374151; display:block; margin-bottom:4px; }
|
|
.nota-section { margin-bottom:14px; }
|
|
.nota-acudiente-bloque { background:#fff0f0; border:1px solid #fca5a5;
|
|
border-radius:10px; padding:10px; }
|
|
.nota-btn-guardar { width:100%; border:none; border-radius:10px; padding:13px;
|
|
font-weight:800; font-size:.92rem; color:#fff; cursor:pointer; }
|
|
|
|
/* ── Acordeón domicilios ── */
|
|
.domcard-header { cursor:pointer; user-select:none; display:flex; align-items:center;
|
|
justify-content:space-between; padding:8px 0 6px;
|
|
border-bottom:1px solid #f0f2f5; }
|
|
.domcard-header .dom-chev { transition:transform .2s; font-size:.68rem; color:#9ca3af;
|
|
flex-shrink:0; margin-left:6px; }
|
|
.domcard-header .dom-chev.open { transform:rotate(180deg); }
|
|
.domcard-body { display:none; padding-top:10px; }
|
|
.domcard-body.open { display:block; }
|
|
.fin-section-toggle { width:100%; background:#f3f4f6; border:1px solid #e5e7eb;
|
|
border-radius:10px; padding:9px 14px; font-size:.8rem;
|
|
font-weight:700; color:#6b7280; cursor:pointer; text-align:left;
|
|
display:flex; align-items:center; gap:8px; margin-top:6px; }
|
|
.fin-section-toggle .fin-chev { margin-left:auto; font-size:.68rem;
|
|
transition:transform .2s; }
|
|
.fin-section-toggle.open .fin-chev { transform:rotate(180deg); }
|
|
#seccion-fin { display:none; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- ── Top bar ───────────────────────────────────────────────────── -->
|
|
<div class="topbar d-flex align-items-center justify-content-between">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<div class="rounded-circle bg-white bg-opacity-25 d-flex align-items-center justify-content-center"
|
|
style="width:36px;height:36px">
|
|
<i class="fas fa-user-nurse text-white"></i>
|
|
</div>
|
|
<div>
|
|
<div class="fw-bold lh-1" id="enf-nombre"><?= htmlspecialchars($usuario['full_name'] ?? $usuario['username'] ?? 'Enfermero') ?></div>
|
|
<small class="opacity-75">Portal Enfermero · <?= date('d/m/Y') ?></small>
|
|
</div>
|
|
</div>
|
|
<?php if ($rol === 'admin'): ?>
|
|
<a href="lab_agenda_admin.php" class="btn btn-sm btn-light text-primary">
|
|
<i class="fas fa-tachometer-alt me-1"></i>Admin
|
|
</a>
|
|
<?php else: ?>
|
|
<a href="logout.php" class="btn btn-sm btn-logout">
|
|
<i class="fas fa-sign-out-alt"></i>
|
|
</a>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- ── Navegación de fecha ──────────────────────────────────────── -->
|
|
<div class="container-fluid px-3 py-2">
|
|
<div class="d-flex align-items-center justify-content-between gap-2 date-nav">
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="portal.cambiarFecha(-1)">
|
|
<i class="fas fa-chevron-left"></i>
|
|
</button>
|
|
<input type="date" class="form-control form-control-sm date-input text-center"
|
|
id="portal-fecha" value="<?= $hoy ?>"
|
|
onchange="portal.cargar()">
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="portal.cambiarFecha(1)">
|
|
<i class="fas fa-chevron-right"></i>
|
|
</button>
|
|
<button class="btn btn-outline-primary btn-sm" onclick="portal.irHoy()" title="Hoy">
|
|
<i class="fas fa-calendar-day"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Resumen del día -->
|
|
<div id="portal-resumen" class="d-flex gap-2 mt-2 flex-wrap" style="font-size:.75rem;"></div>
|
|
</div>
|
|
|
|
<!-- ── Lista de domicilios ─────────────────────────────────────── -->
|
|
<div class="container-fluid px-3 pb-4" id="portal-lista">
|
|
<div class="empty-state">
|
|
<div style="font-size:2.5rem">📋</div>
|
|
<p class="mt-2">Cargando agenda…</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════════════════ MODAL: Confirmar cancel ══════════════════ -->
|
|
<div class="modal fade" id="modalCancelar" tabindex="-1">
|
|
<div class="modal-dialog modal-sm">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-danger text-white py-2">
|
|
<h6 class="modal-title mb-0"><i class="fas fa-ban me-1"></i>Cancelar domicilio</h6>
|
|
<button class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" id="cancel-dom-id">
|
|
<label class="form-label small fw-semibold">Motivo de cancelación <span class="text-danger">*</span></label>
|
|
<textarea class="form-control form-control-sm" id="cancel-notas" rows="3"
|
|
placeholder="Explica brevemente el motivo…"></textarea>
|
|
</div>
|
|
<div class="modal-footer py-2">
|
|
<button class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Atrás</button>
|
|
<button class="btn btn-danger btn-sm" onclick="portal.confirmarCancelacion()">
|
|
<i class="fas fa-ban me-1"></i>Cancelar servicio
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════════════════ MODAL: Servicio extra ═══════════════════ -->
|
|
<div class="modal fade" id="modalServicioExtra" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-purple text-white py-2" style="background:#6f42c1">
|
|
<h6 class="modal-title mb-0"><i class="fas fa-plus-circle me-1"></i>Servicio adicional</h6>
|
|
<button class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" id="se-dom-id">
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-semibold">Tipo <span class="text-danger">*</span></label>
|
|
<select class="form-select form-select-sm" id="se-tipo">
|
|
<option value="inyeccion">💉 Inyección</option>
|
|
<option value="cura">🩹 Cura / Curación</option>
|
|
<option value="nebulizacion">💨 Nebulización</option>
|
|
<option value="toma_muestra">🧪 Toma de muestra</option>
|
|
<option value="tension_arterial">🩺 Tensión arterial</option>
|
|
<option value="glucometria">🩸 Glucometría</option>
|
|
<option value="otro">📋 Otro</option>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-semibold">Descripción <span class="text-danger">*</span></label>
|
|
<input type="text" class="form-control form-control-sm" id="se-descripcion"
|
|
placeholder="Ej: Ampicilina 500mg IM">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label small">Notas adicionales</label>
|
|
<textarea class="form-control form-control-sm" id="se-notas" rows="2"
|
|
placeholder="Observaciones, resultado, indicaciones…"></textarea>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="se-pago">
|
|
<label class="form-check-label small" for="se-pago">Tiene costo adicional</label>
|
|
</div>
|
|
<div id="se-valor-cont" class="mt-2" style="display:none">
|
|
<input type="number" class="form-control form-control-sm" id="se-valor"
|
|
placeholder="Valor en pesos" min="0" step="1000">
|
|
</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:#6f42c1"
|
|
onclick="portal.guardarServicioExtra()">
|
|
<i class="fas fa-save me-1"></i>Guardar servicio
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</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 ?>;
|
|
const ROL = '<?= $rol ?>';
|
|
|
|
// Etiquetas descriptivas
|
|
const ESTADOS_LABEL = {
|
|
programado: { label:'Programado', icon:'🕐', cls:'badge-programado' },
|
|
confirmado: { label:'Confirmado', icon:'✅', cls:'badge-confirmado' },
|
|
en_camino: { label:'En camino', icon:'🚗', cls:'badge-en_camino' },
|
|
en_domicilio: { label:'En domicilio', icon:'🏠', cls:'badge-en_domicilio' },
|
|
completado: { label:'Completado', icon:'🎉', cls:'badge-completado' },
|
|
cancelado: { label:'Cancelado', icon:'❌', cls:'badge-cancelado' },
|
|
};
|
|
|
|
// Transiciones que el enfermero puede hacer
|
|
const TRANSICIONES = {
|
|
programado: [{ estado:'confirmado', label:'✅ Confirmar con cliente', color:'primary' }],
|
|
confirmado: [{ estado:'en_camino', label:'🚗 Salir hacia domicilio', color:'warning' }],
|
|
en_camino: [{ estado:'en_domicilio', label:'🏠 Llegué al domicilio', color:'success' }],
|
|
en_domicilio: [{ estado:'completado', label:'🎉 Marcar completado', color:'success' }],
|
|
};
|
|
|
|
const portal = {
|
|
_agenda: [],
|
|
_cancelModal: null,
|
|
_seModal: null,
|
|
|
|
init() {
|
|
this._cancelModal = new bootstrap.Modal('#modalCancelar');
|
|
this._seModal = new bootstrap.Modal('#modalServicioExtra');
|
|
document.getElementById('se-pago').addEventListener('change', e => {
|
|
document.getElementById('se-valor-cont').style.display = e.target.checked ? '' : 'none';
|
|
});
|
|
this.cargar();
|
|
},
|
|
|
|
async cargar() {
|
|
const fecha = document.getElementById('portal-fecha').value;
|
|
const url = `api/lab/my_agenda.php?fecha=${fecha}` +
|
|
(ENFERMERA_ID ? `&enfermera_id=${ENFERMERA_ID}` : '');
|
|
|
|
document.getElementById('portal-lista').innerHTML =
|
|
'<div class="empty-state"><div class="spinner-border text-primary"></div><p class="mt-2">Cargando…</p></div>';
|
|
|
|
try {
|
|
const r = await fetch(url);
|
|
const d = await r.json();
|
|
if (!d.success) throw new Error(d.error || 'Error al cargar');
|
|
|
|
this._agenda = d.agenda || [];
|
|
|
|
// Nombre de la enfermera (primer registro o fallback)
|
|
// El nombre ya viene del servidor — no se necesita asignarlo aquí
|
|
|
|
this._renderResumen(d.totales || {}, d.total || 0);
|
|
this._renderLista();
|
|
} catch (err) {
|
|
document.getElementById('portal-lista').innerHTML =
|
|
`<div class="alert alert-danger mx-2">${esc(err.message)}</div>`;
|
|
}
|
|
},
|
|
|
|
_renderResumen(totales, total) {
|
|
const res = document.getElementById('portal-resumen');
|
|
if (!total) { res.innerHTML = ''; return; }
|
|
const chips = Object.entries(totales).map(([est, n]) => {
|
|
const info = ESTADOS_LABEL[est] || { label: est, icon:'📌' };
|
|
return `<span class="badge ${info.cls}" style="font-size:.72rem">
|
|
${info.icon} ${n} ${info.label}
|
|
</span>`;
|
|
});
|
|
res.innerHTML = chips.join('') + `<span class="text-muted ms-1">Total: <strong>${total}</strong></span>`;
|
|
},
|
|
|
|
_renderLista() {
|
|
const lista = document.getElementById('portal-lista');
|
|
if (!this._agenda.length) {
|
|
lista.innerHTML = `<div class="empty-state">
|
|
<div style="font-size:3rem">📅</div>
|
|
<p class="mt-2 fw-semibold">Sin servicios para este día</p>
|
|
<small>Los servicios asignados aparecerán aquí.</small>
|
|
</div>`;
|
|
return;
|
|
}
|
|
const ACTIVOS = ['programado','confirmado','en_camino','en_domicilio'];
|
|
const CERRADOS = ['completado','cancelado'];
|
|
const sorted = [...this._agenda].sort((a,b) =>
|
|
(a.hora_programada||'99:99').localeCompare(b.hora_programada||'99:99'));
|
|
const activos = sorted.filter(i => ACTIVOS.includes(i.domicilio_estado||'programado'));
|
|
const cerrados = sorted.filter(i => CERRADOS.includes(i.domicilio_estado||''));
|
|
|
|
let html = activos.map((item, idx) => this._cardHTML(item, idx === 0)).join('');
|
|
|
|
if (cerrados.length) {
|
|
html += `<button class="fin-section-toggle" onclick="toggleFinalizados(this)">
|
|
<i class="fas fa-check-circle text-success"></i>
|
|
Finalizados (${cerrados.length})
|
|
<i class="fas fa-chevron-down fin-chev"></i>
|
|
</button>
|
|
<div id="seccion-fin">
|
|
${cerrados.map(item => this._cardHTML(item, false)).join('')}
|
|
</div>`;
|
|
}
|
|
lista.innerHTML = html;
|
|
},
|
|
|
|
_cardHTML(item, expandido = false) {
|
|
const est = item.domicilio_estado || 'programado';
|
|
const info = ESTADOS_LABEL[est] || { label: est, icon:'📌', cls:'bg-secondary' };
|
|
const hora = item.hora_programada ? item.hora_programada.slice(0,5) : '—';
|
|
const trans = TRANSICIONES[est] || [];
|
|
const seSuf = (item.servicios_extra || []).map(se =>
|
|
`<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}')">
|
|
${t.label}
|
|
</button>`
|
|
).join('');
|
|
|
|
const btnCancelar = !['completado','cancelado'].includes(est)
|
|
? `<button class="btn btn-outline-danger btn-accion"
|
|
onclick="portal.abrirCancelar(${item.domicilio_id})">
|
|
❌ Cancelar
|
|
</button>`
|
|
: '';
|
|
|
|
return `<div class="domcard estado-${est} mb-3 p-3" id="card-${item.domicilio_id}">
|
|
<div class="domcard-header" onclick="toggleDomCard(this)">
|
|
<div class="d-flex align-items-center gap-2 flex-wrap" style="max-width:calc(100% - 22px)">
|
|
<span class="badge ${info.cls} text-white">${info.icon} ${info.label}</span>
|
|
<span class="text-muted small">🕐 ${hora}</span>
|
|
<span class="fw-semibold small" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(item.paciente_nombre || '—')}</span>
|
|
</div>
|
|
<i class="fas fa-chevron-down dom-chev ${expandido ? 'open' : ''}"></i>
|
|
</div>
|
|
<div class="domcard-body ${expandido ? 'open' : ''}">
|
|
<div class="fw-bold mb-1 mt-2">
|
|
<i class="fas fa-user me-1 text-primary"></i>${esc(item.paciente_nombre || '—')}
|
|
</div>
|
|
<!-- Documento y fecha de nacimiento -->
|
|
${(item.paciente_numero_documento || item.paciente_fecha_nacimiento) ? `
|
|
<div class="small text-muted mb-1">
|
|
${item.paciente_tipo_documento && item.paciente_numero_documento
|
|
? `<i class="fas fa-id-card me-1"></i>${esc(item.paciente_tipo_documento)} ${esc(item.paciente_numero_documento)}`
|
|
: ''}
|
|
${item.paciente_fecha_nacimiento
|
|
? `<span class="ms-2"><i class="fas fa-birthday-cake me-1"></i>${esc(item.paciente_fecha_nacimiento)}</span>`
|
|
: ''}
|
|
</div>` : ''}
|
|
<!-- EPS -->
|
|
${item.paciente_eps
|
|
? `<div class="small text-muted mb-1"><i class="fas fa-hospital me-1"></i>${esc(item.paciente_eps)}</div>`
|
|
: ''}
|
|
${item.paciente_telefono
|
|
? `<a href="tel:${esc(item.paciente_telefono)}" class="d-block small text-muted mb-1">
|
|
<i class="fas fa-phone me-1"></i>${esc(item.paciente_telefono)}
|
|
</a>`
|
|
: ''}
|
|
<!-- Correo -->
|
|
${item.paciente_email
|
|
? `<a href="mailto:${esc(item.paciente_email)}" class="d-block small text-muted mb-1">
|
|
<i class="fas fa-envelope me-1"></i>${esc(item.paciente_email)}
|
|
</a>`
|
|
: ''}
|
|
|
|
<div class="small text-secondary mb-1">
|
|
<i class="fas fa-map-marker-alt me-1 text-danger"></i>${esc(item.direccion || '—')}
|
|
${item.barrio ? `<span class="text-muted">, ${esc(item.barrio)}</span>` : ''}
|
|
</div>
|
|
${item.indicaciones_dir
|
|
? `<div class="small text-muted mb-1"><i class="fas fa-info-circle me-1"></i>${esc(item.indicaciones_dir)}</div>`
|
|
: ''}
|
|
${item.notas_admin
|
|
? `<div class="small text-info mb-2"><i class="fas fa-sticky-note me-1"></i>${esc(item.notas_admin)}</div>`
|
|
: ''}
|
|
<!-- Exámenes solicitados -->
|
|
${item.examenes_solicitados
|
|
? `<div class="small text-secondary mb-1"><i class="fas fa-flask me-1 text-primary"></i>${esc(item.examenes_solicitados)}</div>`
|
|
: ''}
|
|
<!-- Tipo cliente / seguro -->
|
|
${item.tipo_cliente === 'seguro'
|
|
? `<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}
|
|
|
|
<!-- Horas de llegada / salida (auto-registradas) -->
|
|
${(item.hora_llegada || item.hora_salida) ? `
|
|
<div class="d-flex gap-3 small mt-1 mb-2">
|
|
${item.hora_llegada ? `<span class="text-success"><i class="fas fa-sign-in-alt me-1"></i>Llegada: <strong>${item.hora_llegada.slice(0,5)}</strong></span>` : ''}
|
|
${item.hora_salida ? `<span class="text-danger"><i class="fas fa-sign-out-alt me-1"></i>Salida: <strong>${item.hora_salida.slice(0,5)}</strong></span>` : ''}
|
|
${item.hora_llegada && item.hora_salida ? (() => {
|
|
const [h1,m1] = item.hora_llegada.slice(0,5).split(':').map(Number);
|
|
const [h2,m2] = item.hora_salida.slice(0,5).split(':').map(Number);
|
|
const mins = (h2*60+m2)-(h1*60+m1);
|
|
if (mins > 0) return `<span class="text-info"><i class="fas fa-stopwatch me-1"></i>${mins>=60?Math.floor(mins/60)+'h '+mins%60+'min':mins+' min'}</span>`;
|
|
return '';
|
|
})() : ''}
|
|
</div>` : ''}
|
|
|
|
<!-- Órdenes médicas (multi-archivo) -->
|
|
${renderOrdenesEnfermero(item)}
|
|
<!-- Servicios extra registrados -->
|
|
${seSuf ? `<div class="d-flex flex-wrap gap-1 mb-2">${seSuf}</div>` : ''}
|
|
|
|
<!-- ── Notas clínicas ── -->
|
|
<div class="mt-2 mb-2">
|
|
<button class="nota-toggle-btn" onclick="notasManager.toggle(${item.domicilio_id}, this)">
|
|
<i class="fas fa-notes-medical"></i>
|
|
<span>Notas clínicas</span>
|
|
<span class="badge rounded-pill ms-1" id="nota-count-${item.domicilio_id}"
|
|
style="background:#f59e0b;color:#fff;font-size:.67rem;display:none"></span>
|
|
<i class="fas fa-chevron-down nota-chevron"></i>
|
|
</button>
|
|
<div class="notas-body d-none" id="notas-body-${item.domicilio_id}"
|
|
data-dom-id="${item.domicilio_id}"
|
|
data-pac-nac="${esc(item.paciente_fecha_nacimiento||'')}">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Acciones -->
|
|
<div class="d-flex flex-wrap gap-2 mt-2">
|
|
${btnsAccion}
|
|
${btnCancelar}
|
|
${!['completado','cancelado'].includes(est)
|
|
? `<button class="btn btn-outline-secondary btn-accion"
|
|
onclick="portal.abrirServicioExtra(${item.domicilio_id})">
|
|
<i class="fas fa-plus me-1"></i>Servicio extra
|
|
</button>` : ''}
|
|
${item.paciente_telefono
|
|
? `<a class="btn btn-outline-success btn-accion"
|
|
href="https://wa.me/${item.paciente_telefono.replace(/\D/g,'')}" target="_blank">
|
|
<i class="fab fa-whatsapp"></i>
|
|
</a>` : ''}
|
|
<button class="btn btn-outline-secondary btn-accion btn-form-envio"
|
|
style="color:#6f42c1;border-color:#6f42c1"
|
|
data-dom-id="${item.domicilio_id}"
|
|
data-nombre="${esc(item.paciente_nombre||'')}"
|
|
data-telefono="${esc(item.paciente_telefono||'')}"
|
|
data-pac-id="${item.paciente_id||0}">
|
|
<i class="fas fa-file-medical me-1"></i>Formulario
|
|
</button>
|
|
${item.paciente_id ? `
|
|
<button class="btn btn-outline-info btn-accion"
|
|
onclick="formVer.abrir(${item.paciente_id}, '${esc(item.paciente_nombre||'')}')">
|
|
<i class="fas fa-folder-open me-1"></i>Ver llenados
|
|
</button>` : ''}
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
},
|
|
|
|
// ── Cambiar estado ─────────────────────────────────────────────────────
|
|
async actualizarEstado(domId, nuevoEstado, notas = '') {
|
|
try {
|
|
const r = await fetch('api/lab/update_domicilio_enfermero.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ domicilio_id: domId, nuevo_estado: nuevoEstado, notas }),
|
|
});
|
|
const d = await r.json();
|
|
if (!d.success) throw new Error(d.error);
|
|
// Actualizar estado en memoria y re-render
|
|
const item = this._agenda.find(a => a.domicilio_id == domId);
|
|
if (item) item.domicilio_estado = nuevoEstado;
|
|
this._renderLista();
|
|
this._renderResumen(
|
|
Object.fromEntries(
|
|
Object.entries(
|
|
this._agenda.reduce((a, i) => {
|
|
a[i.domicilio_estado] = (a[i.domicilio_estado]||0)+1;
|
|
return a;
|
|
}, {})
|
|
)
|
|
),
|
|
this._agenda.length
|
|
);
|
|
} catch (err) {
|
|
alert('Error: ' + err.message);
|
|
}
|
|
},
|
|
|
|
// ── Modal cancelar ────────────────────────────────────────────────────
|
|
abrirCancelar(domId) {
|
|
document.getElementById('cancel-dom-id').value = domId;
|
|
document.getElementById('cancel-notas').value = '';
|
|
this._cancelModal.show();
|
|
},
|
|
async confirmarCancelacion() {
|
|
const domId = +document.getElementById('cancel-dom-id').value;
|
|
const notas = document.getElementById('cancel-notas').value.trim();
|
|
if (!notas) { document.getElementById('cancel-notas').focus(); return; }
|
|
this._cancelModal.hide();
|
|
await this.actualizarEstado(domId, 'cancelado', notas);
|
|
},
|
|
|
|
// ── Modal servicio extra ──────────────────────────────────────────────
|
|
abrirServicioExtra(domId) {
|
|
document.getElementById('se-dom-id').value = domId;
|
|
document.getElementById('se-tipo').value = 'otro';
|
|
document.getElementById('se-descripcion').value = '';
|
|
document.getElementById('se-notas').value = '';
|
|
document.getElementById('se-pago').checked = false;
|
|
document.getElementById('se-valor').value = '';
|
|
document.getElementById('se-valor-cont').style.display = 'none';
|
|
this._seModal.show();
|
|
},
|
|
async guardarServicioExtra() {
|
|
const domId = +document.getElementById('se-dom-id').value;
|
|
const desc = document.getElementById('se-descripcion').value.trim();
|
|
if (!desc) { document.getElementById('se-descripcion').focus(); return; }
|
|
|
|
const datos = {
|
|
domicilio_id: domId,
|
|
tipo: document.getElementById('se-tipo').value,
|
|
descripcion: desc,
|
|
notas: document.getElementById('se-notas').value.trim() || null,
|
|
requiere_pago:document.getElementById('se-pago').checked ? 1 : 0,
|
|
valor: document.getElementById('se-pago').checked
|
|
? parseFloat(document.getElementById('se-valor').value) || null
|
|
: null,
|
|
};
|
|
try {
|
|
const r = await fetch('api/lab/save_servicio_extra.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(datos),
|
|
});
|
|
const d = await r.json();
|
|
if (!d.success) throw new Error(d.error);
|
|
this._seModal.hide();
|
|
// Agregar servicio extra al item en memoria
|
|
const item = this._agenda.find(a => a.domicilio_id == domId);
|
|
if (item) {
|
|
item.servicios_extra = item.servicios_extra || [];
|
|
item.servicios_extra.push({ descripcion: datos.descripcion, id: d.id });
|
|
}
|
|
this._renderLista();
|
|
} catch (err) {
|
|
alert('Error: ' + err.message);
|
|
}
|
|
},
|
|
|
|
// ── Navegación ──────────────────────────────────────────────────────
|
|
cambiarFecha(dias) {
|
|
const input = document.getElementById('portal-fecha');
|
|
const d = new Date(input.value + 'T12:00:00'); // evitar offset tz
|
|
d.setDate(d.getDate() + dias);
|
|
input.value = d.toISOString().slice(0, 10);
|
|
this.cargar();
|
|
},
|
|
irHoy() {
|
|
document.getElementById('portal-fecha').value = '<?= $hoy ?>';
|
|
this.cargar();
|
|
},
|
|
};
|
|
|
|
// Escapar HTML
|
|
function esc(s) {
|
|
return String(s||'').replace(/[<>&"']/g, c =>
|
|
({ '<':'<','>':'>','&':'&','"':'"',"'":''' }[c]));
|
|
}
|
|
|
|
// ── Acordeón domicilios ────────────────────────────────────────────────────
|
|
function toggleDomCard(headerEl) {
|
|
const body = headerEl.nextElementSibling;
|
|
const chev = headerEl.querySelector('.dom-chev');
|
|
const open = body.classList.toggle('open');
|
|
if (chev) chev.classList.toggle('open', open);
|
|
}
|
|
|
|
function toggleFinalizados(btnEl) {
|
|
const sec = document.getElementById('seccion-fin');
|
|
const chev = btnEl.querySelector('.fin-chev');
|
|
const open = btnEl.classList.toggle('open');
|
|
if (sec) sec.style.display = open ? 'block' : 'none';
|
|
if (chev) chev.classList.toggle('open', open);
|
|
}
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
|
|
// Resolver ruta de imagen: evita duplicar "uploads/media/"
|
|
function resImg(f) {
|
|
if (!f) return '';
|
|
if (/^https?:\/\//i.test(f)) return esc(f);
|
|
if (String(f).startsWith('uploads/')) return esc(f);
|
|
return `uploads/media/${esc(f)}`;
|
|
}
|
|
|
|
// Rutar archivos de orden médica a través de descargar_orden.php
|
|
// (detecta MIME real y convierte .bin a extensión correcta)
|
|
function resOrden(f) {
|
|
if (!f) return '';
|
|
return `api/lab/descargar_orden.php?file=${encodeURIComponent(f)}`;
|
|
}
|
|
|
|
/**
|
|
* Renderiza todas las órdenes médicas adjuntas a un domicilio.
|
|
* Soporta multi-archivo (archivos_ordenes[]) con fallback a orden_local_file.
|
|
*/
|
|
function renderOrdenesEnfermero(item) {
|
|
const archivos = item.archivos_ordenes || [];
|
|
if (!archivos.length) return '';
|
|
return archivos.map(a => {
|
|
const lf = a.local_file || '';
|
|
const url = resOrden(lf);
|
|
const oid = a.orden_id || '';
|
|
if (!lf) return '';
|
|
const isImg = /\.(jpe?g|png|gif|webp)$/i.test(lf);
|
|
return `<div class="mt-2 mb-2">
|
|
${isImg ? `<img src="${url}"
|
|
style="max-width:100%;max-height:180px;border-radius:8px;border:1px solid #ddd;cursor:zoom-in;display:block"
|
|
alt="Orden" onclick="window.open('${url}','_blank')"
|
|
onerror="this.style.display='none'">` : ''}
|
|
<a href="${url}" target="_blank"
|
|
class="btn btn-sm btn-outline-danger w-100 mt-1">
|
|
<i class="fas fa-file-medical me-1"></i>
|
|
Ver / Descargar orden${oid ? ' #' + oid : ''}
|
|
</a>
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// notasManager — Sistema de notas clínicas por domicilio
|
|
// ═══════════════════════════════════════════════════════════════
|
|
const notasManager = {
|
|
_cache: {}, // domId → { clinica: nota|null, libres: [] }
|
|
_editDomId: null,
|
|
_editNota: null, // nota completa o null (nueva)
|
|
_editTipo: null, // 'clinica' | 'libre'
|
|
_editPacNac: '',
|
|
|
|
// ── Edad ─────────────────────────────────────────────────────────────
|
|
_esmenor(nacStr) {
|
|
if (!nacStr) return null; // desconocido
|
|
const dif = Date.now() - new Date(nacStr).getTime();
|
|
return Math.floor(dif / (365.25 * 24 * 3600 * 1000)) < 18;
|
|
},
|
|
|
|
// ── Toggle sección ────────────────────────────────────────────────────
|
|
async toggle(domId, btn) {
|
|
const body = document.getElementById(`notas-body-${domId}`);
|
|
const chevron = btn.querySelector('.nota-chevron');
|
|
if (!body) return;
|
|
const abrir = body.classList.contains('d-none');
|
|
body.classList.toggle('d-none', !abrir);
|
|
chevron?.classList.toggle('open', abrir);
|
|
if (abrir && !this._cache[domId]) {
|
|
await this.cargar(domId);
|
|
} else if (abrir) {
|
|
this._renderBody(domId);
|
|
}
|
|
},
|
|
|
|
// ── Cargar desde API ─────────────────────────────────────────────────
|
|
async cargar(domId) {
|
|
const body = document.getElementById(`notas-body-${domId}`);
|
|
if (!body) return;
|
|
body.innerHTML = '<div class="text-center py-2 text-muted small"><i class="fas fa-spinner fa-spin me-1"></i>Cargando…</div>';
|
|
try {
|
|
const r = await fetch(`api/lab/get_notas_domicilio.php?domicilio_id=${domId}`);
|
|
const d = await r.json();
|
|
if (!d.success) throw new Error(d.error || 'Error al cargar notas');
|
|
this._cache[domId] = {
|
|
clinica: d.data.find(n => n.tipo === 'clinica') || null,
|
|
libres: d.data.filter(n => n.tipo === 'libre'),
|
|
};
|
|
this._renderBody(domId);
|
|
// Badge contador
|
|
const total = d.data.length;
|
|
const badge = document.getElementById(`nota-count-${domId}`);
|
|
if (badge) { badge.textContent = total; badge.style.display = total ? '' : 'none'; }
|
|
} catch (e) {
|
|
if (body) body.innerHTML = `<div class="alert alert-danger small py-2 mt-2">${esc(e.message)}</div>`;
|
|
}
|
|
},
|
|
|
|
// ── Render cuerpo expandido ──────────────────────────────────────────
|
|
_renderBody(domId) {
|
|
const body = document.getElementById(`notas-body-${domId}`);
|
|
if (!body) return;
|
|
const pacNac = body.dataset.pacNac || '';
|
|
const menor = this._esmenor(pacNac);
|
|
const cache = this._cache[domId] || { clinica: null, libres: [] };
|
|
|
|
body.innerHTML = `
|
|
<div style="margin-top:6px">
|
|
${this._fichaHTML(cache.clinica, domId, menor)}
|
|
${cache.libres.map(n => this._libreHTML(n)).join('')}
|
|
<button style="width:100%;margin-top:6px;padding:8px;background:#f0f9ff;
|
|
border:1px dashed #7dd3fc;border-radius:8px;color:#0369a1;
|
|
font-size:.78rem;font-weight:700"
|
|
onclick="notasManager.abrirLibre(${domId})">
|
|
<i class="fas fa-plus me-1"></i>Agregar nota
|
|
</button>
|
|
</div>`;
|
|
},
|
|
|
|
_fichaHTML(nota, domId, menor) {
|
|
const vacio = !nota;
|
|
const tieneAcud = nota?.acudiente_nombre || nota?.acudiente_documento;
|
|
return `<div class="nota-ficha-card">
|
|
<div class="ficha-header">
|
|
<div class="ficha-titulo">
|
|
<i class="fas fa-file-medical-alt me-1"></i>Ficha clínica
|
|
${menor === true ? '<span style="color:#dc3545;font-size:.68rem"> · menor de edad</span>' : ''}
|
|
</div>
|
|
<button style="background:${vacio?'#f59e0b':'#ea580c'};color:#fff;border:none;
|
|
border-radius:999px;padding:3px 11px;font-size:.72rem;font-weight:700"
|
|
onclick="notasManager.abrirClinica(${domId})">
|
|
${vacio ? '<i class="fas fa-plus me-1"></i>Completar' : '<i class="fas fa-edit me-1"></i>Editar'}
|
|
</button>
|
|
</div>
|
|
${vacio
|
|
? `<div style="color:#a16207;font-size:.75rem;text-align:center;padding:4px 0">Sin información registrada</div>`
|
|
: `<dl style="margin:0;font-size:.78rem">
|
|
${nota.antecedentes ? `<dt style="color:#9a3412;margin-top:4px">Antecedentes</dt><dd style="margin:0 0 2px">${esc(nota.antecedentes).replace(/\n/g,'<br>')}</dd>` : ''}
|
|
${nota.medicamentos ? `<dt style="color:#9a3412;margin-top:4px">Medicamentos</dt><dd style="margin:0 0 2px">${esc(nota.medicamentos).replace(/\n/g,'<br>')}</dd>` : ''}
|
|
${tieneAcud ? `<dt style="color:#9a3412;margin-top:4px">Acudiente</dt>
|
|
<dd style="margin:0">${esc(nota.acudiente_nombre||'—')} · <span style="color:#6b7280">${esc(nota.acudiente_documento||'—')}</span></dd>` : ''}
|
|
</dl>`}
|
|
</div>`;
|
|
},
|
|
|
|
_libreHTML(nota) {
|
|
const domId = nota.domicilio_id;
|
|
return `<div class="nota-libre-card">
|
|
<div style="display:flex;justify-content:space-between;align-items:flex-start">
|
|
<div class="nota-titulo">${esc(nota.titulo||'Nota')}</div>
|
|
<div style="display:flex;gap:2px;flex-shrink:0;margin-left:6px">
|
|
<button style="background:none;border:none;color:#6b7280;padding:2px 6px;font-size:.8rem"
|
|
onclick="notasManager.abrirEditarLibre(${domId},${nota.id})"><i class="fas fa-edit"></i></button>
|
|
<button style="background:none;border:none;color:#ef4444;padding:2px 6px;font-size:.8rem"
|
|
onclick="notasManager.eliminar(${nota.id},${domId})"><i class="fas fa-trash"></i></button>
|
|
</div>
|
|
</div>
|
|
${nota.cuerpo ? `<div class="nota-cuerpo">${nota.cuerpo}</div>` : ''}
|
|
${nota.imagen_path ? `<img src="uploads/media/${esc(nota.imagen_path)}"
|
|
style="max-width:100%;max-height:160px;border-radius:6px;margin-top:6px;
|
|
border:1px solid #e5e7eb;cursor:zoom-in"
|
|
onclick="window.open('uploads/media/${esc(nota.imagen_path)}','_blank')"
|
|
onerror="this.style.display='none'">` : ''}
|
|
<div class="nota-fecha">${_fmtFechaCorta(nota.created_at||'')}</div>
|
|
</div>`;
|
|
},
|
|
|
|
// ── Abrir bottom sheet: Ficha clínica ────────────────────────────────
|
|
abrirClinica(domId) {
|
|
this._editDomId = domId;
|
|
this._editTipo = 'clinica';
|
|
this._editNota = this._cache[domId]?.clinica || null;
|
|
const bodyEl = document.getElementById(`notas-body-${domId}`);
|
|
this._editPacNac = bodyEl?.dataset?.pacNac || '';
|
|
const menor = this._esmenor(this._editPacNac);
|
|
const nota = this._editNota;
|
|
const inner = document.getElementById('nota-sheet-inner');
|
|
|
|
inner.innerHTML = `
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
|
<div style="font-weight:800;font-size:1rem;color:#9a3412">
|
|
<i class="fas fa-file-medical-alt me-2"></i>Ficha clínica
|
|
</div>
|
|
<button onclick="notasManager.cerrarSheet()"
|
|
style="background:none;border:none;font-size:1.5rem;color:#9ca3af;line-height:1">×</button>
|
|
</div>
|
|
|
|
<div class="nota-section">
|
|
<label class="nota-label">Antecedentes</label>
|
|
<textarea id="nc-antecedentes" class="nota-input" rows="3"
|
|
placeholder="Enfermedades previas, alergias, cirugías, hospitalizaciones…">${esc(nota?.antecedentes||'')}</textarea>
|
|
</div>
|
|
<div class="nota-section">
|
|
<label class="nota-label">Medicamentos actuales</label>
|
|
<textarea id="nc-medicamentos" class="nota-input" rows="3"
|
|
placeholder="Nombre del medicamento, dosis y frecuencia…">${esc(nota?.medicamentos||'')}</textarea>
|
|
</div>
|
|
|
|
<div class="nota-section nota-acudiente-bloque">
|
|
<div style="font-size:.8rem;font-weight:700;color:#dc2626;margin-bottom:10px">
|
|
<i class="fas fa-user-shield me-1"></i>Datos del acudiente
|
|
${menor === true
|
|
? '<span style="font-weight:400"> — <strong>obligatorio</strong> (paciente menor de edad)</span>'
|
|
: '<span style="font-weight:400;color:#9ca3af"> (si aplica)</span>'}
|
|
</div>
|
|
<div style="margin-bottom:8px">
|
|
<label class="nota-label">Nombre completo ${menor===true?'<span style="color:#dc2626">*</span>':''}</label>
|
|
<input id="nc-acud-nombre" type="text" class="nota-input"
|
|
value="${esc(nota?.acudiente_nombre||'')}" placeholder="Nombre del acudiente o responsable">
|
|
</div>
|
|
<div>
|
|
<label class="nota-label">Documento de identidad ${menor===true?'<span style="color:#dc2626">*</span>':''}</label>
|
|
<input id="nc-acud-doc" type="text" class="nota-input" inputmode="numeric"
|
|
value="${esc(nota?.acudiente_documento||'')}" placeholder="Cédula, TI o Pasaporte">
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-top:16px">
|
|
<button id="nc-guardar-btn" class="nota-btn-guardar" style="background:#ea580c"
|
|
onclick="notasManager.guardar()">
|
|
<i class="fas fa-save me-2"></i>Guardar ficha clínica
|
|
</button>
|
|
</div>`;
|
|
|
|
this._abrirSheet();
|
|
},
|
|
|
|
// ── Abrir bottom sheet: Nota libre nueva ────────────────────────────
|
|
abrirLibre(domId) {
|
|
const bodyEl = document.getElementById(`notas-body-${domId}`);
|
|
this._editDomId = domId;
|
|
this._editTipo = 'libre';
|
|
this._editNota = null;
|
|
this._editPacNac = bodyEl?.dataset?.pacNac || '';
|
|
this._renderFormLibre(null);
|
|
this._abrirSheet();
|
|
},
|
|
|
|
// ── Abrir bottom sheet: Editar nota libre ───────────────────────────
|
|
abrirEditarLibre(domId, notaId) {
|
|
const nota = (this._cache[domId]?.libres||[]).find(n => +n.id === +notaId);
|
|
const bodyEl = document.getElementById(`notas-body-${domId}`);
|
|
this._editDomId = domId;
|
|
this._editTipo = 'libre';
|
|
this._editNota = nota || null;
|
|
this._editPacNac = bodyEl?.dataset?.pacNac || '';
|
|
this._renderFormLibre(nota);
|
|
this._abrirSheet();
|
|
},
|
|
|
|
_renderFormLibre(nota) {
|
|
const inner = document.getElementById('nota-sheet-inner');
|
|
inner.innerHTML = `
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
|
<div style="font-weight:800;font-size:1rem">
|
|
<i class="fas fa-sticky-note me-2 text-primary"></i>${nota ? 'Editar nota' : 'Nueva nota'}
|
|
</div>
|
|
<button onclick="notasManager.cerrarSheet()"
|
|
style="background:none;border:none;font-size:1.5rem;color:#9ca3af;line-height:1">×</button>
|
|
</div>
|
|
|
|
<div class="nota-section">
|
|
<label class="nota-label">Título <span style="color:#dc2626">*</span></label>
|
|
<input id="nl-titulo" type="text" class="nota-input" maxlength="200"
|
|
value="${esc(nota?.titulo||'')}" placeholder="Ej: Observación inicial, Sangrado, Reacción…">
|
|
</div>
|
|
|
|
<div class="nota-section">
|
|
<label class="nota-label">Descripción</label>
|
|
<div class="fmt-toolbar">
|
|
<button class="fmt-btn" onclick="notasManager._fmt('bold')" title="Negrita"><b>B</b></button>
|
|
<button class="fmt-btn" style="font-style:italic" onclick="notasManager._fmt('italic')" title="Cursiva">I</button>
|
|
<button class="fmt-btn" style="text-decoration:underline" onclick="notasManager._fmt('underline')" title="Subrayado">U</button>
|
|
<button class="fmt-btn" onclick="notasManager._fmt('strikeThrough')" title="Tachado"><s>S</s></button>
|
|
<button class="fmt-btn" onclick="notasManager._insertLista()" title="Lista">• Lista</button>
|
|
<button class="fmt-btn" onclick="notasManager._limpiarFormato()" title="Sin formato">✕ Formato</button>
|
|
</div>
|
|
<div id="nl-cuerpo" class="nota-editor" contenteditable="true"
|
|
data-placeholder="Escribe los detalles de la nota…"></div>
|
|
</div>
|
|
|
|
<div class="nota-section">
|
|
<label class="nota-label"><i class="fas fa-camera me-1"></i>Imagen adjunta (opcional, máx. 5 MB)</label>
|
|
<div id="nl-preview" style="margin-bottom:6px">
|
|
${nota?.imagen_path ? `<div style="position:relative;display:inline-block">
|
|
<img src="uploads/media/${esc(nota.imagen_path)}"
|
|
style="max-height:100px;border-radius:6px;border:1px solid #e5e7eb">
|
|
<button onclick="notasManager._quitarImg()"
|
|
style="position:absolute;top:-6px;right:-6px;background:#ef4444;color:#fff;
|
|
border:none;border-radius:50%;width:20px;height:20px;font-size:.65rem;
|
|
display:flex;align-items:center;justify-content:center">✕</button>
|
|
</div>` : ''}
|
|
</div>
|
|
<input type="file" id="nl-img-input" accept="image/jpeg,image/png,image/webp"
|
|
capture="environment" style="width:100%;font-size:.82rem"
|
|
onchange="notasManager._previsualizarImg(this)">
|
|
<input type="hidden" id="nl-img-guardada" value="${esc(nota?.imagen_path||'')}">
|
|
</div>
|
|
|
|
<button id="nl-guardar-btn" class="nota-btn-guardar" style="background:#0d6efd"
|
|
onclick="notasManager.guardar()">
|
|
<i class="fas fa-save me-2"></i>Guardar nota
|
|
</button>`;
|
|
|
|
// Cargar HTML en contenteditable
|
|
if (nota?.cuerpo) {
|
|
requestAnimationFrame(() => {
|
|
const el = document.getElementById('nl-cuerpo');
|
|
if (el) el.innerHTML = nota.cuerpo;
|
|
});
|
|
}
|
|
},
|
|
|
|
_fmt(cmd) { document.getElementById('nl-cuerpo')?.focus(); document.execCommand(cmd, false, null); },
|
|
_insertLista() { document.getElementById('nl-cuerpo')?.focus(); document.execCommand('insertUnorderedList', false, null); },
|
|
_limpiarFormato() { document.getElementById('nl-cuerpo')?.focus(); document.execCommand('removeFormat', false, null); },
|
|
|
|
_previsualizarImg(input) {
|
|
if (!input.files?.[0]) return;
|
|
const reader = new FileReader();
|
|
reader.onload = e => {
|
|
const prev = document.getElementById('nl-preview');
|
|
if (prev) prev.innerHTML = `<div style="position:relative;display:inline-block">
|
|
<img src="${e.target.result}" style="max-height:100px;border-radius:6px;border:1px solid #e5e7eb">
|
|
<button onclick="notasManager._quitarImg()"
|
|
style="position:absolute;top:-6px;right:-6px;background:#ef4444;color:#fff;
|
|
border:none;border-radius:50%;width:20px;height:20px;font-size:.65rem;
|
|
display:flex;align-items:center;justify-content:center">✕</button>
|
|
</div>`;
|
|
};
|
|
reader.readAsDataURL(input.files[0]);
|
|
},
|
|
|
|
_quitarImg() {
|
|
document.getElementById('nl-preview').innerHTML = '';
|
|
const inp = document.getElementById('nl-img-input');
|
|
if (inp) inp.value = '';
|
|
const h = document.getElementById('nl-img-guardada');
|
|
if (h) h.value = '';
|
|
},
|
|
|
|
// ── Guardar ──────────────────────────────────────────────────────────
|
|
async guardar() {
|
|
const btnId = this._editTipo === 'clinica' ? 'nc-guardar-btn' : 'nl-guardar-btn';
|
|
const btn = document.getElementById(btnId);
|
|
const labs = { clinica: 'Guardar ficha clínica', libre: 'Guardar nota' };
|
|
if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Guardando…'; }
|
|
|
|
try {
|
|
let imagenPath = null;
|
|
|
|
if (this._editTipo === 'libre') {
|
|
const imgInput = document.getElementById('nl-img-input');
|
|
if (imgInput?.files?.[0]) {
|
|
imagenPath = await this._subirImagen(imgInput.files[0]);
|
|
} else {
|
|
imagenPath = document.getElementById('nl-img-guardada')?.value || null;
|
|
}
|
|
}
|
|
|
|
let payload = { domicilio_id: this._editDomId, tipo: this._editTipo };
|
|
if (this._editNota?.id) payload.id = +this._editNota.id;
|
|
|
|
if (this._editTipo === 'clinica') {
|
|
payload.antecedentes = document.getElementById('nc-antecedentes')?.value.trim() || null;
|
|
payload.medicamentos = document.getElementById('nc-medicamentos')?.value.trim() || null;
|
|
const menor = this._esmenor(this._editPacNac);
|
|
const acNombre = document.getElementById('nc-acud-nombre')?.value.trim() || null;
|
|
const acDoc = document.getElementById('nc-acud-doc')?.value.trim() || null;
|
|
if (menor === true && (!acNombre || !acDoc)) {
|
|
alert('El nombre y documento del acudiente son obligatorios para pacientes menores de edad.');
|
|
if (btn) { btn.disabled = false; btn.innerHTML = `<i class="fas fa-save me-2"></i>${labs.clinica}`; }
|
|
return;
|
|
}
|
|
payload.acudiente_nombre = acNombre;
|
|
payload.acudiente_documento = acDoc;
|
|
} else {
|
|
const titulo = document.getElementById('nl-titulo')?.value.trim();
|
|
if (!titulo) {
|
|
alert('El título de la nota es obligatorio.');
|
|
if (btn) { btn.disabled = false; btn.innerHTML = `<i class="fas fa-save me-2"></i>${labs.libre}`; }
|
|
return;
|
|
}
|
|
payload.titulo = titulo;
|
|
payload.cuerpo = document.getElementById('nl-cuerpo')?.innerHTML.trim() || null;
|
|
payload.imagen_path = imagenPath;
|
|
}
|
|
|
|
const r = await fetch('api/lab/save_nota_domicilio.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
const d = await r.json();
|
|
if (!d.success) throw new Error(d.error || 'Error al guardar');
|
|
|
|
delete this._cache[this._editDomId];
|
|
this.cerrarSheet();
|
|
await this.cargar(this._editDomId);
|
|
} catch (e) {
|
|
alert('Error: ' + e.message);
|
|
if (btn) { btn.disabled = false; btn.innerHTML = `<i class="fas fa-save me-2"></i>Reintentar`; }
|
|
}
|
|
},
|
|
|
|
async _subirImagen(file) {
|
|
const fd = new FormData();
|
|
fd.append('file', file);
|
|
const r = await fetch('api/lab/upload_nota_imagen.php', { method: 'POST', body: fd });
|
|
const d = await r.json();
|
|
if (!d.success) throw new Error(d.error || 'Error al subir imagen');
|
|
return d.imagen_path;
|
|
},
|
|
|
|
async eliminar(notaId, domId) {
|
|
if (!confirm('¿Eliminar esta nota?')) return;
|
|
try {
|
|
const r = await fetch('api/lab/delete_nota_domicilio.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id: notaId }),
|
|
});
|
|
const d = await r.json();
|
|
if (!d.success) throw new Error(d.error || 'Error al eliminar');
|
|
delete this._cache[domId];
|
|
await this.cargar(domId);
|
|
} catch (e) {
|
|
alert('Error: ' + e.message);
|
|
}
|
|
},
|
|
|
|
_abrirSheet() {
|
|
const sheet = document.getElementById('nota-sheet');
|
|
const backdrop = document.getElementById('nota-backdrop');
|
|
if (!sheet || !backdrop) return;
|
|
sheet.style.transform = 'translateY(100%)';
|
|
sheet.style.transition = '';
|
|
sheet.style.display = 'block';
|
|
backdrop.style.display = 'block';
|
|
requestAnimationFrame(() => {
|
|
sheet.style.transition = 'transform .32s cubic-bezier(.4,0,.2,1)';
|
|
sheet.style.transform = 'translateY(0)';
|
|
});
|
|
},
|
|
|
|
cerrarSheet() {
|
|
const sheet = document.getElementById('nota-sheet');
|
|
const backdrop = document.getElementById('nota-backdrop');
|
|
if (!sheet) return;
|
|
sheet.style.transition = 'transform .28s cubic-bezier(.4,0,.2,1)';
|
|
sheet.style.transform = 'translateY(100%)';
|
|
setTimeout(() => {
|
|
sheet.style.display = 'none';
|
|
if (backdrop) backdrop.style.display = 'none';
|
|
}, 300);
|
|
},
|
|
};
|
|
|
|
function _fmtFechaCorta(s) {
|
|
if (!s) return '';
|
|
return new Date(s).toLocaleString('es-CO', { day:'2-digit', month:'short', hour:'2-digit', minute:'2-digit' });
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
portal.init();
|
|
agendaNueva.init();
|
|
formEnvio.cargarPlantillas();
|
|
formVer.init();
|
|
|
|
// Delegated: botones Formulario generados dinámicamente
|
|
document.getElementById('portal-lista').addEventListener('click', e => {
|
|
const btn = e.target.closest('.btn-form-envio');
|
|
if (!btn) return;
|
|
formEnvio.abrirDesdeCard(
|
|
+btn.dataset.domId,
|
|
btn.dataset.nombre || '',
|
|
btn.dataset.telefono || '',
|
|
+btn.dataset.pacId || 0
|
|
);
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<!-- ═══════════ MODAL: Ver Formularios Paciente ═══════════ -->
|
|
<div class="modal fade" id="modalVerForms" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header text-white py-2" style="background:#0891b2">
|
|
<h6 class="modal-title mb-0" id="ver-forms-titulo">
|
|
<i class="fas fa-folder-open me-2"></i>Formularios del paciente
|
|
</h6>
|
|
<button class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body" id="ver-forms-body">
|
|
<div class="text-center py-3">
|
|
<div class="spinner-border spinner-border-sm text-info"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const ESTADO_FORM_BADGE = {
|
|
pendiente: 'bg-secondary',
|
|
enviado: 'bg-primary',
|
|
completado: 'bg-success',
|
|
firmado: 'bg-info',
|
|
};
|
|
|
|
const formVer = {
|
|
_modal: null,
|
|
|
|
init() {
|
|
this._modal = new bootstrap.Modal('#modalVerForms');
|
|
},
|
|
|
|
async abrir(pacienteId, nombre) {
|
|
document.getElementById('ver-forms-titulo').innerHTML =
|
|
`<i class="fas fa-folder-open me-2"></i>${esc(nombre) || 'Formularios del paciente'}`;
|
|
document.getElementById('ver-forms-body').innerHTML =
|
|
'<div class="text-center py-3"><div class="spinner-border spinner-border-sm text-info"></div></div>';
|
|
this._modal.show();
|
|
|
|
if (!pacienteId) {
|
|
document.getElementById('ver-forms-body').innerHTML =
|
|
'<p class="text-muted small text-center py-3">Sin paciente vinculado.</p>';
|
|
return;
|
|
}
|
|
try {
|
|
const r = await fetch(`api/lab/get_formularios.php?envios=1&paciente_id=${pacienteId}`);
|
|
const d = await r.json();
|
|
const items = d.data || [];
|
|
if (!items.length) {
|
|
document.getElementById('ver-forms-body').innerHTML =
|
|
'<div class="text-center py-4"><i class="fas fa-inbox fa-2x text-muted"></i>' +
|
|
'<p class="mt-2 text-muted small mb-0">Sin formularios para este paciente.</p></div>';
|
|
return;
|
|
}
|
|
document.getElementById('ver-forms-body').innerHTML = items.map(e => {
|
|
const badge = ESTADO_FORM_BADGE[e.estado] || 'bg-secondary';
|
|
const fecha = e.created_at ? e.created_at.slice(0, 10) : '—';
|
|
const diligenciado = ['completado', 'firmado'].includes(e.estado);
|
|
return `<div class="border rounded p-2 mb-2">
|
|
<div class="d-flex justify-content-between align-items-start gap-2">
|
|
<div class="fw-semibold small">${esc(e.form_nombre)}</div>
|
|
<span class="badge ${badge} text-nowrap">${esc(e.estado)}</span>
|
|
</div>
|
|
<div class="text-muted small mt-1">
|
|
<i class="fas fa-calendar me-1"></i>${fecha}
|
|
${e.enviado_por_nombre ? `· ${esc(e.enviado_por_nombre)}` : ''}
|
|
</div>
|
|
${ diligenciado
|
|
? `<a href="ver_formulario_enviado.php?id=${e.id}" target="_blank"
|
|
class="btn btn-sm btn-outline-success mt-2 w-100">
|
|
<i class="fas fa-eye me-1"></i>Ver respuesta
|
|
</a>`
|
|
: `<div class="small text-muted mt-1"><i class="fas fa-clock me-1"></i>Pendiente de completar</div>`
|
|
}
|
|
</div>`;
|
|
}).join('');
|
|
} catch (err) {
|
|
document.getElementById('ver-forms-body').innerHTML =
|
|
`<div class="alert alert-danger small">${esc(err.message)}</div>`;
|
|
}
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<!-- ══════════════ BOTTOM SHEET: Notas clínicas ══════════════ -->
|
|
<div id="nota-backdrop" onclick="notasManager.cerrarSheet()"></div>
|
|
<div id="nota-sheet">
|
|
<div class="sheet-handle"><span></span></div>
|
|
<div id="nota-sheet-inner"></div>
|
|
</div>
|
|
|
|
<!-- ══════════════ FAB: Nueva Agenda ══════════════ -->
|
|
<button class="fab-agenda" title="Agendar nuevo domicilio" onclick="agendaNueva.abrir()">
|
|
<i class="fas fa-calendar-plus"></i>
|
|
</button>
|
|
|
|
<!-- ══════════════ FAB: Enviar formulario ══════════════ -->
|
|
<button class="fab-form" title="Enviar formulario a paciente" onclick="formEnvio.abrirLibre()">
|
|
<i class="fas fa-file-medical"></i>
|
|
</button>
|
|
|
|
<!-- ═══════════════════════ MODAL: Nueva Agenda ══════════════════ -->
|
|
<div class="modal fade" id="modalNuevaAgenda" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-primary text-white py-2">
|
|
<h6 class="modal-title mb-0"><i class="fas fa-calendar-plus me-2"></i>Nueva Agenda de Domicilio</h6>
|
|
<button class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
|
|
<!-- Buscar paciente -->
|
|
<div class="mb-3 position-relative">
|
|
<label class="form-label small fw-semibold">Paciente <span class="text-danger">*</span></label>
|
|
<input type="hidden" id="na-paciente-id">
|
|
<input type="text" id="na-paciente-buscar" class="form-control form-control-sm"
|
|
placeholder="Buscar por nombre o teléfono…" autocomplete="off">
|
|
<div id="na-sugerencias" class="sugerencias-pac d-none"></div>
|
|
<div id="na-paciente-elegido" class="d-none mt-1">
|
|
<span class="badge bg-success" id="na-paciente-label"></span>
|
|
<button type="button" class="btn btn-link btn-sm p-0 ms-1 text-danger"
|
|
onclick="agendaNueva.limpiarPaciente()">✕</button>
|
|
</div>
|
|
|
|
<!-- Crear nuevo paciente -->
|
|
<div id="na-crear-toggle" class="mt-2">
|
|
<button type="button" class="btn btn-link btn-sm p-0 text-success"
|
|
onclick="agendaNueva.toggleNuevoPac()">
|
|
<i class="fas fa-user-plus me-1"></i>
|
|
<span id="na-crear-toggle-txt">Crear nuevo paciente</span>
|
|
</button>
|
|
</div>
|
|
<div id="na-nuevo-pac-panel" class="d-none border rounded p-2 mt-2" style="background:#f0fff4">
|
|
<div class="small fw-semibold text-success mb-2">
|
|
<i class="fas fa-user-plus me-1"></i>Datos del nuevo paciente
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label small mb-1">Nombre completo <span class="text-danger">*</span></label>
|
|
<input type="text" id="na-np-nombre" class="form-control form-control-sm"
|
|
placeholder="Juan Pérez García">
|
|
</div>
|
|
<div class="row g-2 mb-2">
|
|
<div class="col-6">
|
|
<label class="form-label small mb-1">Teléfono</label>
|
|
<input type="tel" id="na-np-telefono" class="form-control form-control-sm"
|
|
placeholder="3001234567">
|
|
</div>
|
|
<div class="col-6">
|
|
<label class="form-label small mb-1">Fecha nacimiento</label>
|
|
<input type="date" id="na-np-fnac" class="form-control form-control-sm">
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="form-label small mb-1">Correo electrónico</label>
|
|
<input type="email" id="na-np-email" class="form-control form-control-sm"
|
|
placeholder="paciente@correo.com">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Dirección -->
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-semibold">Dirección <span class="text-danger">*</span></label>
|
|
<input type="text" id="na-direccion" class="form-control form-control-sm"
|
|
placeholder="Calle 123 # 45-67">
|
|
</div>
|
|
<div class="row g-2 mb-3">
|
|
<div class="col-6">
|
|
<label class="form-label small">Barrio</label>
|
|
<input type="text" id="na-barrio" class="form-control form-control-sm" placeholder="Barrio">
|
|
</div>
|
|
<div class="col-6">
|
|
<label class="form-label small">Ciudad</label>
|
|
<input type="text" id="na-ciudad" class="form-control form-control-sm" placeholder="Ciudad">
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label small">Indicaciones de dirección</label>
|
|
<input type="text" id="na-indicaciones" class="form-control form-control-sm"
|
|
placeholder="Casa azul, portón negro…">
|
|
</div>
|
|
|
|
<!-- Fecha y hora -->
|
|
<div class="row g-2 mb-3">
|
|
<div class="col-7">
|
|
<label class="form-label small fw-semibold">Fecha <span class="text-danger">*</span></label>
|
|
<input type="date" id="na-fecha" class="form-control form-control-sm">
|
|
</div>
|
|
<div class="col-5">
|
|
<label class="form-label small">Hora</label>
|
|
<input type="time" id="na-hora" class="form-control form-control-sm">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cobros -->
|
|
<div class="row g-2 mb-3">
|
|
<div class="col-6">
|
|
<label class="form-label small">Valor domicilio</label>
|
|
<div class="input-group input-group-sm">
|
|
<span class="input-group-text">$</span>
|
|
<input type="number" id="na-valor-dom" class="form-control" min="0" step="1000" placeholder="0">
|
|
</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<label class="form-label small">Copago cliente</label>
|
|
<div class="input-group input-group-sm">
|
|
<span class="input-group-text">$</span>
|
|
<input type="number" id="na-valor-cop" class="form-control" min="0" step="1000" placeholder="0">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notas -->
|
|
<div class="mb-2">
|
|
<label class="form-label small">Notas / instrucciones</label>
|
|
<textarea id="na-notas" class="form-control form-control-sm" rows="2"
|
|
placeholder="Instrucciones adicionales para el servicio…"></textarea>
|
|
</div>
|
|
|
|
<div id="na-error" class="alert alert-danger py-2 d-none small"></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-primary btn-sm" id="na-btn-guardar" onclick="agendaNueva.guardar()">
|
|
<i class="fas fa-calendar-check me-1"></i>Agendar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ══════════════ MODAL: Enviar Formulario ══════════════ -->
|
|
<div class="modal fade" id="modalEnvioForm" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header text-white" style="background:#6f42c1">
|
|
<h6 class="modal-title"><i class="fas fa-file-medical me-2"></i>Enviar Formulario</h6>
|
|
<button class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
|
|
<!-- Seleccionar plantilla -->
|
|
<div class="mb-3">
|
|
<label class="form-label fw-semibold small">Plantilla de formulario *</label>
|
|
<select id="fm-plantilla" class="form-select form-select-sm">
|
|
<option value="">— Seleccionar —</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Datos del paciente -->
|
|
<div class="mb-3">
|
|
<label class="form-label fw-semibold small">Nombre del paciente</label>
|
|
<input type="text" id="fm-nombre" class="form-control form-control-sm" placeholder="Nombre completo">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label fw-semibold small">Teléfono (para enviar por WhatsApp)</label>
|
|
<input type="tel" id="fm-telefono" class="form-control form-control-sm" placeholder="573001234567">
|
|
<div class="form-text">Ingrese el número con código de país, ej: 573001234567</div>
|
|
</div>
|
|
|
|
<!-- Resultado -->
|
|
<div id="fm-resultado" class="d-none">
|
|
<div class="form-result-box">
|
|
<div class="fw-semibold text-success mb-1">✅ Formulario generado</div>
|
|
<div class="small mb-2">Comparte este enlace con el paciente:</div>
|
|
<div class="input-group input-group-sm mb-2">
|
|
<input type="text" id="fm-url" class="form-control" readonly>
|
|
<button class="btn btn-outline-secondary" onclick="formEnvio.copiarUrl()"><i class="fas fa-copy"></i></button>
|
|
</div>
|
|
<a id="fm-wa-link" href="#" target="_blank" class="btn btn-success btn-sm w-100">
|
|
<i class="fab fa-whatsapp me-1"></i>Abrir en WhatsApp
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
<div class="modal-footer py-2">
|
|
<button class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cerrar</button>
|
|
<button id="fm-btn-enviar" class="btn btn-sm text-white" style="background:#6f42c1"
|
|
onclick="formEnvio.enviar()">
|
|
<i class="fas fa-paper-plane me-1"></i>Generar enlace
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// ═══════════════════════════════════════════════════════
|
|
// agendaNueva — Agendar domicilio desde el portal enfermero
|
|
// ═══════════════════════════════════════════════════════
|
|
const agendaNueva = {
|
|
_modal: null,
|
|
_buscarTimer: null,
|
|
_nuevoPac: false,
|
|
|
|
init() {
|
|
this._modal = new bootstrap.Modal('#modalNuevaAgenda');
|
|
const inp = document.getElementById('na-paciente-buscar');
|
|
inp.addEventListener('input', () => {
|
|
clearTimeout(this._buscarTimer);
|
|
this._buscarTimer = setTimeout(() => this._buscarPacientes(inp.value.trim()), 320);
|
|
});
|
|
inp.addEventListener('blur', () => {
|
|
setTimeout(() => document.getElementById('na-sugerencias').classList.add('d-none'), 200);
|
|
});
|
|
},
|
|
|
|
abrir() {
|
|
document.getElementById('na-paciente-id').value = '';
|
|
document.getElementById('na-paciente-buscar').value = '';
|
|
document.getElementById('na-direccion').value = '';
|
|
document.getElementById('na-barrio').value = '';
|
|
document.getElementById('na-ciudad').value = '';
|
|
document.getElementById('na-indicaciones').value = '';
|
|
document.getElementById('na-hora').value = '';
|
|
document.getElementById('na-notas').value = '';
|
|
document.getElementById('na-valor-dom').value = '';
|
|
document.getElementById('na-valor-cop').value = '';
|
|
document.getElementById('na-fecha').value = document.getElementById('portal-fecha').value;
|
|
document.getElementById('na-sugerencias').classList.add('d-none');
|
|
document.getElementById('na-paciente-elegido').classList.add('d-none');
|
|
document.getElementById('na-paciente-buscar').classList.remove('d-none');
|
|
document.getElementById('na-error').classList.add('d-none');
|
|
// Reset panel nuevo paciente
|
|
this._nuevoPac = false;
|
|
document.getElementById('na-nuevo-pac-panel').classList.add('d-none');
|
|
document.getElementById('na-np-nombre').value = '';
|
|
document.getElementById('na-np-telefono').value = '';
|
|
document.getElementById('na-np-fnac').value = '';
|
|
document.getElementById('na-np-email').value = '';
|
|
document.getElementById('na-crear-toggle-txt').textContent = 'Crear nuevo paciente';
|
|
document.getElementById('na-paciente-buscar').disabled = false;
|
|
const btn = document.getElementById('na-btn-guardar');
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="fas fa-calendar-check me-1"></i>Agendar';
|
|
this._modal.show();
|
|
},
|
|
|
|
async _buscarPacientes(q) {
|
|
if (q.length < 2) {
|
|
document.getElementById('na-sugerencias').classList.add('d-none');
|
|
return;
|
|
}
|
|
try {
|
|
const r = await fetch(`api/lab/get_pacientes.php?busqueda=${encodeURIComponent(q)}&limit=8`);
|
|
const d = await r.json();
|
|
const pac = d.data || d.pacientes || [];
|
|
const sug = document.getElementById('na-sugerencias');
|
|
if (!pac.length) { sug.classList.add('d-none'); return; }
|
|
sug.innerHTML = pac.map(p =>
|
|
`<div class="item"
|
|
data-id="${p.id}"
|
|
data-nombre="${esc(p.nombre_completo || p.nombre || '')}"
|
|
data-telefono="${esc(p.telefono || '')}"
|
|
data-direccion="${esc(p.direccion || '')}"
|
|
data-barrio="${esc(p.barrio || '')}"
|
|
data-ciudad="${esc(p.ciudad || '')}">
|
|
<strong>${esc(p.nombre_completo || p.nombre || '')}</strong>
|
|
${p.telefono ? `<small class="text-muted ms-1">${esc(p.telefono)}</small>` : ''}
|
|
${p.direccion ? `<br><small class="text-primary"><i class="fas fa-map-marker-alt me-1"></i>${esc(p.direccion)}${p.barrio ? ', '+esc(p.barrio) : ''}</small>` : ''}
|
|
</div>`
|
|
).join('');
|
|
sug.querySelectorAll('.item').forEach(el => {
|
|
el.addEventListener('click', () => {
|
|
this._seleccionarPaciente(
|
|
+el.dataset.id,
|
|
el.dataset.nombre,
|
|
el.dataset.telefono,
|
|
el.dataset.direccion,
|
|
el.dataset.barrio,
|
|
el.dataset.ciudad
|
|
);
|
|
});
|
|
});
|
|
sug.classList.remove('d-none');
|
|
} catch(e) { /* silencioso */ }
|
|
},
|
|
|
|
_seleccionarPaciente(id, nombre, telefono, direccion, barrio = '', ciudad = '') {
|
|
document.getElementById('na-paciente-id').value = id;
|
|
document.getElementById('na-paciente-buscar').value = nombre;
|
|
document.getElementById('na-paciente-label').textContent =
|
|
nombre + (telefono ? ` · ${telefono}` : '');
|
|
document.getElementById('na-paciente-elegido').classList.remove('d-none');
|
|
document.getElementById('na-sugerencias').classList.add('d-none');
|
|
// Ocultar panel nuevo paciente si estaba abierto
|
|
this._nuevoPac = false;
|
|
document.getElementById('na-nuevo-pac-panel').classList.add('d-none');
|
|
document.getElementById('na-crear-toggle-txt').textContent = 'Crear nuevo paciente';
|
|
document.getElementById('na-paciente-buscar').disabled = false;
|
|
// Siempre rellenar la dirección del paciente (editable)
|
|
if (direccion) document.getElementById('na-direccion').value = direccion;
|
|
if (barrio) document.getElementById('na-barrio').value = barrio;
|
|
if (ciudad) document.getElementById('na-ciudad').value = ciudad;
|
|
if (direccion) document.getElementById('na-direccion').focus();
|
|
},
|
|
|
|
limpiarPaciente() {
|
|
document.getElementById('na-paciente-id').value = '';
|
|
document.getElementById('na-paciente-buscar').value = '';
|
|
document.getElementById('na-paciente-elegido').classList.add('d-none');
|
|
document.getElementById('na-paciente-buscar').disabled = false;
|
|
document.getElementById('na-paciente-buscar').focus();
|
|
},
|
|
|
|
toggleNuevoPac() {
|
|
this._nuevoPac = !this._nuevoPac;
|
|
const panel = document.getElementById('na-nuevo-pac-panel');
|
|
const txt = document.getElementById('na-crear-toggle-txt');
|
|
const buscar = document.getElementById('na-paciente-buscar');
|
|
if (this._nuevoPac) {
|
|
panel.classList.remove('d-none');
|
|
txt.textContent = '← Buscar paciente existente';
|
|
// Limpiar selección previa
|
|
document.getElementById('na-paciente-id').value = '';
|
|
document.getElementById('na-paciente-elegido').classList.add('d-none');
|
|
buscar.value = '';
|
|
buscar.disabled = true;
|
|
document.getElementById('na-sugerencias').classList.add('d-none');
|
|
document.getElementById('na-np-nombre').focus();
|
|
} else {
|
|
panel.classList.add('d-none');
|
|
txt.textContent = 'Crear nuevo paciente';
|
|
buscar.disabled = false;
|
|
buscar.focus();
|
|
}
|
|
},
|
|
|
|
async guardar() {
|
|
const pacIdInicial = +document.getElementById('na-paciente-id').value;
|
|
const direccion = document.getElementById('na-direccion').value.trim();
|
|
const fecha = document.getElementById('na-fecha').value;
|
|
document.getElementById('na-error').classList.add('d-none');
|
|
|
|
// Validaciones básicas
|
|
if (!pacIdInicial && !this._nuevoPac) {
|
|
this._mostrarError('Selecciona un paciente de la lista o crea uno nuevo.');
|
|
return;
|
|
}
|
|
if (!direccion) { this._mostrarError('La dirección es obligatoria.'); return; }
|
|
if (!fecha) { this._mostrarError('La fecha es obligatoria.'); return; }
|
|
|
|
const btn = document.getElementById('na-btn-guardar');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Guardando…';
|
|
|
|
let pacId = pacIdInicial;
|
|
|
|
// Si es paciente nuevo, crearlo primero
|
|
if (this._nuevoPac) {
|
|
const nombre = document.getElementById('na-np-nombre').value.trim();
|
|
if (!nombre) {
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="fas fa-calendar-check me-1"></i>Agendar';
|
|
this._mostrarError('El nombre del nuevo paciente es obligatorio.');
|
|
return;
|
|
}
|
|
try {
|
|
const rPac = await fetch('api/lab/save_paciente.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
nombre_completo: nombre,
|
|
telefono: document.getElementById('na-np-telefono').value.trim() || null,
|
|
fecha_nacimiento: document.getElementById('na-np-fnac').value || null,
|
|
email: document.getElementById('na-np-email').value.trim() || null,
|
|
}),
|
|
});
|
|
const dPac = await rPac.json();
|
|
if (!dPac.success) throw new Error(dPac.error || 'Error al crear paciente');
|
|
pacId = dPac.id;
|
|
} catch (e) {
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="fas fa-calendar-check me-1"></i>Agendar';
|
|
this._mostrarError('No se pudo crear el paciente: ' + e.message);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const datos = {
|
|
paciente_id: pacId,
|
|
direccion,
|
|
barrio: document.getElementById('na-barrio').value.trim() || null,
|
|
ciudad: document.getElementById('na-ciudad').value.trim() || null,
|
|
indicaciones_dir: document.getElementById('na-indicaciones').value.trim() || null,
|
|
fecha_programada: fecha,
|
|
hora_programada: document.getElementById('na-hora').value || null,
|
|
tipo_servicio: 'domicilio',
|
|
tipo_cliente: 'particular',
|
|
valor_domicilio: parseFloat(document.getElementById('na-valor-dom').value) || null,
|
|
valor_copago: parseFloat(document.getElementById('na-valor-cop').value) || null,
|
|
notas_admin: document.getElementById('na-notas').value.trim() || null,
|
|
estado: 'programado',
|
|
...(ENFERMERA_ID ? { enfermera_id: ENFERMERA_ID } : {}),
|
|
};
|
|
|
|
try {
|
|
const r = await fetch('api/lab/save_domicilio.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(datos),
|
|
});
|
|
const d = await r.json();
|
|
if (!d.success) throw new Error(d.error || 'Error al guardar');
|
|
this._modal.hide();
|
|
portal.cargar();
|
|
} catch (e) {
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="fas fa-calendar-check me-1"></i>Agendar';
|
|
this._mostrarError(e.message);
|
|
}
|
|
},
|
|
|
|
_mostrarError(msg) {
|
|
const el = document.getElementById('na-error');
|
|
el.textContent = msg;
|
|
el.classList.remove('d-none');
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<script>
|
|
// ═══════════════════════════════════════════════════════
|
|
// formEnvio — Lógica de envío de formularios
|
|
// ═══════════════════════════════════════════════════════
|
|
const formEnvio = {
|
|
_plantillas: [],
|
|
_modalEl: null,
|
|
_modal: null,
|
|
_domicilioId: null,
|
|
_pacienteId: null,
|
|
|
|
// ── Obtener instancia Bootstrap Modal ────────────────
|
|
_getModal() {
|
|
if (!this._modal) {
|
|
this._modalEl = document.getElementById('modalEnvioForm');
|
|
this._modal = bootstrap.Modal.getOrCreateInstance(this._modalEl);
|
|
}
|
|
return this._modal;
|
|
},
|
|
|
|
// ── Cargar plantillas al inicio ───────────────────────
|
|
async cargarPlantillas() {
|
|
try {
|
|
const r = await fetch('api/lab/get_formularios.php');
|
|
const d = await r.json();
|
|
this._plantillas = d.data || [];
|
|
const sel = document.getElementById('fm-plantilla');
|
|
this._plantillas.forEach(f => {
|
|
const o = document.createElement('option');
|
|
o.value = f.id;
|
|
o.textContent = f.nombre + (f.categoria ? ` (${f.categoria})` : '');
|
|
sel.appendChild(o);
|
|
});
|
|
} catch(e) {
|
|
console.warn('No se pudieron cargar formularios:', e);
|
|
}
|
|
},
|
|
|
|
// ── Abrir modal desde botón en tarjeta de domicilio ──
|
|
abrirDesdeCard(domId, nombre, telefono, pacienteId) {
|
|
this._limpiar();
|
|
this._domicilioId = domId;
|
|
this._pacienteId = pacienteId || null;
|
|
if (nombre) document.getElementById('fm-nombre').value = nombre;
|
|
if (telefono) document.getElementById('fm-telefono').value = telefono;
|
|
this._getModal().show();
|
|
},
|
|
|
|
// ── Abrir modal en blanco (FAB) ────────────────────────
|
|
abrirLibre() {
|
|
this._limpiar();
|
|
this._getModal().show();
|
|
},
|
|
|
|
// ── Limpiar estado modal ───────────────────────────────
|
|
_limpiar() {
|
|
this._domicilioId = null;
|
|
this._pacienteId = null;
|
|
document.getElementById('fm-plantilla').value = '';
|
|
document.getElementById('fm-nombre').value = '';
|
|
document.getElementById('fm-telefono').value = '';
|
|
document.getElementById('fm-resultado').classList.add('d-none');
|
|
document.getElementById('fm-btn-enviar').disabled = false;
|
|
},
|
|
|
|
// ── Generar y enviar formulario ──────────────────────
|
|
async enviar() {
|
|
const plantillaId = parseInt(document.getElementById('fm-plantilla').value);
|
|
const nombre = document.getElementById('fm-nombre').value.trim();
|
|
const telefono = document.getElementById('fm-telefono').value.trim();
|
|
|
|
if (!plantillaId) { alert('Selecciona una plantilla de formulario.'); return; }
|
|
|
|
const btn = document.getElementById('fm-btn-enviar');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
|
|
|
|
try {
|
|
const body = {
|
|
formulario_id: plantillaId,
|
|
domicilio_id: this._domicilioId,
|
|
paciente_id: this._pacienteId,
|
|
enviado_via: 'whatsapp',
|
|
datos_prefilled: {
|
|
...(nombre ? { nombre_completo: nombre } : {}),
|
|
...(telefono ? { telefono: telefono } : {}),
|
|
}
|
|
};
|
|
// Si hay paciente_id, dejamos que el servidor enriquezca automáticamente
|
|
if (!this._pacienteId && nombre) {
|
|
body.datos_prefilled.__paciente = { nombre_completo: nombre, telefono };
|
|
}
|
|
|
|
const r = await fetch('api/lab/send_formulario.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
const res = await r.json();
|
|
|
|
if (!res.ok) throw new Error(res.error || 'Error al generar');
|
|
|
|
// Mostrar resultado
|
|
document.getElementById('fm-url').value = res.url;
|
|
const waLink = document.getElementById('fm-wa-link');
|
|
waLink.href = res.whatsapp_url || '#';
|
|
if (!res.whatsapp_url) waLink.style.display = 'none';
|
|
document.getElementById('fm-resultado').classList.remove('d-none');
|
|
|
|
btn.innerHTML = '<i class="fas fa-check me-1"></i>Listo';
|
|
} catch(e) {
|
|
alert('Error: ' + e.message);
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="fas fa-paper-plane me-1"></i>Generar enlace';
|
|
}
|
|
},
|
|
|
|
// ── Copiar URL al portapapeles ─────────────────────────
|
|
async copiarUrl() {
|
|
try {
|
|
await navigator.clipboard.writeText(document.getElementById('fm-url').value);
|
|
const btn = document.querySelector('#modalEnvioForm .input-group .btn');
|
|
btn.innerHTML = '<i class="fas fa-check text-success"></i>';
|
|
setTimeout(() => btn.innerHTML = '<i class="fas fa-copy"></i>', 1500);
|
|
} 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._renderLista();
|
|
} catch(e) {
|
|
alert('Error: ' + e.message);
|
|
}
|
|
},
|
|
};
|
|
|
|
// ══════════════════════════════════════════════════════════
|
|
// OBJETO: notasManager — Sistema de notas clínicas
|
|
// ══════════════════════════════════════════════════════════
|
|
const notasManager = {
|
|
_cache: {}, // domicilio_id → { clinica, libres[] }
|
|
_domId: null, // domicilio actualmente en el sheet
|
|
_notaId: null, // null = nueva nota libre, número = editar
|
|
_imagen: null, // File pendiente de subir
|
|
|
|
// ── Abrir/cerrar sección de notas en la tarjeta ────────────────────────
|
|
async toggle(domId, btn) {
|
|
const body = document.getElementById(`notas-body-${domId}`);
|
|
const chev = btn.querySelector('.nota-chevron');
|
|
if (!body) return;
|
|
|
|
if (!body.classList.contains('d-none')) {
|
|
body.classList.add('d-none');
|
|
chev.classList.remove('open');
|
|
return;
|
|
}
|
|
body.classList.remove('d-none');
|
|
chev.classList.add('open');
|
|
await this._renderSeccion(domId);
|
|
},
|
|
|
|
// ── Cargar notas desde API (con caché) ────────────────────────────────
|
|
async _cargar(domId) {
|
|
const r = await fetch(`api/lab/get_notas_domicilio.php?domicilio_id=${domId}`);
|
|
const d = await r.json();
|
|
this._cache[domId] = d.success
|
|
? { clinica: d.clinica || null, libres: d.libres || [] }
|
|
: { clinica: null, libres: [] };
|
|
// Actualizar badge
|
|
const total = (d.libres || []).length + (d.clinica ? 1 : 0);
|
|
const badge = document.getElementById(`nota-count-${domId}`);
|
|
if (badge) {
|
|
badge.textContent = total;
|
|
badge.style.display = total ? '' : 'none';
|
|
}
|
|
return this._cache[domId];
|
|
},
|
|
|
|
// ── Renderizar sección completa en la tarjeta ─────────────────────────
|
|
async _renderSeccion(domId) {
|
|
const body = document.getElementById(`notas-body-${domId}`);
|
|
if (!body) return;
|
|
const data = await this._cargar(domId);
|
|
const pac = body.dataset.pacNac || '';
|
|
const esMenor = pac ? this._esmenor(pac) : false;
|
|
|
|
let html = '';
|
|
|
|
// 1. Ficha clínica
|
|
const fc = data.clinica;
|
|
html += `<div class="nota-ficha-card">
|
|
<div class="ficha-header">
|
|
<span class="ficha-titulo"><i class="fas fa-file-medical me-1"></i>Ficha clínica</span>
|
|
<button class="btn btn-xs btn-outline-warning py-0 px-2" style="font-size:.72rem"
|
|
onclick="notasManager.abrirFicha(${domId})">
|
|
<i class="fas fa-pen"></i> ${fc ? 'Editar' : 'Completar'}
|
|
</button>
|
|
</div>`;
|
|
if (fc) {
|
|
if (fc.antecedentes) html += `<div class="small mb-1"><span class="fw-semibold">Antecedentes:</span> ${esc(fc.antecedentes)}</div>`;
|
|
if (fc.medicamentos) html += `<div class="small mb-1"><span class="fw-semibold">Medicamentos:</span> ${esc(fc.medicamentos)}</div>`;
|
|
if (fc.acudiente_nombre) html += `<div class="small mb-1"><span class="fw-semibold">Acudiente:</span> ${esc(fc.acudiente_nombre)} · ${esc(fc.acudiente_documento||'—')}</div>`;
|
|
} else {
|
|
html += `<div class="small text-muted">Sin ficha clínica registrada</div>`;
|
|
}
|
|
if (esMenor && (!fc || !fc.acudiente_nombre)) {
|
|
html += `<div class="small text-danger mt-1"><i class="fas fa-exclamation-triangle me-1"></i>Paciente menor de edad — acudiente requerido</div>`;
|
|
}
|
|
html += `</div>`;
|
|
|
|
// 2. Notas libres
|
|
data.libres.forEach(n => {
|
|
html += `<div class="nota-libre-card" id="nota-libre-${n.id}">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div class="nota-titulo">${esc(n.titulo||'Sin título')}</div>
|
|
<div class="d-flex gap-1">
|
|
<button class="btn btn-xs btn-outline-secondary py-0 px-1" style="font-size:.7rem"
|
|
onclick="notasManager.abrirLibre(${domId}, ${n.id})">
|
|
<i class="fas fa-pen"></i>
|
|
</button>
|
|
<button class="btn btn-xs btn-outline-danger py-0 px-1" style="font-size:.7rem"
|
|
onclick="notasManager.eliminar(${n.id}, ${domId})">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="nota-cuerpo">${n.cuerpo ? n.cuerpo.replace(/\n/g,'<br>') : ''}</div>
|
|
${n.imagen_path ? `<img src="uploads/media/${esc(n.imagen_path)}"
|
|
class="img-fluid rounded mt-1" style="max-height:160px;cursor:pointer"
|
|
onclick="notasManager._verImg(this.src)">` : ''}
|
|
<div class="nota-fecha">${esc(n.created_at||'')}</div>
|
|
</div>`;
|
|
});
|
|
|
|
html += `<button class="btn btn-sm w-100 mt-1" style="background:#e0f2fe;color:#0369a1;border:1px dashed #7dd3fc;border-radius:8px;font-size:.78rem"
|
|
onclick="notasManager.abrirLibre(${domId}, null)">
|
|
<i class="fas fa-plus me-1"></i>Agregar nota
|
|
</button>`;
|
|
|
|
body.innerHTML = html;
|
|
},
|
|
|
|
// ── Bottom sheet: Ficha clínica ────────────────────────────────────────
|
|
abrirFicha(domId) {
|
|
this._domId = domId;
|
|
this._notaId = 'ficha';
|
|
this._imagen = null;
|
|
|
|
const body = document.getElementById(`notas-body-${domId}`);
|
|
const pac = body ? body.dataset.pacNac || '' : '';
|
|
const esMenor= pac ? this._esmenor(pac) : false;
|
|
const fc = (this._cache[domId] || {}).clinica || {};
|
|
|
|
document.getElementById('nota-sheet-title').textContent = 'Ficha clínica';
|
|
document.getElementById('nota-sheet-inner').innerHTML = `
|
|
<div class="nota-section">
|
|
<label class="nota-label"><i class="fas fa-history me-1 text-orange"></i>Antecedentes</label>
|
|
<textarea id="ns-antecedentes" class="nota-input" rows="3"
|
|
placeholder="Alergias, enfermedades previas, cirugías...">${esc(fc.antecedentes||'')}</textarea>
|
|
</div>
|
|
<div class="nota-section">
|
|
<label class="nota-label"><i class="fas fa-pills me-1 text-blue"></i>Medicamentos actuales</label>
|
|
<textarea id="ns-medicamentos" class="nota-input" rows="3"
|
|
placeholder="Nombre, dosis y frecuencia...">${esc(fc.medicamentos||'')}</textarea>
|
|
</div>
|
|
<div id="ns-acudiente-bloque" class="nota-acudiente-bloque mb-3 ${esMenor ? '' : 'd-none'}">
|
|
<div class="small fw-bold text-danger mb-2"><i class="fas fa-child me-1"></i>Datos del acudiente (paciente menor de edad)</div>
|
|
<label class="nota-label">Nombre completo acudiente <span class="text-danger">*</span></label>
|
|
<input type="text" id="ns-acud-nombre" class="nota-input mb-2"
|
|
placeholder="Nombre completo" value="${esc(fc.acudiente_nombre||'')}">
|
|
<label class="nota-label">Documento acudiente <span class="text-danger">*</span></label>
|
|
<input type="text" id="ns-acud-doc" class="nota-input"
|
|
placeholder="Número de documento" value="${esc(fc.acudiente_documento||'')}">
|
|
</div>
|
|
<button class="nota-btn-guardar" style="background:#0d6efd"
|
|
onclick="notasManager._guardarFicha()">
|
|
<i class="fas fa-save me-2"></i>Guardar ficha clínica
|
|
</button>`;
|
|
|
|
if (!esMenor && fc.acudiente_nombre) {
|
|
// paciente no reconocido como menor por fecha de nacimiento
|
|
// pero tenía datos del acudiente guardados → se mantienen ocultos
|
|
}
|
|
this._abrirSheet();
|
|
},
|
|
|
|
// ── Bottom sheet: Nota libre ───────────────────────────────────────────
|
|
abrirLibre(domId, notaId) {
|
|
this._domId = domId;
|
|
this._notaId = notaId;
|
|
this._imagen = null;
|
|
|
|
let titulo = '', cuerpo = '';
|
|
if (notaId) {
|
|
const n = (this._cache[domId]?.libres || []).find(x => x.id == notaId);
|
|
if (n) { titulo = n.titulo || ''; cuerpo = n.cuerpo || ''; }
|
|
}
|
|
|
|
document.getElementById('nota-sheet-title').textContent = notaId ? 'Editar nota' : 'Nueva nota';
|
|
document.getElementById('nota-sheet-inner').innerHTML = `
|
|
<div class="nota-section">
|
|
<label class="nota-label">Título</label>
|
|
<input type="text" id="ns-titulo" class="nota-input"
|
|
placeholder="Ej: Observación inicial, Incidente..." value="${esc(titulo)}">
|
|
</div>
|
|
<div class="nota-section">
|
|
<label class="nota-label">Nota</label>
|
|
<div class="fmt-toolbar">
|
|
<button class="fmt-btn" onclick="notasManager._fmt('bold')"><b>N</b></button>
|
|
<button class="fmt-btn" onclick="notasManager._fmt('italic')"><i>K</i></button>
|
|
<button class="fmt-btn" onclick="notasManager._fmt('underline')"><u>U</u></button>
|
|
<button class="fmt-btn" onclick="notasManager._fmt('strikeThrough')"><s>S</s></button>
|
|
<button class="fmt-btn" onclick="notasManager._fmt('insertUnorderedList')">• Lista</button>
|
|
<button class="fmt-btn" onclick="notasManager._fmt('insertOrderedList')">1. Lista</button>
|
|
</div>
|
|
<div id="ns-cuerpo" class="nota-editor" contenteditable="true"
|
|
data-placeholder="Escribe aquí la observación...">${cuerpo}</div>
|
|
</div>
|
|
<div class="nota-section">
|
|
<label class="nota-label"><i class="fas fa-image me-1"></i>Imagen (opcional)</label>
|
|
<div class="d-flex gap-2">
|
|
<label class="btn btn-sm btn-outline-secondary" style="font-size:.76rem">
|
|
<i class="fas fa-camera me-1"></i>Cámara
|
|
<input type="file" accept="image/*" capture="environment" class="d-none"
|
|
onchange="notasManager._prevImagen(this)">
|
|
</label>
|
|
<label class="btn btn-sm btn-outline-secondary" style="font-size:.76rem">
|
|
<i class="fas fa-image me-1"></i>Galería
|
|
<input type="file" accept="image/*" class="d-none"
|
|
onchange="notasManager._prevImagen(this)">
|
|
</label>
|
|
</div>
|
|
<div id="ns-img-preview" class="mt-2"></div>
|
|
</div>
|
|
<button class="nota-btn-guardar" style="background:#059669"
|
|
onclick="notasManager._guardarLibre()">
|
|
<i class="fas fa-save me-2"></i>Guardar nota
|
|
</button>`;
|
|
|
|
this._abrirSheet();
|
|
},
|
|
|
|
// ── Guardar ficha clínica ─────────────────────────────────────────────
|
|
async _guardarFicha() {
|
|
const esMenor = !document.getElementById('ns-acudiente-bloque')?.classList.contains('d-none');
|
|
const antecedentes = document.getElementById('ns-antecedentes')?.value.trim() || '';
|
|
const medicamentos = document.getElementById('ns-medicamentos')?.value.trim() || '';
|
|
const acudNombre = document.getElementById('ns-acud-nombre')?.value.trim() || '';
|
|
const acudDoc = document.getElementById('ns-acud-doc')?.value.trim() || '';
|
|
|
|
if (esMenor && (!acudNombre || !acudDoc)) {
|
|
alert('El paciente es menor de edad — completa nombre y documento del acudiente.');
|
|
return;
|
|
}
|
|
try {
|
|
const r = await fetch('api/lab/save_nota_domicilio.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
tipo: 'clinica',
|
|
domicilio_id: this._domId,
|
|
antecedentes, medicamentos,
|
|
acudiente_nombre: acudNombre || null,
|
|
acudiente_documento: acudDoc || null,
|
|
}),
|
|
});
|
|
const d = await r.json();
|
|
if (!d.success) throw new Error(d.error || 'Error al guardar');
|
|
delete this._cache[this._domId];
|
|
this._cerrarSheet();
|
|
await this._renderSeccion(this._domId);
|
|
} catch(e) { alert('Error: ' + e.message); }
|
|
},
|
|
|
|
// ── Guardar nota libre ─────────────────────────────────────────────────
|
|
async _guardarLibre() {
|
|
const titulo = document.getElementById('ns-titulo')?.value.trim() || '';
|
|
const cuerpo = document.getElementById('ns-cuerpo')?.innerHTML || '';
|
|
if (!titulo) { alert('Escribe un título para la nota.'); return; }
|
|
|
|
let imagenPath = null;
|
|
if (this._imagen) {
|
|
const fd = new FormData();
|
|
fd.append('file', this._imagen);
|
|
const ru = await fetch('api/lab/upload_nota_imagen.php', { method:'POST', body: fd });
|
|
const du = await ru.json();
|
|
if (!du.success) { alert('No se pudo subir la imagen: ' + (du.error||'')); return; }
|
|
imagenPath = du.local_file;
|
|
}
|
|
|
|
try {
|
|
const payload = {
|
|
tipo: 'libre',
|
|
domicilio_id: this._domId,
|
|
titulo,
|
|
cuerpo,
|
|
imagen_path: imagenPath,
|
|
};
|
|
if (this._notaId) payload.id = this._notaId;
|
|
|
|
const r = await fetch('api/lab/save_nota_domicilio.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
const d = await r.json();
|
|
if (!d.success) throw new Error(d.error || 'Error al guardar');
|
|
delete this._cache[this._domId];
|
|
this._cerrarSheet();
|
|
await this._renderSeccion(this._domId);
|
|
} catch(e) { alert('Error: ' + e.message); }
|
|
},
|
|
|
|
// ── Eliminar nota libre ────────────────────────────────────────────────
|
|
async eliminar(notaId, domId) {
|
|
if (!confirm('¿Eliminar esta nota?')) return;
|
|
try {
|
|
const r = await fetch('api/lab/delete_nota_domicilio.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id: notaId }),
|
|
});
|
|
const d = await r.json();
|
|
if (!d.success) throw new Error(d.error);
|
|
delete this._cache[domId];
|
|
await this._renderSeccion(domId);
|
|
} catch(e) { alert('Error: ' + e.message); }
|
|
},
|
|
|
|
// ── Helpers de UI ──────────────────────────────────────────────────────
|
|
_abrirSheet() {
|
|
document.getElementById('nota-backdrop').style.display = 'block';
|
|
document.getElementById('nota-sheet').style.display = 'block';
|
|
document.body.style.overflow = 'hidden';
|
|
},
|
|
_cerrarSheet() {
|
|
document.getElementById('nota-backdrop').style.display = 'none';
|
|
document.getElementById('nota-sheet').style.display = 'none';
|
|
document.body.style.overflow = '';
|
|
this._domId = null; this._notaId = null; this._imagen = null;
|
|
},
|
|
_fmt(cmd) { document.execCommand(cmd, false, null); document.getElementById('ns-cuerpo')?.focus(); },
|
|
_toggleAcudiente(on) {
|
|
document.getElementById('ns-acudiente-bloque')?.classList.toggle('d-none', !on);
|
|
},
|
|
_prevImagen(input) {
|
|
const file = input.files?.[0];
|
|
if (!file) return;
|
|
this._imagen = file;
|
|
const reader = new FileReader();
|
|
reader.onload = e => {
|
|
const prev = document.getElementById('ns-img-preview');
|
|
if (prev) prev.innerHTML = `<img src="${e.target.result}"
|
|
class="img-fluid rounded" style="max-height:140px">
|
|
<div class="small text-muted mt-1">${esc(file.name)}</div>`;
|
|
};
|
|
reader.readAsDataURL(file);
|
|
},
|
|
_verImg(src) {
|
|
const w = window.open('', '_blank');
|
|
if (w) { w.document.write(`<img src="${src}" style="max-width:100%;height:auto">`); }
|
|
},
|
|
_esmenor(fechaNac) {
|
|
if (!fechaNac) return false;
|
|
const d = new Date(fechaNac);
|
|
const hoy = new Date();
|
|
const años = hoy.getFullYear() - d.getFullYear() -
|
|
(hoy < new Date(hoy.getFullYear(), d.getMonth(), d.getDate()) ? 1 : 0);
|
|
return años < 18;
|
|
},
|
|
};
|
|
</script>
|
|
<!-- ── Bottom sheet: editor de notas ──────────────────────────── -->
|
|
<div id="nota-backdrop" onclick="notasManager._cerrarSheet()"></div>
|
|
<div id="nota-sheet" role="dialog" aria-modal="true">
|
|
<div class="sheet-handle"><span></span></div>
|
|
<div style="padding:0 16px 4px;display:flex;align-items:center;justify-content:space-between">
|
|
<span id="nota-sheet-title" style="font-weight:800;font-size:.95rem;color:#111"></span>
|
|
<button onclick="notasManager._cerrarSheet()"
|
|
style="background:none;border:none;font-size:1.3rem;color:#9ca3af;line-height:1">×</button>
|
|
</div>
|
|
<div id="nota-sheet-inner"></div>
|
|
</div>
|
|
|
|
</body>
|
|
</html>
|