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>
271 lines
12 KiB
PHP
271 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* modules/soporte/views/imprimir.php
|
|
* Documentación completa en un solo documento, maquetado para imprimir a PDF.
|
|
*
|
|
* ?s=<seccion> limita la exportación a una sección
|
|
* (sin parámetro) exporta todo lo que el usuario puede ver
|
|
*
|
|
* Se sirve sin el layout del ERP: la idea es que la página sea el entregable.
|
|
*/
|
|
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();
|
|
|
|
// Filtro opcional por sección
|
|
$soloSeccion = preg_replace('/[^a-z0-9-]/', '', $_GET['s'] ?? '');
|
|
if ($soloSeccion && isset($arbol[$soloSeccion])) {
|
|
$arbol = [$soloSeccion => $arbol[$soloSeccion]];
|
|
}
|
|
|
|
// Datos del laboratorio para la portada
|
|
$emp = ['empresa_nombre' => 'Laboratorio Clínico', 'empresa_subtitulo' => '', 'doc_logo_base64' => ''];
|
|
try {
|
|
$rows = Database::getInstance()->getConnection()->query(
|
|
"SELECT clave, valor FROM lab_config
|
|
WHERE clave IN ('empresa_nombre','empresa_subtitulo','doc_logo_base64')"
|
|
)->fetchAll(PDO::FETCH_KEY_PAIR);
|
|
$emp = array_merge($emp, $rows ?: []);
|
|
} catch (\Throwable $_) {}
|
|
|
|
// Numeración: 1. Sección → 1.1 Documento
|
|
$capitulos = [];
|
|
$nSec = 0;
|
|
foreach ($arbol as $sec => $cfg) {
|
|
$nSec++;
|
|
$docs = [];
|
|
$nDoc = 0;
|
|
foreach ($cfg['docs'] as $d) {
|
|
$nDoc++;
|
|
[, $cuerpo] = DocIndex::leer($d['archivo']);
|
|
$md = Generadores::expandir($cuerpo);
|
|
// El H1 del documento se reemplaza por el encabezado numerado
|
|
$md = preg_replace('/^#\s+.*$/m', '', $md, 1);
|
|
$docs[] = [
|
|
'num' => "{$nSec}.{$nDoc}",
|
|
'titulo' => $d['titulo'],
|
|
'id' => 'doc-' . $sec . '-' . $d['slug'],
|
|
'html' => Markdown::render($md),
|
|
];
|
|
}
|
|
$capitulos[] = ['num' => (string)$nSec, 'titulo' => $cfg['titulo'],
|
|
'resumen' => $cfg['resumen'], 'id' => 'cap-' . $sec, 'docs' => $docs];
|
|
}
|
|
|
|
$totalDocs = array_sum(array_map(fn($c) => count($c['docs']), $capitulos));
|
|
$esc = fn($s) => htmlspecialchars((string)$s, ENT_QUOTES);
|
|
?><!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Documentación del Sistema — <?= $esc($emp['empresa_nombre']) ?></title>
|
|
<style>
|
|
@page {
|
|
size: A4;
|
|
margin: 18mm 16mm 16mm 16mm;
|
|
}
|
|
@page :first { margin: 0; }
|
|
|
|
* { box-sizing: border-box; }
|
|
html { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
body {
|
|
font-family: "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
font-size: 10.5pt; line-height: 1.55; color: #1e293b;
|
|
margin: 0; padding: 0; background: #fff;
|
|
}
|
|
|
|
/* ── Barra de acciones (no se imprime) ── */
|
|
.barra {
|
|
position: sticky; top: 0; z-index: 10;
|
|
background: #0f172a; color: #fff; padding: 11px 20px;
|
|
display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
|
|
}
|
|
.barra .t { font-weight: 700; flex: 1; font-size: .95rem; }
|
|
.barra button, .barra a {
|
|
background: #2563eb; color: #fff; border: none; border-radius: 7px;
|
|
padding: 7px 15px; font-size: .84rem; cursor: pointer; text-decoration: none;
|
|
}
|
|
.barra a.sec { background: rgba(255,255,255,.14); }
|
|
.barra button:hover { background: #1d4ed8; }
|
|
.hoja { max-width: 195mm; margin: 0 auto; padding: 14mm 12mm 20mm; }
|
|
|
|
/* ── Portada ── */
|
|
.portada {
|
|
display: flex; flex-direction: column; justify-content: center;
|
|
min-height: 247mm; padding: 0 22mm; text-align: center;
|
|
page-break-after: always; break-after: page;
|
|
}
|
|
.portada img { max-width: 170px; margin: 0 auto 26px; }
|
|
.portada .lab { font-size: 13pt; color: #64748b; letter-spacing: .06em;
|
|
text-transform: uppercase; margin-bottom: 6px; }
|
|
.portada .sub { font-size: 10.5pt; color: #94a3b8; margin-bottom: 46px; }
|
|
.portada h1 { font-size: 30pt; font-weight: 800; color: #0d47a1;
|
|
margin: 0 0 10px; line-height: 1.15; letter-spacing: -.5px; }
|
|
.portada .lin { width: 76px; height: 3px; background: #0d47a1;
|
|
margin: 22px auto 24px; border-radius: 2px; }
|
|
.portada .desc { font-size: 11pt; color: #475569; max-width: 118mm;
|
|
margin: 0 auto 50px; line-height: 1.65; }
|
|
.portada .meta { font-size: 9.5pt; color: #64748b; line-height: 1.9; }
|
|
.portada .meta b { color: #334155; }
|
|
|
|
/* ── Índice ── */
|
|
.indice { page-break-after: always; break-after: page; }
|
|
.indice h2 { font-size: 17pt; color: #0d47a1; margin: 0 0 22px;
|
|
padding-bottom: 10px; border-bottom: 2px solid #e2e8f0; }
|
|
.idx-cap { margin-bottom: 16px; }
|
|
.idx-cap > a { display: block; font-size: 11.5pt; font-weight: 700; color: #0f172a;
|
|
text-decoration: none; margin-bottom: 3px; }
|
|
.idx-cap .res { font-size: 9pt; color: #94a3b8; margin: 0 0 7px 20px; }
|
|
.idx-doc { display: block; font-size: 10pt; color: #334155; text-decoration: none;
|
|
margin: 0 0 3px 20px; }
|
|
.idx-doc .n { color: #94a3b8; display: inline-block; min-width: 30px; }
|
|
|
|
/* ── Capítulo ── */
|
|
.cap { page-break-before: always; break-before: page; padding-top: 26mm; text-align: center; }
|
|
.cap .n { font-size: 46pt; font-weight: 800; color: #dbeafe; line-height: 1; }
|
|
.cap h2 { font-size: 22pt; color: #0d47a1; margin: 6px 0 12px; font-weight: 800; }
|
|
.cap p { color: #64748b; font-size: 10.5pt; max-width: 115mm; margin: 0 auto; }
|
|
|
|
/* ── Documento ── */
|
|
.doc { page-break-before: always; break-before: page; }
|
|
.doc-h {
|
|
border-bottom: 2px solid #0d47a1; padding-bottom: 8px; margin-bottom: 16px;
|
|
page-break-after: avoid; break-after: avoid;
|
|
}
|
|
.doc-h .n { font-size: 9pt; color: #2563eb; font-weight: 700; letter-spacing: .08em; }
|
|
.doc-h h3 { font-size: 16pt; color: #0f172a; margin: 2px 0 0; font-weight: 700; }
|
|
|
|
h2, h3, h4 { page-break-after: avoid; break-after: avoid; }
|
|
.doc h2 { font-size: 12.5pt; color: #1565c0; margin: 17px 0 7px; font-weight: 700; }
|
|
.doc h3 { font-size: 11pt; color: #334155; margin: 13px 0 5px; font-weight: 700; }
|
|
.doc h4 { font-size: 10pt; color: #475569; margin: 11px 0 4px; font-weight: 700; }
|
|
.doc p { margin: 0 0 8px; text-align: justify; }
|
|
.doc ul, .doc ol { margin: 0 0 9px; padding-left: 20px; }
|
|
.doc li { margin-bottom: 2px; }
|
|
.doc a { color: #1d4ed8; text-decoration: none; }
|
|
.doc strong { color: #0f172a; }
|
|
|
|
code { background: #f1f5f9; color: #be185d; padding: 1px 4px; border-radius: 3px;
|
|
font-family: "Consolas", "SF Mono", Menlo, monospace; font-size: .86em; }
|
|
pre { background: #f8fafc; border: 1px solid #cbd5e1; border-left: 3px solid #1565c0;
|
|
padding: 9px 12px; border-radius: 4px; margin: 0 0 10px; overflow-x: auto;
|
|
page-break-inside: avoid; break-inside: avoid; }
|
|
pre code { background: none; color: #0f172a; padding: 0; font-size: 8.2pt; line-height: 1.45; }
|
|
|
|
blockquote { border-left: 3px solid #f59e0b; background: #fffbeb; margin: 0 0 10px;
|
|
padding: 8px 13px; page-break-inside: avoid; break-inside: avoid; }
|
|
blockquote p { margin: 0; color: #78350f; font-size: 9.8pt; text-align: left; }
|
|
|
|
.tabla-scroll { margin: 0 0 11px; page-break-inside: avoid; break-inside: avoid; }
|
|
table { width: 100%; border-collapse: collapse; font-size: 9pt; }
|
|
thead { display: table-header-group; }
|
|
th { background: #1565c0; color: #fff; text-align: left; font-weight: 600;
|
|
padding: 5px 8px; border: 1px solid #1565c0; }
|
|
td { padding: 4px 8px; border: 1px solid #e2e8f0; vertical-align: top; }
|
|
tbody tr:nth-child(even) { background: #f8fafc; }
|
|
|
|
hr { border: none; border-top: 1px solid #e2e8f0; margin: 16px 0; }
|
|
|
|
/* Tablas muy largas (inventarios generados) pueden partirse entre páginas */
|
|
.doc table { page-break-inside: auto; }
|
|
tr { page-break-inside: avoid; break-inside: avoid; }
|
|
|
|
@media print {
|
|
.barra { display: none !important; }
|
|
.hoja { max-width: none; margin: 0; padding: 0; }
|
|
a { color: #1e293b !important; text-decoration: none; }
|
|
.portada { min-height: 250mm; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="barra">
|
|
<span class="t">
|
|
<?= $totalDocs ?> documento<?= $totalDocs === 1 ? '' : 's' ?>
|
|
<?= $soloSeccion ? '· ' . $esc($arbol[$soloSeccion]['titulo']) : '· documentación completa' ?>
|
|
</span>
|
|
<?php if ($soloSeccion): ?>
|
|
<a class="sec" href="<?= BASE_URL ?>erp.php?m=soporte&v=imprimir">Exportar todo</a>
|
|
<?php else: ?>
|
|
<?php foreach (DocIndex::seccionesVisibles() as $s => $c): ?>
|
|
<a class="sec" href="<?= BASE_URL ?>erp.php?m=soporte&v=imprimir&s=<?= urlencode($s) ?>">Solo <?= $esc($c['titulo']) ?></a>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
<a class="sec" href="<?= BASE_URL ?>erp.php?m=soporte&v=documentacion">Volver</a>
|
|
<button onclick="window.print()">Imprimir / Guardar como PDF</button>
|
|
</div>
|
|
|
|
<div class="hoja">
|
|
|
|
<!-- ── Portada ── -->
|
|
<div class="portada">
|
|
<?php if (!empty($emp['doc_logo_base64'])): ?>
|
|
<img src="<?= $esc($emp['doc_logo_base64']) ?>" alt="">
|
|
<?php endif; ?>
|
|
<div class="lab"><?= $esc($emp['empresa_nombre']) ?></div>
|
|
<?php if (!empty($emp['empresa_subtitulo'])): ?>
|
|
<div class="sub"><?= $esc($emp['empresa_subtitulo']) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<h1>Documentación<br>del Sistema</h1>
|
|
<div class="lin"></div>
|
|
<p class="desc">
|
|
<?php if ($soloSeccion): ?>
|
|
<?= $esc($arbol[$soloSeccion]['resumen']) ?>
|
|
<?php else: ?>
|
|
Manual de usuario, documentación técnica, arquitectura y guía de operación
|
|
del sistema de gestión del laboratorio.
|
|
<?php endif; ?>
|
|
</p>
|
|
<div class="meta">
|
|
<?php if ($soloSeccion): ?><b><?= $esc($arbol[$soloSeccion]['titulo']) ?></b><br><?php endif; ?>
|
|
<b><?= $totalDocs ?></b> documentos<br>
|
|
Generado el <b><?= date('d/m/Y') ?></b><br>
|
|
<span style="font-size:8.5pt">Los inventarios técnicos reflejan el estado del sistema a esta fecha</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Índice ── -->
|
|
<div class="indice">
|
|
<h2>Contenido</h2>
|
|
<?php foreach ($capitulos as $cap): ?>
|
|
<div class="idx-cap">
|
|
<a href="#<?= $esc($cap['id']) ?>"><?= $esc($cap['num']) ?>. <?= $esc($cap['titulo']) ?></a>
|
|
<p class="res"><?= $esc($cap['resumen']) ?></p>
|
|
<?php foreach ($cap['docs'] as $d): ?>
|
|
<a class="idx-doc" href="#<?= $esc($d['id']) ?>">
|
|
<span class="n"><?= $esc($d['num']) ?></span><?= $esc($d['titulo']) ?>
|
|
</a>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
|
|
<!-- ── Contenido ── -->
|
|
<?php foreach ($capitulos as $cap): ?>
|
|
<div class="cap" id="<?= $esc($cap['id']) ?>">
|
|
<div class="n"><?= $esc($cap['num']) ?></div>
|
|
<h2><?= $esc($cap['titulo']) ?></h2>
|
|
<p><?= $esc($cap['resumen']) ?></p>
|
|
</div>
|
|
<?php foreach ($cap['docs'] as $d): ?>
|
|
<div class="doc" id="<?= $esc($d['id']) ?>">
|
|
<div class="doc-h">
|
|
<div class="n">CAPÍTULO <?= $esc($d['num']) ?></div>
|
|
<h3><?= $esc($d['titulo']) ?></h3>
|
|
</div>
|
|
<?= $d['html'] ?>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php endforeach; ?>
|
|
|
|
</div>
|
|
</body>
|
|
</html>
|