Files
whatsapp/core/App.php
T
Lizandro GuarnizoandClaude Sonnet 4.6 b2e7130762 feat(turnero): bacteriólogo sin dashboard/historial, redirige por IP
- Auth: agrega isBacteriologo()
- App: guard que bloquea dashboard e historial para bacteriólogo y
  redirige según IP (turnero_dispositivos); IP registrada → ese lugar,
  IP libre → primer lugar de muestras
- module.php: sidebar bacteriólogo sin Dashboard/Historial; IP
  registrada muestra solo ese lugar, IP libre muestra TV + todos los
  lugares muestras

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-08 07:54:48 -05:00

175 lines
7.0 KiB
PHP

<?php
/**
* core/App.php
* Bootstrap y punto de entrada del ERP.
*
* Uso desde index.php (punto de entrada del módulo ERP):
*
* define('APP_ROOT', __DIR__);
* require_once __DIR__ . '/core/App.php';
* App::run();
*
* Los archivos lab_*.php legacy en raíz siguen funcionando directamente
* sin pasar por App — compatibilidad total garantizada.
*/
// ─── Constante de raíz del proyecto ─────────────────────────────────────────
if (!defined('APP_ROOT')) {
define('APP_ROOT', dirname(__DIR__));
}
// ─── Dependencias del core ───────────────────────────────────────────────────
require_once APP_ROOT . '/config/config.php';
require_once APP_ROOT . '/core/Helpers.php';
require_once APP_ROOT . '/core/Auth.php';
require_once APP_ROOT . '/core/Rbac.php';
require_once APP_ROOT . '/core/Router.php';
require_once APP_ROOT . '/core/Layout.php';
class App
{
// ─── Punto de entrada ────────────────────────────────────────────────────
public static function run(): void
{
self::boot();
$router = new Router();
// Rutas públicas: sin verificación de sesión
if (!$router->isPublic()) {
Auth::requireLogin();
// Enfermeros van a su portal propio, no al panel ERP
if (Auth::isEnfermero()) {
header('Location: ' . APP_ROOT . '/../enfermero_portal.php');
exit;
}
// Bacteriólogos: sin dashboard ni historial; redirige según IP
if (Auth::isBacteriologo()) {
$mod = $router->getModule();
$view = $router->getView();
$blocked = ($mod === 'dashboard')
|| ($mod === 'turnero' && in_array($view, ['dashboard', 'historial'], true));
if ($blocked) {
header('Location: ' . self::bacteDefaultUrl());
exit;
}
}
}
self::dispatch($router);
}
// ─── Bootstrap ──────────────────────────────────────────────────────────
/**
* Inicializa la sesión y la zona horaria.
* Se puede llamar también desde archivos legacy para obtener sesión lista.
*/
public static function boot(): void
{
if (session_status() === PHP_SESSION_NONE) {
// Cookies de sesión seguras
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
}
date_default_timezone_set(defined('TIMEZONE') ? TIMEZONE : 'America/Bogota');
}
// ─── Dispatch ───────────────────────────────────────────────────────────
private static function dispatch(Router $router): void
{
try {
$viewFile = $router->resolveFile();
} catch (RuntimeException $e) {
self::render404($router->getModule(), $router->getView());
return;
}
// Verificar permiso de módulo (solo en rutas protegidas)
if (!$router->isPublic()) {
$module = $router->getModule();
// Si el módulo está registrado en SYSTEM_MODULES, verificar acceso
if (defined('SYSTEM_MODULES') && array_key_exists($module, SYSTEM_MODULES)) {
if (!Rbac::hasModule($module)) {
self::render403($module);
return;
}
}
}
// Ejecutar la vista del módulo
include $viewFile;
}
// ─── Helpers de rol ─────────────────────────────────────────────────────
/**
* URL de destino para bacteriólogo según IP del cliente.
* IP registrada en turnero_dispositivos → ese lugar.
* IP no registrada → primer lugar de tipo muestras.
*/
private static function bacteDefaultUrl(): string
{
$base = '/erp.php?m=turnero&v=lugar&lugar_id=';
try {
$pdo = Database::getInstance()->getConnection();
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
// ¿IP registrada?
$dev = $pdo->prepare(
"SELECT lugar_id FROM turnero_dispositivos WHERE ip = ? AND activo = 1 LIMIT 1"
);
$dev->execute([$ip]);
$row = $dev->fetch(PDO::FETCH_ASSOC);
if ($row) {
return $base . (int)$row['lugar_id'];
}
// Primer lugar de toma de muestras
$first = $pdo->query(
"SELECT id FROM turnero_lugares WHERE activo=1 AND tipo='muestras' ORDER BY sort_order LIMIT 1"
)->fetch(PDO::FETCH_ASSOC);
if ($first) {
return $base . (int)$first['id'];
}
} catch (\Throwable $_) {}
// Fallback: turnero sin vista específica
return '/erp.php?m=turnero';
}
// ─── Páginas de error ────────────────────────────────────────────────────
private static function render404(string $module, string $view): void
{
http_response_code(404);
Layout::open('Página no encontrada', 'fas fa-exclamation-triangle');
echo '<div class="container-fluid py-5 text-center">';
echo '<i class="fas fa-exclamation-triangle fa-4x text-warning mb-3"></i>';
echo '<h2>404 — Módulo no encontrado</h2>';
echo '<p class="text-muted">El módulo <code>' . esc($module) . '</code> / vista <code>' . esc($view) . '</code> no existe.</p>';
echo '<a href="' . esc(APP_URL ?? '/') . '" class="btn btn-primary"><i class="fas fa-home me-1"></i>Ir al inicio</a>';
echo '</div>';
Layout::close();
}
private static function render403(string $module): void
{
http_response_code(403);
Layout::open('Acceso denegado', 'fas fa-lock');
echo '<div class="container-fluid py-5 text-center">';
echo '<i class="fas fa-lock fa-4x text-danger mb-3"></i>';
echo '<h2>403 — Sin permiso</h2>';
echo '<p class="text-muted">Tu rol no tiene acceso al módulo <code>' . esc($module) . '</code>.</p>';
echo '<a href="' . esc(APP_URL ?? '/') . '" class="btn btn-secondary"><i class="fas fa-arrow-left me-1"></i>Volver</a>';
echo '</div>';
Layout::close();
}
}