Documentación: visibilidad por rol en cada documento y una sola entrada de menú
Un documento puede declarar qué roles lo ven mediante una cabecera al inicio del archivo; sin ella hereda el permiso de su sección. Los administradores ven todo. Se aplica tanto al índice como al acceso directo por URL, y una sección que queda sin documentos visibles deja de mostrarse. Con esto un recepcionista ve solo el manual de recepción, un bacteriólogo el de toma de muestras y un enfermero el suyo, en vez del manual completo. El módulo declaraba cinco enlaces que apuntaban todos a la misma vista, así que al abrir una sección quedaban dos entradas marcadas como activas a la vez. Queda una sola, "Documentación": la navegación por secciones ya vive dentro de la página. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
32c209710c
commit
dc27184051
@@ -23,6 +23,21 @@ que solo sirve para ordenar:
|
|||||||
El título sale del primer encabezado `#` del archivo. No hay que registrar nada
|
El título sale del primer encabezado `#` del archivo. No hay que registrar nada
|
||||||
en ningún índice: se descubre solo.
|
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
|
## Contenido que se genera solo
|
||||||
|
|
||||||
Estos marcadores, en una línea propia, se reemplazan al cargar la página con
|
Estos marcadores, en una línea propia, se reemplazan al cargar la página con
|
||||||
|
|||||||
@@ -7,6 +7,14 @@
|
|||||||
* Convenciones:
|
* Convenciones:
|
||||||
* docs/<seccion>/<NN>-<slug>.md → el prefijo NN solo ordena, no se muestra
|
* docs/<seccion>/<NN>-<slug>.md → el prefijo NN solo ordena, no se muestra
|
||||||
* El título sale del primer encabezado `# ` del archivo.
|
* 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
|
final class DocIndex
|
||||||
@@ -23,18 +31,63 @@ final class DocIndex
|
|||||||
'resumen' => 'Qué hacer cuando algo falla. Configuraciones críticas.'],
|
'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
|
public static function dir(): string
|
||||||
{
|
{
|
||||||
return __DIR__ . '/docs';
|
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<string,string>, 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? */
|
/** ¿El usuario actual puede ver esta sección? */
|
||||||
public static function puedeVer(string $seccion): bool
|
public static function puedeVer(string $seccion): bool
|
||||||
{
|
{
|
||||||
$cfg = self::SECCIONES[$seccion] ?? null;
|
$cfg = self::SECCIONES[$seccion] ?? null;
|
||||||
if (!$cfg) return false;
|
if (!$cfg) return false;
|
||||||
if (!$cfg['admin']) return true;
|
if (!$cfg['admin']) return true;
|
||||||
return in_array($_SESSION['admin_user']['role'] ?? '', ['admin', 'superadmin'], true);
|
return self::esAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Secciones visibles para el usuario actual. */
|
/** Secciones visibles para el usuario actual. */
|
||||||
@@ -60,6 +113,7 @@ final class DocIndex
|
|||||||
|
|
||||||
$docs = [];
|
$docs = [];
|
||||||
foreach (glob($ruta . '/*.md') ?: [] as $archivo) {
|
foreach (glob($ruta . '/*.md') ?: [] as $archivo) {
|
||||||
|
if (!self::puedeVerDoc($sec, $archivo)) continue;
|
||||||
$base = basename($archivo, '.md');
|
$base = basename($archivo, '.md');
|
||||||
$slug = preg_replace('/^\d+-/', '', $base);
|
$slug = preg_replace('/^\d+-/', '', $base);
|
||||||
$docs[] = [
|
$docs[] = [
|
||||||
@@ -71,6 +125,9 @@ final class DocIndex
|
|||||||
}
|
}
|
||||||
usort($docs, fn($a, $b) => strcmp($a['orden'], $b['orden']));
|
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];
|
$arbol[$sec] = $cfg + ['docs' => $docs];
|
||||||
}
|
}
|
||||||
return $arbol;
|
return $arbol;
|
||||||
@@ -84,7 +141,8 @@ final class DocIndex
|
|||||||
if (!preg_match('/^[a-z0-9-]+$/', $seccion) || !preg_match('/^[a-z0-9-]+$/', $slug)) return null;
|
if (!preg_match('/^[a-z0-9-]+$/', $seccion) || !preg_match('/^[a-z0-9-]+$/', $slug)) return null;
|
||||||
|
|
||||||
foreach (glob(self::dir() . '/' . $seccion . '/*.md') ?: [] as $archivo) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -112,7 +170,7 @@ final class DocIndex
|
|||||||
$out = [];
|
$out = [];
|
||||||
foreach (self::arbol() as $sec => $cfg) {
|
foreach (self::arbol() as $sec => $cfg) {
|
||||||
foreach ($cfg['docs'] as $doc) {
|
foreach ($cfg['docs'] as $doc) {
|
||||||
$texto = @file_get_contents($doc['archivo']) ?: '';
|
[, $texto] = self::leer($doc['archivo']);
|
||||||
// Aplanar: sin marcas, sin saltos, compacto
|
// Aplanar: sin marcas, sin saltos, compacto
|
||||||
$texto = preg_replace('/```.*?```/s', ' ', $texto);
|
$texto = preg_replace('/```.*?```/s', ' ', $texto);
|
||||||
$texto = preg_replace('/[#>*_`|-]+/', ' ', $texto);
|
$texto = preg_replace('/[#>*_`|-]+/', ' ', $texto);
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
---
|
||||||
|
roles: recepcionista, lab_recepcion, supervisor
|
||||||
|
---
|
||||||
|
|
||||||
# Recepción
|
# 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.
|
Guía de la pantalla de recepción del turnero: desde que llamás al paciente hasta que pasa a toma de muestras.
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
---
|
||||||
|
roles: bacteriologo, supervisor
|
||||||
|
---
|
||||||
|
|
||||||
# Toma de muestras
|
# Toma de muestras
|
||||||
|
|
||||||
Guía de la estación de toma de muestras: atender al paciente, recibir sus muestras y firmar los formularios.
|
Guía de la estación de toma de muestras: atender al paciente, recibir sus muestras y firmar los formularios.
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
---
|
||||||
|
roles: enfermero, supervisor
|
||||||
|
---
|
||||||
|
|
||||||
# Enfermeros — domicilios
|
# Enfermeros — domicilios
|
||||||
|
|
||||||
Guía del portal del enfermero: tus visitas domiciliarias, cómo agendarlas y qué hacer en cada una.
|
Guía del portal del enfermero: tus visitas domiciliarias, cómo agendarlas y qué hacer en cada una.
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
---
|
||||||
|
roles: supervisor
|
||||||
|
---
|
||||||
|
|
||||||
# Administración
|
# Administración
|
||||||
|
|
||||||
Tareas de administrador: usuarios, permisos, configuración y reportes.
|
Tareas de administrador: usuarios, permisos, configuración y reportes.
|
||||||
|
|||||||
@@ -5,23 +5,13 @@
|
|||||||
* de secciones (técnica, arquitectura, operación) solo para administradores.
|
* 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 = [
|
$_sopLinks = [
|
||||||
['name' => 'Documentación', 'icon' => 'fas fa-book', 'route' => '/erp.php?m=soporte&v=documentacion'],
|
['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 [
|
return [
|
||||||
'slug' => 'soporte',
|
'slug' => 'soporte',
|
||||||
'name' => 'Soporte',
|
'name' => 'Soporte',
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ $cuerpo = '';
|
|||||||
$toc = [];
|
$toc = [];
|
||||||
|
|
||||||
if ($archivo) {
|
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);
|
$cuerpo = Markdown::render($md);
|
||||||
$toc = Markdown::indice($md);
|
$toc = Markdown::indice($md);
|
||||||
foreach ($arbol[$seccion]['docs'] ?? [] as $d) {
|
foreach ($arbol[$seccion]['docs'] ?? [] as $d) {
|
||||||
|
|||||||
Reference in New Issue
Block a user