1713 lines
88 KiB
HTML
1713 lines
88 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="es">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Documentación — Sistema de Laboratorio</title>
|
||
<style>
|
||
:root {
|
||
--primary: #2563eb;
|
||
--primary-dk: #1d4ed8;
|
||
--secondary: #0891b2;
|
||
--success: #16a34a;
|
||
--warning: #d97706;
|
||
--danger: #dc2626;
|
||
--dark: #1e293b;
|
||
--muted: #64748b;
|
||
--light: #f8fafc;
|
||
--border: #e2e8f0;
|
||
--radius: 8px;
|
||
}
|
||
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
background: #f1f5f9;
|
||
color: #334155;
|
||
font-size: 15px;
|
||
line-height: 1.7;
|
||
}
|
||
|
||
/* ── Layout ── */
|
||
.layout { display: flex; min-height: 100vh; }
|
||
|
||
/* ── Sidebar TOC ── */
|
||
.toc {
|
||
width: 280px;
|
||
flex-shrink: 0;
|
||
background: var(--dark);
|
||
color: #cbd5e1;
|
||
position: sticky;
|
||
top: 0;
|
||
height: 100vh;
|
||
overflow-y: auto;
|
||
padding: 0 0 40px;
|
||
}
|
||
|
||
.toc-brand {
|
||
padding: 20px 20px 16px;
|
||
border-bottom: 1px solid #334155;
|
||
margin-bottom: 8px;
|
||
}
|
||
.toc-brand h2 {
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: .08em;
|
||
color: #94a3b8;
|
||
}
|
||
.toc-brand p {
|
||
font-size: 11px;
|
||
color: #475569;
|
||
margin-top: 3px;
|
||
}
|
||
|
||
.toc-section {
|
||
padding: 8px 20px 4px;
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: .1em;
|
||
color: #475569;
|
||
}
|
||
|
||
.toc a {
|
||
display: block;
|
||
padding: 6px 20px 6px 28px;
|
||
color: #94a3b8;
|
||
text-decoration: none;
|
||
font-size: 13px;
|
||
border-left: 3px solid transparent;
|
||
transition: all .15s;
|
||
}
|
||
.toc a:hover, .toc a.active {
|
||
color: #f8fafc;
|
||
background: #263348;
|
||
border-left-color: var(--primary);
|
||
}
|
||
.toc a.h2-link {
|
||
padding-left: 20px;
|
||
font-weight: 600;
|
||
font-size: 13.5px;
|
||
color: #e2e8f0;
|
||
}
|
||
|
||
/* ── Main content ── */
|
||
.content {
|
||
flex: 1;
|
||
padding: 48px 56px;
|
||
max-width: 1100px;
|
||
}
|
||
|
||
/* ── Hero ── */
|
||
.hero {
|
||
background: linear-gradient(135deg, var(--dark) 0%, #0f172a 100%);
|
||
color: white;
|
||
border-radius: var(--radius);
|
||
padding: 36px 40px;
|
||
margin-bottom: 48px;
|
||
}
|
||
.hero h1 { font-size: 28px; font-weight: 800; }
|
||
.hero p { margin-top: 8px; color: #94a3b8; font-size: 15px; }
|
||
.hero-meta {
|
||
display: flex;
|
||
gap: 24px;
|
||
margin-top: 20px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.hero-meta span {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 12px;
|
||
color: #64748b;
|
||
background: #1e293b;
|
||
padding: 4px 12px;
|
||
border-radius: 20px;
|
||
}
|
||
.hero-meta span b { color: #94a3b8; }
|
||
|
||
/* ── Sections ── */
|
||
.module {
|
||
background: white;
|
||
border-radius: var(--radius);
|
||
box-shadow: 0 1px 3px rgba(0,0,0,.06);
|
||
margin-bottom: 40px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.module-header {
|
||
padding: 20px 28px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
.module-header .icon {
|
||
width: 42px;
|
||
height: 42px;
|
||
border-radius: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 20px;
|
||
flex-shrink: 0;
|
||
}
|
||
.icon-blue { background: #dbeafe; color: var(--primary); }
|
||
.icon-violet { background: #ede9fe; color: #7c3aed; }
|
||
.icon-cyan { background: #cffafe; color: var(--secondary); }
|
||
.icon-green { background: #dcfce7; color: var(--success); }
|
||
.icon-orange { background: #ffedd5; color: var(--warning); }
|
||
|
||
.module-header h2 {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: var(--dark);
|
||
}
|
||
.module-header p {
|
||
font-size: 13px;
|
||
color: var(--muted);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.module-body { padding: 28px; }
|
||
|
||
h3 {
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
color: var(--dark);
|
||
text-transform: uppercase;
|
||
letter-spacing: .06em;
|
||
margin: 28px 0 14px;
|
||
padding-bottom: 6px;
|
||
border-bottom: 2px solid var(--border);
|
||
}
|
||
h3:first-child { margin-top: 0; }
|
||
|
||
h4 {
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
color: #475569;
|
||
margin: 20px 0 10px;
|
||
}
|
||
|
||
p { margin-bottom: 12px; }
|
||
|
||
/* ── Tables ── */
|
||
.table-wrap { overflow-x: auto; margin-bottom: 20px; }
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 13.5px;
|
||
}
|
||
thead tr { background: #f8fafc; }
|
||
th {
|
||
text-align: left;
|
||
padding: 10px 14px;
|
||
font-weight: 600;
|
||
color: #475569;
|
||
border-bottom: 2px solid var(--border);
|
||
white-space: nowrap;
|
||
}
|
||
td {
|
||
padding: 9px 14px;
|
||
border-bottom: 1px solid #f1f5f9;
|
||
vertical-align: top;
|
||
}
|
||
tr:last-child td { border-bottom: none; }
|
||
tr:hover td { background: #fafafa; }
|
||
|
||
td code, th code { font-family: 'SF Mono', Consolas, monospace; }
|
||
|
||
/* ── Code / Pre ── */
|
||
pre {
|
||
background: #0f172a;
|
||
color: #e2e8f0;
|
||
border-radius: var(--radius);
|
||
padding: 20px 24px;
|
||
font-family: 'SF Mono', Consolas, monospace;
|
||
font-size: 13px;
|
||
line-height: 1.65;
|
||
overflow-x: auto;
|
||
margin-bottom: 20px;
|
||
}
|
||
code {
|
||
background: #e2e8f0;
|
||
color: #1e293b;
|
||
padding: 1px 6px;
|
||
border-radius: 4px;
|
||
font-family: 'SF Mono', Consolas, monospace;
|
||
font-size: 12.5px;
|
||
}
|
||
pre code { background: none; color: inherit; padding: 0; font-size: 13px; }
|
||
|
||
/* ── Badges ── */
|
||
.badge {
|
||
display: inline-block;
|
||
padding: 2px 9px;
|
||
border-radius: 12px;
|
||
font-size: 11.5px;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
}
|
||
.badge-blue { background: #dbeafe; color: #1d4ed8; }
|
||
.badge-green { background: #dcfce7; color: #15803d; }
|
||
.badge-orange { background: #ffedd5; color: #c2410c; }
|
||
.badge-red { background: #fee2e2; color: #b91c1c; }
|
||
.badge-violet { background: #ede9fe; color: #6d28d9; }
|
||
.badge-gray { background: #f1f5f9; color: #475569; }
|
||
.badge-cyan { background: #cffafe; color: #0e7490; }
|
||
|
||
/* ── Info callout ── */
|
||
.callout {
|
||
border-left: 4px solid var(--primary);
|
||
background: #eff6ff;
|
||
border-radius: 0 var(--radius) var(--radius) 0;
|
||
padding: 12px 16px;
|
||
margin-bottom: 16px;
|
||
font-size: 13.5px;
|
||
}
|
||
.callout.warning { border-left-color: var(--warning); background: #fffbeb; }
|
||
.callout.success { border-left-color: var(--success); background: #f0fdf4; }
|
||
|
||
/* ── State flow ── */
|
||
.flow {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0;
|
||
align-items: center;
|
||
margin: 16px 0;
|
||
}
|
||
.flow-step {
|
||
background: white;
|
||
border: 2px solid var(--border);
|
||
border-radius: 8px;
|
||
padding: 10px 18px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
text-align: center;
|
||
min-width: 140px;
|
||
}
|
||
.flow-arrow {
|
||
font-size: 20px;
|
||
color: var(--muted);
|
||
padding: 0 6px;
|
||
}
|
||
.flow-step.s-prog { border-color: #94a3b8; color: #475569; }
|
||
.flow-step.s-conf { border-color: #06b6d4; color: #0e7490; }
|
||
.flow-step.s-cam { border-color: var(--primary); color: var(--primary); }
|
||
.flow-step.s-dom { border-color: var(--warning); color: var(--warning); }
|
||
.flow-step.s-comp { border-color: var(--success); color: var(--success); }
|
||
.flow-step.s-canc { border-color: var(--danger); color: var(--danger); }
|
||
.flow-step.s-repr { border-color: #7c3aed; color: #7c3aed; }
|
||
|
||
/* ── Roles grid ── */
|
||
.roles-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin-bottom: 20px; }
|
||
.role-card {
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
padding: 16px;
|
||
}
|
||
.role-card h4 { margin: 0 0 8px; font-size: 13.5px; }
|
||
.role-card ul { padding-left: 18px; font-size: 13px; color: var(--muted); }
|
||
.role-card ul li { margin-bottom: 4px; }
|
||
|
||
/* ── Features grid ── */
|
||
.features { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; margin-bottom: 20px; }
|
||
.feature-item {
|
||
background: #f8fafc;
|
||
border-radius: 8px;
|
||
padding: 12px 14px;
|
||
font-size: 13px;
|
||
border: 1px solid var(--border);
|
||
}
|
||
.feature-item strong { display: block; color: var(--dark); margin-bottom: 2px; }
|
||
|
||
/* ── Step list ── */
|
||
.steps { counter-reset: step; list-style: none; padding: 0; }
|
||
.steps li {
|
||
counter-increment: step;
|
||
display: flex;
|
||
gap: 14px;
|
||
margin-bottom: 14px;
|
||
align-items: flex-start;
|
||
}
|
||
.steps li::before {
|
||
content: counter(step);
|
||
min-width: 26px;
|
||
height: 26px;
|
||
background: var(--primary);
|
||
color: white;
|
||
border-radius: 50%;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
margin-top: 2px;
|
||
}
|
||
.steps li > div { font-size: 13.5px; line-height: 1.6; }
|
||
.steps li > div strong { color: var(--dark); }
|
||
|
||
/* ── Manual de uso ── */
|
||
.man-section { margin-bottom: 36px; }
|
||
|
||
.man-step-block {
|
||
display: flex;
|
||
gap: 16px;
|
||
margin-bottom: 20px;
|
||
align-items: flex-start;
|
||
}
|
||
.man-step-num {
|
||
min-width: 34px;
|
||
height: 34px;
|
||
border-radius: 50%;
|
||
background: var(--primary);
|
||
color: white;
|
||
font-weight: 800;
|
||
font-size: 14px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
margin-top: 2px;
|
||
}
|
||
.man-step-body { flex: 1; }
|
||
.man-step-body h5 {
|
||
font-size: 14.5px;
|
||
font-weight: 700;
|
||
color: var(--dark);
|
||
margin-bottom: 6px;
|
||
}
|
||
.man-step-body p { font-size: 13.5px; margin-bottom: 6px; }
|
||
|
||
.man-tip {
|
||
background: #f0fdf4;
|
||
border-left: 4px solid var(--success);
|
||
border-radius: 0 var(--radius) var(--radius) 0;
|
||
padding: 10px 14px;
|
||
font-size: 13px;
|
||
margin: 8px 0 0;
|
||
color: #166534;
|
||
}
|
||
.man-warn {
|
||
background: #fffbeb;
|
||
border-left: 4px solid var(--warning);
|
||
border-radius: 0 var(--radius) var(--radius) 0;
|
||
padding: 10px 14px;
|
||
font-size: 13px;
|
||
margin: 8px 0 0;
|
||
color: #92400e;
|
||
}
|
||
|
||
.man-ui-tag {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
background: #f1f5f9;
|
||
border: 1px solid #cbd5e1;
|
||
border-radius: 6px;
|
||
padding: 2px 10px;
|
||
font-size: 12.5px;
|
||
font-weight: 600;
|
||
color: #334155;
|
||
}
|
||
|
||
.state-pill {
|
||
display: inline-block;
|
||
padding: 3px 12px;
|
||
border-radius: 20px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
margin: 2px;
|
||
}
|
||
.sp-prog { background:#f1f5f9; color:#475569; border:1px solid #cbd5e1; }
|
||
.sp-conf { background:#cffafe; color:#0e7490; border:1px solid #a5f3fc; }
|
||
.sp-cam { background:#dbeafe; color:#1d4ed8; border:1px solid #93c5fd; }
|
||
.sp-dom { background:#ffedd5; color:#c2410c; border:1px solid #fdba74; }
|
||
.sp-comp { background:#dcfce7; color:#15803d; border:1px solid #86efac; }
|
||
.sp-canc { background:#fee2e2; color:#b91c1c; border:1px solid #fca5a5; }
|
||
|
||
.role-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
padding: 3px 12px;
|
||
border-radius: 20px;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
}
|
||
.rb-admin { background: #dbeafe; color: #1d4ed8; }
|
||
.rb-enf { background: #dcfce7; color: #15803d; }
|
||
.rb-pub { background: #f3e8ff; color: #6d28d9; }
|
||
|
||
/* ── Scroll top button ── */
|
||
#scrollTop {
|
||
position: fixed;
|
||
bottom: 28px;
|
||
right: 28px;
|
||
width: 40px;
|
||
height: 40px;
|
||
background: var(--dark);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 50%;
|
||
cursor: pointer;
|
||
font-size: 18px;
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,.2);
|
||
z-index: 100;
|
||
}
|
||
#scrollTop.show { display: flex; }
|
||
|
||
/* ── Responsive ── */
|
||
@media (max-width: 860px) {
|
||
.toc { display: none; }
|
||
.content { padding: 24px 20px; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="layout">
|
||
|
||
<!-- ══════════════════ SIDEBAR TOC ══════════════════ -->
|
||
<nav class="toc">
|
||
<div class="toc-brand">
|
||
<h2>🔬 Lab System</h2>
|
||
<p>Documentación técnica — v1.0</p>
|
||
</div>
|
||
|
||
<div class="toc-section">Módulo 1 — Domicilios</div>
|
||
<a href="#dom-intro" class="h2-link">¿Qué es?</a>
|
||
<a href="#dom-archivos">Archivos involucrados</a>
|
||
<a href="#dom-bd">Base de datos</a>
|
||
<a href="#dom-bd-domicilios"> ↳ lab_domicilios</a>
|
||
<a href="#dom-bd-enfermeras"> ↳ lab_enfermeras</a>
|
||
<a href="#dom-bd-asignaciones"> ↳ lab_asignaciones</a>
|
||
<a href="#dom-bd-extras"> ↳ lab_servicios_extra</a>
|
||
<a href="#dom-bd-notas"> ↳ lab_domicilio_notas</a>
|
||
<a href="#dom-roles">Roles y permisos</a>
|
||
<a href="#dom-estados">Flujo de estados</a>
|
||
<a href="#dom-funciones">Funcionalidades</a>
|
||
<a href="#dom-api">API Endpoints</a>
|
||
|
||
<div class="toc-section">Módulo 2 — Formularios</div>
|
||
<a href="#form-intro" class="h2-link">¿Qué es?</a>
|
||
|
||
<div class="toc-section">📘 Manual de Uso</div>
|
||
<a href="#man-admin" class="h2-link">Admin — Domicilios</a>
|
||
<a href="#man-admin-crear">Crear domicilio</a>
|
||
<a href="#man-admin-asignar">Asignar enfermero</a>
|
||
<a href="#man-admin-estado">Cambiar estado</a>
|
||
<a href="#man-admin-pago">Registrar pago</a>
|
||
<a href="#man-admin-filtros">Filtros y exportar</a>
|
||
<a href="#man-enf" class="h2-link">Enfermero — Portal</a>
|
||
<a href="#man-enf-acceso">Acceder al portal</a>
|
||
<a href="#man-enf-avanzar">Avanzar un domicilio</a>
|
||
<a href="#man-enf-notas">Registrar notas</a>
|
||
<a href="#man-enf-extras">Servicios extra</a>
|
||
<a href="#man-form" class="h2-link">Admin — Formularios</a>
|
||
<a href="#man-form-crear">Crear plantilla</a>
|
||
<a href="#man-form-enviar">Enviar a paciente</a>
|
||
<a href="#man-form-revisar">Revisar respuestas</a>
|
||
<a href="#man-form-firmar">Firmar como profesional</a>
|
||
<a href="#man-form-verificar">Verificar autenticidad</a>
|
||
<a href="#form-archivos">Archivos involucrados</a>
|
||
<a href="#form-bd">Base de datos</a>
|
||
<a href="#form-bd-plantillas"> ↳ lab_formularios</a>
|
||
<a href="#form-bd-envios"> ↳ lab_form_envios</a>
|
||
<a href="#form-roles">Roles y permisos</a>
|
||
<a href="#form-flujo">Flujo completo</a>
|
||
<a href="#form-campos">Tipos de campos</a>
|
||
<a href="#form-firma-dig">Firma digital</a>
|
||
<a href="#form-enlace">Enlace y vigencia</a>
|
||
<a href="#form-pdf">PDF y visualización</a>
|
||
<a href="#form-sha">Sello SHA-256</a>
|
||
<a href="#form-firmapro">Firma del profesional</a>
|
||
<a href="#form-api">API Endpoints</a>
|
||
</nav>
|
||
|
||
<!-- ══════════════════ CONTENT ══════════════════ -->
|
||
<main class="content">
|
||
|
||
<!-- Hero -->
|
||
<div class="hero">
|
||
<h1>📋 Documentación del Sistema de Laboratorio</h1>
|
||
<p>Sistema Laboratorio Clínico — Módulos de Agendamiento y Formularios</p>
|
||
<div class="hero-meta">
|
||
<span>📅 <b>Última actualización:</b> 30/03/2026</span>
|
||
<span>🔢 <b>Versión:</b> 1.0</span>
|
||
<span>📦 <b>Módulos:</b> Domicilios · Formularios</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ╔═══════════════════════════════════════════╗
|
||
║ MÓDULO 1 — DOMICILIOS ║
|
||
╚═══════════════════════════════════════════╝ -->
|
||
<div class="module" id="dom-intro">
|
||
<div class="module-header">
|
||
<div class="icon icon-blue">🏠</div>
|
||
<div>
|
||
<h2>Módulo 1 — Agendamiento (Domicilios)</h2>
|
||
<p>Gestión de servicios de toma de muestras a domicilio</p>
|
||
</div>
|
||
</div>
|
||
<div class="module-body">
|
||
|
||
<h3>¿Qué es?</h3>
|
||
<p>
|
||
El módulo de agendamiento gestiona los <strong>servicios de toma de muestras a domicilio</strong>.
|
||
Permite crear, asignar, seguir y completar visitas médicas domiciliarias.
|
||
Los administradores gestionan la agenda desde <code>lab_domicilios.php</code>;
|
||
los enfermeros gestionan su propia agenda desde <code>enfermero_portal.php</code>.
|
||
</p>
|
||
|
||
<!-- ARCHIVOS -->
|
||
<h3 id="dom-archivos">Archivos involucrados</h3>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr><th>Archivo</th><th>Descripción</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr><td><code>lab_domicilios.php</code></td><td>Vista principal del admin — tabla con filtros, detalle del domicilio, formularios recibidos, exportar CSV</td></tr>
|
||
<tr><td><code>enfermero_portal.php</code></td><td>Portal exclusivo del enfermero — su agenda personal del día, ordenada por hora, con tarjetas colapsables</td></tr>
|
||
<tr><td><code>lab_enfermeras.php</code></td><td>CRUD del personal de enfermería</td></tr>
|
||
<tr><td><code>classes/lab/Domicilio.php</code></td><td>Clase ORM — crear, editar, cambiar estado, estadísticas</td></tr>
|
||
<tr><td><code>classes/lab/Enfermera.php</code></td><td>Clase ORM — CRUD, agenda por enfermero, carga de trabajo</td></tr>
|
||
<tr><td><code>classes/lab/Asignacion.php</code></td><td>Clase ORM — asignar / reasignar / liberar enfermero a domicilio</td></tr>
|
||
<tr><td><code>api/lab/get_domicilios.php</code></td><td>GET — lista de domicilios con filtros y estadísticas del día</td></tr>
|
||
<tr><td><code>api/lab/save_domicilio.php</code></td><td>POST — crear, actualizar o cambiar estado</td></tr>
|
||
<tr><td><code>api/lab/update_domicilio_enfermero.php</code></td><td>POST — el enfermero avanza el estado desde su portal</td></tr>
|
||
<tr><td><code>api/lab/my_agenda.php</code></td><td>GET — agenda del enfermero actualmente autenticado</td></tr>
|
||
<tr><td><code>api/lab/save_asignacion.php</code></td><td>POST — asignar o reasignar enfermero</td></tr>
|
||
<tr><td><code>api/lab/get_asignaciones.php</code></td><td>GET — asignaciones por fecha</td></tr>
|
||
<tr><td><code>api/lab/registrar_pago.php</code></td><td>POST — registrar pago de un domicilio</td></tr>
|
||
<tr><td><code>api/lab/save_servicio_extra.php</code></td><td>POST — agregar servicio realizado durante la visita</td></tr>
|
||
<tr><td><code>api/lab/get_notas_domicilio.php</code></td><td>GET — notas clínicas y libres del domicilio</td></tr>
|
||
<tr><td><code>api/lab/upload_nota_imagen.php</code></td><td>POST — subir imagen adjunta a una nota</td></tr>
|
||
<tr><td><code>api/lab/crear_desde_whatsapp.php</code></td><td>GET/POST — crear paciente u orden desde una conversación de WhatsApp activa</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- BASE DE DATOS -->
|
||
<h3 id="dom-bd">Base de datos</h3>
|
||
|
||
<h4 id="dom-bd-domicilios">Tabla <code>lab_domicilios</code></h4>
|
||
<p>Tabla principal. Cada fila es un servicio a domicilio.</p>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead><tr><th>Campo</th><th>Tipo</th><th>Descripción</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>id</code></td><td><span class="badge badge-blue">INT PK</span></td><td>Identificador único</td></tr>
|
||
<tr><td><code>paciente_id</code></td><td><span class="badge badge-orange">INT FK</span></td><td>Paciente al que se le realiza el servicio</td></tr>
|
||
<tr><td><code>orden_id</code></td><td><span class="badge badge-gray">INT FK NULL</span></td><td>Orden médica adjunta (opcional)</td></tr>
|
||
<tr><td><code>direccion</code></td><td>TEXT</td><td>Dirección completa de la visita</td></tr>
|
||
<tr><td><code>ciudad</code></td><td>VARCHAR(100)</td><td>Ciudad</td></tr>
|
||
<tr><td><code>barrio</code></td><td>VARCHAR(100)</td><td>Barrio</td></tr>
|
||
<tr><td><code>indicaciones_dir</code></td><td>TEXT</td><td>Referencias o indicaciones adicionales ("apto 302, tocar campanilla")</td></tr>
|
||
<tr><td><code>fecha_programada</code></td><td>DATE</td><td>Fecha de la visita</td></tr>
|
||
<tr><td><code>hora_programada</code></td><td>TIME</td><td>Hora de la visita</td></tr>
|
||
<tr><td><code>tipo_servicio</code></td><td>VARCHAR(100)</td><td>Tipo de servicio (toma de muestra, etc.)</td></tr>
|
||
<tr><td><code>tipo_cliente</code></td><td>ENUM</td><td><code>particular</code> / <code>seguro</code> / <code>eps</code></td></tr>
|
||
<tr><td><code>examenes_solicitados</code></td><td>TEXT</td><td>Lista de exámenes (cuando no hay orden médica)</td></tr>
|
||
<tr><td><code>seguro_nombre</code></td><td>VARCHAR</td><td>Nombre del seguro o EPS</td></tr>
|
||
<tr><td><code>autorizacion</code></td><td>VARCHAR</td><td>Número de autorización</td></tr>
|
||
<tr><td><code>valor_domicilio</code></td><td>DECIMAL</td><td>Valor del servicio <span class="badge badge-gray">nullable</span></td></tr>
|
||
<tr><td><code>valor_copago</code></td><td>DECIMAL</td><td>Copago a cargo del cliente <span class="badge badge-gray">nullable</span></td></tr>
|
||
<tr><td><code>copago_laboratorio</code></td><td>DECIMAL</td><td>Copago al laboratorio <span class="badge badge-gray">nullable</span></td></tr>
|
||
<tr><td><code>pago_estado</code></td><td>ENUM</td><td><code>pendiente</code> / <code>pagado</code> / <code>exento</code></td></tr>
|
||
<tr><td><code>pago_modo</code></td><td>ENUM</td><td><code>efectivo</code> / <code>transferencia</code> / <code>otro</code></td></tr>
|
||
<tr><td><code>pago_monto</code></td><td>DECIMAL</td><td>Monto pagado</td></tr>
|
||
<tr><td><code>pago_fecha</code></td><td>DATETIME</td><td>Fecha del pago</td></tr>
|
||
<tr><td><code>pago_notas</code></td><td>TEXT</td><td>Notas sobre el pago</td></tr>
|
||
<tr><td><code>estado</code></td><td>ENUM</td><td>Ver flujo de estados</td></tr>
|
||
<tr><td><code>motivo_cancelacion</code></td><td>TEXT</td><td>Motivo si fue cancelado (obligatorio al cancelar)</td></tr>
|
||
<tr><td><code>fecha_reprogramada</code></td><td>DATE</td><td>Nueva fecha si fue reprogramado</td></tr>
|
||
<tr><td><code>hora_llegada</code></td><td>TIME</td><td>Registrada automáticamente al iniciar la visita</td></tr>
|
||
<tr><td><code>hora_salida</code></td><td>TIME</td><td>Registrada automáticamente al completar</td></tr>
|
||
<tr><td><code>observaciones</code></td><td>TEXT</td><td>Observaciones del resultado de la visita</td></tr>
|
||
<tr><td><code>muestras_tomadas</code></td><td>TEXT</td><td>Lista de muestras obtenidas</td></tr>
|
||
<tr><td><code>notas_admin</code></td><td>TEXT</td><td>Notas internas del equipo administrativo</td></tr>
|
||
<tr><td><code>creado_por</code></td><td><span class="badge badge-orange">INT FK</span></td><td>Usuario que creó el registro</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h4 id="dom-bd-enfermeras">Tabla <code>lab_enfermeras</code></h4>
|
||
<p>Personal de enfermería disponible para asignación.</p>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead><tr><th>Campos</th><th>Descripción</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>id</code>, <code>numero_documento</code></td><td>PK e identificador del empleado</td></tr>
|
||
<tr><td><code>tipo_documento</code></td><td>CC / CE / TI / PA</td></tr>
|
||
<tr><td><code>nombre_completo</code>, <code>telefono</code>, <code>telefono_alt</code></td><td>Datos de contacto</td></tr>
|
||
<tr><td><code>email</code>, <code>zona</code>, <code>notas</code></td><td>Información adicional</td></tr>
|
||
<tr><td><code>is_active</code></td><td>Soft-delete del registro</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h4 id="dom-bd-asignaciones">Tabla <code>lab_asignaciones</code></h4>
|
||
<p>Asignación de enfermero a domicilio. Máximo un enfermero activo por domicilio (<code>UNIQUE KEY</code> en <code>domicilio_id</code>).</p>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead><tr><th>Campo</th><th>Descripción</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>id</code>, <code>domicilio_id</code>, <code>enfermera_id</code></td><td>PK y relaciones</td></tr>
|
||
<tr><td><code>asignada_por</code></td><td>Usuario admin que realizó la asignación</td></tr>
|
||
<tr><td><code>estado</code></td><td><code>asignada</code> / <code>confirmada</code> / <code>liberada</code> / <code>completada</code></td></tr>
|
||
<tr><td><code>notas</code></td><td>Observaciones de la asignación</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h4 id="dom-bd-extras">Tabla <code>lab_servicios_extra</code></h4>
|
||
<p>Servicios realizados por el enfermero durante la visita, adicionales a la orden original.</p>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead><tr><th>Campo</th><th>Descripción</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>tipo</code></td><td><code>inyeccion</code> / <code>cura</code> / <code>nebulizacion</code> / <code>toma_muestra</code> / <code>tension_arterial</code> / <code>glucometria</code> / <code>otro</code></td></tr>
|
||
<tr><td><code>descripcion</code>, <code>notas</code></td><td>Detalle del servicio</td></tr>
|
||
<tr><td><code>requiere_pago</code>, <code>valor</code></td><td>Si genera cobro adicional y su valor</td></tr>
|
||
<tr><td><code>realizado_por</code></td><td>FK al enfermero que lo realizó</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h4 id="dom-bd-notas">Tabla <code>lab_domicilio_notas</code></h4>
|
||
<p>Notas registradas por el enfermero durante la visita. Dos tipos:</p>
|
||
<div class="features" style="margin-bottom:14px">
|
||
<div class="feature-item">
|
||
<strong>🩺 clinica</strong>
|
||
Ficha clínica: antecedentes, medicamentos, acudiente (si es menor de edad).
|
||
</div>
|
||
<div class="feature-item">
|
||
<strong>📝 libre</strong>
|
||
Nota libre con título, cuerpo de texto enriquecido e imagen adjunta.
|
||
</div>
|
||
</div>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead><tr><th>Campos</th><th>Descripción</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>id</code>, <code>domicilio_id</code>, <code>enfermera_id</code></td><td>PK y relaciones</td></tr>
|
||
<tr><td><code>tipo</code></td><td><code>clinica</code> / <code>libre</code></td></tr>
|
||
<tr><td><code>antecedentes</code>, <code>medicamentos</code></td><td>Sección clínica</td></tr>
|
||
<tr><td><code>acudiente_nombre</code>, <code>acudiente_documento</code></td><td>Datos del acudiente si el paciente es menor</td></tr>
|
||
<tr><td><code>titulo</code>, <code>cuerpo</code>, <code>imagen_path</code></td><td>Sección de nota libre</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- ROLES -->
|
||
<h3 id="dom-roles">Roles y permisos</h3>
|
||
<div class="roles-grid">
|
||
<div class="role-card">
|
||
<h4>🛡️ Admin</h4>
|
||
<ul>
|
||
<li>Crear, editar y ver todos los domicilios</li>
|
||
<li>Asignar / reasignar enfermeros</li>
|
||
<li>Registrar pagos</li>
|
||
<li>Exportar Excel</li>
|
||
<li>Ver informe completo con notas</li>
|
||
<li>Ver agenda de cualquier enfermero con <code>?eid=X</code></li>
|
||
</ul>
|
||
</div>
|
||
<div class="role-card">
|
||
<h4>👩⚕️ Enfermero</h4>
|
||
<ul>
|
||
<li>Solo ve su propia agenda en <code>enfermero_portal.php</code></li>
|
||
<li>Avanza el estado de sus domicilios asignados</li>
|
||
<li>Agrega servicios extra</li>
|
||
<li>Registra notas clínicas y libres</li>
|
||
<li>Visualiza órdenes médicas adjuntas</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="callout warning">
|
||
<strong>⚠️ Redirección automática:</strong> si el usuario autenticado tiene rol <code>enfermero</code>,
|
||
<code>lab_domicilios.php</code> lo redirige inmediatamente a <code>enfermero_portal.php</code>.
|
||
</div>
|
||
|
||
<!-- FLUJO DE ESTADOS -->
|
||
<h3 id="dom-estados">Flujo de estados</h3>
|
||
<div class="flow">
|
||
<div class="flow-step s-prog">programado</div>
|
||
<div class="flow-arrow">→</div>
|
||
<div class="flow-step s-conf">confirmado</div>
|
||
<div class="flow-arrow">→</div>
|
||
<div class="flow-step s-cam">en_camino</div>
|
||
<div class="flow-arrow">→</div>
|
||
<div class="flow-step s-dom">en_domicilio</div>
|
||
<div class="flow-arrow">→</div>
|
||
<div class="flow-step s-comp">completado</div>
|
||
</div>
|
||
<div style="display:flex;gap:12px;margin-bottom:20px;flex-wrap:wrap">
|
||
<div class="flow-step s-canc" style="min-width:auto;padding:6px 14px;font-size:12px">⛔ cancelado</div>
|
||
<div class="flow-step s-repr" style="min-width:auto;padding:6px 14px;font-size:12px">🔄 reprogramado</div>
|
||
<span style="font-size:12px;color:var(--muted);align-self:center">← Posibles desde cualquier estado activo</span>
|
||
</div>
|
||
|
||
<p><strong>Notas automáticas según estado:</strong></p>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead><tr><th>Evento</th><th>Acción automática</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Transición a <code>en_domicilio</code></td><td>Se registra <code>hora_llegada</code> automáticamente</td></tr>
|
||
<tr><td>Transición a <code>completado</code></td><td>Se registra <code>hora_salida</code> automáticamente</td></tr>
|
||
<tr><td>Transición a <code>cancelado</code></td><td>Campo <code>motivo_cancelacion</code> obligatorio</td></tr>
|
||
<tr><td>Transición a <code>reprogramado</code></td><td>Campo <code>fecha_reprogramada</code> obligatorio</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<p><strong>Transiciones permitidas al enfermero</strong> (validadas en <code>update_domicilio_enfermero.php</code>):</p>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead><tr><th>Estado actual</th><th>Estados posibles</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><span class="badge badge-gray">programado</span></td><td><span class="badge badge-cyan">confirmado</span></td></tr>
|
||
<tr><td><span class="badge badge-cyan">confirmado</span></td><td><span class="badge badge-blue">en_camino</span> · <span class="badge badge-red">cancelado</span></td></tr>
|
||
<tr><td><span class="badge badge-blue">en_camino</span></td><td><span class="badge badge-orange">en_domicilio</span> · <span class="badge badge-red">cancelado</span></td></tr>
|
||
<tr><td><span class="badge badge-orange">en_domicilio</span></td><td><span class="badge badge-green">completado</span> · <span class="badge badge-red">cancelado</span></td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="callout">
|
||
El <strong>admin</strong> puede cambiar a cualquier estado directamente, incluyendo cancelar desde cualquier punto.
|
||
</div>
|
||
|
||
<!-- FUNCIONALIDADES -->
|
||
<h3 id="dom-funciones">Funcionalidades</h3>
|
||
<div class="features">
|
||
<div class="feature-item"><strong>🔍 Filtros</strong>Por fecha, estado y enfermero. Botón "Hoy" para filtro rápido.</div>
|
||
<div class="feature-item"><strong>📊 Resumen del día</strong>Conteo de domicilios por estado en la parte superior.</div>
|
||
<div class="feature-item"><strong>📋 Panel de detalle</strong>Abre al hacer clic: info completa, notas clínicas e informe imprimible.</div>
|
||
<div class="feature-item"><strong>👥 Asignar enfermero</strong>Modal con lista del personal disponible. Incluye reasignación.</div>
|
||
<div class="feature-item"><strong>⚠️ Sin asignar</strong>Badge con conteo de domicilios sin enfermero asignado.</div>
|
||
<div class="feature-item"><strong>💰 Registrar pago</strong>Modal para marcar cobro con modalidad y monto.</div>
|
||
<div class="feature-item"><strong>➕ Servicios extra</strong>El enfermero los agrega desde su portal durante la visita.</div>
|
||
<div class="feature-item"><strong>🩺 Notas del enfermero</strong>Ficha clínica y notas libres con imagen adjunta.</div>
|
||
<div class="feature-item"><strong>📄 Informe imprimible</strong>Vista imprimible con datos del paciente, ficha clínica y notas.</div>
|
||
<div class="feature-item"><strong>📁 Exportar CSV / Excel</strong>Incluye columnas de notas del enfermero.</div>
|
||
<div class="feature-item"><strong>📝 Formularios recibidos</strong>Pestaña que muestra formularios enviados con filtros.</div>
|
||
<div class="feature-item"><strong>📱 Portal del enfermero</strong>Tarjetas colapsables, separadas en Activos y Finalizados.</div>
|
||
</div>
|
||
|
||
<!-- API -->
|
||
<h3 id="dom-api">API Endpoints</h3>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead><tr><th>Endpoint</th><th>Método</th><th>Descripción</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>api/lab/get_domicilios.php</code></td><td><span class="badge badge-blue">GET</span></td><td>Lista con filtros. <code>?id=X</code> para uno solo con detalle completo.</td></tr>
|
||
<tr><td><code>api/lab/save_domicilio.php</code></td><td><span class="badge badge-green">POST</span></td><td>Crear, editar o cambiar estado. Con <code>solo_estado=true</code> solo cambia el estado.</td></tr>
|
||
<tr><td><code>api/lab/update_domicilio_enfermero.php</code></td><td><span class="badge badge-green">POST</span></td><td>El enfermero avanza el estado de su domicilio.</td></tr>
|
||
<tr><td><code>api/lab/my_agenda.php</code></td><td><span class="badge badge-blue">GET</span></td><td>Agenda del enfermero autenticado con servicios extra.</td></tr>
|
||
<tr><td><code>api/lab/save_asignacion.php</code></td><td><span class="badge badge-green">POST</span></td><td>Asignar o reasignar enfermero a domicilio.</td></tr>
|
||
<tr><td><code>api/lab/get_asignaciones.php</code></td><td><span class="badge badge-blue">GET</span></td><td>Asignaciones por fecha.</td></tr>
|
||
<tr><td><code>api/lab/registrar_pago.php</code></td><td><span class="badge badge-green">POST</span></td><td>Registrar pago con monto y modalidad.</td></tr>
|
||
<tr><td><code>api/lab/save_servicio_extra.php</code></td><td><span class="badge badge-green">POST</span></td><td>Agregar servicio realizado durante la visita.</td></tr>
|
||
<tr><td><code>api/lab/get_notas_domicilio.php</code></td><td><span class="badge badge-blue">GET</span></td><td>Notas del domicilio (clínicas y libres).</td></tr>
|
||
<tr><td><code>api/lab/upload_nota_imagen.php</code></td><td><span class="badge badge-green">POST</span></td><td>Subir imagen adjunta a una nota libre.</td></tr>
|
||
<tr><td><code>api/lab/crear_desde_whatsapp.php</code></td><td><span class="badge badge-cyan">GET/POST</span></td><td>Crear paciente u orden desde una conversación de WhatsApp activa.</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
</div><!-- /module-body -->
|
||
</div><!-- /module -->
|
||
|
||
|
||
<!-- ╔═══════════════════════════════════════════╗
|
||
║ MÓDULO 2 — FORMULARIOS ║
|
||
╚═══════════════════════════════════════════╝ -->
|
||
<div class="module" id="form-intro">
|
||
<div class="module-header">
|
||
<div class="icon icon-violet">📝</div>
|
||
<div>
|
||
<h2>Módulo 2 — Formularios</h2>
|
||
<p>Builder visual, envío por WhatsApp, firma digital y sello SHA-256</p>
|
||
</div>
|
||
</div>
|
||
<div class="module-body">
|
||
|
||
<h3>¿Qué es?</h3>
|
||
<p>
|
||
El módulo de formularios permite crear <strong>plantillas de documentos</strong>
|
||
(consentimientos, historias clínicas, autorizaciones, encuestas) mediante un builder visual,
|
||
enviarlas a los pacientes por WhatsApp y recopilar sus respuestas con firma digital.
|
||
El documento firmado genera un <strong>sello de integridad SHA-256</strong> que puede verificarse públicamente.
|
||
</p>
|
||
|
||
<!-- ARCHIVOS -->
|
||
<h3 id="form-archivos">Archivos involucrados</h3>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead><tr><th>Archivo</th><th>Descripción</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>lab_formularios.php</code></td><td>Vista principal — lista de plantillas y registro de envíos</td></tr>
|
||
<tr><td><code>lab_formulario_builder.php</code></td><td>Editor visual drag & drop (ventana separada, solo admin)</td></tr>
|
||
<tr><td><code>form_cliente.php</code></td><td>Página pública — el paciente llena y firma sin iniciar sesión</td></tr>
|
||
<tr><td><code>ver_formulario_enviado.php</code></td><td>Vista del documento firmado — acceso por ID (admin/enfermero) o token (cliente)</td></tr>
|
||
<tr><td><code>verificar_formulario.php</code></td><td>Verificación pública de autenticidad por hash SHA-256</td></tr>
|
||
<tr><td><code>classes/lab/Formulario.php</code></td><td>Clase ORM — CRUD de plantillas, crear envíos, guardar respuestas, generar hash</td></tr>
|
||
<tr><td><code>api/lab/get_formularios.php</code></td><td>GET — lista plantillas o envíos</td></tr>
|
||
<tr><td><code>api/lab/save_formulario.php</code></td><td>POST — crear, editar y eliminar plantillas (solo admin)</td></tr>
|
||
<tr><td><code>api/lab/send_formulario.php</code></td><td>POST — crear instancia de envío, devolver URL pública y mensaje WhatsApp</td></tr>
|
||
<tr><td><code>api/lab/submit_formulario.php</code></td><td>GET/POST — cargar el formulario por token / guardar la respuesta del cliente</td></tr>
|
||
<tr><td><code>api/lab/firmar_profesional.php</code></td><td>POST — guardar firma del profesional (requiere sesión activa)</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- BASE DE DATOS -->
|
||
<h3 id="form-bd">Base de datos</h3>
|
||
|
||
<h4 id="form-bd-plantillas">Tabla <code>lab_formularios</code></h4>
|
||
<p>Plantillas de documentos creadas desde el builder.</p>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead><tr><th>Campo</th><th>Tipo</th><th>Descripción</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>id</code></td><td><span class="badge badge-blue">INT PK</span></td><td>Identificador único</td></tr>
|
||
<tr><td><code>nombre</code></td><td>VARCHAR(150)</td><td>Nombre de la plantilla</td></tr>
|
||
<tr><td><code>descripcion</code></td><td>TEXT</td><td>Descripción visible al cliente</td></tr>
|
||
<tr><td><code>categoria</code></td><td>ENUM</td><td><code>consentimiento</code> / <code>historia_clinica</code> / <code>autorizacion</code> / <code>encuesta</code> / <code>otro</code></td></tr>
|
||
<tr><td><code>esquema</code></td><td>LONGTEXT</td><td>JSON con el array de campos del formulario</td></tr>
|
||
<tr><td><code>permite_firma</code></td><td>TINYINT(1)</td><td>El formulario tiene sección de firma global</td></tr>
|
||
<tr><td><code>requiere_firma</code></td><td>TINYINT(1)</td><td>La firma global es obligatoria</td></tr>
|
||
<tr><td><code>firma_modos</code></td><td>VARCHAR(50)</td><td><code>canvas</code>, <code>foto</code> o <code>canvas,foto</code> (separados por coma)</td></tr>
|
||
<tr><td><code>version</code></td><td>SMALLINT</td><td>Se incrementa automáticamente al editar el esquema</td></tr>
|
||
<tr><td><code>is_active</code></td><td>TINYINT(1)</td><td>Soft-delete</td></tr>
|
||
<tr><td><code>creado_por</code></td><td><span class="badge badge-orange">INT FK</span></td><td>Usuario que creó la plantilla</td></tr>
|
||
<tr><td><code>doc_encabezado</code></td><td>VARCHAR</td><td>Override del nombre de empresa en el documento</td></tr>
|
||
<tr><td><code>doc_subtitulo</code></td><td>VARCHAR</td><td>Override del subtítulo en el documento</td></tr>
|
||
<tr><td><code>doc_logo_base64</code></td><td>LONGTEXT</td><td>Override del logo en el documento</td></tr>
|
||
<tr><td><code>doc_color</code></td><td>VARCHAR(20)</td><td>Override del color del encabezado del documento</td></tr>
|
||
<tr><td><code>doc_pie_pagina</code></td><td>TEXT</td><td>Override del pie de página</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h4 id="form-bd-envios">Tabla <code>lab_form_envios</code></h4>
|
||
<p>Cada fila es una instancia enviada a un paciente.</p>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead><tr><th>Campo</th><th>Tipo</th><th>Descripción</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>id</code></td><td><span class="badge badge-blue">INT PK</span></td><td>Identificador único</td></tr>
|
||
<tr><td><code>formulario_id</code></td><td><span class="badge badge-orange">INT FK</span></td><td>Plantilla enviada</td></tr>
|
||
<tr><td><code>paciente_id</code></td><td><span class="badge badge-gray">INT FK NULL</span></td><td>Paciente asociado</td></tr>
|
||
<tr><td><code>domicilio_id</code></td><td><span class="badge badge-gray">INT FK NULL</span></td><td>Domicilio asociado (opcional)</td></tr>
|
||
<tr><td><code>token</code></td><td>CHAR(64) UNIQUE</td><td>Token público — 64 chars hex (acceso sin sesión)</td></tr>
|
||
<tr><td><code>datos_prefilled</code></td><td>LONGTEXT</td><td>JSON con datos pre-llenados al enviar (incluye <code>__paciente.*</code>)</td></tr>
|
||
<tr><td><code>datos_cliente</code></td><td>LONGTEXT</td><td>JSON con las respuestas completadas por el cliente</td></tr>
|
||
<tr><td><code>firma_svg</code></td><td>LONGTEXT</td><td>Firma del paciente (PNG base64 — canvas o foto)</td></tr>
|
||
<tr><td><code>ip_cliente</code></td><td>VARCHAR(45)</td><td>IP del cliente al enviar el formulario</td></tr>
|
||
<tr><td><code>user_agent</code></td><td>VARCHAR(512)</td><td>Navegador del cliente</td></tr>
|
||
<tr><td><code>estado</code></td><td>ENUM</td><td><code>pendiente</code> / <code>completado</code> / <code>firmado</code> / <code>expirado</code></td></tr>
|
||
<tr><td><code>enviado_por</code></td><td><span class="badge badge-orange">INT FK</span></td><td>Usuario que generó el enlace</td></tr>
|
||
<tr><td><code>enviado_via</code></td><td>ENUM</td><td><code>whatsapp</code> / <code>email</code> / <code>link</code></td></tr>
|
||
<tr><td><code>expira_en</code></td><td>DATETIME NULL</td><td>Siempre <code>NULL</code> — el enlace no expira</td></tr>
|
||
<tr><td><code>completado_en</code></td><td>DATETIME</td><td>Fecha y hora en que el cliente completó el formulario</td></tr>
|
||
<tr><td><code>hash_verificacion</code></td><td>CHAR(64)</td><td>Sello de integridad SHA-256 del documento</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- ROLES -->
|
||
<h3 id="form-roles">Roles y permisos</h3>
|
||
<div class="roles-grid">
|
||
<div class="role-card">
|
||
<h4>🛡️ Admin</h4>
|
||
<ul>
|
||
<li>Crear, editar y eliminar plantillas en el builder</li>
|
||
<li>Enviar formularios a cualquier paciente</li>
|
||
<li>Ver todos los envíos</li>
|
||
<li>Descargar PDF de cualquier formulario</li>
|
||
</ul>
|
||
</div>
|
||
<div class="role-card">
|
||
<h4>👩⚕️ Enfermero</h4>
|
||
<ul>
|
||
<li>Enviar formularios existentes a sus pacientes</li>
|
||
<li>Ver solo sus propios envíos (<code>enviado_por = su user_id</code>)</li>
|
||
<li>No puede crear ni editar plantillas</li>
|
||
<li>Puede firmar como profesional en formularios que él mismo envió</li>
|
||
</ul>
|
||
</div>
|
||
<div class="role-card">
|
||
<h4>🌐 Cliente (público)</h4>
|
||
<ul>
|
||
<li>Accede a <code>form_cliente.php?t=TOKEN</code> sin autenticación</li>
|
||
<li>Llena y firma el formulario</li>
|
||
<li>Puede volver al mismo enlace para ver el documento firmado</li>
|
||
<li>Puede descargarlo como PDF</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- FLUJO COMPLETO -->
|
||
<h3 id="form-flujo">Flujo completo</h3>
|
||
<ol class="steps">
|
||
<li>
|
||
<div>
|
||
<strong>Admin crea la plantilla</strong><br>
|
||
Abre <code>lab_formulario_builder.php</code> (ventana nueva), arrastra campos al canvas,
|
||
configura diseño del documento (logo, color, encabezado, pie de página)
|
||
y guarda → POST <code>api/lab/save_formulario.php</code>.
|
||
</div>
|
||
</li>
|
||
<li>
|
||
<div>
|
||
<strong>Admin o Enfermero envía el formulario</strong><br>
|
||
Desde <code>lab_formularios.php</code> → botón "Enviar" → selecciona paciente →
|
||
elige canal (WhatsApp o solo link) → POST <code>api/lab/send_formulario.php</code>.
|
||
Se genera un token de 64 chars (<code>bin2hex(random_bytes(32))</code>)
|
||
y se crea la fila en <code>lab_form_envios</code> con <code>estado = pendiente</code>.
|
||
</div>
|
||
</li>
|
||
<li>
|
||
<div>
|
||
<strong>Cliente recibe el enlace</strong><br>
|
||
Abre <code>form_cliente.php?t=TOKEN</code> → GET <code>api/lab/submit_formulario.php?t=TOKEN</code>
|
||
carga los datos. Si ya fue firmado → muestra pantalla de solo lectura con link al PDF.
|
||
</div>
|
||
</li>
|
||
<li>
|
||
<div>
|
||
<strong>Cliente llena el formulario</strong><br>
|
||
Campos "linked" llegan pre-llenados con datos del paciente (readonly si tienen valor, editables si están vacíos).
|
||
Los campos <code>firma_profesional</code> muestran aviso <em>"uso exclusivo del profesional"</em>.
|
||
El cliente dibuja su firma o adjunta una foto.
|
||
</div>
|
||
</li>
|
||
<li>
|
||
<div>
|
||
<strong>Cliente envía</strong><br>
|
||
POST <code>api/lab/submit_formulario.php</code> → se genera hash SHA-256
|
||
(contenido + firma + ID + token + timestamp) →
|
||
estado cambia a <code>firmado</code> (con firma) o <code>completado</code> (sin firma).
|
||
Pantalla de éxito con hash visible y botón para descargar el PDF.
|
||
</div>
|
||
</li>
|
||
<li>
|
||
<div>
|
||
<strong>Profesional firma (si el formulario lo requiere)</strong><br>
|
||
Admin/Enfermero abre <code>ver_formulario_enviado.php?id=X</code> con sesión activa,
|
||
dibuja su firma en el canvas del campo <code>firma_profesional</code>
|
||
→ POST <code>api/lab/firmar_profesional.php</code>.
|
||
</div>
|
||
</li>
|
||
<li>
|
||
<div>
|
||
<strong>Admin/Enfermero revisa el resultado</strong><br>
|
||
<code>lab_formularios.php</code> → pestaña "Envíos" → icono "Ver respuesta" →
|
||
<code>ver_formulario_enviado.php?id=X</code> — documento HTML imprimible.
|
||
</div>
|
||
</li>
|
||
<li>
|
||
<div>
|
||
<strong>Verificación pública de integridad</strong><br>
|
||
<code>verificar_formulario.php?h=HASH_SHA256</code> → busca en <code>hash_verificacion</code>
|
||
→ muestra nombre del formulario, paciente, fecha, estado y si el sello es válido.
|
||
</div>
|
||
</li>
|
||
</ol>
|
||
|
||
<!-- TIPOS DE CAMPOS -->
|
||
<h3 id="form-campos">Tipos de campos del Builder</h3>
|
||
|
||
<h4>Campos de entrada</h4>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead><tr><th>Tipo</th><th>Descripción</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>texto</code></td><td>Campo de texto corto de una sola línea</td></tr>
|
||
<tr><td><code>textarea</code></td><td>Área de texto largo (varias líneas)</td></tr>
|
||
<tr><td><code>numero</code></td><td>Campo numérico</td></tr>
|
||
<tr><td><code>fecha</code></td><td>Selector de fecha</td></tr>
|
||
<tr><td><code>hora</code></td><td>Selector de hora</td></tr>
|
||
<tr><td><code>select</code></td><td>Lista desplegable con opciones configurables</td></tr>
|
||
<tr><td><code>radio</code></td><td>Selección única con opciones configurables</td></tr>
|
||
<tr><td><code>checkbox</code></td><td>Selección múltiple con opciones configurables</td></tr>
|
||
<tr><td><code>lista_marcable</code></td><td>Lista de ítems numerados con checkboxes</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h4>Campos de firma</h4>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead><tr><th>Tipo</th><th>Descripción</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>firma</code></td><td>Firma del <strong>paciente</strong> — visible y editable en <code>form_cliente.php</code></td></tr>
|
||
<tr><td><code>firma_profesional</code></td><td>Firma del <strong>profesional</strong> — bloqueada para el cliente; solo editable desde el panel admin/enfermero</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h4>Campos de contenido</h4>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead><tr><th>Tipo</th><th>Descripción</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>separador</code></td><td>Separador visual o título de sección</td></tr>
|
||
<tr><td><code>parrafo</code></td><td>Bloque de texto estático (pre-formatado o flujo libre)</td></tr>
|
||
<tr><td><code>parrafo_inline</code></td><td>Párrafo con marcadores <code>{nombre_completo}</code>, <code>{telefono}</code>, etc. que se convierten en espacios editables si el valor está vacío</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h4>Campos vinculados al paciente (<code>tipo: linked</code>)</h4>
|
||
<p>Se auto-rellenan con los datos del paciente al momento de enviar. Si el valor existe → campo de solo lectura. Si está vacío → el cliente puede completarlo.</p>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead><tr><th><code>linked_key</code></th><th>Dato que extrae</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>nombre_completo</code></td><td>Nombre completo del paciente</td></tr>
|
||
<tr><td><code>numero_documento</code></td><td>Número de documento</td></tr>
|
||
<tr><td><code>tipo_documento</code></td><td>Tipo de documento</td></tr>
|
||
<tr><td><code>fecha_nacimiento</code></td><td>Fecha de nacimiento</td></tr>
|
||
<tr><td><code>telefono</code></td><td>Teléfono</td></tr>
|
||
<tr><td><code>email</code></td><td>Correo electrónico</td></tr>
|
||
<tr><td><code>eps</code></td><td>EPS o aseguradora</td></tr>
|
||
<tr><td><code>direccion</code></td><td>Dirección</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- FIRMA DIGITAL -->
|
||
<h3 id="form-firma-dig">Firma digital</h3>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead><tr><th>Modo</th><th>Funcionamiento</th></tr></thead>
|
||
<tbody>
|
||
<tr>
|
||
<td><span class="badge badge-violet">canvas</span></td>
|
||
<td>El cliente dibuja su firma con el dedo o el mouse. Se captura con <code>canvas.toDataURL('image/png')</code>.</td>
|
||
</tr>
|
||
<tr>
|
||
<td><span class="badge badge-orange">foto</span></td>
|
||
<td>El cliente sube una imagen desde su cámara o galería (<code><input accept="image/*" capture="environment"></code>). Se convierte a base64 con <code>FileReader</code>.</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="callout">
|
||
Ambos modos pueden estar activos simultáneamente en la misma plantilla configurando <code>firma_modos = "canvas,foto"</code>.
|
||
</div>
|
||
<p>
|
||
<strong>Firma global vs. firma por campo:</strong><br>
|
||
Si el esquema <strong>no incluye</strong> campos tipo <code>firma</code>, se muestra una sección de firma global al pie del formulario.
|
||
Si el esquema <strong>incluye</strong> campos <code>firma</code>, cada uno tiene su propio widget canvas en la posición configurada.
|
||
</p>
|
||
|
||
<!-- ENLACE Y VIGENCIA -->
|
||
<h3 id="form-enlace">Enlace público y vigencia</h3>
|
||
<div class="features">
|
||
<div class="feature-item"><strong>🔗 URL pública</strong><code>form_cliente.php?t=TOKEN</code></div>
|
||
<div class="feature-item"><strong>🔑 Token</strong>64 chars hex — <code>bin2hex(random_bytes(32))</code></div>
|
||
<div class="feature-item"><strong>🔓 Sin sesión</strong>No requiere cuenta ni iniciar sesión</div>
|
||
<div class="feature-item"><strong>♾️ Sin vencimiento</strong><code>expira_en</code> siempre es <code>NULL</code></div>
|
||
<div class="feature-item"><strong>🔒 Bloqueo por estado</strong>Si ya fue firmado → solo lectura, no permite modificar</div>
|
||
<div class="feature-item"><strong>🔁 Idempotencia</strong>Si el cliente reintenta enviar, devuelve éxito sin duplicar</div>
|
||
</div>
|
||
|
||
<!-- PDF -->
|
||
<h3 id="form-pdf">PDF y visualización del documento</h3>
|
||
<div class="callout success">
|
||
<strong>✅ Sin librerías de PDF:</strong> el documento es la página <code>ver_formulario_enviado.php</code>
|
||
con estilos <code>@media print</code>. El usuario imprime o guarda como PDF directamente desde el navegador.
|
||
</div>
|
||
<p><strong>Contenido del documento impreso:</strong></p>
|
||
<ul style="padding-left:20px;font-size:13.5px;margin-bottom:16px;line-height:2">
|
||
<li>Encabezado con logo, nombre, subtítulo, datos de contacto y color corporativo</li>
|
||
<li>Datos del paciente (nombre, documento, fecha de nacimiento, teléfono, EPS)</li>
|
||
<li>Respuestas del formulario campo por campo en el orden del esquema</li>
|
||
<li>Imagen de la firma del paciente</li>
|
||
<li>Firma del profesional (si fue completada)</li>
|
||
<li>Sello SHA-256 con link para verificar autenticidad</li>
|
||
<li>Pie de página con fecha de generación e ID del documento</li>
|
||
</ul>
|
||
|
||
<p><strong>Formas de acceder al documento:</strong></p>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead><tr><th>URL</th><th>Quién puede acceder</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>ver_formulario_enviado.php?id=X</code></td><td>Admin (cualquier formulario) o Enfermero (solo los que él envió). Requiere sesión.</td></tr>
|
||
<tr><td><code>ver_formulario_enviado.php?t=TOKEN</code></td><td>Cliente u cualquier persona con el enlace. Sin sesión. Solo si el estado es <code>firmado</code> o <code>completado</code>.</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- SHA-256 -->
|
||
<h3 id="form-sha">Sello de integridad SHA-256</h3>
|
||
<p>Al guardar la respuesta del cliente, el sistema genera un hash SHA-256 que vincula de forma única el contenido del formulario con la firma y el momento en que se completó.</p>
|
||
<p><strong>Construcción del hash</strong> (en <code>Formulario::guardarRespuesta()</code>):</p>
|
||
<pre><code>SHA-256 de:
|
||
JSON de los datos del cliente
|
||
+ firma SVG/PNG del paciente
|
||
+ ID interno del envío
|
||
+ token del enlace
|
||
+ timestamp del momento de registro</code></pre>
|
||
<div class="callout">
|
||
<strong>¿Para qué sirve?</strong> Cualquier persona con el hash puede ir a
|
||
<code>verificar_formulario.php?h=HASH</code> para confirmar que el documento existe,
|
||
el nombre del formulario y del paciente, la fecha en que fue completado y el estado actual.
|
||
Si el documento fue alterado, el hash no coincidirá y la verificación fallará.
|
||
</div>
|
||
|
||
<!-- FIRMA DEL PROFESIONAL -->
|
||
<h3 id="form-firmapro">Firma del profesional</h3>
|
||
<ol class="steps">
|
||
<li>
|
||
<div>Al diseñar la plantilla en el builder se agrega un campo <code>tipo: firma_profesional</code> en la posición deseada.</div>
|
||
</li>
|
||
<li>
|
||
<div>Cuando el cliente llena el formulario, ese campo muestra solo un aviso: <em>"Uso exclusivo del profesional de salud"</em>. El cliente no puede interactuar con él.</div>
|
||
</li>
|
||
<li>
|
||
<div>Una vez que el cliente ha completado y enviado, el admin o enfermero abre <code>ver_formulario_enviado.php?id=X</code> con sesión activa y verá el canvas de firma en esa posición.</div>
|
||
</li>
|
||
<li>
|
||
<div>El profesional dibuja su firma y hace clic en "Guardar firma" → POST <code>api/lab/firmar_profesional.php</code>.</div>
|
||
</li>
|
||
</ol>
|
||
|
||
<p><strong>Validaciones en el servidor:</strong></p>
|
||
<ul style="padding-left:20px;font-size:13.5px;margin-bottom:16px;line-height:2">
|
||
<li>Requiere sesión activa (<code>isUserLoggedIn()</code>)</li>
|
||
<li>Si el usuario es enfermero, solo puede firmar en formularios que él mismo envió</li>
|
||
<li>Valida que el <code>campo_id</code> corresponde a un campo <code>tipo: firma_profesional</code> en el esquema</li>
|
||
<li>Valida que la imagen enviada sea un data URI de imagen válido (prevención XSS/inyección)</li>
|
||
</ul>
|
||
<div class="callout warning">
|
||
Si el formulario se accede via <code>?t=TOKEN</code> (cliente público), el canvas <strong>no aparece</strong>.
|
||
En su lugar se muestra un aviso <em>"Pendiente de firma del profesional"</em>
|
||
(solo visible en pantalla, no en el PDF impreso).
|
||
</div>
|
||
|
||
<!-- API -->
|
||
<h3 id="form-api">API Endpoints</h3>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead><tr><th>Endpoint</th><th>Método</th><th>Autenticación</th><th>Descripción</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>api/lab/get_formularios.php</code></td><td><span class="badge badge-blue">GET</span></td><td><span class="badge badge-orange">Sesión</span></td><td>Lista plantillas. <code>?id=X</code> para una sola. <code>?envios=1</code> para lista de envíos.</td></tr>
|
||
<tr><td><code>api/lab/save_formulario.php</code></td><td><span class="badge badge-green">POST</span></td><td><span class="badge badge-red">Admin</span></td><td>Crear, editar o eliminar una plantilla.</td></tr>
|
||
<tr><td><code>api/lab/send_formulario.php</code></td><td><span class="badge badge-green">POST</span></td><td><span class="badge badge-orange">Sesión</span></td><td>Crear instancia de envío. Devuelve URL pública y mensaje para WhatsApp.</td></tr>
|
||
<tr><td><code>api/lab/submit_formulario.php</code></td><td><span class="badge badge-blue">GET</span></td><td><span class="badge badge-green">Público</span></td><td>Cargar el formulario por token (sin sesión).</td></tr>
|
||
<tr><td><code>api/lab/submit_formulario.php</code></td><td><span class="badge badge-green">POST</span></td><td><span class="badge badge-green">Público</span></td><td>Guardar la respuesta y firma del cliente.</td></tr>
|
||
<tr><td><code>api/lab/firmar_profesional.php</code></td><td><span class="badge badge-green">POST</span></td><td><span class="badge badge-orange">Sesión</span></td><td>Guardar la firma del profesional en un campo <code>firma_profesional</code>.</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
</div><!-- /module-body -->
|
||
</div><!-- /module -->
|
||
|
||
<p style="text-align:center;color:var(--muted);font-size:12px;margin-top:8px;padding-bottom:32px">
|
||
Documentación generada para uso interno del equipo · 30/03/2026
|
||
</p>
|
||
|
||
<!-- ╔═══════════════════════════════════════════╗
|
||
║ MANUAL DE USO ║
|
||
╚═══════════════════════════════════════════╝ -->
|
||
|
||
<!-- ── ADMIN: DOMICILIOS ── -->
|
||
<div class="module" id="man-admin">
|
||
<div class="module-header">
|
||
<div class="icon icon-blue">🛡️</div>
|
||
<div>
|
||
<h2>Manual de Uso — Administrador · Domicilios</h2>
|
||
<p><span class="role-badge rb-admin">🛡️ Admin</span> Gestión completa desde <code>lab_domicilios.php</code></p>
|
||
</div>
|
||
</div>
|
||
<div class="module-body">
|
||
|
||
<!-- Crear domicilio -->
|
||
<h3 id="man-admin-crear">Crear un nuevo domicilio</h3>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">1</div>
|
||
<div class="man-step-body">
|
||
<h5>Abrir el formulario</h5>
|
||
<p>Haz clic en el botón <span class="man-ui-tag">+ Nuevo Domicilio</span> ubicado en la esquina superior derecha de la pantalla. Se abrirá un modal con el formulario de creación.</p>
|
||
</div>
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">2</div>
|
||
<div class="man-step-body">
|
||
<h5>Buscar y seleccionar el paciente</h5>
|
||
<p>En el campo <em>Paciente</em>, empieza a escribir el nombre o documento del paciente. Aparecerá un listado de sugerencias; haz clic en el paciente correcto para seleccionarlo.</p>
|
||
<div class="man-tip">✅ El paciente debe existir previamente en el sistema. Si no aparece, créalo primero desde <code>lab_pacientes.php</code>.</div>
|
||
</div>
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">3</div>
|
||
<div class="man-step-body">
|
||
<h5>Llenar los datos del servicio</h5>
|
||
<p>Completa los campos requeridos (<span style="color:var(--danger)">*</span>):</p>
|
||
<ul style="padding-left:18px;font-size:13.5px;line-height:2">
|
||
<li><strong>Dirección completa</strong> — incluye barrio e indicaciones adicionales si aplica</li>
|
||
<li><strong>Fecha programada</strong> — se pre-llena con la fecha del filtro activo</li>
|
||
<li><strong>Tipo de servicio</strong> — ej: "toma de muestras", "inyección"</li>
|
||
<li><strong>Tipo de cliente</strong> — si estás en <em>Seguro / EPS</em>, aparecerá el campo del nombre del seguro</li>
|
||
<li><strong>Valores de cobro</strong> — valor domicilio y/o copago (dejar vacío si no aplica)</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">4</div>
|
||
<div class="man-step-body">
|
||
<h5>Adjuntar orden médica (opcional)</h5>
|
||
<p>Si tienes una imagen o PDF con la orden del médico, haz clic en <span class="man-ui-tag">📎 Adjuntar orden</span>. Se acepta JPG, PNG, PDF y DOC hasta 10 MB.</p>
|
||
</div>
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">5</div>
|
||
<div class="man-step-body">
|
||
<h5>Guardar</h5>
|
||
<p>Haz clic en <span class="man-ui-tag">💾 Guardar</span>. El domicilio aparecerá en la tabla con estado <span class="state-pill sp-prog">programado</span>.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Asignar enfermero -->
|
||
<h3 id="man-admin-asignar">Asignar un enfermero</h3>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">1</div>
|
||
<div class="man-step-body">
|
||
<h5>Seleccionar el domicilio</h5>
|
||
<p>Haz clic en la fila del domicilio en la tabla. Se abrirá el panel de detalle a la derecha. Si el domicilio no tiene enfermero, verás un aviso en naranja: <em>"Sin enfermero asignado"</em>.</p>
|
||
</div>
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">2</div>
|
||
<div class="man-step-body">
|
||
<h5>Abrir el modal de asignación</h5>
|
||
<p>Haz clic en <span class="man-ui-tag">👤 Asignar</span> (o <span class="man-ui-tag">Reasignar</span> si ya tiene uno). Se abrirá un modal con la lista del personal disponible.</p>
|
||
</div>
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">3</div>
|
||
<div class="man-step-body">
|
||
<h5>Seleccionar y confirmar</h5>
|
||
<p>En el desplegable elige el enfermero. Verás cuántos domicilios tiene asignados hoy y su usuario de acceso al portal. Haz clic en <span class="man-ui-tag">✔ Confirmar asignación</span>.</p>
|
||
<div class="man-tip">✅ Una vez asignado, el enfermero verá este domicilio en su portal al ingresar.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Cambiar estado -->
|
||
<h3 id="man-admin-estado">Cambiar el estado de un domicilio</h3>
|
||
<p>Los botones de avance de estado aparecen en el panel de detalle del domicilio seleccionado:</p>
|
||
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:16px;align-items:center">
|
||
<span class="state-pill sp-prog">programado</span>
|
||
<span style="color:var(--muted)">→</span>
|
||
<span class="state-pill sp-conf">Confirmar</span>
|
||
<span style="color:var(--muted)">→</span>
|
||
<span class="state-pill sp-cam">En camino</span>
|
||
<span style="color:var(--muted)">→</span>
|
||
<span class="state-pill sp-dom">En domicilio</span>
|
||
<span style="color:var(--muted)">→</span>
|
||
<span class="state-pill sp-comp">Completar</span>
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">1</div>
|
||
<div class="man-step-body">
|
||
<h5>Seleccionar el domicilio y hacer clic en el botón de estado</h5>
|
||
<p>En el panel de detalle aparecerán uno o dos botones según el estado actual. Haz clic en el que corresponda y confirma en el diálogo que aparece.</p>
|
||
</div>
|
||
</div>
|
||
<div class="man-warn" style="margin-bottom:16px">
|
||
⚠️ Al cambiar a <span class="state-pill sp-canc">cancelado</span> el sistema pedirá un motivo de cancelación. Este campo es obligatorio.
|
||
</div>
|
||
<div class="man-tip" style="margin-bottom:0">
|
||
✅ Los estados <strong>en_domicilio</strong> (hora llegada) y <strong>completado</strong> (hora salida) registran la hora automáticamente si el enfermero realiza la transición desde su portal.
|
||
</div>
|
||
|
||
<!-- Registrar pago -->
|
||
<h3 id="man-admin-pago">Registrar un pago</h3>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">1</div>
|
||
<div class="man-step-body">
|
||
<h5>Domicilio con valores de cobro</h5>
|
||
<p>En el panel de detalle, si el domicilio tiene <em>Valor domicilio</em> o <em>Copago</em> asignados, verás la sección de cobro con un badge <span class="state-pill sp-prog">⏳ Pago pendiente</span>.</p>
|
||
</div>
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">2</div>
|
||
<div class="man-step-body">
|
||
<h5>Abrir el registro de pago</h5>
|
||
<p>Haz clic en el botón <span class="man-ui-tag">💳 Registrar pago</span> dentro del bloque de cobro.</p>
|
||
</div>
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">3</div>
|
||
<div class="man-step-body">
|
||
<h5>Completar los datos del pago</h5>
|
||
<p>Selecciona la <strong>modalidad</strong> (💵 Efectivo / 📲 Transferencia / 📋 Otro), ingresa el <strong>monto recibido</strong> y agrega notas si es necesario. Haz clic en <span class="man-ui-tag">Confirmar pago</span>.</p>
|
||
<div class="man-tip">✅ Si el servicio es para un cliente con seguro y no genera cobro directo, selecciona <strong>Exento</strong> desde las opciones de pago_estado.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Filtros y exportar -->
|
||
<h3 id="man-admin-filtros">Filtros y exportar a Excel</h3>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead><tr><th>Control</th><th>Descripción</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><span class="man-ui-tag">📅 Fecha</span></td><td>Filtra los domicilios por fecha programada. Se carga con la fecha actual al abrir la pantalla.</td></tr>
|
||
<tr><td><span class="man-ui-tag">Estado</span></td><td>Filtra por estado del domicilio. Deja en blanco para ver todos.</td></tr>
|
||
<tr><td><span class="man-ui-tag">Enfermero</span></td><td>Filtra solo los domicilios asignados a ese enfermero.</td></tr>
|
||
<tr><td><span class="man-ui-tag">📅 Hoy</span></td><td>Restablece el filtro de fecha a la fecha de hoy.</td></tr>
|
||
<tr><td><span class="man-ui-tag">📁 Excel</span></td><td>Exporta todos los domicilios que coincidan con los filtros activos, incluyendo notas del enfermero.</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="man-tip">✅ Los cambios en los filtros se aplican automáticamente sin necesidad de hacer clic en un botón de buscar.</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── ENFERMERO: PORTAL ── -->
|
||
<div class="module" id="man-enf">
|
||
<div class="module-header">
|
||
<div class="icon icon-green">👩⚕️</div>
|
||
<div>
|
||
<h2>Manual de Uso — Enfermero · Portal</h2>
|
||
<p><span class="role-badge rb-enf">👩⚕️ Enfermero</span> Gestión de agenda personal desde <code>enfermero_portal.php</code></p>
|
||
</div>
|
||
</div>
|
||
<div class="module-body">
|
||
|
||
<!-- Acceder -->
|
||
<h3 id="man-enf-acceso">Acceder al portal</h3>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">1</div>
|
||
<div class="man-step-body">
|
||
<h5>Iniciar sesión</h5>
|
||
<p>Ve a <code>login.php</code> e ingresa con el usuario y contraseña que el administrador te asignó. El sistema te redirigirá automáticamente a <code>enfermero_portal.php</code>.</p>
|
||
<div class="man-tip">✅ Si intentas acceder a otra sección del sistema, serás redirigido de vuelta a tu portal automáticamente.</div>
|
||
</div>
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">2</div>
|
||
<div class="man-step-body">
|
||
<h5>Ver tu agenda del día</h5>
|
||
<p>Encontrarás dos bloques: <strong>Activos</strong> (domicilios pendientes de hoy) y <strong>Finalizados</strong> (completados o cancelados). Cada domicilio aparece como una tarjeta colapsable ordenada por hora.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Avanzar estado -->
|
||
<h3 id="man-enf-avanzar">Avanzar el estado de un domicilio</h3>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">1</div>
|
||
<div class="man-step-body">
|
||
<h5>Expandir la tarjeta</h5>
|
||
<p>Haz clic en la tarjeta del domicilio para expandirla y ver el detalle completo: paciente, dirección, hora, orden médica adjunta y botones de acción.</p>
|
||
</div>
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">2</div>
|
||
<div class="man-step-body">
|
||
<h5>Usar el botón de estado correspondiente</h5>
|
||
<div class="table-wrap" style="margin-top:8px">
|
||
<table>
|
||
<thead><tr><th>Estado actual</th><th>Botón disponible</th><th>¿Qué ocurre?</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><span class="state-pill sp-prog">programado</span></td><td><span class="man-ui-tag">✔ Confirmar</span></td><td>Confirmas que realizarás la visita</td></tr>
|
||
<tr><td><span class="state-pill sp-conf">confirmado</span></td><td><span class="man-ui-tag">🚗 En camino</span></td><td>Indicas que saliste hacia el domicilio</td></tr>
|
||
<tr><td><span class="state-pill sp-cam">en_camino</span></td><td><span class="man-ui-tag">🏠 En domicilio</span></td><td>Indicas que llegaste — se registra la <strong>hora de llegada</strong> automáticamente</td></tr>
|
||
<tr><td><span class="state-pill sp-dom">en_domicilio</span></td><td><span class="man-ui-tag">✅ Completar</span></td><td>Finalizas la visita — se registra la <strong>hora de salida</strong> automáticamente</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="man-warn">
|
||
⚠️ Si necesitas cancelar un domicilio, deberás ingresar el motivo. Consulta con el administrador si tienes dudas sobre cuándo cancelar.
|
||
</div>
|
||
|
||
<!-- Registrar notas -->
|
||
<h3 id="man-enf-notas">Registrar notas clínicas y libres</h3>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">1</div>
|
||
<div class="man-step-body">
|
||
<h5>Ficha clínica del paciente</h5>
|
||
<p>Dentro de la tarjeta del domicilio, busca la sección <strong>Ficha clínica</strong>. Puedes registrar:</p>
|
||
<ul style="padding-left:18px;font-size:13.5px;line-height:2">
|
||
<li><strong>Antecedentes</strong> — patologías previas del paciente</li>
|
||
<li><strong>Medicamentos</strong> — medicamentos que toma actualmente</li>
|
||
<li><strong>Acudiente</strong> — nombre y documento del acudiente si el paciente es menor de edad</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">2</div>
|
||
<div class="man-step-body">
|
||
<h5>Nota libre</h5>
|
||
<p>Haz clic en <span class="man-ui-tag">+ Agregar nota</span>. Puedes escribir un título, el cuerpo de la nota y adjuntar una imagen (por ejemplo, una foto del estado del paciente o de las muestras tomadas).</p>
|
||
<div class="man-tip">✅ Las notas quedan visibles para el administrador en el panel de detalle y en el informe imprimible del domicilio.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Servicios extra -->
|
||
<h3 id="man-enf-extras">Agregar servicios extra</h3>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">1</div>
|
||
<div class="man-step-body">
|
||
<h5>¿Cuándo usar esto?</h5>
|
||
<p>Si durante la visita realizas servicios adicionales a los de la orden original (ej: una inyección, una curación, toma de tensión arterial), puedes registrarlos para que queden en el historial.</p>
|
||
</div>
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">2</div>
|
||
<div class="man-step-body">
|
||
<h5>Agregar el servicio</h5>
|
||
<p>En la tarjeta del domicilio, haz clic en <span class="man-ui-tag">+ Servicio extra</span>. Selecciona el tipo, agrega una descripción y, si genera cobro adicional, marca la opción <em>Requiere pago</em> e ingresa el valor.</p>
|
||
</div>
|
||
</div>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead><tr><th>Tipo disponible</th><th>Descripción</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>inyeccion</code></td><td>Administración de medicamento inyectable</td></tr>
|
||
<tr><td><code>cura</code></td><td>Curación de herida o procedimiento de limpieza</td></tr>
|
||
<tr><td><code>nebulizacion</code></td><td>Terapia de nebulización</td></tr>
|
||
<tr><td><code>toma_muestra</code></td><td>Toma de muestra adicional</td></tr>
|
||
<tr><td><code>tension_arterial</code></td><td>Medición de tensión arterial</td></tr>
|
||
<tr><td><code>glucometria</code></td><td>Medición de glucosa en sangre</td></tr>
|
||
<tr><td><code>otro</code></td><td>Cualquier otro procedimiento (especificar en descripción)</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── ADMIN: FORMULARIOS ── -->
|
||
<div class="module" id="man-form">
|
||
<div class="module-header">
|
||
<div class="icon icon-violet">📋</div>
|
||
<div>
|
||
<h2>Manual de Uso — Administrador · Formularios</h2>
|
||
<p><span class="role-badge rb-admin">🛡️ Admin</span> <span class="role-badge rb-enf" style="margin-left:4px">👩⚕️ Enfermero (envío)</span> Desde <code>lab_formularios.php</code></p>
|
||
</div>
|
||
</div>
|
||
<div class="module-body">
|
||
|
||
<!-- Crear plantilla -->
|
||
<h3 id="man-form-crear">Crear una plantilla de formulario</h3>
|
||
<div class="callout" style="margin-bottom:16px">
|
||
<strong>Solo el Admin</strong> puede crear y editar plantillas. Los enfermeros solo pueden enviar formularios existentes.
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">1</div>
|
||
<div class="man-step-body">
|
||
<h5>Ir al builder</h5>
|
||
<p>Desde <code>lab_formularios.php</code>, haz clic en <span class="man-ui-tag">+ Nueva plantilla</span>. Se abrirá el editor <code>lab_formulario_builder.php</code> en una ventana nueva.</p>
|
||
</div>
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">2</div>
|
||
<div class="man-step-body">
|
||
<h5>Configurar el encabezado del documento</h5>
|
||
<p>En el panel derecho del builder puedes personalizar: nombre de la empresa, subtítulo, logo (imagen base64), color del encabezado y pie de página. Estos datos aparecerán en el PDF imprimible.</p>
|
||
</div>
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">3</div>
|
||
<div class="man-step-body">
|
||
<h5>Agregar campos al formulario</h5>
|
||
<p>Arrastra campos del panel izquierdo al canvas central. Tipos disponibles:</p>
|
||
<div style="display:flex;flex-wrap:wrap;gap:6px;margin:8px 0">
|
||
<span class="man-ui-tag">Texto</span>
|
||
<span class="man-ui-tag">Textarea</span>
|
||
<span class="man-ui-tag">Número</span>
|
||
<span class="man-ui-tag">Fecha / Hora</span>
|
||
<span class="man-ui-tag">Select</span>
|
||
<span class="man-ui-tag">Radio</span>
|
||
<span class="man-ui-tag">Checkbox</span>
|
||
<span class="man-ui-tag">Lista marcable</span>
|
||
<span class="man-ui-tag">Vinculado al paciente</span>
|
||
<span class="man-ui-tag">Párrafo inline</span>
|
||
<span class="man-ui-tag">Firma paciente</span>
|
||
<span class="man-ui-tag">Firma profesional</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">4</div>
|
||
<div class="man-step-body">
|
||
<h5>Configurar la firma</h5>
|
||
<p>En la sección <em>Firma</em> del builder puedes activar los modos permitidos: <span class="man-ui-tag">✍️ Canvas</span> (dibujo) y/o <span class="man-ui-tag">📷 Foto</span>. Si la firma es obligatoria, marca <em>Requerida</em>.</p>
|
||
</div>
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">5</div>
|
||
<div class="man-step-body">
|
||
<h5>Guardar la plantilla</h5>
|
||
<p>Haz clic en <span class="man-ui-tag">💾 Guardar</span>. La plantilla quedará disponible en <code>lab_formularios.php</code> para ser enviada a pacientes.</p>
|
||
<div class="man-tip">✅ Cada vez que edites el esquema de campos, la versión de la plantilla se incrementa automáticamente.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Enviar a paciente -->
|
||
<h3 id="man-form-enviar">Enviar un formulario a un paciente</h3>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">1</div>
|
||
<div class="man-step-body">
|
||
<h5>Seleccionar la plantilla y hacer clic en Enviar</h5>
|
||
<p>En <code>lab_formularios.php</code> busca la plantilla que deseas enviar y haz clic en el botón <span class="man-ui-tag">📤 Enviar</span>. Se abrirá un modal.</p>
|
||
</div>
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">2</div>
|
||
<div class="man-step-body">
|
||
<h5>Buscar y seleccionar el paciente</h5>
|
||
<p>Empieza a escribir el nombre o documento del paciente. Al seleccionarlo, verás una vista previa de los campos que se pre-llenarán automáticamente con sus datos.</p>
|
||
</div>
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">3</div>
|
||
<div class="man-step-body">
|
||
<h5>Elegir el canal de envío</h5>
|
||
<p>Selecciona cómo compartirás el enlace:</p>
|
||
<ul style="padding-left:18px;font-size:13.5px;line-height:2">
|
||
<li><strong>WhatsApp</strong> — genera un mensaje pre-redactado con el enlace listo para enviar desde en la conversación del paciente</li>
|
||
<li><strong>Solo link</strong> — copia el enlace y compártelo como prefieras</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">4</div>
|
||
<div class="man-step-body">
|
||
<h5>Confirmar el envío</h5>
|
||
<p>Haz clic en <span class="man-ui-tag">Generar enlace</span>. Se creará un token único para ese paciente. El enlace generado no tiene fecha de vencimiento y es de un solo uso (no permite modificar la respuesta una vez enviada).</p>
|
||
<div class="man-tip">✅ El enlace funciona sin que el paciente necesite crear cuenta ni iniciar sesión.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Revisar respuestas -->
|
||
<h3 id="man-form-revisar">Revisar las respuestas de un formulario</h3>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">1</div>
|
||
<div class="man-step-body">
|
||
<h5>Ir a la pestaña de envíos</h5>
|
||
<p>En <code>lab_formularios.php</code>, haz clic en la pestaña <span class="man-ui-tag">📋 Envíos</span>. Verás el historial de todos los formularios enviados con su estado actual.</p>
|
||
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:8px">
|
||
<span class="state-pill sp-prog">⏳ pendiente</span>
|
||
<span class="state-pill sp-comp">✅ completado</span>
|
||
<span class="state-pill sp-conf">✍️ firmado</span>
|
||
<span class="state-pill sp-canc">❌ expirado</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">2</div>
|
||
<div class="man-step-body">
|
||
<h5>Ver la respuesta del paciente</h5>
|
||
<p>En formularios con estado <strong>completado</strong> o <strong>firmado</strong>, aparecerá el botón <span class="man-ui-tag">👁 Ver</span>. Este abrirá un modal con todas las respuestas campo por campo y la imagen de la firma.</p>
|
||
</div>
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">3</div>
|
||
<div class="man-step-body">
|
||
<h5>Descargar el PDF</h5>
|
||
<p>Haz clic en el ícono <span class="man-ui-tag">🖨️</span> junto al botón Ver, o en el botón <span class="man-ui-tag">Ver / Imprimir PDF</span> dentro del modal. Esto abre <code>ver_formulario_enviado.php</code> con el documento completo listo para imprimir o guardar como PDF.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Firmar como profesional -->
|
||
<h3 id="man-form-firmar">Firmar como profesional</h3>
|
||
<div class="callout warning" style="margin-bottom:16px">
|
||
⚠️ Esta opción solo aplica cuando la plantilla tiene un campo <strong>firma_profesional</strong> en su diseño.
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">1</div>
|
||
<div class="man-step-body">
|
||
<h5>Abrir el formulario completado</h5>
|
||
<p>Ve a <code>ver_formulario_enviado.php?id=X</code> con tu sesión activa (admin o el enfermero que lo envió). Verás el documento con el canvas de firma del profesional en la posición configurada.</p>
|
||
<div class="man-tip">✅ Si el paciente aún no ha completado el formulario, el canvas de firma profesional no aparecerá hasta que lo complete.</div>
|
||
</div>
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">2</div>
|
||
<div class="man-step-body">
|
||
<h5>Dibujar la firma y guardar</h5>
|
||
<p>Dibuja tu firma en el canvas usando el mouse o la pantalla táctil. Haz clic en <span class="man-ui-tag">💾 Guardar firma</span>. La firma quedará vinculada al documento de forma permanente.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Verificar autenticidad -->
|
||
<h3 id="man-form-verificar">Verificar la autenticidad de un documento</h3>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">1</div>
|
||
<div class="man-step-body">
|
||
<h5>Obtener el hash SHA-256 del documento</h5>
|
||
<p>Al pie del documento en <code>ver_formulario_enviado.php</code> encontrarás el sello de integridad — una cadena hexadecimal de 64 caracteres. Este hash identifica únicamente este documento.</p>
|
||
</div>
|
||
</div>
|
||
<div class="man-step-block">
|
||
<div class="man-step-num">2</div>
|
||
<div class="man-step-body">
|
||
<h5>Ir al verificador público</h5>
|
||
<p>Accede a <code>verificar_formulario.php?h=HASH_AQUI</code> o comparte ese enlace con la persona que necesita verificar la autenticidad. El sistema mostrará:</p>
|
||
<ul style="padding-left:18px;font-size:13.5px;line-height:2">
|
||
<li>Nombre del formulario y del paciente</li>
|
||
<li>Fecha en que fue completado</li>
|
||
<li>Estado actual del documento</li>
|
||
<li>Confirmación de que el sello es válido ✅ o inválido ❌</li>
|
||
</ul>
|
||
<div class="man-tip">✅ Esta verificación es pública — cualquier persona con el hash puede usarla sin necesidad de tener cuenta en el sistema.</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<p style="text-align:center;color:var(--muted);font-size:12px;margin-top:8px;padding-bottom:32px">
|
||
Manual de uso — Sistema de Laboratorio · 30/03/2026
|
||
</p>
|
||
|
||
</main><!-- /content -->
|
||
</div><!-- /layout -->
|
||
|
||
<button id="scrollTop" onclick="window.scrollTo({top:0,behavior:'smooth'})" title="Volver arriba">↑</button>
|
||
|
||
<script>
|
||
// Scroll top button
|
||
const btn = document.getElementById('scrollTop');
|
||
window.addEventListener('scroll', () => {
|
||
btn.classList.toggle('show', window.scrollY > 400);
|
||
});
|
||
|
||
// Active TOC link highlighting
|
||
const sections = document.querySelectorAll('[id]');
|
||
const links = document.querySelectorAll('.toc a[href^="#"]');
|
||
const observer = new IntersectionObserver(entries => {
|
||
entries.forEach(e => {
|
||
if (e.isIntersecting) {
|
||
links.forEach(l => l.classList.remove('active'));
|
||
const active = document.querySelector(`.toc a[href="#${e.target.id}"]`);
|
||
if (active) active.classList.add('active');
|
||
}
|
||
});
|
||
}, { rootMargin: '-20% 0px -70% 0px' });
|
||
sections.forEach(s => observer.observe(s));
|
||
</script>
|
||
</body>
|
||
</html>
|