// ============================================================ // App principal: layout + router por rol // ============================================================ const STORAGE_KEY = window.APP_CONFIG.LOCAL_STORAGE_KEYS.APP_STATE; const loadState = () => { try { const raw = localStorage.getItem(STORAGE_KEY); if (raw) return JSON.parse(raw); } catch (e) {} return null; }; const saveState = (state) => { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) {} }; const App = () => { const stored = loadState(); const [currentUser, setCurrentUser] = useState(null); const [authState, setAuthState] = useState('initializing'); const [authRetry, setAuthRetry] = useState(0); const [screen, setScreen] = useState(stored?.screen || 'dashboard'); const [libros, setLibros] = useState(() => window.APP_CONFIG.AUTH.MODE === 'server' ? [] : (stored?.libros || APP_DATA.LIBROS) ); const [usuarios, setUsuarios] = useState( stored?.usuarios || APP_DATA.USUARIOS ); const [selectedBook, setSelectedBook] = useState(null); // Sincroniza usuarios con APP_DATA para conservar referencias locales. useEffect(() => { APP_DATA.USUARIOS = usuarios; }, [usuarios]); /** * Define la pantalla inicial de acuerdo con el rol. * * El administrador debe entrar siempre al Dashboard. * El orden de las validaciones es importante para usuarios con varios roles. */ const initialScreenForUser = (user) => { if (window.authMapper.hasRole(user, 'administrator')) { return 'dashboard'; } if (window.authMapper.hasRole(user, 'supervisor')) { return 'supervisor'; } if (window.authMapper.hasRole(user, 'digitizer')) { return 'digitalizacion'; } if (window.authMapper.hasRole(user, 'editor')) { return 'edicion'; } if (window.authMapper.hasRole(user, 'quality')) { return 'cc'; } if (window.authMapper.hasRole(user, 'converter')) { return 'conversion'; } return 'dashboard'; }; useEffect(() => { let active = true; const restoreSession = async () => { setAuthState('initializing'); try { const initialized = await window.persistenceAdapter.initialize(); if (!active) return; if ( (initialized.mode === 'server' && !initialized.apiAvailable) || ( initialized.mode === 'local_demo' && !initialized.localDemoAllowed ) ) { setCurrentUser(null); setAuthState('unavailable'); return; } if (!window.APP_CONFIG.AUTH.SESSION_CHECK_ON_START) { setAuthState('unauthenticated'); return; } const session = await window.persistenceAdapter.getSession(); if (!active) return; if (session.authenticated && session.user) { setCurrentUser(session.user); setScreen(initialScreenForUser(session.user)); setAuthState('authenticated'); } else { setCurrentUser(null); setAuthState('unauthenticated'); } } catch (error) { if (!active) return; setCurrentUser(null); setAuthState('unavailable'); } }; restoreSession(); return () => { active = false; }; }, [authRetry]); // Persistencia exclusiva para modo de demostración local. useEffect(() => { if ( window.persistenceAdapter.mode !== 'local_demo' || !window.APP_CONFIG.AUTH.ALLOW_LOCAL_DEMO ) { return; } const state = { screen, libros, usuarios, currentUser, }; saveState(state); }, [currentUser, screen, libros, usuarios]); const onLogin = (user) => { setCurrentUser(user); setScreen(initialScreenForUser(user)); setAuthState('authenticated'); }; const clearVisualSession = () => { setCurrentUser(null); setSelectedBook(null); setScreen('dashboard'); setAuthState('unauthenticated'); }; const onSessionExpired = () => { clearVisualSession(); }; const onLogout = async () => { try { await window.persistenceAdapter.logout(); } catch (error) { console.warn( 'No fue posible confirmar el cierre de sesión con el servidor.' ); } finally { clearVisualSession(); } }; const updateLibro = (cons, patch) => { setLibros((previous) => previous.map((libro) => libro.cons === cons ? { ...libro, ...patch } : libro ) ); }; const updateUsuario = (id, data) => { setUsuarios((previous) => previous.map((usuario) => usuario.id === id ? { ...usuario, ...data } : usuario ) ); if ( currentUser?.authSource === 'local_demo' && currentUser.id === id ) { setCurrentUser((user) => window.authMapper.normalizeAuthUser( { ...user, ...data }, 'local_demo' ) ); } }; const createUsuario = (data) => { if (usuarios.find((usuario) => usuario.id === data.id)) { alert( `El usuario "${data.id}" ya existe. Elige otro nombre.` ); return; } setUsuarios((previous) => [ ...previous, { ...data }, ]); }; const deleteUsuario = (id) => { setUsuarios((previous) => previous.filter((usuario) => usuario.id !== id) ); }; if (authState !== 'authenticated' || !currentUser) { return ( setAuthRetry((value) => value + 1) } /> ); } const isSupervisor = window.authMapper.hasAnyRole( currentUser, ['administrator', 'supervisor'] ); const isServerMode = window.APP_CONFIG.AUTH.MODE === 'server'; // Pantallas disponibles de acuerdo con el rol. const navItems = []; navItems.push({ key: 'dashboard', label: 'Dashboard', icon: 'dashboard', }); if (isSupervisor) { navItems.push({ key: 'supervisor', label: 'Asignación FIFO', icon: 'fifo', }); } if ( isSupervisor || window.authMapper.hasRole(currentUser, 'digitizer') ) { navItems.push({ key: 'digitalizacion', label: 'Digitalización', icon: 'camera', }); } if ( isSupervisor || window.authMapper.hasRole(currentUser, 'editor') ) { navItems.push({ key: 'edicion', label: 'Edición', icon: 'edit', }); } if ( isSupervisor || window.authMapper.hasRole(currentUser, 'quality') ) { navItems.push({ key: 'cc', label: 'Control de Calidad', icon: 'check_shield', }); } if ( isSupervisor || window.authMapper.hasAnyRole( currentUser, ['digitizer', 'editor', 'quality'] ) ) { navItems.push({ key: 'buzon', label: 'Buzón de correcciones', icon: 'alert', }); } if ( window.authMapper.hasAnyRole( currentUser, ['administrator', 'supervisor', 'converter'] ) ) { navItems.push({ key: 'conversion', label: 'Conversión', icon: 'convert', }); } navItems.push({ key: 'catalogo', label: 'Catálogo', icon: 'library', }); if (isSupervisor) { navItems.push({ key: 'rutas', label: 'Rutas de imágenes', icon: 'settings', }); navItems.push({ key: 'publicacion', label: 'Publicar BD', icon: 'convert', }); navItems.push({ key: 'usuarios', label: 'Usuarios y perfiles', icon: 'users', }); navItems.push({ key: 'nuevo_proyecto', label: 'Nuevo Proyecto', icon: 'plus_circle', }); } // Contadores del menú. const counts = { supervisor: libros.filter( (libro) => libro.estatus === 'digitalizado' ).length, digitalizacion: isServerMode ? 0 : libros.filter( (libro) => ( libro.estatus === 'en_digitalizacion' && libro.digitalizadoPor === currentUser.id ) || libro.estatus === 'nuevo' ).length, edicion: isServerMode ? 0 : libros.filter( (libro) => libro.estatus === 'en_edicion' && libro.editadoPor === currentUser.id ).length, cc: libros.filter( (libro) => libro.estatus === 'editado' ).length, buzon: 0, conversion: libros.filter( (libro) => ['aceptado', 'en_conversion'].includes( libro.estatus ) ).length, }; const screenTitle = { dashboard: 'Dashboard estadístico', supervisor: 'Asignación FIFO · Supervisor', digitalizacion: 'Digitalización de libros', edicion: 'Edición de imágenes', cc: 'Control de calidad', buzon: 'Buzón de correcciones', conversion: 'Conversión de formatos', catalogo: 'Catálogo de libros', rutas: 'Rutas de imágenes', publicacion: 'Publicación de base de datos', usuarios: 'Gestión de usuarios y perfiles', nuevo_proyecto: 'Alta de nuevo proyecto', }[screen]; const screenSub = { dashboard: 'Meta 409 libros · 70,000 imágenes · Deadline 12 sep 2026', supervisor: 'Asigna libros digitalizados a editores respetando el orden de entrada', digitalizacion: 'Procesa libros físicos y registra imágenes resultantes', edicion: 'Edita imágenes y registra productividad por corte horario', cc: 'Revisa la calidad de la edición y decide aceptar o rechazar', buzon: 'Consulta y atiende observaciones asignadas', conversion: 'Genera los formatos JPG, TIFF, PDF y EPUB', catalogo: 'Base de datos completa del proyecto', nuevo_proyecto: 'Crear un nuevo proyecto con las mismas características', }[screen]; return (
{/* Sidebar */}
Digitalización
de Libros
Control 2026
General
{navItems .filter((item) => ['dashboard', 'supervisor'].includes(item.key) ) .map((item) => (
setScreen(item.key)} > {item.label} {counts[item.key] > 0 && ( {counts[item.key]} )}
))}
Mis procesos
{navItems .filter((item) => [ 'digitalizacion', 'edicion', 'cc', 'buzon', 'conversion', ].includes(item.key) ) .map((item) => (
setScreen(item.key)} > {item.label} {counts[item.key] > 0 && ( {counts[item.key]} )}
))}
Administración
{navItems .filter((item) => [ 'catalogo', 'rutas', 'publicacion', 'usuarios', 'nuevo_proyecto', ].includes(item.key) ) .map((item) => (
setScreen(item.key)} > {item.label}
))}
{currentUser.nombre}
{window.authMapper.hasRole( currentUser, 'administrator' ) ? 'Administrador' : isSupervisor ? 'Supervisor' : currentUser.roles.length > 2 ? 'Multi-rol' : window.authMapper.roleLabel( currentUser.roles[0] )}
{/* Contenido principal */}
{APP_DATA.PROYECTO.nombre}
{screenTitle}
{new Date().toLocaleDateString( 'es-MX', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric', } )}
{screen === 'dashboard' && ( )} {screen === 'supervisor' && ( )} {screen === 'digitalizacion' && ( )} {screen === 'edicion' && ( )} {screen === 'cc' && ( )} {screen === 'buzon' && ( )} {screen === 'conversion' && ( )} {screen === 'catalogo' && ( )} {screen === 'rutas' && ( )} {screen === 'publicacion' && ( )} {screen === 'usuarios' && ( )} {screen === 'nuevo_proyecto' && ( {}} onClose={() => setScreen('dashboard') } /> )}
setSelectedBook(null)} />
); }; // Montar aplicación. ReactDOM .createRoot(document.getElementById('root')) .render();