diff --git a/src/components/AppSidebar.tsx b/src/components/AppSidebar.tsx index f8d3494..4fca906 100644 --- a/src/components/AppSidebar.tsx +++ b/src/components/AppSidebar.tsx @@ -100,46 +100,66 @@ export function AppSidebar() { // Admin can always access everything if (isAdmin) return true; - // Mapeamento de chave de menu/submenu da sidebar para resource_key do BD + // Mapeamento completo de chave de menu/submenu da sidebar para resource_key do BD const itemToResourceMap: Record = { + // PCP & Produção 'pcp': 'producao-pcp', 'dashboard-producao': 'producao-dashboard', 'apontamento-producao': 'producao-apontamento', 'diario-producao': 'diario-producao', 'prioridades-fabricacao': 'prioridades-fabricacao', 'visao-geral': 'producao-visao', + 'painel-industrial': 'producao-dashboard', + // Engenharia, 3D & OFs 'visualizador-3d': 'visualizador-3d', 'ordens-fabricacao': 'ofs-lista', + 'ofs-concluidas': 'ofs-lista', 'ficha-tecnica-of': 'cadastro-of', 'cronograma': 'ofs-cronograma', 'cadastro-pecas': 'cadastro-pecas', 'equipamentos': 'equipamentos', + // Expedição & Estoque 'expedicao': 'expedicao', 'estoque-main': 'estoque', 'estoque': 'estoque', 'solicitacao-compras': 'estoque-solicitacao-compras', + // Obra & Campo 'dashboard-obras': 'obra-dashboard', 'configuracoes-obra': 'obra-configuracoes', 'obra': 'obra-dashboard', + // Tarefas & Ferramentas + 'tarefas': 'tarefas', + 'lista-tarefas': 'tarefas', + 'historico-tarefas': 'tarefas', + 'ferramentas': 'ferramentas', + 'conversores': 'ferramentas-inconsistencias', + 'inconsistencias': 'ferramentas-inconsistencias', + // Biblioteca & Sistema + 'biblioteca': 'sistema', + 'catalogos': 'sistema', + 'normas': 'sistema', + 'referencias': 'sistema', + 'sistema': 'sistema', + 'sugestoes': 'sugestoes', + 'atribuicoes': 'user-management', + 'mapa-interativo': 'dashboard', + 'dashboard': 'dashboard', + // Administração & Usuários 'gerenciar-usuarios': 'user-management', 'user-management': 'user-management', 'configuracoes-gerais': 'configuracoes-gerais', - 'inconsistencias': 'ferramentas-inconsistencias', - 'ferramentas': 'ferramentas', - 'sistema': 'sistema', - 'sugestoes': 'sugestoes', + 'personalizacao-tema': 'admin', 'admin': 'admin', - 'dashboard': 'dashboard', }; const resKey = itemToResourceMap[itemKey] || itemKey; - // Se há restrição direta configurada no BD para este recurso + // 1. Se há restrição direta configurada no BD para este recurso if (resourcePermissions && resourcePermissions[resKey]) { return resourcePermissions[resKey] !== 'no_access'; } - // Se o usuário tem perfil de "apenas Smart" (tem modo-smart mas não tem esse recurso) + // 2. Se o usuário tem perfil de "apenas Smart" (tem modo-smart mas não tem outros recursos) const temAcessoSmart = resourcePermissions && resourcePermissions['modo-smart'] && @@ -156,8 +176,23 @@ export function AppSidebar() { if (soTemSmart && !resKey.startsWith('smart') && resKey !== 'modo-smart') { return false; } + + // 3. Se há permissões individuais salvas na Matriz para este usuário: + // Se a matriz foi configurada para o usuário, qualquer tela que não foi concedida (exceto dashboard básico) fica oculta + if (permsCadastradas.length > 0) { + if (resourcePermissions[resKey] === 'no_access') { + return false; + } + if (resKey !== 'dashboard' && !resKey.startsWith('smart')) { + // Se não consta na lista de permissões ativas concedidas + const perm = resourcePermissions[resKey]; + if (!perm || perm === 'no_access') { + return false; + } + } + } - // Se o item requer permissão especial, verificar permissões específicas + // 4. Se o item requer permissão especial, verificar permissões específicas if (requiresSpecialPermission) { if (itemKey === 'ferramentas') { return canAccessTools(); @@ -235,12 +270,25 @@ export function AppSidebar() { {menuGroups.map(group => { // Filter admin groups for non-admin users if (!isAdmin && group.name === 'Administração') { - logger.debug('Ocultando grupo admin para usuário não-admin'); - return null; + const hasAnyAdminItem = group.items.some(item => canAccessItem(item.key, item.requiresSpecialPermission)); + if (!hasAnyAdminItem) { + return null; + } } - // Skip groups with no items - if (!group.items || group.items.length === 0) { + // Filtrar itens do grupo permitidos para o usuário logado + const itemsPermitidos = (group.items || []).filter(item => { + if (item.subItems && item.subItems.length > 0) { + return isAdmin || item.subItems.some(sub => { + if (sub.url === "/admin/theme-customization" && !isAdmin) return false; + return canAccessItem(sub.key, sub.requiresSpecialPermission); + }); + } + return isAdmin || canAccessItem(item.key, item.requiresSpecialPermission); + }); + + // Se não houver itens visíveis no grupo, não renderiza o grupo (nem o título) + if (itemsPermitidos.length === 0) { return null; } @@ -254,7 +302,7 @@ export function AppSidebar() { - {group.items.map((item) => ( + {itemsPermitidos.map((item) => ( = ({ onMenuItemClick, getIconProps }) => { - const canAccess = canAccessItem(item.key); - - if (!canAccess && !isAdmin) { - return null; // Don't show item if no access - } - // Filtrar subitens permitidos para o usuário logado const subItemsPermitidos = (item.subItems || []).filter((subItem) => { if (subItem.url === "/admin/theme-customization" && !isAdmin) { @@ -42,8 +36,16 @@ export const AppSidebarMenuItem: React.FC = ({ }); // Se o item for apenas um agrupador de subitens e todos estiverem bloqueados, não renderizar o agrupador - if (item.subItems && item.subItems.length > 0 && subItemsPermitidos.length === 0 && !isAdmin) { - return null; + if (item.subItems && item.subItems.length > 0) { + if (!isAdmin && subItemsPermitidos.length === 0) { + return null; + } + } else { + // Se for item direto sem subitens + const canAccess = canAccessItem(item.key); + if (!canAccess && !isAdmin) { + return null; + } } return ( diff --git a/src/components/users/matriz/MatrizAcessosPainel.tsx b/src/components/users/matriz/MatrizAcessosPainel.tsx index cb1bbfe..1b103b3 100644 --- a/src/components/users/matriz/MatrizAcessosPainel.tsx +++ b/src/components/users/matriz/MatrizAcessosPainel.tsx @@ -3,6 +3,7 @@ import { useMatrizAcessos, UsuarioMatriz } from '@/hooks/useMatrizAcessos'; import { MatrizGrid } from './MatrizGrid'; import { MatrizTemplatesModal } from './MatrizTemplatesModal'; import { MatrizSmartAvancadoModal } from './MatrizSmartAvancadoModal'; +import { MatrizGrupoAvancadoModal } from './MatrizGrupoAvancadoModal'; import { SimuladorVisaoBar } from './SimuladorVisaoBar'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; @@ -30,6 +31,7 @@ export const MatrizAcessosPainel: React.FC = ({ const [busca, setBusca] = useState(''); const [selectedUserForTemplate, setSelectedUserForTemplate] = useState(null); const [smartAvancadoOpen, setSmartAvancadoOpen] = useState(false); + const [grupoAvancadoKey, setGrupoAvancadoKey] = useState(null); // Filtragem por nome, email ou cargo const usuariosFiltrados = usuarios.filter((u) => { @@ -152,6 +154,7 @@ export const MatrizAcessosPainel: React.FC = ({ onToggleNivel={toggleNivel} onOpenTemplates={(user) => setSelectedUserForTemplate(user)} onOpenSmartAvancado={() => setSmartAvancadoOpen(true)} + onOpenGrupoAvancado={(key) => setGrupoAvancadoKey(key)} /> )} @@ -168,6 +171,13 @@ export const MatrizAcessosPainel: React.FC = ({ isOpen={smartAvancadoOpen} onClose={() => setSmartAvancadoOpen(false)} /> + + {/* 7. MODAL DE GESTÃO AVANÇADA POR CATEGORIA / GRUPO */} + setGrupoAvancadoKey(null)} + /> ); }; diff --git a/src/components/users/matriz/MatrizGrid.tsx b/src/components/users/matriz/MatrizGrid.tsx index 65b2c54..d13c22b 100644 --- a/src/components/users/matriz/MatrizGrid.tsx +++ b/src/components/users/matriz/MatrizGrid.tsx @@ -12,6 +12,7 @@ interface MatrizGridProps { onToggleNivel: (userId: string, resourceKey: string, tipo: 'V' | 'C') => void; onOpenTemplates: (user: UsuarioMatriz) => void; onOpenSmartAvancado?: () => void; + onOpenGrupoAvancado?: (grupoKey: string) => void; } export const MatrizGrid: React.FC = ({ @@ -20,6 +21,7 @@ export const MatrizGrid: React.FC = ({ onToggleNivel, onOpenTemplates, onOpenSmartAvancado, + onOpenGrupoAvancado, }) => { // Cores de fundo e borda para os grupos const corGrupoHeader: Record = { @@ -31,6 +33,15 @@ export const MatrizGrid: React.FC = ({ rose: 'bg-rose-500/10 text-rose-400 border-rose-500/30', }; + const corBotaoAvancado: Record = { + emerald: 'bg-emerald-500/20 hover:bg-emerald-500/30 text-emerald-300 border border-emerald-500/40', + sky: 'bg-sky-500/20 hover:bg-sky-500/30 text-sky-300 border border-sky-500/40', + indigo: 'bg-indigo-500/20 hover:bg-indigo-500/30 text-indigo-300 border border-indigo-500/40', + amber: 'bg-amber-500/20 hover:bg-amber-500/30 text-amber-300 border border-amber-500/40', + teal: 'bg-teal-500/20 hover:bg-teal-500/30 text-teal-300 border border-teal-500/40', + rose: 'bg-rose-500/20 hover:bg-rose-500/30 text-rose-300 border border-rose-500/40', + }; + return (
@@ -52,7 +63,7 @@ export const MatrizGrid: React.FC = ({ >
{grupo.titulo} - {grupo.key === 'smart' && onOpenSmartAvancado && ( + {grupo.key === 'smart' && onOpenSmartAvancado ? ( - )} + ) : onOpenGrupoAvancado ? ( + + ) : null}
{grupo.descricao} diff --git a/src/components/users/matriz/MatrizGrupoAvancadoModal.tsx b/src/components/users/matriz/MatrizGrupoAvancadoModal.tsx new file mode 100644 index 0000000..a1bc459 --- /dev/null +++ b/src/components/users/matriz/MatrizGrupoAvancadoModal.tsx @@ -0,0 +1,384 @@ +import React, { useState } from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; +import { Switch } from '@/components/ui/switch'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { + SlidersHorizontal, + Search, + CheckCircle2, + Save, + RotateCcw, + Shield, + Factory, + Box, + Truck, + HardHat, + Sparkles, + Lock, + ArrowUpDown, + Layers, + AlertTriangle, + Gauge, + Download, + DollarSign, + Network, + UploadCloud, + Compass, + Scale, + Percent, + FileSignature, + EyeOff, + Barcode, + PackageCheck, + CheckSquare, + AlertOctagon, + Activity, + DownloadCloud, + Settings, + Crown, + Wrench, + Calendar, +} from 'lucide-react'; +import { useGrupoAvancadoPermissions } from '@/hooks/useGrupoAvancadoPermissions'; + +interface MatrizGrupoAvancadoModalProps { + grupoKey: string | null; + isOpen: boolean; + onClose: () => void; +} + +export const MatrizGrupoAvancadoModal: React.FC = ({ + grupoKey, + isOpen, + onClose, +}) => { + const { + config, + loading, + saving, + users, + selectedUserId, + setSelectedUserId, + selectedUser, + toggleRegra, + aplicarTemplate, + salvarConfiguracaoUsuario, + recarregar, + } = useGrupoAvancadoPermissions(isOpen ? grupoKey : null); + + const [busca, setBusca] = useState(''); + + if (!config) return null; + + const usersFiltrados = users.filter((u) => { + const termo = busca.toLowerCase(); + return ( + u.fullName.toLowerCase().includes(termo) || + u.email.toLowerCase().includes(termo) || + u.functionName.toLowerCase().includes(termo) + ); + }); + + const corBadge: Record = { + sky: 'bg-sky-500/15 text-sky-400 border-sky-500/40', + indigo: 'bg-indigo-500/15 text-indigo-400 border-indigo-500/40', + amber: 'bg-amber-500/15 text-amber-400 border-amber-500/40', + teal: 'bg-teal-500/15 text-teal-400 border-teal-500/40', + rose: 'bg-rose-500/15 text-rose-400 border-rose-500/40', + }; + + const corFundoIcone: Record = { + sky: 'bg-sky-500/15 text-sky-400 border-sky-500/30', + indigo: 'bg-indigo-500/15 text-indigo-400 border-indigo-500/30', + amber: 'bg-amber-500/15 text-amber-400 border-amber-500/30', + teal: 'bg-teal-500/15 text-teal-400 border-teal-500/30', + rose: 'bg-rose-500/15 text-rose-400 border-rose-500/30', + }; + + const getIconComponent = (icone: string) => { + switch (icone) { + case 'Calendar': + return ; + case 'Lock': + return ; + case 'ArrowUpDown': + return ; + case 'Layers': + return ; + case 'AlertTriangle': + return ; + case 'Gauge': + return ; + case 'CheckCircle2': + return ; + case 'Download': + return ; + case 'DollarSign': + return ; + case 'Network': + return ; + case 'UploadCloud': + return ; + case 'Scale': + return ; + case 'Percent': + return ; + case 'FileSignature': + return ; + case 'EyeOff': + return ; + case 'RotateCcw': + return ; + case 'PackageCheck': + return ; + case 'CheckSquare': + return ; + case 'AlertOctagon': + return ; + case 'Activity': + return ; + case 'DownloadCloud': + return ; + case 'Settings': + return ; + case 'Crown': + return ; + case 'Shield': + return ; + case 'Wrench': + return ; + default: + return ; + } + }; + + return ( + !open && onClose()}> + + {/* CABEÇALHO DO MODAL */} +
+
+
+ +
+
+ + Regras & Governança Avançada + + {config.titulo} + + + + {config.descricao} + +
+
+
+ + {/* CORPO PRINCIPAL: SIDEBAR DE USUÁRIOS + ÁREA DE REGRAS */} +
+ {/* SIDEBAR DE USUÁRIOS */} +
+
+
+ + setBusca(e.target.value)} + className="pl-8 text-xs h-8 bg-card border-slate-300 dark:border-border text-foreground font-medium" + /> +
+
+ +
+ {loading ? ( +
+ Carregando colaboradores... +
+ ) : usersFiltrados.length === 0 ? ( +
+ Nenhum colaborador encontrado. +
+ ) : ( + usersFiltrados.map((user) => { + const isSelected = user.userId === selectedUserId; + const totalAtivas = Object.values(user.regras).filter(Boolean).length; + + return ( + + ); + }) + )} +
+
+ + {/* ÁREA DE CONFIGURAÇÃO DO USUÁRIO */} +
+ {selectedUser ? ( +
+ {/* CABEÇALHO DO USUÁRIO SELECIONADO */} +
+
+ + + + {selectedUser.fullName.substring(0, 2).toUpperCase()} + + +
+

+ {selectedUser.fullName} +

+

+ {selectedUser.email} + • + + {selectedUser.functionName} + +

+
+
+ +
+ +
+
+ + {/* TEMPLATES RÁPIDOS */} +
+
+ + + Presets Rápidos: + + {config.templates.map((tpl) => ( + + ))} +
+
+ + {/* LISTAGEM DE REGRAS GRANULARES */} +
+
+ {config.regras.map((regra) => { + const ativo = Boolean(selectedUser.regras[regra.key]); + + return ( +
toggleRegra(regra.key)} + className={`p-3.5 rounded-xl border transition-all cursor-pointer flex items-center justify-between gap-4 ${ + ativo + ? 'bg-muted/40 border-primary/40 shadow-sm' + : 'bg-background border-border/50 opacity-70 hover:opacity-100 hover:border-border' + }`} + > +
+
+ {getIconComponent(regra.icone)} +
+
+
+ + {regra.nome} + + {ativo && ( + + LIBERADO + + )} +
+

+ {regra.descricao} +

+
+
+ +
e.stopPropagation()}> + toggleRegra(regra.key)} + /> +
+
+ ); + })} +
+
+
+ ) : ( +
+ Selecione um colaborador ao lado para configurar as regras avançadas. +
+ )} +
+
+
+
+ ); +}; diff --git a/src/hooks/useGrupoAvancadoPermissions.ts b/src/hooks/useGrupoAvancadoPermissions.ts new file mode 100644 index 0000000..6db7ae6 --- /dev/null +++ b/src/hooks/useGrupoAvancadoPermissions.ts @@ -0,0 +1,233 @@ +import { useState, useEffect, useCallback, useMemo } from 'react'; +import { supabase } from '@/integrations/supabase/client'; +import { toast } from 'sonner'; +import { GRUPOS_AVANCADOS_CONFIG, GrupoAvancadoConfig } from '@/types/grupoAvancadoTypes'; + +export interface UserGrupoAvancado { + userId: string; + email: string; + fullName: string; + functionName: string; + privilegeName: string; + profileImageUrl: string | null; + regras: Record; // regraKey -> true/false +} + +export function useGrupoAvancadoPermissions(grupoKey: string | null) { + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [users, setUsers] = useState([]); + const [selectedUserId, setSelectedUserId] = useState(null); + + const config: GrupoAvancadoConfig | undefined = grupoKey + ? GRUPOS_AVANCADOS_CONFIG[grupoKey] + : undefined; + + const carregarDados = useCallback(async () => { + if (!config) return; + + try { + setLoading(true); + + // 1. Carregar profiles com cargo e privilégio + const { data: profiles, error: profError } = await supabase + .from('profiles') + .select(` + id, + email, + full_name, + profile_image_url, + privileges:privilege_id ( + id, + name + ), + user_functions:function_id ( + id, + name + ) + `) + .order('full_name', { ascending: true }); + + if (profError) throw profError; + + // 2. Carregar permissões das regras deste grupo na tabela user_interface_permissions + const chavesRegras = config.regras.map((r) => r.key); + const { data: permsData, error: permsError } = await supabase + .from('user_interface_permissions') + .select('user_id, resource_key, permission') + .in('resource_key', chavesRegras); + + if (permsError) { + console.warn('Aviso ao carregar permissões avançadas:', permsError); + } + + // Indexar permissões por user_id -> resource_key -> ativo + const userPermsMap: Record> = {}; + (permsData || []).forEach((p: any) => { + if (!userPermsMap[p.user_id]) userPermsMap[p.user_id] = {}; + userPermsMap[p.user_id][p.resource_key] = + p.permission === 'can_create_update_delete' || + p.permission === 'can_admin' || + p.permission === 'can_create_only' || + p.permission === 'can_view_only'; + }); + + // Mapear colaboradores + const mapped: UserGrupoAvancado[] = (profiles || []).map((p: any) => { + const privName = p.privileges?.name || 'Padrão'; + const isAdmin = privName.toLowerCase().includes('admin'); + const userRules: Record = {}; + + config.regras.forEach((r) => { + if (userPermsMap[p.id] && userPermsMap[p.id][r.key] !== undefined) { + userRules[r.key] = userPermsMap[p.id][r.key]; + } else if (isAdmin) { + userRules[r.key] = true; + } else { + userRules[r.key] = Boolean(r.padraoAtivo); + } + }); + + return { + userId: p.id, + email: p.email || '', + fullName: p.full_name || p.email?.split('@')[0] || 'Usuário', + functionName: p.user_functions?.name || 'Sem Cargo', + privilegeName: privName, + profileImageUrl: p.profile_image_url, + regras: userRules, + }; + }); + + setUsers(mapped); + if (!selectedUserId && mapped.length > 0) { + setSelectedUserId(mapped[0].userId); + } + } catch (err: any) { + console.error('Erro ao carregar permissões avançadas do grupo:', err); + toast.error('Erro ao carregar governança avançada.'); + } finally { + setLoading(false); + } + }, [config, selectedUserId]); + + useEffect(() => { + if (grupoKey) { + carregarDados(); + } + }, [grupoKey]); + + const selectedUser = useMemo( + () => users.find((u) => u.userId === selectedUserId) || null, + [users, selectedUserId] + ); + + // Toggle de uma regra específica para o usuário selecionado + const toggleRegra = (regraKey: string) => { + if (!selectedUserId) return; + + setUsers((prev) => + prev.map((u) => { + if (u.userId !== selectedUserId) return u; + return { + ...u, + regras: { + ...u.regras, + [regraKey]: !u.regras[regraKey], + }, + }; + }) + ); + }; + + // Aplica um template pré-definido para o usuário selecionado + const aplicarTemplate = (templateId: string) => { + if (!config || !selectedUserId) return; + const tpl = config.templates.find((t) => t.id === templateId); + if (!tpl) return; + + const novasRegras: Record = {}; + config.regras.forEach((r) => { + novasRegras[r.key] = tpl.regrasAtivas.includes(r.key); + }); + + setUsers((prev) => + prev.map((u) => { + if (u.userId !== selectedUserId) return u; + return { + ...u, + regras: novasRegras, + }; + }) + ); + + toast.info(`Template "${tpl.nome}" aplicado. Clique em Salvar para gravar.`); + }; + + // Salvar alterações no banco de dados + const salvarConfiguracaoUsuario = async () => { + if (!config || !selectedUser) return; + + try { + setSaving(true); + + // Upsert para cada regra na tabela user_interface_permissions + const promises = config.regras.map(async (r) => { + const estaAtivo = Boolean(selectedUser.regras[r.key]); + const permission = estaAtivo ? 'can_create_update_delete' : 'no_access'; + + // Garantir que a chave exista em interface_resources para evitar erro de Foreign Key + await supabase + .from('interface_resources') + .upsert( + { + resource_key: r.key, + resource_name: r.nome, + is_submenu: false, + }, + { onConflict: 'resource_key' } + ); + + return supabase + .from('user_interface_permissions') + .upsert( + { + user_id: selectedUser.userId, + resource_key: r.key, + permission: permission as any, + updated_at: new Date().toISOString(), + }, + { onConflict: 'user_id,resource_key' } + ); + }); + + const results = await Promise.all(promises); + const hasError = results.some((res) => res.error); + + if (hasError) { + throw new Error('Falha ao persistir algumas regras no banco.'); + } + + toast.success(`Configurações avançadas salvas para ${selectedUser.fullName}!`); + } catch (err: any) { + console.error('Erro ao salvar regras avançadas:', err); + toast.error('Erro ao salvar alterações no banco.'); + } finally { + setSaving(false); + } + }; + + return { + config, + loading, + saving, + users, + selectedUserId, + setSelectedUserId, + selectedUser, + toggleRegra, + aplicarTemplate, + salvarConfiguracaoUsuario, + recarregar: carregarDados, + }; +} diff --git a/src/pages/UserManagement.tsx b/src/pages/UserManagement.tsx index 753b840..036e738 100644 --- a/src/pages/UserManagement.tsx +++ b/src/pages/UserManagement.tsx @@ -6,7 +6,6 @@ import { Users, UserPlus, Shield, Settings, Mail, Activity, LayoutGrid } from 'l import { UsersTable } from '@/components/users/UsersTable'; import { PendingUsersTable } from '@/components/users/PendingUsersTable'; import { FunctionsManager } from '@/components/users/FunctionsManager'; -import { PrivilegesManager } from '@/components/users/PrivilegesManager'; import { PasswordResetRequests } from '@/components/users/PasswordResetRequests'; import { SessionLogsSimple } from '@/components/users/SessionLogsSimple'; import { UserModal } from '@/components/users/UserModal'; @@ -40,9 +39,6 @@ const UserManagement = () => { createFunction, updateFunction, deleteFunction, - createPrivilege: createPrivilegeBase, - updatePrivilege: updatePrivilegeBase, - deletePrivilege, updateUser, toggleUserStatus, deleteUser, @@ -67,15 +63,6 @@ const UserManagement = () => { ); } - // Wrapper functions to match expected signatures - const createPrivilege = async (data: { name: string; description?: string; permissions: Record }): Promise => { - await createPrivilegeBase(data); - }; - - const updatePrivilege = async (id: string, data: { name: string; description?: string; permissions: Record }): Promise => { - await updatePrivilegeBase(id, data); - }; - const handleEditUser = (user: UserProfile) => { if (!canEdit()) return; setSelectedUser(user); @@ -158,7 +145,7 @@ const UserManagement = () => {
- + { )} - {canEdit() && ( - - - Privilégios - Priv - - )} - {canView() && ( { )} - {canEdit() && ( - - - - - - Gerenciar Privilégios - - - - - - - - )} - {canView() && ( diff --git a/src/types/grupoAvancadoTypes.ts b/src/types/grupoAvancadoTypes.ts new file mode 100644 index 0000000..50d54d0 --- /dev/null +++ b/src/types/grupoAvancadoTypes.ts @@ -0,0 +1,330 @@ +export interface RegraAvancada { + key: string; + nome: string; + descricao: string; + icone: string; + padraoAtivo?: boolean; +} + +export interface GrupoAvancadoConfig { + key: string; + titulo: string; + cor: 'sky' | 'indigo' | 'amber' | 'teal' | 'rose'; + icone: string; + descricao: string; + regras: RegraAvancada[]; + templates: { + id: string; + nome: string; + descricao: string; + icone: string; + regrasAtivas: string[]; + }[]; +} + +export const GRUPOS_AVANCADOS_CONFIG: Record = { + producao_pcp: { + key: 'producao_pcp', + titulo: 'Produção & PCP', + cor: 'sky', + icone: 'Factory', + descricao: 'Regras avançadas de apontamentos, tolerâncias fabris, fila de produção e homologação de diários.', + regras: [ + { + key: 'pcp_retroativo_permitido', + nome: 'Apontamento Retroativo de Peças', + descricao: 'Permite que o operador ou encarregado aponte produção com datas anteriores ao dia de trabalho atual.', + icone: 'Calendar', + padraoAtivo: false, + }, + { + key: 'pcp_fechar_diario', + nome: 'Fechamento & Homologação do Diário', + descricao: 'Autorização para encerrar e consolidar o diário de produção fabril, travando apontamentos do dia.', + icone: 'Lock', + padraoAtivo: false, + }, + { + key: 'pcp_reordenar_prioridades', + nome: 'Reordenação da Fila de Fabricação', + descricao: 'Permite alterar a prioridade tática de OFs e bancas, furando filas de corte, montagem ou solda.', + icone: 'ArrowUpDown', + padraoAtivo: false, + }, + { + key: 'pcp_apontamento_massa', + nome: 'Apontamento em Lote / Massa', + descricao: 'Libera a ferramenta de avanço conjunto de múltiplas peças de uma vez só.', + icone: 'Layers', + padraoAtivo: true, + }, + { + key: 'pcp_liberar_retrabalho', + nome: 'Autorização de Retrabalho & Refugo', + descricao: 'Permite apontar peças refugadas e liberar ordens de retrabalho com consumo extra de matéria-prima.', + icone: 'AlertTriangle', + padraoAtivo: false, + }, + { + key: 'pcp_capacidade_excedente', + nome: 'Alocação de Capacidade Excedente (>100%)', + descricao: 'Força o agendamento de máquinas e postos de trabalho acima da capacidade nominal cadastrada.', + icone: 'Gauge', + padraoAtivo: false, + }, + ], + templates: [ + { + id: 'operador_pcp', + nome: 'Apontador Fabril Padrão', + descricao: 'Apenas apontamentos normais e avanço de lotes, sem permissão de retroativo ou travamento.', + icone: 'Wrench', + regrasAtivas: ['pcp_apontamento_massa'], + }, + { + id: 'lider_producao', + nome: 'Líder / Encarregado de Fábrica', + descricao: 'Liberação de retroativo, reordenação de prioridades e autorização de retrabalho.', + icone: 'Shield', + regrasAtivas: ['pcp_retroativo_permitido', 'pcp_reordenar_prioridades', 'pcp_apontamento_massa', 'pcp_liberar_retrabalho'], + }, + { + id: 'gestor_pcp_total', + nome: 'Planejador PCP Total', + descricao: 'Acesso total com fechamento de diários, capacidade excedente e reordenação.', + icone: 'Crown', + regrasAtivas: ['pcp_retroativo_permitido', 'pcp_fechar_diario', 'pcp_reordenar_prioridades', 'pcp_apontamento_massa', 'pcp_liberar_retrabalho', 'pcp_capacidade_excedente'], + }, + ], + }, + + engenharia_3d: { + key: 'engenharia_3d', + titulo: 'Engenharia, OFs & BIM', + cor: 'indigo', + icone: 'Box', + descricao: 'Controle de revisões técnicas, exportação de modelos IFC/BIM, arquivos CAD e fichas com custos.', + regras: [ + { + key: 'eng_aprovar_revisoes', + nome: 'Aprovação & Liberação de Revisões de OF', + descricao: 'Autorização formal para aprovar revisões de projetos e emitir novas listas de corte para a fábrica.', + icone: 'CheckCircle2', + padraoAtivo: false, + }, + { + key: 'eng_download_ifc', + nome: 'Download de Modelos 3D & Arquivos IFC', + descricao: 'Permite exportar e baixar os modelos tridimensionais brutos e arquivos IFC/BIM do projeto.', + icone: 'Download', + padraoAtivo: true, + }, + { + key: 'eng_ver_custos', + nome: 'Visualização de Fichas Técnicas & Custos', + descricao: 'Exibe valores monetários, custo unitário por quilo e tabelas financeiras confidenciais de peças.', + icone: 'DollarSign', + padraoAtivo: false, + }, + { + key: 'eng_editar_estruturas', + nome: 'Edição de Estruturas & Subconjuntos', + descricao: 'Permite alterar hierarquias de peças, pesos de projeto e marcas de montagem já cadastradas.', + icone: 'Network', + padraoAtivo: false, + }, + { + key: 'eng_upload_cad', + nome: 'Upload de Arquivos CAD/DWG/DSTV', + descricao: 'Permite importar novos arquivos de engenharia, perfis de corte e desenhos 2D na OF.', + icone: 'UploadCloud', + padraoAtivo: true, + }, + ], + templates: [ + { + id: 'desenhista_projetista', + nome: 'Projetista / Detalhista', + descricao: 'Upload de arquivos técnicos e download de IFC para verificação e modelagem.', + icone: 'Compass', + regrasAtivas: ['eng_download_ifc', 'eng_upload_cad'], + }, + { + id: 'coordenador_engenharia', + nome: 'Coordenador de Engenharia', + descricao: 'Aprovação de revisões, edição de conjuntos e gestão técnica completa.', + icone: 'Shield', + regrasAtivas: ['eng_aprovar_revisoes', 'eng_download_ifc', 'eng_editar_estruturas', 'eng_upload_cad'], + }, + { + id: 'diretoria_tecnica', + nome: 'Diretoria Técnica & Orçamentos', + descricao: 'Controle irrestrito com acesso a custos confidenciais e fichas financeiras.', + icone: 'Crown', + regrasAtivas: ['eng_aprovar_revisoes', 'eng_download_ifc', 'eng_ver_custos', 'eng_editar_estruturas', 'eng_upload_cad'], + }, + ], + }, + + expedicao_logistica: { + key: 'expedicao_logistica', + titulo: 'Expedição & Romaneios', + cor: 'amber', + icone: 'Truck', + descricao: 'Regras de pesagem rodoviária, autorização de embarque, conferência cega e estorno de cargas.', + regras: [ + { + key: 'exp_sem_pesagem', + nome: 'Liberação de Romaneio sem Pesagem', + descricao: 'Permite despachar carretas sem registrar o ticket físico da balança rodoviária.', + icone: 'Scale', + padraoAtivo: false, + }, + { + key: 'exp_tolerancia_alta', + nome: 'Margem Ampliada de Tolerância de Balança', + descricao: 'Aceita diferença de peso acima de ±100kg entre o peso teórico dos romaneios e a balança.', + icone: 'Percent', + padraoAtivo: false, + }, + { + key: 'exp_despacho_sem_assinatura', + nome: 'Despacho sem Assinatura Digital do Motorista', + descricao: 'Autoriza a saída de carretas sem a coleta prévia da assinatura presencial do transportador.', + icone: 'FileSignature', + padraoAtivo: false, + }, + { + key: 'exp_conferencia_cega', + nome: 'Exigência de Conferência Cega no Embarque', + descricao: 'Força o operador a bipar e contar peça a peça sem visualizar a quantidade esperada na tela.', + icone: 'EyeOff', + padraoAtivo: true, + }, + { + key: 'exp_estorno_romaneio', + nome: 'Estorno & Cancelamento de Romaneios Despachados', + descricao: 'Permite cancelar romaneios já faturados/despachados com retorno automático das peças ao estoque.', + icone: 'RotateCcw', + padraoAtivo: false, + }, + ], + templates: [ + { + id: 'conferente_expedicao', + nome: 'Conferente de Pátio', + descricao: 'Focado em conferência cega e bipe de peças no carregamento das carretas.', + icone: 'Barcode', + regrasAtivas: ['exp_conferencia_cega'], + }, + { + id: 'lider_logistica', + nome: 'Líder de Expedição / Logística', + descricao: 'Autorização com margem de balança e despacho com assinatura flexível.', + icone: 'Shield', + regrasAtivas: ['exp_tolerancia_alta', 'exp_conferencia_cega', 'exp_sem_pesagem'], + }, + { + id: 'gerente_logistica_total', + nome: 'Gerente de Logística Total', + descricao: 'Acesso total com estorno de cargas despachadas e todas as liberações de saída.', + icone: 'Crown', + regrasAtivas: ['exp_sem_pesagem', 'exp_tolerancia_alta', 'exp_despacho_sem_assinatura', 'exp_conferencia_cega', 'exp_estorno_romaneio'], + }, + ], + }, + + obra_montagem: { + key: 'obra_montagem', + titulo: 'Obra & Montagem Externa', + cor: 'teal', + icone: 'HardHat', + descricao: 'Controle de descarga no canteiro, liberação de içamento e registro de não-conformidades de campo.', + regras: [ + { + key: 'obra_receber_romaneio', + nome: 'Confirmação de Descarga & Recebimento no Canteiro', + descricao: 'Permite registrar o recebimento físico das carretas e atestar o estado das peças entregues.', + icone: 'PackageCheck', + padraoAtivo: true, + }, + { + key: 'obra_liberar_montagem', + nome: 'Autorização Formal de Içamento & Fixação', + descricao: 'Libera a equipe de montadores para içar conjuntos e soldar/aparafusar na estrutura definitiva.', + icone: 'CheckSquare', + padraoAtivo: false, + }, + { + key: 'obra_apontar_nao_conformidade', + nome: 'Registro de Não-Conformidade de Campo (RNC)', + descricao: 'Permite abrir ocorrências de peças com furo desalinhado, empenamentos ou danos de transporte.', + icone: 'AlertOctagon', + padraoAtivo: true, + }, + ], + templates: [ + { + id: 'montador_campo', + nome: 'Montador / Encarregado de Campo', + descricao: 'Recebimento de romaneios e registro de problemas ou RNCs de campo.', + icone: 'Wrench', + regrasAtivas: ['obra_receber_romaneio', 'obra_apontar_nao_conformidade'], + }, + { + id: 'engenheiro_obra_total', + nome: 'Engenheiro Residente da Obra', + descricao: 'Controle irrestrito no canteiro com liberação de içamento e homologação estrutural.', + icone: 'Crown', + regrasAtivas: ['obra_receber_romaneio', 'obra_liberar_montagem', 'obra_apontar_nao_conformidade'], + }, + ], + }, + + admin_sistema: { + key: 'admin_sistema', + titulo: 'Administração & Sistema', + cor: 'rose', + icone: 'Shield', + descricao: 'Acesso a auditorias de segurança completas, exportações em massa e parâmetros globais do ERP.', + regras: [ + { + key: 'adm_auditoria_completa', + nome: 'Auditoria Completa & Logs de Segurança', + descricao: 'Permite visualizar todos os acessos por IP, histórico de operações críticas e falhas de login.', + icone: 'Activity', + padraoAtivo: false, + }, + { + key: 'adm_exportar_dados', + nome: 'Exportação Massiva de Dados (Excel/CSV/JSON)', + descricao: 'Libera a exportação de tabelas completas de clientes, faturamento, peças e ordens de produção.', + icone: 'DownloadCloud', + padraoAtivo: false, + }, + { + key: 'adm_configuracoes_globais', + nome: 'Alteração de Parâmetros Globais do Sistema', + descricao: 'Autoriza alterar configurações gerais da empresa, logotipos, regras fiscais e integrações.', + icone: 'Settings', + padraoAtivo: false, + }, + ], + templates: [ + { + id: 'auditor_sistema', + nome: 'Auditor de Segurança', + descricao: 'Acesso de auditoria e exportação para compliance sem alterar parâmetros globais.', + icone: 'Search', + regrasAtivas: ['adm_auditoria_completa', 'adm_exportar_dados'], + }, + { + id: 'super_admin_total', + nome: 'Super Administrador Total', + descricao: 'Controle total de governança, parâmetros de infraestrutura e exportações.', + icone: 'Crown', + regrasAtivas: ['adm_auditoria_completa', 'adm_exportar_dados', 'adm_configuracoes_globais'], + }, + ], + }, +};