Nueva vista que reúne los 32 documentos en un solo entregable maquetado para imprimir: portada con los datos del laboratorio, índice numerado, y cada capítulo y documento empezando en página nueva. Se resolvió como vista y no como archivo generado para que el entregable refleje siempre el estado actual, incluidos los inventarios técnicos que se generan del código y de la base en cada consulta. No hay paso de regeneración que alguien pueda olvidar. El CSS de impresión evita cortes a mitad de tabla, bloque de código o cita, repite el encabezado en tablas que abarcan varias páginas y oculta la barra de acciones. Permite exportar todo o una sección puntual, y respeta la visibilidad por rol: cada usuario exporta lo que puede leer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
513 lines
25 KiB
PHP
513 lines
25 KiB
PHP
<?php
|
||
/**
|
||
* modules/soporte/views/documentacion.php
|
||
* Visor de la documentación del proyecto.
|
||
*
|
||
* ?s=<seccion>&d=<documento> documento puntual
|
||
* ?s=<seccion> portada de la sección
|
||
* (sin parámetros) portada general
|
||
*/
|
||
require_once APP_ROOT . '/config/config.php';
|
||
if (!isUserLoggedIn()) { header('Location: ' . BASE_URL . 'login.php'); exit; }
|
||
|
||
require_once __DIR__ . '/../Markdown.php';
|
||
require_once __DIR__ . '/../DocIndex.php';
|
||
require_once __DIR__ . '/../Generadores.php';
|
||
|
||
$arbol = DocIndex::arbol();
|
||
$seccion = preg_replace('/[^a-z0-9-]/', '', $_GET['s'] ?? '');
|
||
$docSlug = preg_replace('/[^a-z0-9-]/', '', $_GET['d'] ?? '');
|
||
|
||
$archivo = ($seccion && $docSlug) ? DocIndex::resolver($seccion, $docSlug) : null;
|
||
$titulo = 'Documentación';
|
||
$cuerpo = '';
|
||
$toc = [];
|
||
|
||
if ($archivo) {
|
||
[, $crudo] = DocIndex::leer($archivo); // descarta la cabecera de permisos
|
||
$md = Generadores::expandir($crudo);
|
||
$cuerpo = Markdown::render($md);
|
||
$toc = Markdown::indice($md);
|
||
foreach ($arbol[$seccion]['docs'] ?? [] as $d) {
|
||
if ($d['slug'] === $docSlug) { $titulo = $d['titulo']; break; }
|
||
}
|
||
} elseif ($seccion && isset($arbol[$seccion])) {
|
||
$titulo = $arbol[$seccion]['titulo'];
|
||
}
|
||
|
||
Layout::open('Soporte · Documentación', 'fas fa-life-ring');
|
||
?>
|
||
<style>
|
||
.doc-wrap { display:flex; gap:0; align-items:flex-start; min-height:calc(100vh - 60px); background:#fff; }
|
||
|
||
/* ── Índice lateral ── */
|
||
.doc-nav { width:280px; flex-shrink:0; border-right:1px solid #e2e8f0; background:#f8fafc;
|
||
align-self:stretch; padding:18px 0 60px; position:sticky; top:0; max-height:100vh; overflow-y:auto; }
|
||
.doc-nav .buscador { padding:0 16px 14px; }
|
||
.doc-nav .buscador input { width:100%; font-size:.84rem; padding:7px 11px; border:1px solid #cbd5e1;
|
||
border-radius:8px; background:#fff; }
|
||
.doc-nav .buscador input:focus { outline:none; border-color:#2563eb; box-shadow:0 0 0 3px rgba(37,99,235,.12); }
|
||
.doc-nav .btn-pdf { display:block; margin-top:8px; text-align:center; font-size:.78rem;
|
||
padding:6px 10px; border:1px solid #cbd5e1; border-radius:8px;
|
||
color:#475569; text-decoration:none; background:#fff; }
|
||
.doc-nav .btn-pdf:hover { border-color:#2563eb; color:#1d4ed8; }
|
||
.doc-nav-sec { padding:10px 16px 4px; font-size:.68rem; font-weight:800; text-transform:uppercase;
|
||
letter-spacing:.08em; color:#64748b; display:flex; align-items:center; gap:6px; }
|
||
.doc-nav a { display:block; padding:5px 16px 5px 30px; font-size:.83rem; color:#334155;
|
||
text-decoration:none; border-left:2px solid transparent; }
|
||
.doc-nav a:hover { background:#eef2f7; color:#1e293b; }
|
||
.doc-nav a.activo { background:#e0edff; color:#1d4ed8; font-weight:600; border-left-color:#2563eb; }
|
||
|
||
/* ── Resultados de búsqueda ── */
|
||
#resultados { padding:0 16px; }
|
||
#resultados .r { display:block; padding:8px 10px; border-radius:8px; text-decoration:none;
|
||
margin-bottom:4px; background:#fff; border:1px solid #e2e8f0; }
|
||
#resultados .r:hover { border-color:#2563eb; }
|
||
#resultados .r-t { font-size:.83rem; font-weight:600; color:#1e293b; }
|
||
#resultados .r-c { font-size:.68rem; color:#64748b; text-transform:uppercase; letter-spacing:.05em; }
|
||
#resultados .r-x { font-size:.75rem; color:#475569; margin-top:2px; }
|
||
#resultados .vacio { font-size:.8rem; color:#94a3b8; padding:8px 4px; }
|
||
|
||
/* ── Contenido ── */
|
||
.doc-main { flex:1; min-width:0; display:flex; gap:0; }
|
||
.doc-body { flex:1; min-width:0; padding:26px 40px 80px; max-width:900px; }
|
||
.doc-ruta { font-size:.73rem; color:#94a3b8; margin-bottom:14px; }
|
||
.doc-ruta a { color:#64748b; text-decoration:none; }
|
||
.doc-ruta a:hover { text-decoration:underline; }
|
||
|
||
.doc-body h1 { font-size:1.65rem; font-weight:800; color:#0f172a; margin:0 0 18px;
|
||
padding-bottom:12px; border-bottom:1px solid #e2e8f0; }
|
||
.doc-body h2 { font-size:1.2rem; font-weight:700; color:#1e293b; margin:32px 0 12px;
|
||
padding-top:6px; scroll-margin-top:20px; }
|
||
.doc-body h3 { font-size:1rem; font-weight:700; color:#334155; margin:24px 0 9px; scroll-margin-top:20px; }
|
||
.doc-body h4 { font-size:.9rem; font-weight:700; color:#475569; margin:18px 0 7px; }
|
||
.doc-body p { font-size:.9rem; line-height:1.7; color:#334155; margin:0 0 13px; }
|
||
.doc-body ul, .doc-body ol { font-size:.9rem; line-height:1.7; color:#334155; margin:0 0 13px; padding-left:24px; }
|
||
.doc-body li { margin-bottom:4px; }
|
||
.doc-body li > ul, .doc-body li > ol { margin:4px 0 2px; }
|
||
.doc-body a { color:#2563eb; }
|
||
.doc-body strong { color:#0f172a; }
|
||
.doc-body code { background:#f1f5f9; color:#be185d; padding:1px 5px; border-radius:4px;
|
||
font-size:.83em; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; }
|
||
.doc-body pre { background:#0f172a; color:#e2e8f0; padding:14px 16px; border-radius:10px;
|
||
overflow-x:auto; margin:0 0 15px; }
|
||
.doc-body pre code { background:none; color:inherit; padding:0; font-size:.8rem; line-height:1.6; }
|
||
.doc-body blockquote { border-left:3px solid #f59e0b; background:#fffbeb; margin:0 0 15px;
|
||
padding:10px 16px; border-radius:0 8px 8px 0; }
|
||
.doc-body blockquote p { margin:0; color:#78350f; font-size:.86rem; }
|
||
.doc-body hr { border:none; border-top:1px solid #e2e8f0; margin:26px 0; }
|
||
.tabla-scroll { overflow-x:auto; margin:0 0 16px; }
|
||
.doc-body table { width:100%; border-collapse:collapse; font-size:.82rem; }
|
||
.doc-body thead th { background:#f8fafc; text-align:left; font-weight:700; color:#475569;
|
||
padding:8px 11px; border-bottom:2px solid #e2e8f0; white-space:nowrap; }
|
||
.doc-body tbody td { padding:7px 11px; border-bottom:1px solid #f1f5f9; color:#334155; vertical-align:top; }
|
||
.doc-body tbody tr:hover { background:#f8fafc; }
|
||
|
||
/* ── Tabla de contenidos ── */
|
||
.doc-toc { width:210px; flex-shrink:0; padding:32px 20px 60px; position:sticky; top:0;
|
||
max-height:100vh; overflow-y:auto; }
|
||
.doc-toc-t { font-size:.66rem; font-weight:800; text-transform:uppercase; letter-spacing:.08em;
|
||
color:#94a3b8; margin-bottom:9px; }
|
||
.doc-toc a { display:block; font-size:.76rem; color:#64748b; text-decoration:none;
|
||
padding:3px 0 3px 9px; border-left:2px solid #e2e8f0; line-height:1.4; }
|
||
.doc-toc a:hover { color:#2563eb; border-left-color:#93c5fd; }
|
||
.doc-toc a.n3 { padding-left:20px; font-size:.73rem; }
|
||
|
||
/* ── Portadas ── */
|
||
.doc-hero { margin-bottom:26px; }
|
||
.doc-hero h1 { font-size:1.7rem; font-weight:800; color:#0f172a; margin:0 0 8px; }
|
||
.doc-hero p { font-size:.92rem; color:#64748b; margin:0; }
|
||
.tarjetas { display:grid; grid-template-columns:repeat(auto-fill,minmax(260px,1fr)); gap:14px; }
|
||
.tarjeta { display:block; padding:18px; border:1px solid #e2e8f0; border-radius:12px;
|
||
text-decoration:none; background:#fff; transition:border-color .15s, transform .15s; }
|
||
.tarjeta:hover { border-color:#2563eb; transform:translateY(-2px); }
|
||
.tarjeta i { font-size:1.3rem; color:#2563eb; }
|
||
.tarjeta .t { font-size:1rem; font-weight:700; color:#1e293b; margin:9px 0 5px; }
|
||
.tarjeta .d { font-size:.82rem; color:#64748b; line-height:1.5; }
|
||
.tarjeta .n { font-size:.72rem; color:#94a3b8; margin-top:9px; }
|
||
|
||
.lista-docs { list-style:none; padding:0; margin:0; }
|
||
.lista-docs li { border-bottom:1px solid #f1f5f9; }
|
||
.lista-docs a { display:block; padding:12px 4px; text-decoration:none; color:#1e293b; font-size:.92rem; }
|
||
.lista-docs a:hover { color:#2563eb; }
|
||
|
||
@media (max-width:1100px) { .doc-toc { display:none; } }
|
||
@media (max-width:820px) {
|
||
.doc-nav { position:static; width:100%; max-height:none; border-right:none;
|
||
border-bottom:1px solid #e2e8f0; }
|
||
.doc-wrap { flex-direction:column; }
|
||
.doc-body { padding:20px 18px 60px; }
|
||
}
|
||
@media print {
|
||
.doc-nav, .doc-toc, .doc-ruta, #btnAI, #aiPanel { display:none !important; }
|
||
.doc-body { max-width:none; padding:0; }
|
||
.doc-body pre { background:#f8fafc; color:#0f172a; border:1px solid #cbd5e1; }
|
||
}
|
||
|
||
/* ── Panel LIA — mismo diseño que el dashboard del turnero ── */
|
||
#aiPanel {
|
||
position:fixed; bottom:0; right:24px; width:400px; z-index:1080;
|
||
box-shadow:0 -8px 40px rgba(0,0,0,.22); border-radius:20px 20px 0 0;
|
||
background:#fff; border:1px solid #e2e8f0; border-bottom:none;
|
||
transform:translateY(100%); transition:transform .35s cubic-bezier(.4,0,.2,1);
|
||
display:flex; flex-direction:column; max-height:580px;
|
||
}
|
||
#aiPanel.open { transform:translateY(0); }
|
||
#aiPanel.minimized .ai-body { display:none; }
|
||
#aiPanel.minimized { max-height:none; }
|
||
.ai-header {
|
||
background:linear-gradient(135deg,#0d47a1,#1565c0,#0288d1);
|
||
color:#fff; padding:14px 18px; border-radius:20px 20px 0 0;
|
||
display:flex; align-items:center; gap:12px; flex-shrink:0; cursor:pointer; user-select:none;
|
||
}
|
||
.lia-logo {
|
||
width:44px; height:44px; border-radius:50%;
|
||
background:rgba(255,255,255,.15); border:2px solid rgba(255,255,255,.4);
|
||
display:flex; align-items:center; justify-content:center;
|
||
font-size:1rem; font-weight:900; letter-spacing:-1px; flex-shrink:0;
|
||
transition:transform .3s;
|
||
}
|
||
.lia-logo:hover { transform:scale(1.08); }
|
||
.lia-logo .lia-i { color:#7dd3fc; }
|
||
.ai-title { font-weight:800; font-size:1.05rem; letter-spacing:.5px; }
|
||
.btn-tts { background:rgba(255,255,255,.15); border:1px solid rgba(255,255,255,.3);
|
||
color:rgba(255,255,255,.7); cursor:pointer; padding:5px 9px; border-radius:8px;
|
||
font-size:.88rem; transition:all .2s; }
|
||
.btn-tts:hover { background:rgba(255,255,255,.25); color:#fff; }
|
||
.ai-header .btn-close { filter:invert(1) brightness(2); opacity:.7; }
|
||
.ai-header .btn-close:hover { opacity:1; }
|
||
.ai-body { display:flex; flex-direction:column; flex:1; overflow:hidden; }
|
||
.ai-quick { display:flex; gap:7px; flex-wrap:wrap; padding:10px 14px;
|
||
border-bottom:1px solid #f1f5f9; flex-shrink:0; background:#fafbff; }
|
||
.ai-chip { background:#fff; border:1.5px solid #e2e8f0; border-radius:99px; padding:6px 13px;
|
||
font-size:.82rem; cursor:pointer; color:#334155; white-space:nowrap;
|
||
transition:all .15s; font-weight:500; }
|
||
.ai-chip:hover { background:#eff6ff; border-color:#93c5fd; color:#1d4ed8; }
|
||
.ai-messages { flex:1; overflow-y:auto; padding:14px 16px; display:flex; flex-direction:column; gap:10px; }
|
||
.ai-msg { max-width:92%; padding:10px 14px; border-radius:16px; font-size:.9rem;
|
||
line-height:1.55; animation:msg-in .2s ease; word-wrap:break-word; }
|
||
@keyframes msg-in { from{opacity:0;transform:translateY(6px)} to{opacity:1;transform:none} }
|
||
.ai-msg.user { background:linear-gradient(135deg,#1565c0,#0288d1); color:#fff;
|
||
align-self:flex-end; border-radius:16px 16px 2px 16px; }
|
||
.ai-msg.bot { background:#f1f5f9; color:#1e293b; align-self:flex-start; border-radius:16px 16px 16px 2px; }
|
||
.ai-msg.bot.typing::after { content:'●●●'; animation:dots 1.2s infinite; letter-spacing:2px; }
|
||
@keyframes dots { 0%,100%{opacity:.3} 50%{opacity:1} }
|
||
.ai-msg.bot strong { color:#1565c0; }
|
||
.ai-msg.bot ul, .ai-msg.bot ol { margin:4px 0 4px 16px; padding:0; }
|
||
.ai-msg.bot code { background:#e2e8f0; color:#be185d; padding:1px 4px; border-radius:3px; font-size:.9em; }
|
||
.ai-msg.bot pre { background:#0f172a; color:#e2e8f0; padding:9px 11px; border-radius:8px;
|
||
overflow-x:auto; font-size:.76rem; margin:6px 0; }
|
||
.ai-fuentes { font-size:.68rem; color:#94a3b8; align-self:flex-start; margin:-6px 0 0 4px; }
|
||
.ai-footer { padding:10px 14px; border-top:1px solid #f1f5f9; display:flex; gap:8px;
|
||
flex-shrink:0; background:#fafbff; }
|
||
.ai-footer input { flex:1; border:1.5px solid #e2e8f0; border-radius:10px; padding:9px 14px;
|
||
font-size:.9rem; outline:none; background:#fff; }
|
||
.ai-footer input:focus { border-color:#1565c0; }
|
||
.ai-footer .btn-send { background:linear-gradient(135deg,#1565c0,#0288d1); color:#fff; border:none;
|
||
border-radius:10px; padding:9px 16px; cursor:pointer; font-size:.88rem; min-width:42px; }
|
||
.ai-footer .btn-send:disabled { opacity:.5; cursor:default; }
|
||
#btnAI {
|
||
position:fixed; bottom:24px; right:24px; z-index:1079;
|
||
background:linear-gradient(135deg,#0d47a1,#1565c0,#0288d1);
|
||
color:#fff; border:none; border-radius:50%; width:58px; height:58px;
|
||
font-size:.78rem; font-weight:900; letter-spacing:-.5px;
|
||
cursor:pointer; box-shadow:0 4px 20px rgba(21,101,192,.45);
|
||
transition:transform .2s,box-shadow .2s;
|
||
display:flex; align-items:center; justify-content:center;
|
||
}
|
||
#btnAI:hover { transform:scale(1.1); box-shadow:0 6px 28px rgba(21,101,192,.55); }
|
||
#btnAI.hidden { display:none; }
|
||
@media(max-width:600px) { #aiPanel { width:100%; right:0; border-radius:16px 16px 0 0; } }
|
||
</style>
|
||
|
||
<div class="doc-wrap">
|
||
<!-- ── Índice lateral ── -->
|
||
<nav class="doc-nav">
|
||
<div class="buscador">
|
||
<input type="search" id="q" placeholder="Buscar en la documentación…" autocomplete="off">
|
||
<a class="btn-pdf" href="<?= BASE_URL ?>erp.php?m=soporte&v=imprimir" target="_blank">
|
||
<i class="fas fa-file-pdf"></i> Exportar a PDF
|
||
</a>
|
||
</div>
|
||
<div id="resultados" style="display:none"></div>
|
||
<div id="arbol">
|
||
<?php foreach ($arbol as $sec => $cfg): ?>
|
||
<div class="doc-nav-sec">
|
||
<i class="<?= htmlspecialchars($cfg['icono']) ?>"></i>
|
||
<a href="<?= BASE_URL ?>erp.php?m=soporte&v=documentacion&s=<?= urlencode($sec) ?>"
|
||
style="padding:0;border:none;color:inherit;font:inherit;letter-spacing:inherit">
|
||
<?= htmlspecialchars($cfg['titulo']) ?>
|
||
</a>
|
||
</div>
|
||
<?php foreach ($cfg['docs'] as $d): ?>
|
||
<a href="<?= BASE_URL ?>erp.php?m=soporte&v=documentacion&s=<?= urlencode($sec) ?>&d=<?= urlencode($d['slug']) ?>"
|
||
class="<?= ($sec === $seccion && $d['slug'] === $docSlug) ? 'activo' : '' ?>">
|
||
<?= htmlspecialchars($d['titulo']) ?>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</nav>
|
||
|
||
<div class="doc-main">
|
||
<div class="doc-body">
|
||
<?php if ($archivo): ?>
|
||
<div class="doc-ruta">
|
||
<a href="<?= BASE_URL ?>erp.php?m=soporte&v=documentacion">Documentación</a>
|
||
›
|
||
<a href="<?= BASE_URL ?>erp.php?m=soporte&v=documentacion&s=<?= urlencode($seccion) ?>">
|
||
<?= htmlspecialchars($arbol[$seccion]['titulo'] ?? $seccion) ?>
|
||
</a>
|
||
› <?= htmlspecialchars($titulo) ?>
|
||
</div>
|
||
<?= $cuerpo ?>
|
||
|
||
<?php elseif ($seccion && isset($arbol[$seccion])): ?>
|
||
<div class="doc-ruta">
|
||
<a href="<?= BASE_URL ?>erp.php?m=soporte&v=documentacion">Documentación</a>
|
||
› <?= htmlspecialchars($arbol[$seccion]['titulo']) ?>
|
||
</div>
|
||
<div class="doc-hero">
|
||
<h1><?= htmlspecialchars($arbol[$seccion]['titulo']) ?></h1>
|
||
<p><?= htmlspecialchars($arbol[$seccion]['resumen']) ?></p>
|
||
</div>
|
||
<?php if ($arbol[$seccion]['docs']): ?>
|
||
<ul class="lista-docs">
|
||
<?php foreach ($arbol[$seccion]['docs'] as $d): ?>
|
||
<li><a href="<?= BASE_URL ?>erp.php?m=soporte&v=documentacion&s=<?= urlencode($seccion) ?>&d=<?= urlencode($d['slug']) ?>">
|
||
<?= htmlspecialchars($d['titulo']) ?>
|
||
</a></li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
<?php else: ?>
|
||
<p class="text-muted">Esta sección aún no tiene documentos.</p>
|
||
<?php endif; ?>
|
||
|
||
<?php else: ?>
|
||
<div class="doc-hero">
|
||
<h1>Documentación del sistema</h1>
|
||
<p>Todo sobre cómo funciona este ERP: cómo se usa, cómo está construido y qué hacer cuando algo falla.</p>
|
||
</div>
|
||
<div class="tarjetas">
|
||
<?php foreach ($arbol as $sec => $cfg): ?>
|
||
<a class="tarjeta" href="<?= BASE_URL ?>erp.php?m=soporte&v=documentacion&s=<?= urlencode($sec) ?>">
|
||
<i class="<?= htmlspecialchars($cfg['icono']) ?>"></i>
|
||
<div class="t"><?= htmlspecialchars($cfg['titulo']) ?></div>
|
||
<div class="d"><?= htmlspecialchars($cfg['resumen']) ?></div>
|
||
<div class="n"><?= count($cfg['docs']) ?> documento<?= count($cfg['docs']) === 1 ? '' : 's' ?></div>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<?php if ($toc): ?>
|
||
<aside class="doc-toc">
|
||
<div class="doc-toc-t">En esta página</div>
|
||
<?php foreach ($toc as $t): ?>
|
||
<a href="#<?= htmlspecialchars($t['slug']) ?>" class="<?= $t['nivel'] === 3 ? 'n3' : '' ?>">
|
||
<?= htmlspecialchars($t['texto']) ?>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
</aside>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Panel LIA — asistente sobre la documentación ── -->
|
||
<button id="btnAI" onclick="toggleAI()" title="Preguntar a LIA">LIA</button>
|
||
<div id="aiPanel">
|
||
<div class="ai-header" onclick="minimizarIA(event)">
|
||
<div class="lia-logo" id="liaLogo">
|
||
<span>L</span><span class="lia-i">I</span><span>A</span>
|
||
</div>
|
||
<div style="flex:1;line-height:1.1">
|
||
<div class="ai-title">LIA</div>
|
||
<div style="font-size:.68rem;opacity:.75;font-weight:400">Asistente de Documentación</div>
|
||
</div>
|
||
<button class="btn-tts" onclick="event.stopPropagation();minimizarIA(event)" title="Minimizar" id="btnMin">
|
||
<i class="fas fa-minus" id="iconMin"></i>
|
||
</button>
|
||
<button class="btn-close" onclick="event.stopPropagation();toggleAI()"></button>
|
||
</div>
|
||
<div class="ai-body" id="aiBody">
|
||
<div class="ai-quick" id="aiQuick"></div>
|
||
<div class="ai-messages" id="aiMessages"></div>
|
||
<div class="ai-footer">
|
||
<input type="text" id="aiInput" placeholder="Pregunta cómo hacer algo…" maxlength="400"
|
||
onkeydown="if(event.key==='Enter')enviarIA()">
|
||
<button class="btn-send" onclick="enviarIA()"><i class="fas fa-paper-plane"></i></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
/* ── Buscador: índice completo servido con la página ── */
|
||
const DOCS = <?= json_encode(DocIndex::indiceBusqueda(), JSON_UNESCAPED_UNICODE) ?>;
|
||
const BASEQ = '<?= BASE_URL ?>erp.php?m=soporte&v=documentacion';
|
||
|
||
const norm = s => (s || '').toLowerCase()
|
||
.normalize('NFD').replace(/[̀-ͯ]/g, '');
|
||
|
||
function buscar(termino) {
|
||
const q = norm(termino).trim();
|
||
const cajaR = document.getElementById('resultados');
|
||
const cajaA = document.getElementById('arbol');
|
||
if (q.length < 2) { cajaR.style.display = 'none'; cajaA.style.display = ''; return; }
|
||
|
||
const palabras = q.split(/\s+/);
|
||
const hits = [];
|
||
DOCS.forEach(d => {
|
||
const heno = norm(d.t + ' ' + d.c + ' ' + d.x);
|
||
if (!palabras.every(p => heno.includes(p))) return;
|
||
// El título pesa más que el cuerpo
|
||
const enTitulo = palabras.every(p => norm(d.t).includes(p));
|
||
const pos = heno.indexOf(palabras[0]);
|
||
hits.push({ d, score: (enTitulo ? 0 : 1000) + pos, pos });
|
||
});
|
||
hits.sort((a, b) => a.score - b.score);
|
||
|
||
cajaA.style.display = 'none';
|
||
cajaR.style.display = '';
|
||
if (!hits.length) {
|
||
cajaR.innerHTML = '<div class="vacio">Sin resultados para «' + esc(termino) + '»</div>';
|
||
return;
|
||
}
|
||
cajaR.innerHTML = hits.slice(0, 20).map(h => {
|
||
const ini = Math.max(0, h.pos - 40);
|
||
const frag = h.d.x.substr(ini, 120).trim();
|
||
return `<a class="r" href="${BASEQ}&s=${encodeURIComponent(h.d.s)}&d=${encodeURIComponent(h.d.u)}">
|
||
<div class="r-c">${esc(h.d.c)}</div>
|
||
<div class="r-t">${esc(h.d.t)}</div>
|
||
<div class="r-x">${ini > 0 ? '…' : ''}${esc(frag)}…</div>
|
||
</a>`;
|
||
}).join('');
|
||
}
|
||
|
||
function esc(s) {
|
||
const d = document.createElement('div');
|
||
d.textContent = s == null ? '' : String(s);
|
||
return d.innerHTML;
|
||
}
|
||
|
||
/* ── LIA: asistente sobre la documentación ──
|
||
Mismo comportamiento y nombres que el panel del dashboard del turnero. */
|
||
const AI_API = '<?= BASE_URL ?>modules/soporte/api/ai_docs.php';
|
||
let _liaHistorial = [];
|
||
|
||
function toggleAI() {
|
||
const panel = document.getElementById('aiPanel');
|
||
const fab = document.getElementById('btnAI');
|
||
const abre = !panel.classList.contains('open');
|
||
panel.classList.toggle('open', abre);
|
||
panel.classList.remove('minimized');
|
||
fab.classList.toggle('hidden', abre);
|
||
if (abre) {
|
||
if (!document.getElementById('aiMessages').children.length) saludarIA();
|
||
document.getElementById('aiInput').focus();
|
||
}
|
||
}
|
||
|
||
function minimizarIA(e) {
|
||
if (e) e.stopPropagation();
|
||
const panel = document.getElementById('aiPanel');
|
||
panel.classList.toggle('minimized');
|
||
const icono = document.getElementById('iconMin');
|
||
icono.className = panel.classList.contains('minimized') ? 'fas fa-chevron-up' : 'fas fa-minus';
|
||
}
|
||
|
||
function saludarIA() {
|
||
agregarMsg('bot', 'Hola, soy LIA. Pregúnteme cómo hacer algo en el sistema y le respondo con la documentación.');
|
||
// Atajos armados con los documentos que este usuario realmente puede ver
|
||
const quick = document.getElementById('aiQuick');
|
||
quick.innerHTML = DOCS.slice(0, 4)
|
||
.map(d => `<button class="ai-chip" onclick="enviarRapido('¿Qué explica ${esc(d.t)}?')">${esc(d.t)}</button>`)
|
||
.join('');
|
||
}
|
||
|
||
function enviarRapido(texto) {
|
||
document.getElementById('aiInput').value = texto;
|
||
enviarIA();
|
||
}
|
||
|
||
function agregarMsg(tipo, html) {
|
||
const msgs = document.getElementById('aiMessages');
|
||
const div = document.createElement('div');
|
||
div.className = 'ai-msg ' + tipo;
|
||
div.innerHTML = html;
|
||
msgs.appendChild(div);
|
||
msgs.scrollTop = msgs.scrollHeight;
|
||
return div;
|
||
}
|
||
|
||
/* Formato mínimo de la respuesta: negritas, código, listas y saltos. */
|
||
function mdToHtml(md) {
|
||
let h = esc(md);
|
||
h = h.replace(/```[\s\S]*?```/g, m => '<pre>' + m.replace(/```\w*\n?/g, '') + '</pre>');
|
||
h = h.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||
h = h.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
||
h = h.replace(/^\s*[-*]\s+(.*)$/gm, '<li>$1</li>');
|
||
h = h.replace(/(<li>[\s\S]*?<\/li>)/g, '<ul>$1</ul>');
|
||
return h.replace(/\n{2,}/g, '<br><br>').replace(/\n/g, '<br>');
|
||
}
|
||
|
||
async function enviarIA() {
|
||
const input = document.getElementById('aiInput');
|
||
const pregunta = input.value.trim();
|
||
if (!pregunta) return;
|
||
|
||
document.getElementById('aiQuick').innerHTML = '';
|
||
agregarMsg('user', esc(pregunta));
|
||
|
||
const typing = agregarMsg('bot typing', '');
|
||
input.value = '';
|
||
input.disabled = true;
|
||
|
||
try {
|
||
const res = await fetch(AI_API, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ pregunta, historial: _liaHistorial }),
|
||
});
|
||
const d = await res.json();
|
||
typing.className = 'ai-msg bot';
|
||
|
||
if (d.ok) {
|
||
typing.innerHTML = mdToHtml(d.respuesta);
|
||
if (d.fuentes && d.fuentes.length) {
|
||
const f = document.createElement('div');
|
||
f.className = 'ai-fuentes';
|
||
f.textContent = 'Según: ' + d.fuentes.join(' · ');
|
||
document.getElementById('aiMessages').appendChild(f);
|
||
}
|
||
_liaHistorial.push({ rol: 'user', texto: pregunta }, { rol: 'model', texto: d.respuesta });
|
||
if (_liaHistorial.length > 6) _liaHistorial = _liaHistorial.slice(-6);
|
||
} else {
|
||
typing.textContent = d.error || 'No pude responder.';
|
||
}
|
||
} catch (_) {
|
||
typing.className = 'ai-msg bot';
|
||
typing.textContent = 'Error de conexión';
|
||
} finally {
|
||
input.disabled = false;
|
||
input.focus();
|
||
const msgs = document.getElementById('aiMessages');
|
||
msgs.scrollTop = msgs.scrollHeight;
|
||
}
|
||
}
|
||
|
||
const inputQ = document.getElementById('q');
|
||
let _t = null;
|
||
inputQ.addEventListener('input', () => {
|
||
clearTimeout(_t);
|
||
_t = setTimeout(() => buscar(inputQ.value), 120);
|
||
});
|
||
// Atajo: "/" enfoca el buscador
|
||
document.addEventListener('keydown', e => {
|
||
if (e.key === '/' && document.activeElement !== inputQ) { e.preventDefault(); inputQ.focus(); }
|
||
if (e.key === 'Escape' && document.activeElement === inputQ) { inputQ.value = ''; buscar(''); inputQ.blur(); }
|
||
});
|
||
</script>
|
||
|
||
<?php Layout::close(); ?>
|