diff --git a/README_DOCS.md b/README_DOCS.md index 4bf6d97..f32247f 100644 --- a/README_DOCS.md +++ b/README_DOCS.md @@ -23,6 +23,21 @@ que solo sirve para ordenar: El título sale del primer encabezado `#` del archivo. No hay que registrar nada en ningún índice: se descubre solo. +## Limitar un documento a ciertos roles + +Por defecto un documento hereda el permiso de su sección. Para restringirlo más, +se declara al inicio del archivo: + + --- + roles: enfermero, supervisor + --- + + # Enfermeros — domicilios + +Solo esos roles lo ven; para el resto no aparece en el índice ni es accesible +por URL. Los administradores ven todo, siempre. Una sección que queda sin +documentos visibles no se muestra. + ## Contenido que se genera solo Estos marcadores, en una línea propia, se reemplazan al cargar la página con diff --git a/modules/soporte/DocIndex.php b/modules/soporte/DocIndex.php index c8b5c39..dc4c8e3 100644 --- a/modules/soporte/DocIndex.php +++ b/modules/soporte/DocIndex.php @@ -7,6 +7,14 @@ * Convenciones: * docs//-.md → el prefijo NN solo ordena, no se muestra * El título sale del primer encabezado `# ` del archivo. + * + * Un documento puede restringir su visibilidad con una cabecera al inicio: + * + * --- + * roles: enfermero, recepcionista + * --- + * + * Sin cabecera, hereda el permiso de su sección. Los administradores ven todo. */ final class DocIndex @@ -23,18 +31,63 @@ final class DocIndex 'resumen' => 'Qué hacer cuando algo falla. Configuraciones críticas.'], ]; + /** Roles que ven todo, sin importar lo que declare cada documento. */ + public const ROLES_TOTALES = ['admin', 'superadmin']; + public static function dir(): string { return __DIR__ . '/docs'; } + private static function rolActual(): string + { + return $_SESSION['admin_user']['role'] ?? ''; + } + + private static function esAdmin(): bool + { + return in_array(self::rolActual(), self::ROLES_TOTALES, true); + } + + /** + * Separa la cabecera del cuerpo de un documento. + * @return array{0: array, 1: string} [metadatos, cuerpo] + */ + public static function leer(string $archivo): array + { + $texto = (string)@file_get_contents($archivo); + if (!preg_match('/^---\R(.*?)\R---\R?(.*)$/s', $texto, $m)) { + return [[], $texto]; + } + $meta = []; + foreach (preg_split('/\R/', $m[1]) as $linea) { + if (preg_match('/^\s*([\w-]+)\s*:\s*(.*)$/', $linea, $kv)) { + $meta[strtolower($kv[1])] = trim($kv[2]); + } + } + return [$meta, ltrim($m[2])]; + } + + /** ¿El usuario actual puede ver este documento concreto? */ + public static function puedeVerDoc(string $seccion, string $archivo): bool + { + if (self::esAdmin()) return true; + if (!self::puedeVer($seccion)) return false; + + [$meta] = self::leer($archivo); + if (empty($meta['roles'])) return true; // sin cabecera: hereda la sección + + $permitidos = array_filter(array_map('trim', explode(',', strtolower($meta['roles'])))); + return in_array(strtolower(self::rolActual()), $permitidos, true); + } + /** ¿El usuario actual puede ver esta sección? */ public static function puedeVer(string $seccion): bool { $cfg = self::SECCIONES[$seccion] ?? null; if (!$cfg) return false; if (!$cfg['admin']) return true; - return in_array($_SESSION['admin_user']['role'] ?? '', ['admin', 'superadmin'], true); + return self::esAdmin(); } /** Secciones visibles para el usuario actual. */ @@ -60,6 +113,7 @@ final class DocIndex $docs = []; foreach (glob($ruta . '/*.md') ?: [] as $archivo) { + if (!self::puedeVerDoc($sec, $archivo)) continue; $base = basename($archivo, '.md'); $slug = preg_replace('/^\d+-/', '', $base); $docs[] = [ @@ -71,6 +125,9 @@ final class DocIndex } usort($docs, fn($a, $b) => strcmp($a['orden'], $b['orden'])); + // Una sección sin documentos visibles no se muestra + if (!$docs) continue; + $arbol[$sec] = $cfg + ['docs' => $docs]; } return $arbol; @@ -84,7 +141,8 @@ final class DocIndex if (!preg_match('/^[a-z0-9-]+$/', $seccion) || !preg_match('/^[a-z0-9-]+$/', $slug)) return null; foreach (glob(self::dir() . '/' . $seccion . '/*.md') ?: [] as $archivo) { - if (preg_replace('/^\d+-/', '', basename($archivo, '.md')) === $slug) return $archivo; + if (preg_replace('/^\d+-/', '', basename($archivo, '.md')) !== $slug) continue; + return self::puedeVerDoc($seccion, $archivo) ? $archivo : null; } return null; } @@ -112,7 +170,7 @@ final class DocIndex $out = []; foreach (self::arbol() as $sec => $cfg) { foreach ($cfg['docs'] as $doc) { - $texto = @file_get_contents($doc['archivo']) ?: ''; + [, $texto] = self::leer($doc['archivo']); // Aplanar: sin marcas, sin saltos, compacto $texto = preg_replace('/```.*?```/s', ' ', $texto); $texto = preg_replace('/[#>*_`|-]+/', ' ', $texto); diff --git a/modules/soporte/docs/manual/20-recepcion.md b/modules/soporte/docs/manual/20-recepcion.md index 7609b2a..b80af4f 100644 --- a/modules/soporte/docs/manual/20-recepcion.md +++ b/modules/soporte/docs/manual/20-recepcion.md @@ -1,3 +1,7 @@ +--- +roles: recepcionista, lab_recepcion, supervisor +--- + # Recepción Guía de la pantalla de recepción del turnero: desde que llamás al paciente hasta que pasa a toma de muestras. diff --git a/modules/soporte/docs/manual/30-toma-de-muestras.md b/modules/soporte/docs/manual/30-toma-de-muestras.md index ac0ccf0..ccaba06 100644 --- a/modules/soporte/docs/manual/30-toma-de-muestras.md +++ b/modules/soporte/docs/manual/30-toma-de-muestras.md @@ -1,3 +1,7 @@ +--- +roles: bacteriologo, supervisor +--- + # Toma de muestras Guía de la estación de toma de muestras: atender al paciente, recibir sus muestras y firmar los formularios. diff --git a/modules/soporte/docs/manual/40-enfermeros.md b/modules/soporte/docs/manual/40-enfermeros.md index 67fdfe8..73b95af 100644 --- a/modules/soporte/docs/manual/40-enfermeros.md +++ b/modules/soporte/docs/manual/40-enfermeros.md @@ -1,3 +1,7 @@ +--- +roles: enfermero, supervisor +--- + # Enfermeros — domicilios Guía del portal del enfermero: tus visitas domiciliarias, cómo agendarlas y qué hacer en cada una. diff --git a/modules/soporte/docs/manual/50-administracion.md b/modules/soporte/docs/manual/50-administracion.md index 67fd21a..ad85b8c 100644 --- a/modules/soporte/docs/manual/50-administracion.md +++ b/modules/soporte/docs/manual/50-administracion.md @@ -1,3 +1,7 @@ +--- +roles: supervisor +--- + # Administración Tareas de administrador: usuarios, permisos, configuración y reportes. diff --git a/modules/soporte/module.php b/modules/soporte/module.php index 582a073..819d411 100644 --- a/modules/soporte/module.php +++ b/modules/soporte/module.php @@ -5,23 +5,13 @@ * de secciones (técnica, arquitectura, operación) solo para administradores. */ -$_sopEsAdmin = in_array($_SESSION['admin_user']['role'] ?? '', ['admin', 'superadmin'], true); - +// Una sola entrada en el menú: la navegación por secciones y documentos vive +// dentro de la propia página. Tener aquí un enlace por sección hacía que al +// abrir una quedaran dos entradas marcadas como activas a la vez. $_sopLinks = [ ['name' => 'Documentación', 'icon' => 'fas fa-book', 'route' => '/erp.php?m=soporte&v=documentacion'], - ['name' => 'Manual de usuario', 'icon' => 'fas fa-book-reader', - 'route' => '/erp.php?m=soporte&v=documentacion&s=manual'], ]; -if ($_sopEsAdmin) { - $_sopLinks[] = ['name' => 'Documentación técnica', 'icon' => 'fas fa-code', - 'route' => '/erp.php?m=soporte&v=documentacion&s=tecnica']; - $_sopLinks[] = ['name' => 'Arquitectura', 'icon' => 'fas fa-sitemap', - 'route' => '/erp.php?m=soporte&v=documentacion&s=arquitectura']; - $_sopLinks[] = ['name' => 'Operación y soporte', 'icon' => 'fas fa-life-ring', - 'route' => '/erp.php?m=soporte&v=documentacion&s=operacion']; -} - return [ 'slug' => 'soporte', 'name' => 'Soporte', diff --git a/modules/soporte/views/documentacion.php b/modules/soporte/views/documentacion.php index 1972479..e231c80 100644 --- a/modules/soporte/views/documentacion.php +++ b/modules/soporte/views/documentacion.php @@ -24,7 +24,8 @@ $cuerpo = ''; $toc = []; if ($archivo) { - $md = Generadores::expandir((string)file_get_contents($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) {