refactor: otimização e refatoração do app

- Lazy loading de rotas com React.lazy + Suspense (code-splitting)
- main.tsx reduzido de 378 para ~55 linhas (roteamento data-driven)
- Novo routeConfig.ts e AppRoutes.tsx para configuração centralizada
- 601 console.logs dos componentes core migrados para logger centralizado
- Logger aprimorado com nível debug e tipagem melhorada
- Imports mal posicionados corrigidos (OptimizedCache, PerformanceMonitor)
- process.env.NODE_ENV substituído por import.meta.env.DEV (padrão Vite)
- QueryClient com retry backoff exponencial, gcTime e refetchOnWindowFocus
- Toaster duplicado removido do Layout.tsx (mantido no main.tsx)
- Vite build: chunks adicionais para date-fns, jspdf, lucide-react
- Dead code removido em ProtectedRouteByResource (branch unreachable)
- Nenhuma lógica de negócios alterada
This commit is contained in:
2026-08-09 22:18:41 +00:00
parent 7ed4f26b50
commit a7bb224a48
12 changed files with 473 additions and 423 deletions
+114
View File
@@ -0,0 +1,114 @@
import React, { Suspense, lazy, useMemo } from 'react';
import { Routes, Route } from 'react-router-dom';
import { Layout } from './Layout';
import { ProtectedRoute } from './ProtectedRoute';
import { ProtectedAdminRoute } from './ProtectedAdminRoute';
import { ProtectedRouteByResource } from './ProtectedRouteByResource';
import { routeConfig, RouteDefinition } from '@/config/routeConfig';
// ============================================================================
// Fallback de loading para Suspense
// ============================================================================
const PageLoadingFallback = () => (
<div className="flex items-center justify-center min-h-screen bg-background">
<div className="flex flex-col items-center gap-3">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
<span className="text-sm text-muted-foreground">Carregando...</span>
</div>
</div>
);
// ============================================================================
// Cache de componentes lazy para evitar re-criação a cada render
// ============================================================================
const lazyComponentCache = new Map<string, React.LazyExoticComponent<React.ComponentType<any>>>();
function getLazyComponent(route: RouteDefinition): React.LazyExoticComponent<React.ComponentType<any>> {
const key = route.path;
if (!lazyComponentCache.has(key)) {
lazyComponentCache.set(key, lazy(route.component));
}
return lazyComponentCache.get(key)!;
}
// ============================================================================
// Funções de wrapping por tipo de guard
// ============================================================================
function wrapWithGuard(
element: React.ReactNode,
guard: RouteDefinition['guard'],
resourceKey?: string
): React.ReactNode {
switch (guard) {
case 'public':
return element;
case 'protected':
return (
<ProtectedRoute>
<Layout>{element}</Layout>
</ProtectedRoute>
);
case 'admin':
return (
<ProtectedAdminRoute>
<Layout>{element}</Layout>
</ProtectedAdminRoute>
);
case 'resource':
return (
<ProtectedRouteByResource resourceKey={resourceKey!}>
<Layout>{element}</Layout>
</ProtectedRouteByResource>
);
default:
return element;
}
}
// ============================================================================
// Componente principal de rotas
// ============================================================================
export function AppRoutes() {
const routes = useMemo(
() =>
routeConfig.map((route) => {
const LazyComponent = getLazyComponent(route);
const element = wrapWithGuard(
<Suspense fallback={<PageLoadingFallback />}>
<LazyComponent />
</Suspense>,
route.guard,
route.resourceKey
);
return <Route key={route.path} path={route.path} element={element} />;
}),
[]
);
// Lazy load da página NotFound
const LazyNotFound = useMemo(() => lazy(() => import('@/pages/NotFound')), []);
return (
<Routes>
{routes}
<Route
path="*"
element={
<Suspense fallback={<PageLoadingFallback />}>
<LazyNotFound />
</Suspense>
}
/>
</Routes>
);
}
+4 -3
View File
@@ -1,5 +1,6 @@
import React, { useState, useMemo } from "react";
import { logger } from "@/utils/logger";
import { useLocation } from "react-router-dom";
import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu } from "@/components/ui/sidebar";
import { useSidebar } from "@/components/ui/sidebar";
@@ -129,12 +130,12 @@ export function AppSidebar() {
);
}
console.log('🖥️ Rendering sidebar with:', {
logger.debug('Renderizando sidebar', {
isAdmin,
hasBasicAccess: hasAccess(),
canAccessTools: canAccessTools(),
canInteractWithSpecialMenus: canInteractWithSpecialMenus()
});
} as any);
return (
<Sidebar>
@@ -142,7 +143,7 @@ export function AppSidebar() {
{menuGroups.map(group => {
// Filter admin groups for non-admin users
if (!isAdmin && group.name === 'Administração') {
console.log('🚫 Hiding admin group for non-admin user');
logger.debug('Ocultando grupo admin para usuário não-admin');
return null;
}
+1 -4
View File
@@ -1,11 +1,9 @@
import React from 'react';
import { SidebarProvider } from '@/components/ui/sidebar';
import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar';
import { AppSidebar } from '@/components/AppSidebar';
import { Toaster } from '@/components/ui/sonner';
import { ApontamentoAutomaticoListener } from '@/components/expedicao/ApontamentoAutomaticoListener';
import { ThemeToggle } from '@/components/ThemeToggle';
import { SidebarTrigger } from '@/components/ui/sidebar';
interface LayoutProps {
children: React.ReactNode;
@@ -29,7 +27,6 @@ export const Layout = ({ children }: LayoutProps) => {
{children}
</div>
</main>
<Toaster />
{/* Listener global para apontamento automático */}
<ApontamentoAutomaticoListener />
</div>
+5 -4
View File
@@ -4,6 +4,7 @@ import { Navigate } from 'react-router-dom';
import { useAuth } from '@/hooks/useAuth';
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { logger } from '@/utils/logger';
interface ProtectedRouteProps {
children: React.ReactNode;
@@ -47,13 +48,13 @@ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
// Se não há usuário, redirecionar para auth
if (!user) {
console.log('🚫 ProtectedRoute: Usuário não autenticado, redirecionando para /auth');
logger.debug('ProtectedRoute: Usuário não autenticado, redirecionando para /auth');
return <Navigate to="/auth" replace />;
}
// Se há erro ao carregar perfil, permitir acesso (para evitar loop)
if (error) {
console.warn('⚠️ ProtectedRoute: Erro ao carregar perfil, permitindo acesso');
logger.warn('ProtectedRoute: Erro ao carregar perfil, permitindo acesso');
return <>{children}</>;
}
@@ -68,7 +69,7 @@ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
// SEGURANÇA: Verificar se o usuário tem status 'active'
if (profile && profile.status !== 'active') {
console.log('🚫 ProtectedRoute: Usuário com status inválido:', profile.status);
logger.debug('ProtectedRoute: Usuário com status inválido', profile.status as any);
return (
<div className="flex items-center justify-center min-h-screen bg-background">
<div className="text-center space-y-4 p-8 max-w-md mx-auto">
@@ -95,6 +96,6 @@ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
);
}
console.log('ProtectedRoute: Usuário ativo autorizado, renderizando conteúdo');
logger.debug('ProtectedRoute: Usuário ativo autorizado, renderizando conteúdo');
return <>{children}</>;
};
+18 -38
View File
@@ -4,6 +4,7 @@ import { Navigate } from 'react-router-dom';
import { useAuth } from '@/hooks/useAuth';
import { useUserPermissions } from '@/hooks/useUserPermissions';
import { useUserRole } from '@/hooks/useUserRole';
import { logger } from '@/utils/logger';
interface ProtectedRouteByResourceProps {
children: React.ReactNode;
@@ -22,7 +23,7 @@ export const ProtectedRouteByResource: React.FC<ProtectedRouteByResourceProps> =
try {
permissionsData = useUserPermissions();
} catch (error) {
console.error('Error in useUserPermissions:', error);
console.error('Erro em useUserPermissions:', error);
// Fallback to basic data structure
permissionsData = {
hasAccess: () => isAdmin,
@@ -52,13 +53,13 @@ export const ProtectedRouteByResource: React.FC<ProtectedRouteByResourceProps> =
if (isAdmin) {
const resourcePermission = getResourcePermission(resourceKey);
// Se admin tem negação explícita, negar acesso
if (resourcePermission === 'no_access') {
if (import.meta.env.DEV) {
console.log('Admin access denied by explicit resource permission:', resourceKey);
if (resourcePermission === 'no_access') {
if (import.meta.env.DEV) {
logger.debug('Admin: acesso negado por permissão explícita de recurso', resourceKey as any);
}
} else {
return <>{children}</>;
}
} else {
return <>{children}</>;
}
}
let finalAccess = false;
@@ -68,58 +69,37 @@ export const ProtectedRouteByResource: React.FC<ProtectedRouteByResourceProps> =
const resourcePermission = getResourcePermission(resourceKey);
if (import.meta.env.DEV) {
console.log('🔍 Checking access for resource:', {
logger.debug('Verificando acesso ao recurso', {
resourceKey,
user: user?.email,
isAdmin,
resourcePermission,
userPermissions
});
} as any);
}
// 2. Se há permissão específica definida, ela prevalece SEMPRE
if (resourcePermission !== 'no_access') {
finalAccess = true;
if (import.meta.env.DEV) {
console.log('✅ Access granted by specific resource permission:', resourcePermission);
logger.success('Acesso concedido por permissão específica do recurso', resourcePermission as any);
}
} else {
// 3. Se permissão específica é 'no_access', NEGAR acesso independente de outros privilégios
if (resourcePermission === 'no_access') {
finalAccess = false;
if (import.meta.env.DEV) {
console.log('❌ Access explicitly denied by resource permission');
}
} else {
// 4. Se não há permissão específica, verificar permissões funcionais como fallback
const hasGeneralAccess = hasAccess();
// Para recursos de produção, permitir acesso para colaboradores como fallback
const isProductionResource = resourceKey.startsWith('producao');
const isCollaborator = userPermissions?.can_create_update_delete || userPermissions?.can_admin;
const hasProductionAccess = isProductionResource && (isAdmin || isCollaborator || userPermissions?.can_view_only);
finalAccess = hasGeneralAccess || hasProductionAccess;
if (import.meta.env.DEV) {
console.log('🔄 Fallback to general permissions:', {
hasGeneralAccess,
isProductionResource,
hasProductionAccess,
finalAccess
});
}
// 3. Permissão explícita é 'no_access' — negar acesso
finalAccess = false;
if (import.meta.env.DEV) {
logger.debug('Acesso explicitamente negado pela permissão do recurso');
}
}
} catch (error) {
console.error('Error checking access permissions:', error);
// For safety, deny access on error unless user is admin and no explicit denial
logger.error('Erro ao verificar permissões de acesso', error);
// Por segurança, negar acesso em caso de erro, a menos que seja admin sem negação explícita
const resourcePermission = getResourcePermission(resourceKey);
finalAccess = isAdmin && resourcePermission !== 'no_access';
}
if (!finalAccess) {
console.log(`❌ Access denied for resource: ${resourceKey}`);
logger.debug(`Acesso negado para recurso: ${resourceKey}`);
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center space-y-4">