up
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
<?php
|
||||
/**
|
||||
* core/ModuleRegistry.php
|
||||
* Catálogo dinámico de módulos del ERP.
|
||||
*
|
||||
* Fuentes de datos (prioridad en orden):
|
||||
* 1. Base de datos — tabla system_modules (si la migración 004 se ejecutó)
|
||||
* 2. Archivos modules/{slug}/module.php (escaneo de la carpeta modules/)
|
||||
* 3. Constante SYSTEM_MODULES de config.php (fallback mínimo)
|
||||
*
|
||||
* Los datos de BD prevalecen sobre los descriptores locales para los campos
|
||||
* is_active y sort_order (el administrador puede cambiarlos via UI).
|
||||
*
|
||||
* CACHÉ: estática por request (no Redis necesario).
|
||||
*/
|
||||
|
||||
class ModuleRegistry
|
||||
{
|
||||
/** @var array<string, array>|null Cache del registro completo */
|
||||
private static ?array $registry = null;
|
||||
|
||||
/** Orden de visualización de categorías en el sidebar */
|
||||
private const CATEGORY_ORDER = ['bot', 'lab', 'turnero', 'clinico', 'reportes', 'sistema'];
|
||||
|
||||
/** Etiquetas e íconos de cada categoría */
|
||||
private const CATEGORIES = [
|
||||
'bot' => ['label' => 'WhatsApp', 'icon' => 'fab fa-whatsapp'],
|
||||
'lab' => ['label' => 'Laboratorio', 'icon' => 'fas fa-flask'],
|
||||
'turnero' => ['label' => 'Turnero', 'icon' => 'fas fa-ticket-alt'],
|
||||
'clinico' => ['label' => 'Clínico', 'icon' => 'fas fa-vials'],
|
||||
'reportes'=> ['label' => 'Reportes', 'icon' => 'fas fa-chart-bar'],
|
||||
'sistema' => ['label' => 'Sistema', 'icon' => 'fas fa-cog'],
|
||||
];
|
||||
|
||||
// ─── API pública ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Retorna todos los módulos registrados (activos e inactivos).
|
||||
*
|
||||
* @return array<string, array> Indexado por slug.
|
||||
*/
|
||||
public static function getAll(): array
|
||||
{
|
||||
self::load();
|
||||
return self::$registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna solo los módulos ACTIVOS que el usuario actual puede ver.
|
||||
* Si el usuario es admin sin role_id → todos los activos.
|
||||
*
|
||||
* @return array<string, array> Indexado por slug.
|
||||
*/
|
||||
public static function forCurrentUser(): array
|
||||
{
|
||||
self::load();
|
||||
$result = [];
|
||||
foreach (self::$registry as $slug => $mod) {
|
||||
if (!$mod['is_active']) {
|
||||
continue;
|
||||
}
|
||||
// hasModule() definida en config.php / core/Rbac.php (retrocompat)
|
||||
if (function_exists('hasModule') && !hasModule($slug)) {
|
||||
continue;
|
||||
}
|
||||
$result[$slug] = $mod;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna módulos activos agrupados por categoría para el sidebar.
|
||||
* Solo incluye módulos accesibles por el usuario actual.
|
||||
*
|
||||
* @return array ['bot' => ['label'=>'WhatsApp','icon'=>'...','modules'=>[...]], ...]
|
||||
*/
|
||||
public static function forSidebar(): array
|
||||
{
|
||||
$userModules = self::forCurrentUser();
|
||||
|
||||
// Agrupar por categoría
|
||||
$grouped = [];
|
||||
foreach ($userModules as $slug => $mod) {
|
||||
$cat = $mod['category'] ?? 'sistema';
|
||||
if (!isset($grouped[$cat])) {
|
||||
$catMeta = self::CATEGORIES[$cat] ?? ['label' => ucfirst($cat), 'icon' => 'fas fa-cube'];
|
||||
$grouped[$cat] = [
|
||||
'label' => $catMeta['label'],
|
||||
'icon' => $catMeta['icon'],
|
||||
'modules' => [],
|
||||
];
|
||||
}
|
||||
$grouped[$cat]['modules'][$slug] = $mod;
|
||||
}
|
||||
|
||||
// Ordenar grupos según CATEGORY_ORDER
|
||||
$ordered = [];
|
||||
foreach (self::CATEGORY_ORDER as $cat) {
|
||||
if (isset($grouped[$cat])) {
|
||||
// Ordenar módulos dentro de la categoría por sort_order
|
||||
uasort($grouped[$cat]['modules'], fn($a, $b) => $a['sort_order'] <=> $b['sort_order']);
|
||||
$ordered[$cat] = $grouped[$cat];
|
||||
}
|
||||
}
|
||||
// Categorías extra no definidas en CATEGORY_ORDER al final
|
||||
foreach ($grouped as $cat => $data) {
|
||||
if (!isset($ordered[$cat])) {
|
||||
$ordered[$cat] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
return $ordered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna el descriptor de un módulo por slug, o null si no existe.
|
||||
*/
|
||||
public static function get(string $slug): ?array
|
||||
{
|
||||
self::load();
|
||||
return self::$registry[$slug] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Activa o desactiva un módulo en la base de datos.
|
||||
*
|
||||
* @throws RuntimeException Si no se puede conectar a la BD.
|
||||
*/
|
||||
public static function setActive(string $slug, bool $active): bool
|
||||
{
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
$stmt = $pdo->prepare(
|
||||
"UPDATE system_modules SET is_active = ? WHERE slug = ?"
|
||||
);
|
||||
$ok = $stmt->execute([(int) $active, $slug]);
|
||||
if ($ok) {
|
||||
self::$registry = null; // invalidar caché
|
||||
}
|
||||
return $ok;
|
||||
} catch (Exception $e) {
|
||||
error_log("ModuleRegistry::setActive error: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualiza el sort_order de un módulo.
|
||||
*/
|
||||
public static function setSortOrder(string $slug, int $order): bool
|
||||
{
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
$stmt = $pdo->prepare(
|
||||
"UPDATE system_modules SET sort_order = ? WHERE slug = ?"
|
||||
);
|
||||
$ok = $stmt->execute([$order, $slug]);
|
||||
if ($ok) {
|
||||
self::$registry = null;
|
||||
}
|
||||
return $ok;
|
||||
} catch (Exception $e) {
|
||||
error_log("ModuleRegistry::setSortOrder error: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalida la caché para forzar recarga en la próxima llamada.
|
||||
* Útil después de cambios por el administrador.
|
||||
*/
|
||||
public static function flush(): void
|
||||
{
|
||||
self::$registry = null;
|
||||
}
|
||||
|
||||
// ─── Carga interna ──────────────────────────────────────────────────────
|
||||
|
||||
private static function load(): void
|
||||
{
|
||||
if (self::$registry !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::$registry = [];
|
||||
|
||||
// 1. Escanear module.php locales para obtener metadatos base
|
||||
self::loadFromFiles();
|
||||
|
||||
// 2. Sobreescribir is_active / sort_order desde la BD (si existe)
|
||||
self::mergeFromDatabase();
|
||||
|
||||
// 3. Fallback: añadir slugs de SYSTEM_MODULES que no tengan module.php
|
||||
self::fillFromConstant();
|
||||
}
|
||||
|
||||
/**
|
||||
* Escanea modules/{slug}/module.php y carga sus descriptores.
|
||||
*/
|
||||
private static function loadFromFiles(): void
|
||||
{
|
||||
if (!defined('APP_ROOT')) {
|
||||
return;
|
||||
}
|
||||
$pattern = APP_ROOT . '/modules/*/module.php';
|
||||
foreach (glob($pattern) ?: [] as $file) {
|
||||
$data = @include $file;
|
||||
if (is_array($data) && isset($data['slug'])) {
|
||||
self::$registry[$data['slug']] = self::normalize($data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lee la tabla system_modules y sobreescribe campos controlados por el admin.
|
||||
*/
|
||||
private static function mergeFromDatabase(): void
|
||||
{
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
// Verificar si existe la tabla
|
||||
$check = $pdo->query("SHOW TABLES LIKE 'system_modules'");
|
||||
if (!$check || $check->rowCount() === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rows = $pdo->query(
|
||||
"SELECT slug, name, icon, category, route, is_active, sort_order, oleada, description
|
||||
FROM system_modules ORDER BY sort_order ASC"
|
||||
)->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$slug = $row['slug'];
|
||||
if (isset(self::$registry[$slug])) {
|
||||
// Actualizar solo los campos que maneja el admin
|
||||
self::$registry[$slug]['is_active'] = (bool)(int)$row['is_active'];
|
||||
self::$registry[$slug]['sort_order'] = (int)$row['sort_order'];
|
||||
// Si en BD hay datos más completos, usar los de BD
|
||||
if (!empty($row['name'])) self::$registry[$slug]['name'] = $row['name'];
|
||||
if (!empty($row['icon'])) self::$registry[$slug]['icon'] = $row['icon'];
|
||||
if (!empty($row['category'])) self::$registry[$slug]['category'] = $row['category'];
|
||||
if (!empty($row['route'])) self::$registry[$slug]['route'] = $row['route'];
|
||||
if (!empty($row['description'])) self::$registry[$slug]['description'] = $row['description'];
|
||||
} else {
|
||||
// Módulo solo en BD (sin module.php local)
|
||||
self::$registry[$slug] = self::normalize([
|
||||
'slug' => $slug,
|
||||
'name' => $row['name'],
|
||||
'icon' => $row['icon'],
|
||||
'category' => $row['category'],
|
||||
'route' => $row['route'],
|
||||
'is_active' => (bool)(int)$row['is_active'],
|
||||
'sort_order' => (int)$row['sort_order'],
|
||||
'oleada' => (int)$row['oleada'],
|
||||
'description' => $row['description'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// BD no disponible → seguir con datos de archivos
|
||||
error_log("ModuleRegistry DB merge error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Agrega módulos del array SYSTEM_MODULES que no estén ya registrados.
|
||||
* Garantiza compatibilidad si no se ejecutó la migración 004.
|
||||
*/
|
||||
private static function fillFromConstant(): void
|
||||
{
|
||||
if (!defined('SYSTEM_MODULES')) {
|
||||
return;
|
||||
}
|
||||
foreach (SYSTEM_MODULES as $slug => $name) {
|
||||
if (!isset(self::$registry[$slug])) {
|
||||
self::$registry[$slug] = self::normalize([
|
||||
'slug' => $slug,
|
||||
'name' => $name,
|
||||
'icon' => 'fas fa-cube',
|
||||
'category' => 'sistema',
|
||||
'route' => null,
|
||||
'is_active' => true,
|
||||
'sort_order' => 99,
|
||||
'oleada' => 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normaliza un descriptor garantizando que todos los campos existen.
|
||||
*/
|
||||
private static function normalize(array $data): array
|
||||
{
|
||||
return [
|
||||
'slug' => $data['slug'] ?? '',
|
||||
'name' => $data['name'] ?? ucfirst($data['slug'] ?? ''),
|
||||
'icon' => $data['icon'] ?? 'fas fa-cube',
|
||||
'category' => $data['category'] ?? 'sistema',
|
||||
'route' => $data['route'] ?? null,
|
||||
'is_active' => (bool)($data['is_active'] ?? true),
|
||||
'sort_order' => (int)($data['sort_order'] ?? 99),
|
||||
'oleada' => (int)($data['oleada'] ?? 0),
|
||||
'description' => $data['description'] ?? '',
|
||||
'links' => $data['links'] ?? [],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user