Files
whatsapp/lab_domicilios.php
2026-05-27 20:40:17 -05:00

1742 lines
92 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Gestión de Domicilios — Módulo Administrativo de Laboratorio
*/
require_once 'config/config.php';
if (!defined('APP_ROOT')) { define('APP_ROOT', __DIR__); }
if (!isUserLoggedIn()) {
header('Location: login.php');
exit;
}
if (isEnfermero()) { header('Location: enfermero_portal.php'); exit; }
$adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['username'] ?? 'Admin';
$puedeEscribir = hasModuleWrite('lab_domicilios');
$ordenIdParam = (int)($_GET['orden_id'] ?? 0);
$pacIdParam = (int)($_GET['paciente_id']?? 0);
$domIdParam = (int)($_GET['id'] ?? 0);
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Domicilios — Módulo Lab</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="./assets/css/styles.css?v=12" rel="stylesheet">
<style>
.dom-row { cursor:pointer; }
.estado-badge { font-size:.72rem; }
.agenda-card { border-left: 4px solid #dee2e6; padding: 8px 12px; margin-bottom: 8px; border-radius: 0 8px 8px 0; }
.agenda-card.programado { border-left-color: #6c757d; }
.agenda-card.en_camino { border-left-color: #0d6efd; }
.agenda-card.completado { border-left-color: #198754; }
.agenda-card.cancelado { border-left-color: #dc3545; }
#seccion-formularios .ff-ctrl { height:31px; padding-top:.25rem; padding-bottom:.25rem; }
</style>
</head>
<body>
<?php
$SIDEBAR_TITLE = 'Panel Lab';
$SIDEBAR_ICON = 'fas fa-flask';
require_once __DIR__ . '/shared/components/sidebar.php';
?>
<main class="main-content">
<header class="content-header d-flex align-items-center justify-content-between">
<div>
<h1 id="header-titulo"><i class="fas fa-house-medical text-primary"></i> Domicilios</h1>
<small class="text-muted"><?= htmlspecialchars($adminNombre) ?></small>
</div>
<?php if ($puedeEscribir): ?>
<button class="btn btn-primary btn-sm" id="btn-nuevo-domicilio" onclick="abrirFormulario()">
<i class="fas fa-plus me-1"></i> Nuevo Domicilio
</button>
<?php endif; ?>
</header>
<!-- Tabs -->
<div class="container-fluid pt-2 pb-0">
<ul class="nav nav-tabs border-bottom-0">
<li class="nav-item">
<a class="nav-link active" id="tab-btn-domicilios" href="#" onclick="switchTab('domicilios',this);return false;">
<i class="fas fa-house-medical me-1"></i>Domicilios
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="tab-btn-formularios" href="#" onclick="switchTab('formularios',this);return false;">
<i class="fas fa-wpforms me-1"></i>Formularios recibidos
</a>
</li>
</ul>
</div>
<div class="container-fluid py-3" id="seccion-domicilios">
<!-- Filtros -->
<div class="row g-2 mb-3">
<div class="col-md-2">
<input type="date" id="filtro-fecha" class="form-control form-control-sm"
value="<?= date('Y-m-d') ?>" onchange="cargarLista()">
</div>
<div class="col-md-2">
<select id="filtro-estado" class="form-select form-select-sm" onchange="cargarLista()">
<option value="">Todos los estados</option>
<option>programado</option><option>confirmado</option><option>en_camino</option>
<option>en_domicilio</option><option>completado</option><option>cancelado</option>
</select>
</div>
<div class="col-md-2">
<select id="filtro-enfermera" class="form-select form-select-sm" onchange="cargarLista()">
<option value="">Todos los enfermeros</option>
</select>
</div>
<div class="col-md-2">
<button class="btn btn-sm btn-secondary w-100" onclick="verHoy()">
<i class="fas fa-calendar-day me-1"></i> Hoy
</button>
</div>
<div class="col-md-2">
<button class="btn btn-sm btn-success w-100" onclick="exportarDomicilios()">
<i class="fas fa-file-excel me-1"></i> Excel
</button>
</div>
</div>
<!-- Resumen del día -->
<div class="row g-2 mb-3" id="resumen-hoy"></div>
<div class="row g-3">
<!-- Tabla -->
<div class="col-lg-5">
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr><th>Fecha</th><th>Hora</th><th>Paciente</th><th>Enfermero</th><th>Estado</th></tr>
</thead>
<tbody id="tabla-body">
<tr><td colspan="5" class="text-center py-4 text-muted"><i class="fas fa-spinner fa-spin"></i></td></tr>
</tbody>
</table>
</div>
<div class="d-flex align-items-center justify-content-between px-3 py-2 border-top" id="paginacion"></div>
</div>
</div>
</div>
<!-- Detalle -->
<div class="col-lg-7">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-0 d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-semibold">Detalle <span id="detail-id" class="text-muted"></span></h6>
<div class="d-flex gap-2">
<button class="btn btn-sm btn-outline-success d-none" id="btn-informe-dom"
onclick="verInformeDom()">
<i class="fas fa-file-medical me-1"></i>Informe
</button>
<button class="btn btn-sm btn-outline-primary d-none" id="btn-editar-dom"
onclick="editarDomicilio(domSeleccionado)">
<i class="fas fa-edit me-1"></i>Editar
</button>
<button class="btn-close" onclick="cerrarDetalle()"></button>
</div>
</div>
<div class="card-body" id="detail-body">
<div class="text-center py-5 text-muted">
<i class="fas fa-arrow-left me-2"></i>Selecciona un domicilio
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ═══════════ SECCIÓN FORMULARIOS RECIBIDOS ═══════════ -->
<div class="container-fluid py-3" id="seccion-formularios" style="display:none">
<!-- Filtros compactos -->
<div class="card border-0 bg-light mb-3 px-3 py-2">
<div class="row g-2 align-items-end">
<div class="col-6 col-sm-2">
<label class="form-label form-label-sm mb-1 text-muted">Desde</label>
<input type="date" id="ff-fecha-desde" class="form-control form-control-sm ff-ctrl" onchange="ffFiltrar()">
</div>
<div class="col-6 col-sm-2">
<label class="form-label form-label-sm mb-1 text-muted">Hasta</label>
<input type="date" id="ff-fecha-hasta" class="form-control form-control-sm ff-ctrl" onchange="ffFiltrar()">
</div>
<div class="col-12 col-sm-3">
<label class="form-label form-label-sm mb-1 text-muted">Formulario</label>
<select id="ff-plantilla" class="form-select form-select-sm ff-ctrl" onchange="ffFiltrar()">
<option value="">Todos los formularios</option>
</select>
</div>
<div class="col-6 col-sm-2">
<label class="form-label form-label-sm mb-1 text-muted">Estado</label>
<select id="ff-estado" class="form-select form-select-sm ff-ctrl" onchange="ffFiltrar()">
<option value="">Todos</option>
<option value="pendiente">⏳ Pendiente</option>
<option value="completado">✅ Completado</option>
<option value="firmado">✍️ Firmado</option>
<option value="expirado">❌ Expirado</option>
</select>
</div>
<div class="col-6 col-sm-3">
<label class="form-label form-label-sm mb-1 text-muted">Paciente</label>
<input type="text" id="ff-paciente" class="form-control form-control-sm ff-ctrl"
placeholder="Buscar…" oninput="ffFiltrar()">
</div>
<div class="col-12 col-sm-auto ms-sm-auto d-flex gap-2">
<button class="btn btn-success btn-sm" onclick="ffExportarExcel()" title="Exportar a Excel">
<i class="fas fa-file-excel me-1"></i><span class="d-none d-md-inline">Excel</span>
</button>
<button class="btn btn-outline-secondary btn-sm" onclick="ffLimpiar()" title="Limpiar">
<i class="fas fa-times"></i>
</button>
</div>
</div>
</div>
<!-- Spinner -->
<div id="ff-spinner" class="text-center py-5" style="display:none">
<div class="spinner-border text-primary" role="status" style="width:2rem;height:2rem">
<span class="visually-hidden">Cargando…</span>
</div>
<p class="text-muted small mt-2 mb-0">Cargando formularios…</p>
</div>
<div class="card border-0 shadow-sm" id="ff-tabla-wrap">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Fecha</th>
<th>Paciente</th>
<th>Formulario</th>
<th>Enviado por</th>
<th>Estado</th>
<th></th>
</tr>
</thead>
<tbody id="ff-tbody">
<tr><td colspan="6" class="text-center py-4 text-muted"><i class="fas fa-spinner fa-spin"></i></td></tr>
</tbody>
</table>
</div>
<!-- Paginación -->
<div class="d-flex align-items-center justify-content-between px-3 py-2 border-top flex-wrap gap-2">
<small class="text-muted" id="ff-info"></small>
<nav><ul class="pagination pagination-sm mb-0" id="ff-paginacion"></ul></nav>
</div>
</div>
</div>
</div>
</main>
<!-- Modal Formulario (Nuevo / Editar) -->
<div class="modal fade" id="modalDomicilio" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-house-medical me-2"></i><span id="modal-titulo">Nuevo Domicilio</span></h5>
<button class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="form-domicilio">
<input type="hidden" name="id" id="dom-id">
<input type="hidden" name="orden_id_url" id="dom-orden-id" value="<?= $ordenIdParam ?>">
<!-- ── Paciente ── -->
<h6 class="text-muted text-uppercase small fw-semibold border-bottom pb-1 mb-3">
<i class="fas fa-user me-1"></i> Paciente y servicio
</h6>
<div class="row g-3 mb-3">
<div class="col-md-8">
<label class="form-label">Paciente <span class="text-danger">*</span></label>
<input type="hidden" name="paciente_id" id="dom-paciente-id" value="<?= $pacIdParam ?>">
<input type="text" class="form-control" id="dom-paciente-nombre" placeholder="Buscar paciente…" autocomplete="off">
<div class="list-group mt-1 shadow-sm" id="pac-suggest" style="position:absolute;z-index:1000;max-height:200px;overflow-y:auto;display:none"></div>
</div>
<div class="col-md-4">
<label class="form-label">Tipo de servicio</label>
<input type="text" class="form-control" name="tipo_servicio" id="dom-tipo" placeholder="Ej: toma de muestras">
</div>
<div class="col-md-4">
<label class="form-label">Tipo de cliente</label>
<select class="form-select" name="tipo_cliente" id="dom-tipo-cliente" onchange="toggleSeguro(this.value)">
<option value="particular">Particular</option>
<option value="seguro">Seguro / EPS</option>
</select>
</div>
<div class="col-md-4" id="bloque-seguro" style="display:none">
<label class="form-label">Nombre del seguro / EPS</label>
<input type="text" class="form-control" name="seguro_nombre" id="dom-seguro-nombre" placeholder="Ej: Sura, Nueva EPS…">
</div>
<div class="col-md-4">
<label class="form-label">N° de autorización</label>
<input type="text" class="form-control" name="autorizacion" id="dom-autorizacion" placeholder="Número de autorización">
</div>
</div>
<!-- ── Dirección ── -->
<h6 class="text-muted text-uppercase small fw-semibold border-bottom pb-1 mb-3">
<i class="fas fa-map-marker-alt me-1"></i> Dirección
</h6>
<div class="row g-3 mb-3">
<div class="col-md-12">
<label class="form-label">Dirección completa <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="direccion" id="dom-dir" required>
</div>
<div class="col-md-4">
<label class="form-label">Ciudad</label>
<input type="text" class="form-control" name="ciudad" id="dom-ciudad">
</div>
<div class="col-md-4">
<label class="form-label">Barrio</label>
<input type="text" class="form-control" name="barrio" id="dom-barrio">
</div>
<div class="col-md-4">
<label class="form-label">Indicaciones adicionales</label>
<input type="text" class="form-control" name="indicaciones_dir" id="dom-indic" placeholder="Ej: apto 302, tocar campanilla">
</div>
</div>
<!-- ── Fecha / hora ── -->
<h6 class="text-muted text-uppercase small fw-semibold border-bottom pb-1 mb-3">
<i class="fas fa-calendar-alt me-1"></i> Programación
</h6>
<div class="row g-3 mb-3">
<div class="col-md-4">
<label class="form-label">Fecha programada <span class="text-danger">*</span></label>
<input type="date" class="form-control" name="fecha_programada" id="dom-fecha" required>
</div>
<div class="col-md-4">
<label class="form-label">Hora</label>
<input type="time" class="form-control" name="hora_programada" id="dom-hora">
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-md-4">
<label class="form-label"><i class="fas fa-sign-in-alt text-success me-1"></i>Hora llegada</label>
<input type="time" class="form-control" name="hora_llegada" id="dom-hora-llegada">
<div class="form-text">Registrada automáticamente al iniciar el domicilio</div>
</div>
<div class="col-md-4">
<label class="form-label"><i class="fas fa-sign-out-alt text-danger me-1"></i>Hora salida</label>
<input type="time" class="form-control" name="hora_salida" id="dom-hora-salida">
<div class="form-text">Registrada automáticamente al completar</div>
</div>
</div>
<!-- ── Cobros ── -->
<h6 class="text-muted text-uppercase small fw-semibold border-bottom pb-1 mb-3">
<i class="fas fa-cash-register me-1"></i> Cobros
</h6>
<div class="row g-3 mb-3">
<div class="col-md-4">
<label class="form-label">Valor domicilio</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" class="form-control" name="valor_domicilio" id="dom-valor-dom"
min="0" step="1" placeholder="0">
</div>
</div>
<div class="col-md-4">
<label class="form-label">Copago cliente</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" class="form-control" name="valor_copago" id="dom-valor-cop"
min="0" step="1" placeholder="0">
</div>
</div>
<div class="col-md-4">
<label class="form-label">Copago laboratorio</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" class="form-control" name="copago_laboratorio" id="dom-copago-lab"
min="0" step="1" placeholder="0">
</div>
</div>
</div>
<!-- ── Orden Médica ── -->
<h6 class="text-muted text-uppercase small fw-semibold border-bottom pb-1 mb-3">
<i class="fas fa-file-medical me-1"></i> Orden Médica Adjunta
</h6>
<div class="mb-3">
<input type="hidden" name="orden_id" id="dom-orden-id-form">
<!-- Lista de chips (archivos ya guardados + nuevos pendientes) -->
<div id="dom-orden-list" class="d-flex flex-wrap gap-2 mb-2"></div>
<div id="dom-orden-empty" class="small text-muted mb-2">
<i class="fas fa-paperclip me-1"></i> Sin archivos adjuntos
</div>
<div class="d-flex gap-2 flex-wrap align-items-center">
<label class="btn btn-sm btn-outline-primary mb-0" for="dom-orden-input">
<i class="fas fa-upload me-1"></i>
<span id="dom-orden-btn-lbl">Adjuntar archivos</span>
</label>
</div>
<input type="file" id="dom-orden-input" class="d-none" multiple
accept="image/jpeg,image/png,image/gif,image/webp,application/pdf,.pdf,.doc,.docx,.xls,.xlsx"
onchange="domSeleccionarOrdenFiles(this)">
<small class="text-muted d-block mt-1">Imágenes, PDF, DOC, XLS — máx. 10 MB por archivo</small>
</div>
<!-- ── Notas ── -->
<div class="mb-2">
<label class="form-label">Notas internas</label>
<textarea class="form-control" name="notas_admin" id="dom-notas" rows="2"
placeholder="Observaciones para el equipo…"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button class="btn btn-primary" onclick="guardarDomicilio()"><i class="fas fa-save me-1"></i> Guardar</button>
</div>
</div>
</div>
</div>
<!-- Modal Ver Respuesta de Formulario -->
<div class="modal fade" id="modalRespuestaForm" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-wpforms me-2"></i>Formulario diligenciado</h5>
<button class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="resp-form-body">
<div class="text-center py-3"><i class="fas fa-spinner fa-spin"></i></div>
</div>
<div class="modal-footer">
<a href="#" id="btn-resp-pdf" target="_blank" class="btn btn-outline-secondary btn-sm">
<i class="fas fa-print me-1"></i>Ver / Imprimir PDF
</a>
<button class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
<!-- Modal Informe Domicilio -->
<div class="modal fade" id="modalInformeDom" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-file-medical me-2 text-success"></i>Informe de domicilio</h5>
<div class="d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary" onclick="window.print()">
<i class="fas fa-print me-1"></i>Imprimir
</button>
<button class="btn-close" data-bs-dismiss="modal"></button>
</div>
</div>
<div class="modal-body" id="informe-body">
<!-- Datos del paciente -->
<h6 class="text-muted text-uppercase mb-2"><i class="fas fa-user me-1"></i>Datos del paciente</h6>
<dl class="row small mb-3">
<dt class="col-4">Nombre</dt> <dd class="col-8 fw-semibold" id="inf-nombre"></dd>
<dt class="col-4">Tipo doc.</dt> <dd class="col-8" id="inf-tipodoc"></dd>
<dt class="col-4">Nº documento</dt> <dd class="col-8" id="inf-numdoc"></dd>
<dt class="col-4">Teléfono</dt> <dd class="col-8" id="inf-telefono"></dd>
<dt class="col-4">F. nacimiento</dt><dd class="col-8" id="inf-fnac"></dd>
<dt class="col-4">Correo</dt> <dd class="col-8" id="inf-correo"></dd>
<dt class="col-4">Dirección</dt> <dd class="col-8" id="inf-direccion"></dd>
<dt class="col-4">Seguro/tipo</dt> <dd class="col-8" id="inf-seguro"></dd>
<dt class="col-4">Fecha servicio</dt><dd class="col-8" id="inf-fecha"></dd>
<dt class="col-4">Exámenes</dt> <dd class="col-8" id="inf-examenes"></dd>
<dt class="col-4 d-none" id="inf-fila-temperaturas"><i class="fas fa-thermometer-half me-1 text-primary"></i>Temperatura</dt>
<dd class="col-8 d-none" id="inf-temperaturas"></dd>
</dl>
<hr>
<!-- Ficha clínica -->
<h6 class="text-muted text-uppercase mb-2"><i class="fas fa-clipboard-list me-1"></i>Ficha clínica</h6>
<dl class="row small mb-3">
<dt class="col-4">Antecedentes</dt> <dd class="col-8" id="inf-antecedentes"></dd>
<dt class="col-4">Medicamentos</dt> <dd class="col-8" id="inf-medicamentos"></dd>
<dt class="col-4 d-none" id="inf-fila-acudiente">Acudiente</dt>
<dd class="col-8 d-none" id="inf-acudiente"></dd>
</dl>
<hr>
<!-- Servicios extras -->
<hr>
<h6 class="text-muted text-uppercase mb-2"><i class="fas fa-plus-circle me-1 text-primary"></i>Servicios extras</h6>
<div id="inf-servicios-extra"></div>
<hr>
<!-- Notas libres -->
<h6 class="text-muted text-uppercase mb-2"><i class="fas fa-sticky-note me-1"></i>Notas del enfermero</h6>
<div id="inf-notas-libres"></div>
</div>
</div>
</div>
</div>
<!-- ═══════════════ MODAL: Reprogramar Domicilio ═══════════════ -->
<div class="modal fade" id="modalReprogramar" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header py-2" style="background:#6f42c1;color:#fff">
<h6 class="modal-title mb-0"><i class="fas fa-calendar-alt me-1"></i>Reprogramar domicilio</h6>
<button class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="repr-dom-id">
<div class="mb-3">
<label class="form-label small fw-semibold">Nueva fecha <span class="text-danger">*</span></label>
<input type="date" class="form-control form-control-sm" id="repr-fecha">
</div>
<div class="mb-3">
<label class="form-label small fw-semibold">Nueva hora</label>
<input type="time" class="form-control form-control-sm" id="repr-hora">
</div>
<div class="mb-2">
<label class="form-label small fw-semibold">Motivo / nota</label>
<textarea class="form-control form-control-sm" id="repr-motivo" rows="2"
placeholder="Ej: Paciente solicitó cambio…"></textarea>
</div>
<div id="repr-error" class="text-danger small d-none">Debes ingresar la nueva fecha.</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="confirmarReprogramar()">
<i class="fas fa-calendar-check me-1"></i>Reprogramar
</button>
</div>
</div>
</div>
</div>
<!-- Modal Asignar / Reasignar Enfermero -->
<div class="modal fade" id="modalAsignarEnf" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-user-nurse me-2"></i>Asignar Enfermero</h5>
<button class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="asig-dom-id">
<div class="mb-3">
<label class="form-label fw-semibold">Selecciona el enfermero</label>
<select class="form-select" id="asig-sel-enfermero">
<option value="">— Cargando... —</option>
</select>
</div>
<div id="asig-info-enf" class="alert alert-info py-2 small d-none"></div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button class="btn btn-primary" onclick="confirmarAsignacion()">
<i class="fas fa-check me-1"></i>Confirmar asignación
</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
const modal = new bootstrap.Modal('#modalDomicilio');
let _modalRespForm = null;
let _modalAsignarEnf = null;
// ── Estado órdenes en el modal (multi-archivo) ────────────────────────────
let _domNewFiles = []; // File[] nuevos pendientes de subir
let _domCurrentFiles = []; // [{orden_id, local_file}] ya guardados
let _domRemovedOrdenIds = []; // orden_ids a desvincular al guardar
function _domResetOrdenes() {
_domNewFiles = [];
_domCurrentFiles = [];
_domRemovedOrdenIds = [];
document.getElementById('dom-orden-input').value = '';
_domRenderOrdenes();
}
// Alias para retrocompat con llamadas _domResetOrden()
const _domResetOrden = _domResetOrdenes;
function _domRenderOrdenes() {
const list = document.getElementById('dom-orden-list');
const empty = document.getElementById('dom-orden-empty');
const btn = document.getElementById('dom-orden-btn-lbl');
const chipStyle = 'width:72px;height:72px;border:1px solid #dee2e6;border-radius:6px;background:#f8f9fa;display:flex;align-items:center;justify-content:center;overflow:hidden';
const delStyle = 'position:absolute;top:-5px;right:-5px;width:18px;height:18px;border-radius:50%;background:#dc3545;color:#fff;border:none;font-size:.7rem;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0';
list.innerHTML = '';
_domCurrentFiles.forEach((f, i) => {
const lf = f.local_file || '';
const isImg = /\.(jpe?g|png|gif|webp)$/i.test(lf);
const thumb = isImg
? `<img src="uploads/media/${encodeURIComponent(lf)}" style="width:100%;height:100%;object-fit:cover">`
: `<i class="fas fa-file-alt fa-2x text-secondary"></i>`;
const fname = lf.split('/').pop();
list.insertAdjacentHTML('beforeend', `
<div class="position-relative" style="width:72px">
<div style="${chipStyle}">${thumb}</div>
<span class="d-block text-truncate" style="font-size:.6rem;color:#666;max-width:72px" title="${esc(fname)}">${esc(fname)}</span>
<button type="button" onclick="domQuitarExistente(${i})" style="${delStyle}" title="Quitar">×</button>
</div>`);
});
_domNewFiles.forEach((f, i) => {
const isImg = f.type.startsWith('image/');
const thumb = isImg
? `<img src="${URL.createObjectURL(f)}" style="width:100%;height:100%;object-fit:cover">`
: `<i class="fas fa-file-alt fa-2x text-secondary"></i>`;
list.insertAdjacentHTML('beforeend', `
<div class="position-relative" style="width:72px">
<div style="${chipStyle}">${thumb}</div>
<span class="d-block text-truncate" style="font-size:.6rem;color:#666;max-width:72px" title="${esc(f.name)}">${esc(f.name)}</span>
<span style="font-size:.55rem;background:#ffc107;color:#000;border-radius:3px;padding:0 3px">nuevo</span>
<button type="button" onclick="domQuitarNuevo(${i})" style="${delStyle}" title="Quitar">×</button>
</div>`);
});
const total = _domCurrentFiles.length + _domNewFiles.length;
empty.classList.toggle('d-none', total > 0);
btn.textContent = total > 0 ? 'Agregar más archivos' : 'Adjuntar archivos';
}
function domSeleccionarOrdenFiles(input) {
for (const file of Array.from(input.files)) {
if (file.size > 10 * 1024 * 1024) {
alert(`"${file.name}" supera los 10 MB permitidos`);
continue;
}
_domNewFiles.push(file);
}
input.value = '';
_domRenderOrdenes();
}
function domQuitarExistente(i) {
const removed = _domCurrentFiles.splice(i, 1)[0];
if (removed?.orden_id) _domRemovedOrdenIds.push(removed.orden_id);
_domRenderOrdenes();
}
function domQuitarNuevo(i) {
_domNewFiles.splice(i, 1);
_domRenderOrdenes();
}
const COLOR_DOM = { programado:'secondary', confirmado:'info', en_camino:'primary', en_domicilio:'warning', completado:'success', cancelado:'danger', reprogramado:'dark' };
const PUEDE_ESCRIBIR = <?= $puedeEscribir ? 'true' : 'false' ?>;
let domSeleccionado = <?= $domIdParam ?: 'null' ?>;
let _domActual = null; // objeto completo del domicilio activo en el panel de detalle
let paginaActual = 1;
let _listaToken = 0; // anti race-condition
// ── Init ───────────────────────────────────────────────────────────────────
(async function init() {
await cargarEnfermeras();
await cargarLista();
if (domSeleccionado) verDomicilio(domSeleccionado);
if (<?= $ordenIdParam ?> && <?= $pacIdParam ?>) abrirFormulario();
})();
async function cargarEnfermeras() {
const r = await fetch('api/lab/get_enfermeras.php');
const d = await r.json();
const sel = document.getElementById('filtro-enfermera');
(d.data||[]).forEach(e => {
sel.insertAdjacentHTML('beforeend', `<option value="${e.id}">${esc(e.nombre_completo)}</option>`);
});
}
// ── Lista ──────────────────────────────────────────────────────────────────
async function cargarLista(pag = 1) {
paginaActual = pag;
const miToken = ++_listaToken; // capturar token antes del await
const params = new URLSearchParams({
fecha: document.getElementById('filtro-fecha').value || '',
estado: document.getElementById('filtro-estado').value,
enfermera_id: document.getElementById('filtro-enfermera').value,
page: pag, limit: 25,
});
const r = await fetch(`api/lab/get_domicilios.php?${params}`);
const d = await r.json();
if (miToken !== _listaToken) return; // respuesta desactualizada, ignorar
renderResumen(d.estadisticas_hoy, d.domicilios?.sin_asignar ?? d.sin_asignar_hoy);
const tbody = document.getElementById('tabla-body');
if (!d.data?.length) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center py-4 text-muted">Sin domicilios</td></tr>';
document.getElementById('paginacion').innerHTML = '';
return;
}
tbody.innerHTML = d.data.map(dom => `
<tr class="dom-row ${domSeleccionado===dom.id?'table-active':''}" id="row-${dom.id}" onclick="verDomicilio(${dom.id})">
<td class="text-nowrap small">${dom.fecha_programada ? dom.fecha_programada.slice(5).replace('-','/') : '—'}</td>
<td>${dom.hora_programada ? dom.hora_programada.slice(0,5) : '—'}</td>
<td>
<div class="fw-semibold">${esc(dom.paciente_nombre)}</div>
<small class="text-muted">${esc(dom.barrio||'')}</small>
</td>
<td>
${dom.enfermera_nombre ? `<small>${esc(dom.enfermera_nombre)}</small>` : '<span class="text-danger small"><i class="fas fa-exclamation-triangle me-1"></i>Sin asignar</span>'}
${dom.tiempo_recorrido_min != null ? `<div><span class="badge rounded-pill mt-1" style="background:#e0f2fe;color:#075985;border:1px solid #7dd3fc">Recorrido: ${formatearMinutos(dom.tiempo_recorrido_min)}</span></div>` : ''}
</td>
<td>
<span class="badge bg-${COLOR_DOM[dom.estado]||'secondary'} estado-badge">${esc(dom.estado)}</span>
${(dom.pago_estado === 'pagado')
? '<span class="badge rounded-pill ms-1" style="background:#d1fae5;color:#065f46;border:1px solid #34d399">✅ Pagado</span>'
: (dom.pago_estado === 'exento'
? '<span class="badge rounded-pill ms-1" style="background:#e0e7ff;color:#3730a3;border:1px solid #818cf8">🔵 Exento</span>'
: ((parseFloat(dom.valor_domicilio)||0)+(parseFloat(dom.valor_copago)||0) > 0
? '<span class="badge rounded-pill ms-1" style="background:#fff3cd;color:#664d03;border:1px solid #ffc107">⏳ Cobro</span>'
: ''))}
</td>
</tr>
`).join('');
const el = document.getElementById('paginacion');
if (d.paginas > 1) {
el.innerHTML = `
<small class="text-muted">${d.total} domicilio(s)</small>
<div class="btn-group btn-group-sm">
${pag>1?`<button class="btn btn-outline-secondary" onclick="cargarLista(${pag-1})"><i class="fas fa-chevron-left"></i></button>`:''}
<button class="btn btn-secondary disabled">${pag}/${d.paginas}</button>
${pag<d.paginas?`<button class="btn btn-outline-secondary" onclick="cargarLista(${pag+1})"><i class="fas fa-chevron-right"></i></button>`:''}
</div>`;
} else {
el.innerHTML = `<small class="text-muted">${d.total} domicilio(s)</small>`;
}
}
function renderResumen(stats, sinAsignar) {
if (!stats) return;
const items = [
{ label:'Programados', val: stats.programado||0, color:'secondary' },
{ label:'En camino', val: stats.en_camino||0, color:'primary' },
{ label:'Completados', val: stats.completado||0, color:'success' },
{ label:'Sin enfermero',val: sinAsignar||0, color:'danger' },
];
document.getElementById('resumen-hoy').innerHTML = items.map(i =>
`<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm text-center py-2">
<div class="fs-4 fw-bold text-${i.color}">${i.val}</div>
<div class="text-muted small">${i.label}</div>
</div>
</div>`
).join('');
}
// ── Detalle ────────────────────────────────────────────────────────────────
async function verDomicilio(id) {
domSeleccionado = id;
document.querySelectorAll('.dom-row').forEach(r => r.classList.remove('table-active'));
const rowEl = document.getElementById(`row-${id}`);
if (rowEl) rowEl.classList.add('table-active');
document.getElementById('detail-id').textContent = `#${id}`;
document.getElementById('detail-body').innerHTML = '<div class="text-center py-3"><i class="fas fa-spinner fa-spin"></i></div>';
if (PUEDE_ESCRIBIR) document.getElementById('btn-editar-dom').classList.remove('d-none');
document.getElementById('btn-informe-dom').classList.remove('d-none');
const r = await fetch(`api/lab/get_domicilios.php?id=${id}&no_stats=1`);
const d = await r.json();
const dom = d.domicilio;
if (!dom) return;
_domActual = dom;
// Botones de estado
const SIGUIENTES = {
programado: [['confirmado','Confirmar','info'],['cancelado','Cancelar','danger']],
confirmado: [['en_camino','En camino','primary'],['cancelado','Cancelar','danger']],
en_camino: [['en_domicilio','En domicilio','warning']],
en_domicilio:[['completado','Completar','success']],
};
// Reprogramar disponible en todos los estados abiertos
const puedeReprogramar = !['completado','reprogramado'].includes(dom.estado);
const btnRepr = puedeReprogramar
? `<button class="btn btn-sm" style="background:#6f42c1;color:#fff" onclick="abrirReprogramar(${dom.id})">
<i class="fas fa-calendar-alt me-1"></i>Reprogramar
</button>`
: '';
const btns = (SIGUIENTES[dom.estado]||[]).map(([e,lbl,col]) =>
`<button class="btn btn-sm btn-${col}" onclick="cambiarEstado(${dom.id},'${e}')">
<i class="fas fa-arrow-right me-1"></i>${lbl}
</button>`
).join('');
document.getElementById('detail-body').innerHTML = `
<div class="row g-3">
<div class="col-md-6">
<dl class="row small">
<dt class="col-5 text-muted">Paciente</dt><dd class="col-7 fw-semibold">${esc(dom.paciente_nombre)}</dd>
<dt class="col-5 text-muted">Documento</dt><dd class="col-7">${esc(dom.paciente_tipo_documento||'')} ${esc(dom.paciente_numero_documento||'—')}</dd>
<dt class="col-5 text-muted">Teléfono</dt><dd class="col-7">${esc(dom.paciente_telefono||'—')}</dd>
<dt class="col-5 text-muted">F. nacimiento</dt><dd class="col-7">${formatFechaNac(dom.paciente_fecha_nacimiento)}</dd>
<dt class="col-5 text-muted">Dirección</dt><dd class="col-7">${esc(dom.direccion)}</dd>
<dt class="col-5 text-muted">Barrio</dt><dd class="col-7">${esc(dom.barrio||'—')}</dd>
<dt class="col-5 text-muted">Indicaciones</dt><dd class="col-7">${esc(dom.indicaciones_dir||'—')}</dd>
<dt class="col-5 text-muted">Fecha</dt><dd class="col-7">${esc(dom.fecha_programada)} ${esc(dom.hora_programada||'')}</dd>
${(dom.hora_llegada || dom.hora_salida) ? `
<dt class="col-5 text-muted">Hora llegada</dt><dd class="col-7 text-success fw-semibold">${dom.hora_llegada ? dom.hora_llegada.slice(0,5) : '—'}</dd>
<dt class="col-5 text-muted">Hora salida</dt><dd class="col-7 text-danger fw-semibold">${dom.hora_salida ? dom.hora_salida.slice(0,5) : '—'}</dd>
${dom.hora_llegada && dom.hora_salida ? `<dt class="col-5 text-muted">Duración</dt><dd class="col-7 fw-semibold text-info">${calcDuracion(dom.hora_llegada, dom.hora_salida)}</dd>` : ''}` : ''}
${dom.tiempo_recorrido_min != null ? `<dt class="col-5 text-muted">Tiempo recorrido</dt><dd class="col-7 fw-semibold" style="color:#0369a1">${formatearMinutos(dom.tiempo_recorrido_min)} <span class="text-muted">(desde fin turno anterior)</span></dd>` : ''}
${dom.temperatura_inicio != null ? `<dt class="col-5 text-muted"><i class="fas fa-thermometer-half me-1 text-primary"></i>Temp. inicio</dt><dd class="col-7 fw-semibold text-primary">${dom.temperatura_inicio}°C</dd>` : ''}
${dom.temperatura_salida != null ? `<dt class="col-5 text-muted"><i class="fas fa-thermometer-full me-1 text-warning"></i>Temp. salida</dt><dd class="col-7 fw-semibold text-warning">${dom.temperatura_salida}°C</dd>` : ''}
<dt class="col-5 text-muted">Servicio</dt><dd class="col-7">${esc(dom.tipo_servicio||'—')}</dd>
<dt class="col-5 text-muted">Tipo cliente</dt><dd class="col-7">
${dom.tipo_cliente === 'seguro'
? `<span class="badge bg-info text-dark"><i class="fas fa-shield-alt me-1"></i>Seguro${dom.seguro_nombre ? ' — '+esc(dom.seguro_nombre) : ''}</span>`
: `<span class="badge bg-secondary"><i class="fas fa-wallet me-1"></i>Particular</span>`}
</dd>
${dom.autorizacion ? `<dt class="col-5 text-muted">Autorización</dt><dd class="col-7">${esc(dom.autorizacion)}</dd>` : ''}
${dom.copago_laboratorio ? `<dt class="col-5 text-muted">Copago lab.</dt><dd class="col-7">$${Number(dom.copago_laboratorio).toLocaleString('es-CO')}</dd>` : ''}
</dl>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label small text-muted text-uppercase">Estado actual</label><br>
<span class="badge bg-${COLOR_DOM[dom.estado]||'secondary'} fs-6">${esc(dom.estado)}</span>
</div>
<!-- Enfermero asignado -->
<div class="mb-3">
<label class="form-label small text-muted text-uppercase">Enfermero</label>
${dom.enfermera_nombre
? `<div class="d-flex align-items-center gap-2 flex-wrap">
<span><i class="fas fa-user-nurse text-success me-1"></i>${esc(dom.enfermera_nombre)}</span>
<button class="btn btn-xs btn-outline-secondary py-0 px-2" onclick="mostrarAsignacion(${dom.id})">Reasignar</button>
${dom.enfermera_lab_id ? `<a href="enfermero_portal.php?eid=${dom.enfermera_lab_id}" target="_blank" class="btn btn-xs btn-outline-info py-0 px-2"><i class="fas fa-eye me-1"></i>Ver portal</a>` : ''}
</div>`
: `<div class="alert alert-warning py-2 small d-flex align-items-center justify-content-between mb-0">
<span><i class="fas fa-exclamation-triangle me-2"></i>Sin enfermero asignado</span>
<button class="btn btn-sm btn-warning" onclick="mostrarAsignacion(${dom.id})"><i class="fas fa-user-plus me-1"></i>Asignar</button>
</div>`}
</div>
<!-- Orden médica -->
${renderOrdenesDetail(dom)}
<!-- Botones de flujo -->
<div class="d-flex gap-2 flex-wrap">${btns}${btnRepr}</div>
<!-- Vínculos reprogramación -->
${dom.domicilio_origen_id ? `
<div class="alert alert-warning py-2 small mt-2 mb-0">
<i class="fas fa-history me-1"></i>
Reprogramado desde domicilio
<a href="#" onclick="event.preventDefault();verDomicilio(${dom.domicilio_origen_id})" class="fw-semibold">#${dom.domicilio_origen_id}</a>
${dom.origen_info ? `(${esc(dom.origen_info.fecha_programada)})` : ''}
</div>` : ''}
${dom.reprogramaciones && dom.reprogramaciones.length ? `
<div class="alert alert-info py-2 small mt-2 mb-0">
<i class="fas fa-calendar-check me-1"></i>
Reprogramado a:
${dom.reprogramaciones.map(r =>
`<a href="#" onclick="event.preventDefault();verDomicilio(${r.id})" class="fw-semibold">#${r.id}</a> (${esc(r.fecha_programada)})`
).join(', ')}
</div>` : ''}
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<label class="form-label small text-muted text-uppercase">Exámenes de la orden</label>
<div class="bg-light p-2 rounded small">${esc(dom.examenes_solicitados||'—')}</div>
</div>
</div>
${(dom.valor_domicilio || dom.valor_copago) ? (() => {
const totalC = (parseFloat(dom.valor_domicilio)||0)+(parseFloat(dom.valor_copago)||0);
const PAGO_INFO = {
pendiente: { bg:'#fff3cd', color:'#664d03', border:'#ffc107', icon:'⏳', txt:'Pago pendiente' },
pagado: { bg:'#d1fae5', color:'#065f46', border:'#34d399', icon:'✅', txt:'Pagado' },
exento: { bg:'#e0e7ff', color:'#3730a3', border:'#818cf8', icon:'🔵', txt:'Exento' },
};
const pEst = dom.pago_estado || 'pendiente';
const pInfo = PAGO_INFO[pEst] || PAGO_INFO.pendiente;
const MODO_LABEL = { efectivo:'💵 Efectivo', transferencia:'📲 Transferencia', otro:'📋 Otro' };
return `
<div class="row mt-3">
<div class="col-12">
<div class="p-2 rounded" style="background:#d1fae5;border:1px solid #6ee7b7">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="small fw-semibold text-success"><i class="fas fa-cash-register me-1"></i>Cobro al cliente</span>
<span class="badge rounded-pill small" style="background:${pInfo.bg};color:${pInfo.color};border:1px solid ${pInfo.border}">${pInfo.icon} ${pInfo.txt}</span>
</div>
<div class="d-flex gap-3 flex-wrap">
${dom.valor_domicilio ? `<span class="small">Domicilio: <strong>$${Number(dom.valor_domicilio).toLocaleString('es-CO')}</strong></span>` : ''}
${dom.valor_copago ? `<span class="small">Copago: <strong>$${Number(dom.valor_copago).toLocaleString('es-CO')}</strong></span>` : ''}
${totalC > 0 ? `<span class="small fw-bold text-success">Total: $${totalC.toLocaleString('es-CO')}</span>` : ''}
</div>
${dom.pago_estado === 'pagado' ? (() => {
// Usar pagos_detalle (multi-método) si existen
const detalle = dom.pagos_detalle || [];
if (detalle.length) {
return `<div class="small mt-1 text-success">
${detalle.map(p =>
`<div>${MODO_LABEL[p.modo] || p.modo}: <strong>$${Number(p.monto).toLocaleString('es-CO')}</strong>${p.notas ? ` · <em>${esc(p.notas)}</em>` : ''}</div>`
).join('')}
</div>`;
}
// Fallback registro legacy (un solo método)
return `<div class="small mt-1 text-success">
${MODO_LABEL[dom.pago_modo] || (dom.pago_modo||'')}
${dom.pago_monto ? ` · $${Number(dom.pago_monto).toLocaleString('es-CO')} cobrado` : ''}
${dom.pago_notas ? ` · <em>${esc(dom.pago_notas)}</em>` : ''}
</div>`;
})() : ''}
</div>
</div>
</div>`;
})() : ''}
${(dom.servicios_extra && dom.servicios_extra.length) ? `
<div class="row mt-3">
<div class="col-12">
<label class="form-label small text-muted text-uppercase"><i class="fas fa-plus-circle me-1 text-primary"></i>Servicios extras del enfermero</label>
<div class="table-responsive">
<table class="table table-sm table-bordered mb-0 small">
<thead class="table-light">
<tr><th>Tipo</th><th>Descripción</th><th>Notas</th><th>Valor</th></tr>
</thead>
<tbody>
${dom.servicios_extra.map(se => `
<tr>
<td><span class="badge bg-primary bg-opacity-75">${esc(se.tipo||'—')}</span></td>
<td>${esc(se.descripcion||'—')}</td>
<td class="text-muted">${esc(se.notas||'—')}</td>
<td>${se.valor ? '$'+Number(se.valor).toLocaleString('es-CO') : (se.requiere_pago ? '<span class="text-warning">Pendiente</span>' : '—')}</td>
</tr>`).join('')}
</tbody>
</table>
</div>
</div>
</div>` : ''}
${dom.notas_admin ? `
<div class="row mt-2">
<div class="col-12">
<label class="form-label small text-muted text-uppercase">Notas internas</label>
<div class="bg-light p-2 rounded small">${esc(dom.notas_admin)}</div>
</div>
</div>` : ''}
`;
cargarNotasDetalle(id);
}
function cerrarDetalle() {
domSeleccionado = null;
_domActual = null;
document.querySelectorAll('.dom-row').forEach(r => r.classList.remove('table-active'));
document.getElementById('detail-id').textContent = '';
document.getElementById('btn-editar-dom').classList.add('d-none');
document.getElementById('btn-informe-dom').classList.add('d-none');
document.getElementById('detail-body').innerHTML = '<div class="text-center py-5 text-muted"><i class="fas fa-arrow-left me-2"></i>Selecciona un domicilio</div>';
}
// ── Notas del enfermero (read-only en detalle) ────────────────────────────
async function cargarNotasDetalle(domId) {
try {
const r = await fetch(`api/lab/get_notas_domicilio.php?domicilio_id=${domId}`);
const d = await r.json();
if (!d.success) return;
const notas = d.data || [];
const c = notas.find(n => n.tipo === 'clinica') || {};
const libres = notas.filter(n => n.tipo === 'libre');
const hayFicha = c.antecedentes || c.medicamentos || c.acudiente_nombre;
const hayLibres = libres.length > 0;
if (!hayFicha && !hayLibres) return;
let html = `<div class="row mt-3" id="notas-enfermero-bloque">
<div class="col-12">
<hr class="my-2">
<label class="form-label small text-muted text-uppercase">
<i class="fas fa-notes-medical me-1"></i>Notas del enfermero
</label>`;
if (hayFicha) {
html += `<div class="p-2 rounded mb-2" style="background:#fff7ed;border:1px solid #fdba74">
<div class="fw-semibold small mb-1" style="color:#9a3412">
<i class="fas fa-clipboard-list me-1"></i>Ficha clínica
</div>`;
if (c.antecedentes) html += `<div class="small mb-1"><strong>Antecedentes:</strong> ${esc(c.antecedentes)}</div>`;
if (c.medicamentos) html += `<div class="small mb-1"><strong>Medicamentos:</strong> ${esc(c.medicamentos)}</div>`;
if (c.acudiente_nombre) html += `<div class="small mb-1"><strong>Acudiente:</strong> ${esc(c.acudiente_nombre)}${c.acudiente_documento ? ' · ' + esc(c.acudiente_documento) : ''}</div>`;
html += `</div>`;
}
libres.forEach(n => {
html += `<div class="p-2 rounded mb-2" style="background:#f8fafc;border:1px solid #e2e8f0">
<div class="fw-semibold small">${esc(n.titulo||'Sin título')}</div>
<div class="small mt-1" style="white-space:pre-line">${n.cuerpo||''}</div>
${_renderArchivosAdmin(n)}
<div class="text-muted mt-1" style="font-size:.7rem">${esc(n.created_at||'')}</div>
</div>`;
});
html += `</div></div>`;
document.getElementById('detail-body').insertAdjacentHTML('beforeend', html);
} catch(e) { /* silencioso */ }
}
// ── Informe del domicilio ─────────────────────────────────────────────────
async function verInformeDom() {
if (!_domActual) return;
const dom = _domActual;
// Llenar datos básicos del paciente
document.getElementById('inf-nombre').textContent = dom.paciente_nombre || '—';
document.getElementById('inf-tipodoc').textContent = dom.paciente_tipo_documento || '—';
document.getElementById('inf-numdoc').textContent = dom.paciente_numero_documento || '—';
document.getElementById('inf-telefono').textContent= dom.paciente_telefono || '—';
document.getElementById('inf-correo').textContent = dom.paciente_email || '—';
document.getElementById('inf-fnac').textContent = formatFechaNac(dom.paciente_fecha_nacimiento);
document.getElementById('inf-direccion').textContent = (dom.direccion||'') + (dom.barrio ? ', ' + dom.barrio : '');
document.getElementById('inf-seguro').textContent = dom.tipo_cliente === 'seguro'
? ('Seguro' + (dom.seguro_nombre ? ' — ' + dom.seguro_nombre : ''))
: 'Particular';
document.getElementById('inf-examenes').textContent = dom.examenes_solicitados || '—';
document.getElementById('inf-fecha').textContent = (dom.fecha_programada||'') + ' ' + (dom.hora_programada||'');
// Temperaturas
const tieneTemp = dom.temperatura_inicio != null || dom.temperatura_salida != null;
document.getElementById('inf-fila-temperaturas').classList.toggle('d-none', !tieneTemp);
document.getElementById('inf-temperaturas').classList.toggle('d-none', !tieneTemp);
if (tieneTemp) {
const parts = [];
if (dom.temperatura_inicio != null) parts.push(`<span class="text-primary fw-semibold"><i class="fas fa-thermometer-half me-1"></i>Inicio: ${dom.temperatura_inicio}°C</span>`);
if (dom.temperatura_salida != null) parts.push(`<span class="text-warning fw-semibold"><i class="fas fa-thermometer-full me-1"></i>Salida: ${dom.temperatura_salida}°C</span>`);
document.getElementById('inf-temperaturas').innerHTML = parts.join(' &nbsp;·&nbsp; ');
}
// Servicios extras (síncronos, vienen en _domActual)
const seEl = document.getElementById('inf-servicios-extra');
if (dom.servicios_extra && dom.servicios_extra.length) {
seEl.innerHTML = `<table class="table table-sm table-bordered small mb-0">
<thead class="table-light"><tr><th>Tipo</th><th>Descripción</th><th>Notas</th><th>Valor</th></tr></thead>
<tbody>${dom.servicios_extra.map(se =>
`<tr>
<td><span class="badge bg-primary bg-opacity-75">${esc(se.tipo||'—')}</span></td>
<td>${esc(se.descripcion||'—')}</td>
<td class="text-muted">${esc(se.notas||'—')}</td>
<td>${se.valor ? '$'+Number(se.valor).toLocaleString('es-CO') : (se.requiere_pago ? '<span class="text-warning">Pendiente</span>' : '—')}</td>
</tr>`).join('')}
</tbody>
</table>`;
} else {
seEl.innerHTML = '<span class="text-muted small">Sin servicios extras registrados</span>';
}
// Limpiar campos clínicos mientras cargamos
['inf-antecedentes','inf-medicamentos','inf-acudiente','inf-notas-libres'].forEach(id => {
const el = document.getElementById(id);
if (el) el.innerHTML = '<em class="text-muted">Cargando...</em>';
});
const _modalInforme = bootstrap.Modal.getOrCreateInstance('#modalInformeDom');
_modalInforme.show();
try {
const r = await fetch(`api/lab/get_notas_domicilio.php?domicilio_id=${dom.id}`);
const d = await r.json();
const notas = d.success ? (d.data || []) : [];
const c = notas.find(n => n.tipo === 'clinica') || {};
const libres = notas.filter(n => n.tipo === 'libre');
document.getElementById('inf-antecedentes').textContent = c.antecedentes || '—';
document.getElementById('inf-medicamentos').textContent = c.medicamentos || '—';
if (c.acudiente_nombre) {
document.getElementById('inf-acudiente').innerHTML =
`<strong>${esc(c.acudiente_nombre)}</strong> · ${esc(c.acudiente_documento||'')}`;
document.getElementById('inf-fila-acudiente').classList.remove('d-none');
} else {
document.getElementById('inf-fila-acudiente').classList.add('d-none');
document.getElementById('inf-acudiente').textContent = '';
}
if (libres.length) {
document.getElementById('inf-notas-libres').innerHTML = libres.map(n =>
`<div class="mb-2 p-2 rounded" style="background:#f8fafc;border:1px solid #e2e8f0">
<div class="fw-semibold small">${esc(n.titulo||'Sin título')}</div>
<div class="small mt-1" style="white-space:pre-line">${n.cuerpo||''}</div>
${_renderArchivosAdmin(n)}
<div class="text-muted mt-1" style="font-size:.68rem">${esc(n.enfermera_nombre||'')}${n.created_at ? ' · ' + esc(n.created_at) : ''}</div>
</div>`
).join('');
} else {
document.getElementById('inf-notas-libres').textContent = '—';
}
} catch(e) {
document.getElementById('inf-antecedentes').textContent = '—';
document.getElementById('inf-medicamentos').textContent = '—';
document.getElementById('inf-notas-libres').textContent = '—';
}
}
// ──────────────────────────────────────────────────────────────────────────
// ── Asignación rápida ──────────────────────────────────────────────────────
async function mostrarAsignacion(domId) {
document.getElementById('asig-dom-id').value = domId;
const sel = document.getElementById('asig-sel-enfermero');
sel.innerHTML = '<option value="">— Cargando... —</option>';
document.getElementById('asig-info-enf').classList.add('d-none');
if (!_modalAsignarEnf) _modalAsignarEnf = new bootstrap.Modal('#modalAsignarEnf');
_modalAsignarEnf.show();
const re = await fetch('api/lab/get_enfermeras.php');
const de = await re.json();
sel.innerHTML = '<option value="">— Selecciona enfermero —</option>'
+ (de.data||[]).map(e =>
`<option value="${e.id}" data-user="${esc(e.username_acceso||'')}">${esc(e.nombre_completo)} (${e.domicilios_hoy||0} hoy)</option>`
).join('');
sel.addEventListener('change', function onSelChange() {
const opt = this.options[this.selectedIndex];
const user = opt?.dataset?.user;
const info = document.getElementById('asig-info-enf');
if (this.value && user) {
info.textContent = `Usuario de acceso al portal: ${user}`;
info.classList.remove('d-none');
} else {
info.classList.add('d-none');
}
sel.removeEventListener('change', onSelChange);
sel.addEventListener('change', function() {
const o = this.options[this.selectedIndex];
const u = o?.dataset?.user;
const inf = document.getElementById('asig-info-enf');
if (this.value && u) { inf.textContent = `Usuario de acceso al portal: ${u}`; inf.classList.remove('d-none'); }
else inf.classList.add('d-none');
});
});
}
async function confirmarAsignacion() {
const domId = parseInt(document.getElementById('asig-dom-id').value);
const enfId = parseInt(document.getElementById('asig-sel-enfermero').value);
if (!enfId) { mostrarToast('Selecciona un enfermero', 'warning'); return; }
const rr = await fetch('api/lab/save_asignacion.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ domicilio_id: domId, enfermera_id: enfId }),
});
const dd = await rr.json();
mostrarToast(dd.success ? dd.message : (dd.error||'Error'), dd.success ? 'success' : 'danger');
if (dd.success) {
_modalAsignarEnf.hide();
cargarLista(paginaActual);
verDomicilio(domId);
}
}
// ── Reprogramar domicilio ───────────────────────────────────
let _reprModal = null;
function abrirReprogramar(id) {
document.getElementById('repr-dom-id').value = id;
document.getElementById('repr-fecha').value = '';
document.getElementById('repr-hora').value = _domActual?.hora_programada?.slice(0,5) || '';
document.getElementById('repr-motivo').value = '';
document.getElementById('repr-error').classList.add('d-none');
if (!_reprModal) _reprModal = new bootstrap.Modal('#modalReprogramar');
_reprModal.show();
}
async function confirmarReprogramar() {
const id = +document.getElementById('repr-dom-id').value;
const fecha = document.getElementById('repr-fecha').value;
const hora = document.getElementById('repr-hora').value;
const motivo = document.getElementById('repr-motivo').value.trim();
if (!fecha) { document.getElementById('repr-error').classList.remove('d-none'); return; }
_reprModal.hide();
const r = await fetch('api/lab/save_domicilio.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, reprogramar: true, nueva_fecha: fecha, nueva_hora: hora, motivo }),
});
const d = await r.json();
mostrarToast(d.success ? d.message : (d.error || 'Error'), d.success ? 'success' : 'danger');
if (d.success) { cargarLista(paginaActual); verDomicilio(id); }
}
// ── Cambio de estado ───────────────────────────────────────────────────────
async function cambiarEstado(id, estado) {
if (!confirm(`¿Cambiar estado a "${estado}"?`)) return;
const r = await fetch('api/lab/save_domicilio.php', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({ id, solo_estado:true, nuevo_estado:estado }),
});
const d = await r.json();
mostrarToast(d.success ? d.message : (d.error||'Error'), d.success?'success':'danger');
if (d.success) { cargarLista(paginaActual); verDomicilio(id); }
}
// ── Formulario nuevo ───────────────────────────────────────────────────────
function abrirFormulario() {
document.getElementById('form-domicilio').reset();
document.getElementById('dom-id').value = '';
document.getElementById('dom-fecha').value = document.getElementById('filtro-fecha').value || '<?= date('Y-m-d') ?>';
document.getElementById('dom-tipo-cliente').value = 'particular';
toggleSeguro('particular');
_domResetOrden();
document.getElementById('modal-titulo').textContent = 'Nuevo Domicilio';
modal.show();
// Autocompletar paciente desde parámetros URL
<?php if ($pacIdParam): ?>
document.getElementById('dom-paciente-id').value = '<?= $pacIdParam ?>';
document.getElementById('dom-paciente-nombre').value = 'Paciente #<?= $pacIdParam ?>';
<?php endif; ?>
}
// ── Editar domicilio existente ─────────────────────────────────────────────
async function editarDomicilio(id) {
if (!id) return;
const r = await fetch(`api/lab/get_domicilios.php?id=${id}&no_stats=1`);
const d = await r.json();
const dom = d.domicilio;
if (!dom) { mostrarToast('No se pudo cargar el domicilio', 'danger'); return; }
document.getElementById('modal-titulo').textContent = `Editar Domicilio #${dom.id}`;
document.getElementById('dom-id').value = dom.id;
document.getElementById('dom-orden-id').value = dom.orden_id || '';
document.getElementById('dom-paciente-id').value = dom.paciente_id;
document.getElementById('dom-paciente-nombre').value = dom.paciente_nombre || '';
document.getElementById('dom-tipo').value = dom.tipo_servicio || '';
document.getElementById('dom-dir').value = dom.direccion || '';
document.getElementById('dom-ciudad').value = dom.ciudad || '';
document.getElementById('dom-barrio').value = dom.barrio || '';
document.getElementById('dom-indic').value = dom.indicaciones_dir || '';
document.getElementById('dom-fecha').value = dom.fecha_programada || '';
document.getElementById('dom-hora').value = (dom.hora_programada || '').slice(0, 5);
document.getElementById('dom-hora-llegada').value = (dom.hora_llegada || '').slice(0, 5);
document.getElementById('dom-hora-salida').value = (dom.hora_salida || '').slice(0, 5);
document.getElementById('dom-tipo-cliente').value = dom.tipo_cliente || 'particular';
toggleSeguro(dom.tipo_cliente || 'particular');
document.getElementById('dom-seguro-nombre').value = dom.seguro_nombre || '';
document.getElementById('dom-autorizacion').value = dom.autorizacion || '';
document.getElementById('dom-valor-dom').value = dom.valor_domicilio || '';
document.getElementById('dom-valor-cop').value = dom.valor_copago || '';
document.getElementById('dom-copago-lab').value = dom.copago_laboratorio || '';
document.getElementById('dom-notas').value = dom.notas_admin || '';
// Órdenes médicas adjuntas (multi-archivo)
_domResetOrdenes();
let archivosExistentes = dom.archivos_ordenes || [];
if (!archivosExistentes.length && dom.orden_local_file) {
// Retrocompat: domicilio viejo con solo orden_id FK
archivosExistentes = [{ orden_id: dom.orden_id || null, local_file: dom.orden_local_file }];
}
_domCurrentFiles = archivosExistentes;
_domRenderOrdenes();
modal.show();
}
// Mostrar/ocultar campo seguro según tipo_cliente
function toggleSeguro(val) {
document.getElementById('bloque-seguro').style.display = val === 'seguro' ? '' : 'none';
}
async function guardarDomicilio() {
const form = document.getElementById('form-domicilio');
if (!form.checkValidity()) { form.reportValidity(); return; }
const pacienteId = document.getElementById('dom-paciente-id').value;
if (!pacienteId) { alert('Debes seleccionar un paciente'); return; }
const datos = Object.fromEntries(new FormData(form).entries());
if (!datos.id) delete datos.id;
delete datos.orden_id; // archivos se vinculan por domicilio_id; no pasamos FK
delete datos.orden_id_url;
// Desvincular archivos que el usuario eliminó
for (const ordenId of _domRemovedOrdenIds) {
try {
await fetch('api/lab/save_orden.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: ordenId, domicilio_id: null }),
});
} catch (_e) { /* silencioso */ }
}
_domRemovedOrdenIds = [];
// Guardar domicilio
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) {
mostrarToast(d.error||'Error', 'danger');
return;
}
const domicilioId = datos.id ? parseInt(datos.id) : (d.id || null);
// Subir nuevos archivos vinculados al domicilio
if (_domNewFiles.length && domicilioId) {
const fd = new FormData();
for (const f of _domNewFiles) fd.append('files[]', f);
fd.append('domicilio_id', domicilioId);
fd.append('paciente_id', pacienteId);
try {
const upR = await fetch('api/lab/upload_orden.php', { method: 'POST', body: fd });
const upD = await upR.json();
if (!upD.success) mostrarToast('No se pudieron subir algunos archivos: ' + (upD.error || ''), 'warning');
} catch (_e) { mostrarToast('Error al subir archivos', 'warning'); }
}
_domNewFiles = [];
modal.hide();
mostrarToast(d.message, 'success');
_domResetOrdenes();
await cargarLista(paginaActual);
if (domicilioId) verDomicilio(domicilioId);
}
// Autocompletado de paciente
let pacTimer;
document.getElementById('dom-paciente-nombre').addEventListener('input', function() {
clearTimeout(pacTimer);
const v = this.value.trim();
if (v.length < 2) { document.getElementById('pac-suggest').style.display='none'; return; }
pacTimer = setTimeout(async () => {
const r = await fetch(`api/lab/get_pacientes.php?busqueda=${encodeURIComponent(v)}&limit=8`);
const d = await r.json();
const list = document.getElementById('pac-suggest');
if (!d.data?.length) { list.style.display='none'; return; }
list.innerHTML = d.data.map(p =>
`<button type="button" class="list-group-item list-group-item-action small py-1"
onclick="selPaciente(${p.id},'${esc(p.nombre_completo)}')">${esc(p.nombre_completo)} — ${esc(p.numero_documento||p.telefono||'')}</button>`
).join('');
list.style.display = 'block';
}, 300);
});
function selPaciente(id, nombre) {
document.getElementById('dom-paciente-id').value = id;
document.getElementById('dom-paciente-nombre').value= nombre;
document.getElementById('pac-suggest').style.display = 'none';
}
function verHoy() {
document.getElementById('filtro-fecha').value = '<?= date('Y-m-d') ?>';
cargarLista();
}
function exportarDomicilios() {
const fecha = document.getElementById('filtro-fecha').value;
const estado = document.getElementById('filtro-estado').value;
const enfId = document.getElementById('filtro-enfermera').value;
let url = `lab_reportes.php?export=domicilios`;
if (fecha) url += `&fecha=${encodeURIComponent(fecha)}`;
if (estado) url += `&estado=${encodeURIComponent(estado)}`;
if (enfId) url += `&enfermera_id=${encodeURIComponent(enfId)}`;
window.open(url, '_blank');
}
// ── Utils ──────────────────────────────────────────────────────────────────
const esc = s => String(s||'').replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]));
// Renderiza los archivos adjuntos de una nota (soporta nuevo campo `archivos` JSON
// y el campo legacy `imagen_path`).
function _renderArchivosAdmin(n) {
let lista = [];
if (n.archivos) {
try { lista = typeof n.archivos === 'string' ? JSON.parse(n.archivos) : n.archivos; } catch(e) {}
}
if (!lista.length && n.imagen_path) {
lista = [{ nombre: n.imagen_path, path: n.imagen_path, mime: 'image/jpeg', size: 0 }];
}
if (!lista.length) return '';
return '<div class="d-flex flex-wrap gap-2 mt-2">' + lista.map(a => {
const src = `uploads/media/${esc(a.path)}`;
const esImg = (a.mime || '').startsWith('image/');
if (esImg) {
return `<img src="${src}" class="rounded" style="height:80px;width:80px;object-fit:cover;cursor:pointer"
onclick="window.open('${src}','_blank')" title="${esc(a.nombre)}">`;
}
const icon = (a.mime||'').includes('pdf') ? 'fa-file-pdf text-danger'
: (a.mime||'').includes('word') ? 'fa-file-word text-primary'
: (a.mime||'').includes('excel') || (a.mime||'').includes('sheet') ? 'fa-file-excel text-success'
: 'fa-file text-secondary';
return `<a href="${src}" target="_blank" class="border rounded p-1 text-center text-decoration-none"
style="width:80px;height:80px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;color:inherit">
<i class="fas ${icon} fa-lg"></i>
<div style="font-size:.6rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:74px">${esc(a.nombre)}</div>
</a>`;
}).join('') + '</div>';
}
function formatFechaNac(fnac) {
if (!fnac) return '—';
const d = new Date(fnac + 'T00:00:00');
if (isNaN(d)) return fnac;
const hoy = new Date();
let edad = hoy.getFullYear() - d.getFullYear();
const m = hoy.getMonth() - d.getMonth();
if (m < 0 || (m === 0 && hoy.getDate() < d.getDate())) edad--;
return d.toLocaleDateString('es-CO') + ' (' + edad + ' años)';
}
function calcDuracion(hl, hs) {
if (!hl || !hs) return '—';
const [h1, m1] = hl.slice(0,5).split(':').map(Number);
const [h2, m2] = hs.slice(0,5).split(':').map(Number);
const mins = (h2 * 60 + m2) - (h1 * 60 + m1);
if (mins <= 0) return '—';
return mins >= 60 ? `${Math.floor(mins/60)}h ${mins%60}min` : `${mins} min`;
}
function formatearMinutos(mins) {
const n = Number(mins);
if (!Number.isFinite(n) || n < 0) return '—';
if (n < 60) return `${n} min`;
const h = Math.floor(n / 60);
const m = n % 60;
return m ? `${h}h ${m}min` : `${h}h`;
}
/**
* Renderiza el bloque de órdenes médicas (multi-archivo) en el detalle de domicilio.
*/
/**
* Resuelve la URL directa para mostrar un archivo de orden médica.
* Equivalente al _resolveImg de lab_ordenes.php — NO usa encodeURIComponent
* para no romper las / del subdirectorio (ej. 2026/02/archivo.jpg).
*/
function _resolveOrdenUrl(f) {
if (!f) return '';
if (/^https?:\/\//i.test(f)) return f;
if (f.startsWith('uploads/')) return f;
return `uploads/media/${f}`;
}
function renderOrdenesDetail(dom) {
const archivos = dom.archivos_ordenes || [];
if (!archivos.length && !dom.orden_id) return '';
// Sin archivos adjuntos pero sí orden_id — solo link a la ficha
if (!archivos.length) {
return `<div class="mb-3">
<label class="form-label small text-muted text-uppercase">Órdenes médicas</label>
<a href="lab_ordenes.php?id=${dom.orden_id}" class="d-block small">
<i class="fas fa-file-medical me-1"></i>Ver orden #${dom.orden_id}
</a>
</div>`;
}
const items = archivos.map(a => {
const lf = a.local_file || '';
const isImg = /\.(jpe?g|png|gif|webp)$/i.test(lf);
const imgUrl = _resolveOrdenUrl(lf); // URL directa para <img>
const descUrl = lf ? `api/lab/descargar_orden.php?file=${encodeURIComponent(lf)}` : ''; // descarga
const ficha = a.orden_id
? `<a href="lab_ordenes.php?id=${a.orden_id}" class="d-block small text-muted mt-1">
<i class="fas fa-external-link-alt me-1"></i>Ficha orden #${a.orden_id}
</a>`
: '';
if (lf && isImg) return `
<div class="mb-2">
<img src="${imgUrl}" alt="Orden médica"
style="max-width:100%;max-height:200px;object-fit:contain;border-radius:6px;border:1px solid #dee2e6;display:block;cursor:zoom-in"
onclick="verImagenDom('${imgUrl}')"
onerror="this.style.display='none'">
<div class="d-flex gap-2 mt-1">
<a href="${imgUrl}" target="_blank" class="btn btn-sm btn-outline-secondary flex-fill">
<i class="fas fa-expand me-1"></i>Ver completa
</a>
<a href="${descUrl}" target="_blank" class="btn btn-sm btn-outline-secondary flex-fill">
<i class="fas fa-download me-1"></i>Descargar
</a>
</div>
${ficha}
</div>`;
if (lf) return `
<div class="mb-2">
<a href="${descUrl}" target="_blank"
class="btn btn-sm btn-outline-secondary w-100 mb-1">
<i class="fas fa-file-download me-1"></i>Abrir / Descargar archivo
</a>
${ficha}
</div>`;
return `<div class="mb-2">${ficha}</div>`;
}).join('');
return `<div class="mb-3">
<label class="form-label small text-muted text-uppercase">Órdenes médicas${archivos.length > 1 ? ` (${archivos.length})` : ''}</label>
${items}
</div>`;
}
function mostrarToast(msg, tipo='success') {
const div = document.createElement('div');
div.className = `alert alert-${tipo} alert-dismissible position-fixed bottom-0 end-0 m-3`;
div.style.zIndex = 9999;
div.innerHTML = `${esc(msg)}<button type="button" class="btn-close" data-bs-dismiss="alert"></button>`;
document.body.appendChild(div);
setTimeout(() => div.remove(), 4000);
}
// ══════════════════════════════════════════════════════════════════════
// TABS
// ══════════════════════════════════════════════════════════════════════
function switchTab(tab, linkEl) {
// Actualizar estilos de tabs
document.querySelectorAll('.nav-tabs .nav-link').forEach(a => a.classList.remove('active'));
linkEl.classList.add('active');
const esFormularios = tab === 'formularios';
document.getElementById('seccion-domicilios').style.display = esFormularios ? 'none' : '';
document.getElementById('seccion-formularios').style.display = esFormularios ? '' : 'none';
document.getElementById('btn-nuevo-domicilio').style.display = esFormularios ? 'none' : '';
if (esFormularios) {
cargarPlantillasFF();
cargarFormulariosEnviados();
}
}
// ══════════════════════════════════════════════════════════════════════
// FORMULARIOS RECIBIDOS
// ══════════════════════════════════════════════════════════════════════
const FF_POR_PAG = 20;
let _ffPagina = 1;
let _ffTotal = 0;
let _ffPaginas = 1;
let _ffPaginaData = [];
let _ffDebTimer = null;
let _ffPlantillasCargadas = false;
function ffParams(pag = _ffPagina) {
return new URLSearchParams({
envios: 1,
page: pag,
limit: FF_POR_PAG,
fecha_desde: document.getElementById('ff-fecha-desde').value || '',
fecha_hasta: document.getElementById('ff-fecha-hasta').value || '',
formulario_id: document.getElementById('ff-plantilla').value || '',
estado: document.getElementById('ff-estado').value || '',
paciente: (document.getElementById('ff-paciente')?.value || '').trim(),
});
}
async function cargarPlantillasFF() {
if (_ffPlantillasCargadas) return;
_ffPlantillasCargadas = true;
const r = await fetch('api/lab/get_formularios.php');
const d = await r.json();
const sel = document.getElementById('ff-plantilla');
(d.data || []).forEach(f => {
const opt = document.createElement('option');
opt.value = f.id;
opt.textContent = f.nombre;
sel.appendChild(opt);
});
}
async function cargarFormulariosEnviados(pag = 1) {
_ffPagina = pag;
const spinner = document.getElementById('ff-spinner');
const wrap = document.getElementById('ff-tabla-wrap');
if (spinner) spinner.style.display = '';
if (wrap) wrap.style.display = 'none';
try {
const r = await fetch('api/lab/get_formularios.php?' + ffParams(pag));
const d = await r.json();
_ffPaginaData = d.data || [];
_ffTotal = d.total || 0;
_ffPaginas = d.paginas || 1;
ffRender();
} catch(e) {
const tb = document.getElementById('ff-tbody');
if (tb) tb.innerHTML = '<tr><td colspan="6" class="text-center text-danger py-4">Error al cargar</td></tr>';
} finally {
if (spinner) spinner.style.display = 'none';
if (wrap) wrap.style.display = '';
}
}
function ffFiltrar() {
clearTimeout(_ffDebTimer);
_ffDebTimer = setTimeout(() => cargarFormulariosEnviados(1), 350);
}
function ffLimpiar() {
['ff-fecha-desde','ff-fecha-hasta','ff-paciente'].forEach(id => {
const el = document.getElementById(id);
if (el) el.value = '';
});
document.getElementById('ff-plantilla').value = '';
document.getElementById('ff-estado').value = '';
cargarFormulariosEnviados(1);
}
async function ffExportarExcel() {
const btn = document.querySelector('[onclick="ffExportarExcel()"]');
if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Exportando…'; }
let rows = [];
try {
const p = ffParams(1);
p.set('export', '1'); p.delete('page'); p.delete('limit');
const r = await fetch('api/lab/get_formularios.php?' + p);
const d = await r.json();
rows = d.data || [];
} finally {
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-file-excel me-1"></i><span class="d-none d-md-inline">Excel</span>'; }
}
if (!rows.length) { mostrarToast('Sin datos para exportar', 'warning'); return; }
const fixedCols = ['Formulario','Categoría','Paciente','Enviado por','Estado','Fecha envío','Fecha completado'];
const fieldMap = new Map();
rows.forEach(e => {
try { JSON.parse(e.esquema||'[]').forEach(c => {
if (!['separador','firma','firma_profesional','parrafo','parrafo_inline','lista_marcable'].includes(c.tipo)
&& c.id && c.label && !fieldMap.has(c.label)) fieldMap.set(c.label, c.id);
}); } catch(_) {}
});
const fieldCols = [...fieldMap.entries()];
const csvRows = [[...fixedCols, ...fieldCols.map(([l])=>l)]];
rows.forEach(e => {
let cliente={}, prefill={};
try { cliente=JSON.parse(e.datos_cliente||'{}'); } catch(_) {}
try { prefill=JSON.parse(e.datos_prefilled||'{}'); } catch(_) {}
const todos = {...prefill,...cliente};
const lmap = new Map();
try { JSON.parse(e.esquema||'[]').forEach(c=>{if(c.id&&c.label)lmap.set(c.label,c.id);}); } catch(_) {}
csvRows.push([
e.form_nombre||'', e.categoria||'', e.paciente_nombre||'', e.enviado_por_nombre||'', e.estado||'',
(e.created_at||'').slice(0,16).replace('T',' '), (e.completado_en||'').slice(0,16).replace('T',' '),
...fieldCols.map(([l,d])=>{ const v=todos[lmap.get(l)||d]; return v==null?'':(Array.isArray(v)?v.join('; '):String(v)); }),
]);
});
const csv = '\uFEFF' + csvRows.map(r=>r.map(v=>'"'+String(v).replace(/"/g,'""')+'"').join(',')).join('\r\n');
const blob = new Blob([csv],{type:'text/csv;charset=utf-8;'});
const url = URL.createObjectURL(blob);
const a = Object.assign(document.createElement('a'),{href:url,download:'formularios_domicilios_'+new Date().toISOString().slice(0,10)+'.csv'});
document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
mostrarToast('✅ Excel descargado — ' + rows.length + ' registro(s)', 'success');
}
const FF_ESTADO_BADGE = { pendiente:'badge bg-warning text-dark', completado:'badge bg-success', firmado:'badge bg-primary', expirado:'badge bg-secondary' };
const FF_ESTADO_ICON = { pendiente:'⏳', completado:'✅', firmado:'✍️', expirado:'❌' };
function ffRender() {
const tbody = document.getElementById('ff-tbody');
if (!_ffPaginaData.length) {
tbody.innerHTML = '<tr><td colspan="6" class="text-center py-4 text-muted">Sin registros para los filtros seleccionados</td></tr>';
document.getElementById('ff-info').textContent = '';
document.getElementById('ff-paginacion').innerHTML = '';
return;
}
const inicio = (_ffPagina - 1) * FF_POR_PAG;
tbody.innerHTML = _ffPaginaData.map(e => {
const fecha = (e.created_at || '').slice(0, 16).replace('T', ' ');
const puedeVer = e.estado === 'completado' || e.estado === 'firmado';
return `<tr>
<td class="small text-nowrap">${esc(fecha)}</td>
<td class="small">${esc(e.paciente_nombre || '—')}</td>
<td class="small">${esc(e.form_nombre)}</td>
<td class="small">${esc(e.enviado_por_nombre || '—')}</td>
<td><span class="${FF_ESTADO_BADGE[e.estado]||'badge bg-secondary'}">${FF_ESTADO_ICON[e.estado]||''} ${esc(e.estado)}</span></td>
<td>
${puedeVer
? `<button class="btn btn-xs btn-outline-primary py-0 px-2 me-1" onclick="verRespuestaFF(${e.id})"><i class="fas fa-eye me-1"></i>Ver</button>
<a href="ver_formulario_enviado.php?id=${e.id}" target="_blank" class="btn btn-xs btn-outline-secondary py-0 px-2"><i class="fas fa-print"></i></a>`
: '—'}
</td>
</tr>`;
}).join('');
document.getElementById('ff-info').textContent =
`Mostrando ${inicio+1}${inicio+_ffPaginaData.length} de ${_ffTotal} registro(s)`;
// Paginación
const ul = document.getElementById('ff-paginacion');
const pag = _ffPagina, tot = _ffPaginas;
ul.innerHTML = '';
if (tot <= 1) return;
const li = (lbl, p, dis, act) => {
const el = document.createElement('li');
el.className = `page-item${dis?' disabled':''}${act?' active':''}`;
el.innerHTML = `<a class="page-link" href="#" onclick="event.preventDefault();${!dis?`cargarFormulariosEnviados(${p})`:''}"
>${lbl}</a>`;
ul.appendChild(el);
};
li('«',1,pag===1,false); li('',pag-1,pag===1,false);
const pMin=Math.max(1,pag-2), pMax=Math.min(tot,pag+2);
if(pMin>1){li('1',1,false,false);if(pMin>2)li('…',null,true,false);}
for(let p=pMin;p<=pMax;p++) li(p,p,false,p===pag);
if(pMax<tot){if(pMax<tot-1)li('…',null,true,false);li(tot,tot,false,false);}
li('',pag+1,pag===tot,false); li('»',tot,pag===tot,false);
}
function verRespuestaFF(envioId) {
const e = _ffPaginaData.find(x => x.id == envioId);
if (!e) return;
const cliente = JSON.parse(e.datos_cliente || '{}');
const prefill = JSON.parse(e.datos_prefilled || '{}');
const todos = { ...prefill, ...cliente };
const esquema = JSON.parse(e.esquema || '[]');
const labels = {};
esquema.forEach(c => { if (c.id) labels[c.id] = c.label || c.id; });
let html = `<h6 class="fw-bold mb-1">${esc(e.form_nombre)}</h6>
<p class="small text-muted mb-3">
Paciente: <strong>${esc(e.paciente_nombre || '—')}</strong>
&nbsp;·&nbsp;
${e.completado_en ? 'Completado: ' + esc(e.completado_en.slice(0,16).replace('T',' ')) : 'Pendiente'}
</p><hr class="my-2">`;
const ordenados = esquema.filter(c =>
c.tipo !== 'separador' && c.tipo !== 'firma' && todos[c.id] !== undefined
);
if (ordenados.length) {
ordenados.forEach(c => {
const v = todos[c.id];
html += `<div class="row mb-2">
<div class="col-5 text-muted small">${esc(c.label || c.id)}</div>
<div class="col-7 small fw-semibold">${esc(Array.isArray(v)?v.join(', '):String(v??'—'))}</div>
</div>`;
});
} else {
Object.entries(todos).forEach(([k, v]) => {
if (k.startsWith('__')) return;
html += `<div class="row mb-2">
<div class="col-5 text-muted small">${esc(labels[k]||k)}</div>
<div class="col-7 small fw-semibold">${esc(Array.isArray(v)?v.join(', '):String(v??'—'))}</div>
</div>`;
});
}
if (e.firma_svg) {
html += `<hr class="my-3"><p class="small fw-semibold text-muted">Firma digital:</p>
<img src="${e.firma_svg}" class="border rounded p-2" style="max-width:260px;max-height:160px;display:block">`;
}
document.getElementById('resp-form-body').innerHTML = html;
document.getElementById('btn-resp-pdf').href = `ver_formulario_enviado.php?id=${envioId}`;
if (!_modalRespForm) _modalRespForm = new bootstrap.Modal('#modalRespuestaForm');
_modalRespForm.show();
}
</script>
<script src="assets/js/lab-sidebar.js"></script>
<!-- Modal visor de imagen -->
<div class="modal fade" id="modalImagenDom" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content bg-dark">
<div class="modal-header border-0 py-2">
<button class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center p-0">
<img id="img-visor-dom" src="" class="img-fluid" style="max-height:80vh;object-fit:contain">
</div>
</div>
</div>
</div>
<script>
const _modalImgDom = new bootstrap.Modal('#modalImagenDom');
function verImagenDom(src) {
document.getElementById('img-visor-dom').src = src;
_modalImgDom.show();
}
</script>
</body>
</html>