Compare commits

10 Commits
Author SHA1 Message Date
Lizandro GuarnizoandClaude Sonnet 4.6 062f565ab5 fix(pacientes): corregir acceso a d.found y d.user_id en verificarWhatsapp
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 09:29:36 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 6d63112d76 fix(api): convertir user_id vacío a null antes de guardar paciente
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 09:28:43 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 9e7637582d fix(api): agregar db() helper, corregir requireAuth() en check_whatsapp
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 09:27:03 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 92e4d01181 feat(sidebar): color por categoría via CSS custom property --cat-color
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 09:05:57 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 e90cfda3f3 feat(dashboard): accesos rápidos en dropdown por grupo, filtrado por rol
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 09:03:57 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 d6e1d083f4 revert(sidebar): eliminar sub-acordeón — restaurar sidebar y module.php a estado estable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 08:56:42 -05:00
Lizandro Guarnizo f724e2e94c Revert "fix(recepcion): mostrar nombre, cédula y celular en resultados de búsqueda de paciente"
This reverts commit 1af731ee44.
2026-06-24 08:54:17 -05:00
Lizandro Guarnizo 86be0669af Revert "fix(recepcion): reemplazar template literals anidados por concatenación compatible"
This reverts commit 0021af36a5.
2026-06-24 08:54:17 -05:00
Lizandro Guarnizo 3b1b7c9333 Revert "fix(sidebar): null-check en route de children para evitar TypeError en PHP 8.2"
This reverts commit ba27511414.
2026-06-24 08:54:17 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 ba27511414 fix(sidebar): null-check en route de children para evitar TypeError en PHP 8.2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 08:52:26 -05:00
10 changed files with 132 additions and 164 deletions
+7
View File
@@ -48,6 +48,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
requireAuthentication();
/**
* Shorthand para obtener la conexión PDO.
*/
function db(): \PDO {
return Database::getInstance()->getConnection();
}
/**
* ID del admin autenticado actualmente.
*/
+1 -1
View File
@@ -4,7 +4,7 @@
* Verifica si un número de teléfono está registrado en WhatsApp (tabla users).
*/
require_once __DIR__ . '/_helpers.php';
requireAuth();
// requireAuthentication() ya fue llamado por _helpers.php
$phone = trim($_GET['phone'] ?? '');
if (!$phone) { jsonError('phone requerido', 400); }
+5
View File
@@ -13,6 +13,11 @@ requireMethod('POST');
try {
$datos = inputJson();
// user_id vacío → null para evitar error de tipo en la BD
if (isset($datos['user_id']) && $datos['user_id'] === '') {
$datos['user_id'] = null;
}
// ── Validación de formato de campos del paciente ────────────────────────
$nombreVal = trim($datos['nombre_completo'] ?? '');
if ($nombreVal !== '') {
+3 -2
View File
@@ -132,11 +132,12 @@ body {
}
.sidebar-menu .nav-link.active {
color: var(--brand-dark, #0d47a1);
color: var(--cat-color, var(--brand-dark, #0d47a1));
background: rgba(255, 255, 255, 0.95);
font-weight: 600;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
transform: translateX(3px);
border-left: 3px solid var(--cat-color, var(--brand, #1565c0));
}
.sidebar-menu .nav-link i {
@@ -169,7 +170,7 @@ body {
}
.sidebar-menu li.sidebar-section:hover { color: rgba(255,255,255,.9); background: rgba(255,255,255,.07); }
.sidebar-menu li.sidebar-section.has-active {
border-left-color: rgba(255,255,255,.7);
border-left-color: var(--cat-color, rgba(255,255,255,.7));
background: rgba(255,255,255,.1);
color: #fff;
}
+1 -1
View File
@@ -65,7 +65,7 @@ class Layout
<!-- Font Awesome 6 -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<!-- Estilos del sistema -->
<link href="<?= $base ?>/assets/css/styles.css?v=13" rel="stylesheet">
<link href="<?= $base ?>/assets/css/styles.css?v=14" rel="stylesheet">
<style>
:root {
+2 -2
View File
@@ -531,9 +531,9 @@ async function verificarWhatsapp() {
try {
const r = await fetch(`api/lab/check_whatsapp.php?phone=${encodeURIComponent(tel)}`);
const d = await r.json();
if (d.ok && d.data.found) {
if (d.ok && d.found) {
badge.innerHTML = '<span class="text-success"><i class="fab fa-whatsapp me-1"></i>Registrado en WhatsApp</span>';
hiddenUid.value = d.data.user_id;
hiddenUid.value = d.user_id;
} else {
badge.innerHTML = '<span class="text-muted"><i class="fas fa-times-circle me-1"></i>Sin WhatsApp registrado</span>';
hiddenUid.value = '';
+47 -50
View File
@@ -1,18 +1,22 @@
<?php
/**
* Descriptor del módulo Turnero — Oleada 1
* Links filtrados por rol con sub-acordeones para recepción y muestras.
* Los links visibles se filtran según el rol del usuario.
*/
$_trRole = $_SESSION['admin_user']['role'] ?? 'admin';
$_trIsRecep = $_trRole === 'recepcionista';
$_trIsBacte = $_trRole === 'bacteriologo';
$_trRole = $_SESSION['admin_user']['role'] ?? 'admin';
$_trAdminRoles = ['superadmin', 'admin', 'supervisor'];
$_trIsAdmin = in_array($_trRole, $_trAdminRoles, true);
$_trIsRecep = $_trRole === 'recepcionista';
$_trIsBacte = $_trRole === 'bacteriologo';
try {
$_trPdo = Database::getInstance()->getConnection();
$_trDesks = $_trPdo->query(
"SELECT id, nombre FROM turnero_lugares WHERE activo=1 AND tipo='recepcion' ORDER BY sort_order"
)->fetchAll(PDO::FETCH_ASSOC);
$_trMuestras = $_trPdo->query(
"SELECT id, nombre FROM turnero_lugares WHERE activo=1 AND tipo='muestras' ORDER BY sort_order"
)->fetchAll(PDO::FETCH_ASSOC);
@@ -21,67 +25,60 @@ try {
$_trMuestras = [];
}
// Sub-links de recepción
$_trRecepChildren = [];
foreach ($_trDesks as $_d) {
$_trRecepChildren[] = [
'name' => $_d['nombre'],
'icon' => 'fas fa-chair',
'route' => '/erp.php?m=turnero&v=recepcion&desk_id=' . (int)$_d['id'],
];
}
if (empty($_trRecepChildren)) {
$_trRecepChildren[] = ['name' => 'Recepción', 'icon' => 'fas fa-chair', 'route' => '/erp.php?m=turnero&v=recepcion'];
}
// Sub-links de muestras
$_trMuestrasChildren = [];
foreach ($_trMuestras as $_m) {
$_trMuestrasChildren[] = [
'name' => $_m['nombre'],
'icon' => 'fas fa-flask',
'route' => '/erp.php?m=turnero&v=lugar&lugar_id=' . (int)$_m['id'],
];
}
$_trLinks = [];
// ── Recepcionista ─────────────────────────────────────────────
// ── Recepcionista: solo sus escritorios + pantalla TV ─────────
if ($_trIsRecep) {
$_trLinks[] = ['name' => 'Pantalla TV', 'icon' => 'fas fa-tv', 'route' => '/erp.php?m=turnero&v=display_global'];
if (count($_trRecepChildren) > 1) {
$_trLinks[] = ['name' => 'Recepción', 'icon' => 'fas fa-concierge-bell', 'route' => null, 'children' => $_trRecepChildren];
if (!empty($_trDesks)) {
foreach ($_trDesks as $_d) {
$_trLinks[] = [
'name' => $_d['nombre'],
'icon' => 'fas fa-concierge-bell',
'route' => '/erp.php?m=turnero&v=recepcion&desk_id=' . (int)$_d['id'],
];
}
} else {
$_trLinks[] = array_merge($_trRecepChildren[0], ['icon' => 'fas fa-concierge-bell']);
$_trLinks[] = ['name' => 'Recepción', 'icon' => 'fas fa-concierge-bell', 'route' => '/erp.php?m=turnero&v=recepcion'];
}
// ── Bacteriólogo ──────────────────────────────────────────────
// ── Bacteriólogo: dashboard, historial, TV + sus estaciones ──
} elseif ($_trIsBacte) {
$_trLinks[] = ['name' => 'Dashboard', 'icon' => 'fas fa-tachometer-alt', 'route' => '/erp.php?m=turnero&v=dashboard'];
$_trLinks[] = ['name' => 'Historial', 'icon' => 'fas fa-history', 'route' => '/erp.php?m=turnero&v=historial'];
$_trLinks[] = ['name' => 'Pantalla TV', 'icon' => 'fas fa-tv', 'route' => '/erp.php?m=turnero&v=display_global'];
if (count($_trMuestrasChildren) > 1) {
$_trLinks[] = ['name' => 'Estaciones', 'icon' => 'fas fa-flask', 'route' => null, 'children' => $_trMuestrasChildren];
} elseif (!empty($_trMuestrasChildren)) {
$_trLinks[] = $_trMuestrasChildren[0];
foreach ($_trMuestras as $_m) {
$_trLinks[] = [
'name' => $_m['nombre'],
'icon' => 'fas fa-flask',
'route' => '/erp.php?m=turnero&v=lugar&lugar_id=' . (int)$_m['id'],
];
}
// ── Admin / Supervisor / otros ────────────────────────────────
// ── Admin / Supervisor / otros: acceso completo ───────────────
} else {
$_trLinks[] = ['name' => 'Dashboard', 'icon' => 'fas fa-tachometer-alt', 'route' => '/erp.php?m=turnero&v=dashboard'];
$_trLinks[] = ['name' => 'Historial', 'icon' => 'fas fa-history', 'route' => '/erp.php?m=turnero&v=historial'];
$_trLinks[] = ['name' => 'Configuración', 'icon' => 'fas fa-sliders-h', 'route' => '/erp.php?m=turnero&v=configuracion'];
$_trLinks[] = ['name' => 'Kiosko', 'icon' => 'fas fa-desktop', 'route' => '/erp.php?m=turnero&v=kiosko'];
$_trLinks[] = ['name' => 'Pantalla TV', 'icon' => 'fas fa-tv', 'route' => '/erp.php?m=turnero&v=display_global'];
if (count($_trRecepChildren) > 1) {
$_trLinks[] = ['name' => 'Recepción', 'icon' => 'fas fa-concierge-bell', 'route' => null, 'children' => $_trRecepChildren];
} elseif (!empty($_trRecepChildren)) {
$_trLinks[] = array_merge($_trRecepChildren[0], ['icon' => 'fas fa-concierge-bell']);
$_trLinks[] = ['name' => 'Dashboard', 'icon' => 'fas fa-tachometer-alt', 'route' => '/erp.php?m=turnero&v=dashboard'];
$_trLinks[] = ['name' => 'Historial', 'icon' => 'fas fa-history', 'route' => '/erp.php?m=turnero&v=historial'];
$_trLinks[] = ['name' => 'Configuración', 'icon' => 'fas fa-sliders-h', 'route' => '/erp.php?m=turnero&v=configuracion'];
$_trLinks[] = ['name' => 'Kiosko', 'icon' => 'fas fa-desktop', 'route' => '/erp.php?m=turnero&v=kiosko'];
$_trLinks[] = ['name' => 'Pantalla TV Global', 'icon' => 'fas fa-th-large', 'route' => '/erp.php?m=turnero&v=display_global'];
if (!empty($_trDesks)) {
foreach ($_trDesks as $_d) {
$_trLinks[] = [
'name' => $_d['nombre'],
'icon' => 'fas fa-concierge-bell',
'route' => '/erp.php?m=turnero&v=recepcion&desk_id=' . (int)$_d['id'],
];
}
} else {
$_trLinks[] = ['name' => 'Recepción', 'icon' => 'fas fa-concierge-bell', 'route' => '/erp.php?m=turnero&v=recepcion'];
}
if (count($_trMuestrasChildren) > 1) {
$_trLinks[] = ['name' => 'Estaciones', 'icon' => 'fas fa-flask', 'route' => null, 'children' => $_trMuestrasChildren];
} elseif (!empty($_trMuestrasChildren)) {
$_trLinks[] = $_trMuestrasChildren[0];
foreach ($_trMuestras as $_m) {
$_trLinks[] = [
'name' => $_m['nombre'],
'icon' => 'fas fa-flask',
'route' => '/erp.php?m=turnero&v=lugar&lugar_id=' . (int)$_m['id'],
];
}
}
+41 -13
View File
@@ -116,26 +116,54 @@ Layout::open('Dashboard Turnero', 'fas fa-chart-bar');
</style>
<?php
// Cargar lugares activos para los botones de acceso rápido
$_dbLugares = [];
$_dashRole = $_SESSION['admin_user']['role'] ?? 'admin';
$_dashIsAdmin = in_array($_dashRole, ['superadmin', 'admin', 'supervisor'], true);
$_dashIsRecep = $_dashRole === 'recepcionista';
$_dashIsBacte = $_dashRole === 'bacteriologo';
$_dashReceps = [];
$_dashMuestras = [];
try {
$_pdo = Database::getInstance()->getConnection();
// Recepción: primer escritorio
$_receps = $_pdo->query("SELECT id, nombre FROM turnero_lugares WHERE activo=1 AND tipo='recepcion' ORDER BY sort_order")->fetchAll(PDO::FETCH_ASSOC);
$_muestras = $_pdo->query("SELECT id, nombre FROM turnero_lugares WHERE activo=1 AND tipo='muestras' ORDER BY sort_order")->fetchAll(PDO::FETCH_ASSOC);
foreach ($_receps as $_l) $_dbLugares[] = ['icon'=>'fas fa-concierge-bell', 'label'=>$_l['nombre'], 'url'=>BASE_URL.'erp.php?m=turnero&v=recepcion&desk_id='.$_l['id']];
foreach ($_muestras as $_l) $_dbLugares[] = ['icon'=>'fas fa-flask', 'label'=>$_l['nombre'], 'url'=>BASE_URL.'erp.php?m=turnero&v=lugar&lugar_id='.$_l['id']];
if ($_dashIsAdmin || $_dashIsRecep)
$_dashReceps = $_pdo->query("SELECT id, nombre FROM turnero_lugares WHERE activo=1 AND tipo='recepcion' ORDER BY sort_order")->fetchAll(PDO::FETCH_ASSOC);
if ($_dashIsAdmin || $_dashIsBacte)
$_dashMuestras = $_pdo->query("SELECT id, nombre FROM turnero_lugares WHERE activo=1 AND tipo='muestras' ORDER BY sort_order")->fetchAll(PDO::FETCH_ASSOC);
} catch (\Throwable $_) {}
?>
<div class="page-header">
<h1><i class="fas fa-chart-bar me-2 text-primary"></i>Dashboard Turnero</h1>
<!-- Accesos rápidos a lugares -->
<?php foreach ($_dbLugares as $_btn): ?>
<a href="<?= htmlspecialchars($_btn['url'], ENT_QUOTES) ?>" class="btn btn-sm btn-outline-primary">
<i class="<?= $_btn['icon'] ?> me-1"></i><?= htmlspecialchars($_btn['label'], ENT_QUOTES) ?>
</a>
<?php endforeach; ?>
<!-- Accesos rápidos: dropdown por grupo, filtrado por rol -->
<?php if (!empty($_dashReceps)): ?>
<div class="dropdown">
<button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="fas fa-concierge-bell me-1"></i>Recepción
</button>
<ul class="dropdown-menu shadow-sm">
<?php foreach ($_dashReceps as $_r): ?>
<li><a class="dropdown-item" href="<?= BASE_URL ?>erp.php?m=turnero&v=recepcion&desk_id=<?= (int)$_r['id'] ?>">
<i class="fas fa-chair me-1 text-primary"></i><?= htmlspecialchars($_r['nombre'], ENT_QUOTES) ?>
</a></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php if (!empty($_dashMuestras)): ?>
<div class="dropdown">
<button class="btn btn-sm btn-outline-success dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="fas fa-flask me-1"></i>Toma de Muestras
</button>
<ul class="dropdown-menu shadow-sm">
<?php foreach ($_dashMuestras as $_m): ?>
<li><a class="dropdown-item" href="<?= BASE_URL ?>erp.php?m=turnero&v=lugar&lugar_id=<?= (int)$_m['id'] ?>">
<i class="fas fa-vial me-1 text-success"></i><?= htmlspecialchars($_m['nombre'], ENT_QUOTES) ?>
</a></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<!-- Selector de fecha -->
<div class="d-flex align-items-center gap-2 ms-auto">
+7 -12
View File
@@ -150,8 +150,7 @@ $adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['
}
.pac-resultado:hover { background: #f1f5f9; }
.pac-resultado .pac-nombre { font-weight: 600; font-size: .9rem; }
.pac-resultado .pac-doc { font-size: .78rem; color: #64748b; display:flex; gap:10px; flex-wrap:wrap; margin-top:2px; }
.pac-resultado .pac-doc span { display:flex; align-items:center; gap:4px; }
.pac-resultado .pac-doc { font-size: .78rem; color: #64748b; }
.pac-seleccionado {
background: #eff6ff; border: 1px solid #93c5fd;
border-radius: 8px; padding: .65rem .9rem;
@@ -907,16 +906,12 @@ async function buscarPaciente() {
<a href="#" onclick="abrirNuevoPaciente('${escHtml(q)}');return false">Crear nuevo</a></div>`;
return;
}
lista.innerHTML = datos.map(function(p) {
var doc = (p.tipo_documento||'') + (p.numero_documento ? ' ' + p.numero_documento : '');
var tel = p.telefono || '';
var docHtml = doc.trim() ? '<span><i class="fas fa-id-card"></i> ' + escHtml(doc) + '</span>' : '';
var telHtml = tel ? '<span><i class="fas fa-phone"></i> ' + escHtml(tel) + '</span>' : '';
return '<div class="pac-resultado" onclick="seleccionarPaciente(' + JSON.stringify(p).replace(/"/g,'&quot;') + ')">'
+ '<div class="pac-nombre">' + escHtml(p.nombre_completo||'Sin nombre') + '</div>'
+ '<div class="pac-doc">' + docHtml + telHtml + '</div>'
+ '</div>';
}).join('');
lista.innerHTML = datos.map(p => `
<div class="pac-resultado" onclick="seleccionarPaciente(${JSON.stringify(p).replace(/"/g,'&quot;')})">
<div class="pac-nombre">${escHtml(p.nombre_completo||p.full_name||p.nombre||'')}</div>
<div class="pac-doc">${escHtml(p.tipo_documento||'')} ${escHtml(p.documento||p.numero_documento||'')}
${p.telefono||p.celular ? '· ' + escHtml(p.telefono||p.celular) : ''}</div>
</div>`).join('');
} catch (_) {}
}
+18 -83
View File
@@ -135,6 +135,16 @@ $_base = defined('APP_URL') ? rtrim(APP_URL, '/') : '';
<ul class="sidebar-menu" id="sb-menu">
<?php
// ── Paleta de colores por categoría ────────────────────────────────────────
$_catPalette = [
'lab' => '#60a5fa',
'bot' => '#4ade80',
'turnero' => '#fbbf24',
'clinico' => '#c084fc',
'formularios' => '#22d3ee',
'sistema' => '#94a3b8',
];
// ── Renderizado dinámico desde ModuleRegistry ───────────────────────────────
if (class_exists('ModuleRegistry', false)) {
$sidebarGroups = ModuleRegistry::forSidebar();
@@ -155,10 +165,12 @@ if (class_exists('ModuleRegistry', false)) {
if ($__route && $_isActive($__route) !== '') { $_catHasActive = true; break; }
}
}
$catId = 'sb-cat-' . htmlspecialchars($catKey, ENT_QUOTES, 'UTF-8');
$catId = 'sb-cat-' . htmlspecialchars($catKey, ENT_QUOTES, 'UTF-8');
$_catColor = $_catPalette[$catKey] ?? '#94a3b8';
?>
<li class="sidebar-section<?= $_catHasActive ? ' has-active' : '' ?>"
data-cat="<?= $catId ?>"
style="--cat-color:<?= $_catColor ?>"
onclick="sbToggleCat('<?= $catId ?>',this,event)">
<span class="sb-cat-label">
<i class="<?= htmlspecialchars($catData['icon'], ENT_QUOTES, 'UTF-8') ?> me-1"></i>
@@ -166,48 +178,20 @@ if (class_exists('ModuleRegistry', false)) {
</span>
<i class="fas fa-chevron-down sb-chevron"></i>
</li>
<li class="sb-group" id="<?= $catId ?>">
<li class="sb-group" id="<?= $catId ?>" style="--cat-color:<?= $_catColor ?>">
<ul>
<?php foreach ($catData['modules'] as $slug => $mod):
$links = $mod['links'] ?? [];
if (!empty($links)):
foreach ($links as $lnkIdx => $lnk):
$children = $lnk['children'] ?? [];
if (!empty($children)):
// Sub-acordeón para links con hijos
$subId = $catId . '-sub-' . $slug . '-' . $lnkIdx;
$hasActive = false;
foreach ($children as $ch) { if ($_isActive($ch['route']) !== '') { $hasActive = true; break; } }
?>
<li class="sb-sub-header<?= $hasActive ? ' has-active' : '' ?>"
onclick="sbToggleSub('<?= $subId ?>',this,event)">
<i class="<?= htmlspecialchars($lnk['icon'], ENT_QUOTES, 'UTF-8') ?>"></i>
<span><?= htmlspecialchars($lnk['name'], ENT_QUOTES, 'UTF-8') ?></span>
<i class="fas fa-chevron-down sb-sub-chev"></i>
</li>
<li class="sb-sub-group<?= $hasActive ? '' : ' collapsed' ?>" id="<?= $subId ?>">
<ul>
<?php foreach ($children as $ch):
$href = $_base . $ch['route'];
$active = $_isActive($ch['route']);
?>
<li><a href="<?= htmlspecialchars($href, ENT_QUOTES, 'UTF-8') ?>"
class="nav-link nav-link-sub<?= $active ?>">
<i class="<?= htmlspecialchars($ch['icon'], ENT_QUOTES, 'UTF-8') ?>"></i>
<?= htmlspecialchars($ch['name'], ENT_QUOTES, 'UTF-8') ?></a></li>
<?php endforeach; ?>
</ul>
</li>
<?php else:
$href = $_base . ($lnk['route'] ?? '');
$active = isset($lnk['route']) ? $_isActive($lnk['route']) : '';
foreach ($links as $lnk):
$href = $_base . $lnk['route'];
$active = $_isActive($lnk['route']);
?>
<li><a href="<?= htmlspecialchars($href, ENT_QUOTES, 'UTF-8') ?>"
class="nav-link<?= $active ?>">
<i class="<?= htmlspecialchars($lnk['icon'], ENT_QUOTES, 'UTF-8') ?>"></i>
<?= htmlspecialchars($lnk['name'], ENT_QUOTES, 'UTF-8') ?></a></li>
<?php endif;
endforeach;
<?php endforeach;
else:
$route = $mod['route'] ?? '';
$href = $route ? $_base . $route : '#';
@@ -382,25 +366,6 @@ if (class_exists('ModuleRegistry', false)) {
.sb-group { overflow: hidden; transition: max-height .28s ease, opacity .2s ease; }
.sb-group > ul { list-style: none; padding: 0 0 4px; margin: 0; }
.sb-group.collapsed { max-height: 0 !important; opacity: 0; pointer-events: none; }
/* Sub-acordeón dentro de un módulo */
.sb-sub-header {
display: flex; align-items: center; gap: 8px;
padding: 7px 16px 7px 20px;
cursor: pointer; user-select: none;
color: rgba(255,255,255,.75);
font-size: .84rem;
border-radius: 6px; margin: 1px 6px;
transition: background .15s, color .15s;
}
.sb-sub-header:hover, .sb-sub-header.has-active { color: #fff; background: rgba(255,255,255,.1); }
.sb-sub-header span { flex: 1; }
.sb-sub-chev { font-size: .6rem; opacity: .7; transition: transform .22s ease; flex-shrink: 0; }
.sb-sub-header.collapsed .sb-sub-chev { transform: rotate(-90deg); }
.sb-sub-group { overflow: hidden; transition: max-height .25s ease, opacity .2s ease; }
.sb-sub-group > ul { list-style: none; padding: 0; margin: 0; }
.sb-sub-group.collapsed { max-height: 0 !important; opacity: 0; pointer-events: none; }
.nav-link-sub { padding-left: 36px !important; font-size: .82rem !important; }
</style>
<script>
@@ -427,28 +392,6 @@ if (class_exists('ModuleRegistry', false)) {
}
}
window.sbToggleSub = function(subId, headerEl, ev) {
if (ev) { ev.preventDefault(); ev.stopPropagation(); }
var group = document.getElementById(subId);
if (!group) return;
var collapsed = group.classList.contains('collapsed');
if (collapsed) {
group.classList.remove('collapsed');
group.style.maxHeight = group.scrollHeight + 'px';
group.style.opacity = '1';
headerEl.classList.remove('collapsed');
} else {
group.style.maxHeight = group.scrollHeight + 'px';
group.style.opacity = '1';
requestAnimationFrame(function(){
group.classList.add('collapsed');
headerEl.classList.add('collapsed');
group.style.maxHeight = '0';
group.style.opacity = '0';
});
}
};
window.sbToggleCat = function(catId, sectionEl, ev) {
if (ev) { ev.preventDefault(); ev.stopPropagation(); }
var nav = document.getElementById('erp-sidebar');
@@ -535,14 +478,6 @@ if (class_exists('ModuleRegistry', false)) {
style.textContent = '.sb-group { transition: none !important; }';
document.head.appendChild(style);
// Inicializar sub-grupos
document.querySelectorAll('.sb-sub-group').forEach(function(sg) {
if (!sg.classList.contains('collapsed')) {
sg.style.maxHeight = sg.scrollHeight + 'px';
sg.style.opacity = '1';
}
});
document.querySelectorAll('.sb-group').forEach(function(group) {
var catId = group.id;
var section = document.querySelector('[data-cat="' + catId + '"]');