feat: consolidacao matriz de acessos, modais avancados e ocultacao dinamica de menus
This commit is contained in:
@@ -100,46 +100,66 @@ export function AppSidebar() {
|
|||||||
// Admin can always access everything
|
// Admin can always access everything
|
||||||
if (isAdmin) return true;
|
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<string, string> = {
|
const itemToResourceMap: Record<string, string> = {
|
||||||
|
// PCP & Produção
|
||||||
'pcp': 'producao-pcp',
|
'pcp': 'producao-pcp',
|
||||||
'dashboard-producao': 'producao-dashboard',
|
'dashboard-producao': 'producao-dashboard',
|
||||||
'apontamento-producao': 'producao-apontamento',
|
'apontamento-producao': 'producao-apontamento',
|
||||||
'diario-producao': 'diario-producao',
|
'diario-producao': 'diario-producao',
|
||||||
'prioridades-fabricacao': 'prioridades-fabricacao',
|
'prioridades-fabricacao': 'prioridades-fabricacao',
|
||||||
'visao-geral': 'producao-visao',
|
'visao-geral': 'producao-visao',
|
||||||
|
'painel-industrial': 'producao-dashboard',
|
||||||
|
// Engenharia, 3D & OFs
|
||||||
'visualizador-3d': 'visualizador-3d',
|
'visualizador-3d': 'visualizador-3d',
|
||||||
'ordens-fabricacao': 'ofs-lista',
|
'ordens-fabricacao': 'ofs-lista',
|
||||||
|
'ofs-concluidas': 'ofs-lista',
|
||||||
'ficha-tecnica-of': 'cadastro-of',
|
'ficha-tecnica-of': 'cadastro-of',
|
||||||
'cronograma': 'ofs-cronograma',
|
'cronograma': 'ofs-cronograma',
|
||||||
'cadastro-pecas': 'cadastro-pecas',
|
'cadastro-pecas': 'cadastro-pecas',
|
||||||
'equipamentos': 'equipamentos',
|
'equipamentos': 'equipamentos',
|
||||||
|
// Expedição & Estoque
|
||||||
'expedicao': 'expedicao',
|
'expedicao': 'expedicao',
|
||||||
'estoque-main': 'estoque',
|
'estoque-main': 'estoque',
|
||||||
'estoque': 'estoque',
|
'estoque': 'estoque',
|
||||||
'solicitacao-compras': 'estoque-solicitacao-compras',
|
'solicitacao-compras': 'estoque-solicitacao-compras',
|
||||||
|
// Obra & Campo
|
||||||
'dashboard-obras': 'obra-dashboard',
|
'dashboard-obras': 'obra-dashboard',
|
||||||
'configuracoes-obra': 'obra-configuracoes',
|
'configuracoes-obra': 'obra-configuracoes',
|
||||||
'obra': 'obra-dashboard',
|
'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',
|
'gerenciar-usuarios': 'user-management',
|
||||||
'user-management': 'user-management',
|
'user-management': 'user-management',
|
||||||
'configuracoes-gerais': 'configuracoes-gerais',
|
'configuracoes-gerais': 'configuracoes-gerais',
|
||||||
'inconsistencias': 'ferramentas-inconsistencias',
|
'personalizacao-tema': 'admin',
|
||||||
'ferramentas': 'ferramentas',
|
|
||||||
'sistema': 'sistema',
|
|
||||||
'sugestoes': 'sugestoes',
|
|
||||||
'admin': 'admin',
|
'admin': 'admin',
|
||||||
'dashboard': 'dashboard',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const resKey = itemToResourceMap[itemKey] || itemKey;
|
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]) {
|
if (resourcePermissions && resourcePermissions[resKey]) {
|
||||||
return resourcePermissions[resKey] !== 'no_access';
|
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 =
|
const temAcessoSmart =
|
||||||
resourcePermissions &&
|
resourcePermissions &&
|
||||||
resourcePermissions['modo-smart'] &&
|
resourcePermissions['modo-smart'] &&
|
||||||
@@ -156,8 +176,23 @@ export function AppSidebar() {
|
|||||||
if (soTemSmart && !resKey.startsWith('smart') && resKey !== 'modo-smart') {
|
if (soTemSmart && !resKey.startsWith('smart') && resKey !== 'modo-smart') {
|
||||||
return false;
|
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 (requiresSpecialPermission) {
|
||||||
if (itemKey === 'ferramentas') {
|
if (itemKey === 'ferramentas') {
|
||||||
return canAccessTools();
|
return canAccessTools();
|
||||||
@@ -235,12 +270,25 @@ export function AppSidebar() {
|
|||||||
{menuGroups.map(group => {
|
{menuGroups.map(group => {
|
||||||
// Filter admin groups for non-admin users
|
// Filter admin groups for non-admin users
|
||||||
if (!isAdmin && group.name === 'Administração') {
|
if (!isAdmin && group.name === 'Administração') {
|
||||||
logger.debug('Ocultando grupo admin para usuário não-admin');
|
const hasAnyAdminItem = group.items.some(item => canAccessItem(item.key, item.requiresSpecialPermission));
|
||||||
return null;
|
if (!hasAnyAdminItem) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip groups with no items
|
// Filtrar itens do grupo permitidos para o usuário logado
|
||||||
if (!group.items || group.items.length === 0) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +302,7 @@ export function AppSidebar() {
|
|||||||
</SidebarGroupLabel>
|
</SidebarGroupLabel>
|
||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{group.items.map((item) => (
|
{itemsPermitidos.map((item) => (
|
||||||
<AppSidebarMenuItem
|
<AppSidebarMenuItem
|
||||||
key={item.key || item.title}
|
key={item.key || item.title}
|
||||||
item={item}
|
item={item}
|
||||||
|
|||||||
@@ -27,12 +27,6 @@ export const AppSidebarMenuItem: React.FC<SidebarMenuItemProps> = ({
|
|||||||
onMenuItemClick,
|
onMenuItemClick,
|
||||||
getIconProps
|
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
|
// Filtrar subitens permitidos para o usuário logado
|
||||||
const subItemsPermitidos = (item.subItems || []).filter((subItem) => {
|
const subItemsPermitidos = (item.subItems || []).filter((subItem) => {
|
||||||
if (subItem.url === "/admin/theme-customization" && !isAdmin) {
|
if (subItem.url === "/admin/theme-customization" && !isAdmin) {
|
||||||
@@ -42,8 +36,16 @@ export const AppSidebarMenuItem: React.FC<SidebarMenuItemProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Se o item for apenas um agrupador de subitens e todos estiverem bloqueados, não renderizar o agrupador
|
// 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) {
|
if (item.subItems && item.subItems.length > 0) {
|
||||||
return null;
|
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 (
|
return (
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useMatrizAcessos, UsuarioMatriz } from '@/hooks/useMatrizAcessos';
|
|||||||
import { MatrizGrid } from './MatrizGrid';
|
import { MatrizGrid } from './MatrizGrid';
|
||||||
import { MatrizTemplatesModal } from './MatrizTemplatesModal';
|
import { MatrizTemplatesModal } from './MatrizTemplatesModal';
|
||||||
import { MatrizSmartAvancadoModal } from './MatrizSmartAvancadoModal';
|
import { MatrizSmartAvancadoModal } from './MatrizSmartAvancadoModal';
|
||||||
|
import { MatrizGrupoAvancadoModal } from './MatrizGrupoAvancadoModal';
|
||||||
import { SimuladorVisaoBar } from './SimuladorVisaoBar';
|
import { SimuladorVisaoBar } from './SimuladorVisaoBar';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -30,6 +31,7 @@ export const MatrizAcessosPainel: React.FC<MatrizAcessosPainelProps> = ({
|
|||||||
const [busca, setBusca] = useState('');
|
const [busca, setBusca] = useState('');
|
||||||
const [selectedUserForTemplate, setSelectedUserForTemplate] = useState<UsuarioMatriz | null>(null);
|
const [selectedUserForTemplate, setSelectedUserForTemplate] = useState<UsuarioMatriz | null>(null);
|
||||||
const [smartAvancadoOpen, setSmartAvancadoOpen] = useState(false);
|
const [smartAvancadoOpen, setSmartAvancadoOpen] = useState(false);
|
||||||
|
const [grupoAvancadoKey, setGrupoAvancadoKey] = useState<string | null>(null);
|
||||||
|
|
||||||
// Filtragem por nome, email ou cargo
|
// Filtragem por nome, email ou cargo
|
||||||
const usuariosFiltrados = usuarios.filter((u) => {
|
const usuariosFiltrados = usuarios.filter((u) => {
|
||||||
@@ -152,6 +154,7 @@ export const MatrizAcessosPainel: React.FC<MatrizAcessosPainelProps> = ({
|
|||||||
onToggleNivel={toggleNivel}
|
onToggleNivel={toggleNivel}
|
||||||
onOpenTemplates={(user) => setSelectedUserForTemplate(user)}
|
onOpenTemplates={(user) => setSelectedUserForTemplate(user)}
|
||||||
onOpenSmartAvancado={() => setSmartAvancadoOpen(true)}
|
onOpenSmartAvancado={() => setSmartAvancadoOpen(true)}
|
||||||
|
onOpenGrupoAvancado={(key) => setGrupoAvancadoKey(key)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -168,6 +171,13 @@ export const MatrizAcessosPainel: React.FC<MatrizAcessosPainelProps> = ({
|
|||||||
isOpen={smartAvancadoOpen}
|
isOpen={smartAvancadoOpen}
|
||||||
onClose={() => setSmartAvancadoOpen(false)}
|
onClose={() => setSmartAvancadoOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 7. MODAL DE GESTÃO AVANÇADA POR CATEGORIA / GRUPO */}
|
||||||
|
<MatrizGrupoAvancadoModal
|
||||||
|
grupoKey={grupoAvancadoKey}
|
||||||
|
isOpen={Boolean(grupoAvancadoKey)}
|
||||||
|
onClose={() => setGrupoAvancadoKey(null)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ interface MatrizGridProps {
|
|||||||
onToggleNivel: (userId: string, resourceKey: string, tipo: 'V' | 'C') => void;
|
onToggleNivel: (userId: string, resourceKey: string, tipo: 'V' | 'C') => void;
|
||||||
onOpenTemplates: (user: UsuarioMatriz) => void;
|
onOpenTemplates: (user: UsuarioMatriz) => void;
|
||||||
onOpenSmartAvancado?: () => void;
|
onOpenSmartAvancado?: () => void;
|
||||||
|
onOpenGrupoAvancado?: (grupoKey: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MatrizGrid: React.FC<MatrizGridProps> = ({
|
export const MatrizGrid: React.FC<MatrizGridProps> = ({
|
||||||
@@ -20,6 +21,7 @@ export const MatrizGrid: React.FC<MatrizGridProps> = ({
|
|||||||
onToggleNivel,
|
onToggleNivel,
|
||||||
onOpenTemplates,
|
onOpenTemplates,
|
||||||
onOpenSmartAvancado,
|
onOpenSmartAvancado,
|
||||||
|
onOpenGrupoAvancado,
|
||||||
}) => {
|
}) => {
|
||||||
// Cores de fundo e borda para os grupos
|
// Cores de fundo e borda para os grupos
|
||||||
const corGrupoHeader: Record<string, string> = {
|
const corGrupoHeader: Record<string, string> = {
|
||||||
@@ -31,6 +33,15 @@ export const MatrizGrid: React.FC<MatrizGridProps> = ({
|
|||||||
rose: 'bg-rose-500/10 text-rose-400 border-rose-500/30',
|
rose: 'bg-rose-500/10 text-rose-400 border-rose-500/30',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const corBotaoAvancado: Record<string, string> = {
|
||||||
|
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 (
|
return (
|
||||||
<div className="rounded-2xl border border-border bg-card overflow-hidden shadow-sm">
|
<div className="rounded-2xl border border-border bg-card overflow-hidden shadow-sm">
|
||||||
<div className="overflow-x-auto max-w-full">
|
<div className="overflow-x-auto max-w-full">
|
||||||
@@ -52,7 +63,7 @@ export const MatrizGrid: React.FC<MatrizGridProps> = ({
|
|||||||
>
|
>
|
||||||
<div className="flex items-center justify-center gap-2 flex-wrap">
|
<div className="flex items-center justify-center gap-2 flex-wrap">
|
||||||
<span>{grupo.titulo}</span>
|
<span>{grupo.titulo}</span>
|
||||||
{grupo.key === 'smart' && onOpenSmartAvancado && (
|
{grupo.key === 'smart' && onOpenSmartAvancado ? (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@@ -63,7 +74,18 @@ export const MatrizGrid: React.FC<MatrizGridProps> = ({
|
|||||||
<SlidersHorizontal className="h-3 w-3" />
|
<SlidersHorizontal className="h-3 w-3" />
|
||||||
Avançado
|
Avançado
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
) : onOpenGrupoAvancado ? (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => onOpenGrupoAvancado(grupo.key)}
|
||||||
|
className={`h-6 px-2 py-0 text-[10px] ${corBotaoAvancado[grupo.cor] || 'bg-primary/20 text-primary border-primary/40'} rounded-md font-semibold flex items-center gap-1 shadow-sm transition-transform active:scale-95`}
|
||||||
|
title={`Abrir Regras & Governança Avançada de ${grupo.titulo}`}
|
||||||
|
>
|
||||||
|
<SlidersHorizontal className="h-3 w-3" />
|
||||||
|
Avançado
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[10px] font-normal opacity-80 block truncate">
|
<span className="text-[10px] font-normal opacity-80 block truncate">
|
||||||
{grupo.descricao}
|
{grupo.descricao}
|
||||||
|
|||||||
@@ -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<MatrizGrupoAvancadoModalProps> = ({
|
||||||
|
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<string, string> = {
|
||||||
|
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<string, string> = {
|
||||||
|
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 <Calendar className="h-4 w-4 text-sky-400" />;
|
||||||
|
case 'Lock':
|
||||||
|
return <Lock className="h-4 w-4 text-rose-400" />;
|
||||||
|
case 'ArrowUpDown':
|
||||||
|
return <ArrowUpDown className="h-4 w-4 text-amber-400" />;
|
||||||
|
case 'Layers':
|
||||||
|
return <Layers className="h-4 w-4 text-indigo-400" />;
|
||||||
|
case 'AlertTriangle':
|
||||||
|
return <AlertTriangle className="h-4 w-4 text-orange-400" />;
|
||||||
|
case 'Gauge':
|
||||||
|
return <Gauge className="h-4 w-4 text-purple-400" />;
|
||||||
|
case 'CheckCircle2':
|
||||||
|
return <CheckCircle2 className="h-4 w-4 text-emerald-400" />;
|
||||||
|
case 'Download':
|
||||||
|
return <Download className="h-4 w-4 text-sky-400" />;
|
||||||
|
case 'DollarSign':
|
||||||
|
return <DollarSign className="h-4 w-4 text-emerald-500" />;
|
||||||
|
case 'Network':
|
||||||
|
return <Network className="h-4 w-4 text-indigo-400" />;
|
||||||
|
case 'UploadCloud':
|
||||||
|
return <UploadCloud className="h-4 w-4 text-teal-400" />;
|
||||||
|
case 'Scale':
|
||||||
|
return <Scale className="h-4 w-4 text-amber-400" />;
|
||||||
|
case 'Percent':
|
||||||
|
return <Percent className="h-4 w-4 text-cyan-400" />;
|
||||||
|
case 'FileSignature':
|
||||||
|
return <FileSignature className="h-4 w-4 text-orange-400" />;
|
||||||
|
case 'EyeOff':
|
||||||
|
return <EyeOff className="h-4 w-4 text-rose-400" />;
|
||||||
|
case 'RotateCcw':
|
||||||
|
return <RotateCcw className="h-4 w-4 text-amber-400" />;
|
||||||
|
case 'PackageCheck':
|
||||||
|
return <PackageCheck className="h-4 w-4 text-teal-400" />;
|
||||||
|
case 'CheckSquare':
|
||||||
|
return <CheckSquare className="h-4 w-4 text-emerald-400" />;
|
||||||
|
case 'AlertOctagon':
|
||||||
|
return <AlertOctagon className="h-4 w-4 text-rose-400" />;
|
||||||
|
case 'Activity':
|
||||||
|
return <Activity className="h-4 w-4 text-rose-400" />;
|
||||||
|
case 'DownloadCloud':
|
||||||
|
return <DownloadCloud className="h-4 w-4 text-sky-400" />;
|
||||||
|
case 'Settings':
|
||||||
|
return <Settings className="h-4 w-4 text-slate-400" />;
|
||||||
|
case 'Crown':
|
||||||
|
return <Crown className="h-3.5 w-3.5 text-amber-400" />;
|
||||||
|
case 'Shield':
|
||||||
|
return <Shield className="h-3.5 w-3.5 text-sky-400" />;
|
||||||
|
case 'Wrench':
|
||||||
|
return <Wrench className="h-3.5 w-3.5 text-emerald-400" />;
|
||||||
|
default:
|
||||||
|
return <Sparkles className="h-4 w-4 text-amber-400" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||||
|
<DialogContent className="max-w-5xl w-[95vw] max-h-[92vh] h-[850px] p-0 flex flex-col bg-card border-border shadow-2xl rounded-2xl overflow-hidden">
|
||||||
|
{/* CABEÇALHO DO MODAL */}
|
||||||
|
<div className="px-6 py-4 border-b border-border bg-muted/40 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`p-2.5 rounded-xl border ${corFundoIcone[config.cor]}`}>
|
||||||
|
<SlidersHorizontal className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<DialogTitle className="text-base sm:text-lg font-bold flex items-center gap-2 text-foreground">
|
||||||
|
Regras & Governança Avançada
|
||||||
|
<Badge variant="outline" className={`text-[10px] uppercase tracking-wider font-bold ${corBadge[config.cor]}`}>
|
||||||
|
{config.titulo}
|
||||||
|
</Badge>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
{config.descricao}
|
||||||
|
</DialogDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CORPO PRINCIPAL: SIDEBAR DE USUÁRIOS + ÁREA DE REGRAS */}
|
||||||
|
<div className="flex-1 flex overflow-hidden">
|
||||||
|
{/* SIDEBAR DE USUÁRIOS */}
|
||||||
|
<div className="w-[280px] sm:w-[310px] border-r border-border bg-background/60 flex flex-col shrink-0">
|
||||||
|
<div className="p-3 border-b border-border">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Buscar colaborador..."
|
||||||
|
value={busca}
|
||||||
|
onChange={(e) => setBusca(e.target.value)}
|
||||||
|
className="pl-8 text-xs h-8 bg-card border-slate-300 dark:border-border text-foreground font-medium"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto divide-y divide-border/40 p-1.5 space-y-0.5">
|
||||||
|
{loading ? (
|
||||||
|
<div className="py-12 text-center text-xs text-muted-foreground">
|
||||||
|
Carregando colaboradores...
|
||||||
|
</div>
|
||||||
|
) : usersFiltrados.length === 0 ? (
|
||||||
|
<div className="py-12 text-center text-xs text-muted-foreground">
|
||||||
|
Nenhum colaborador encontrado.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
usersFiltrados.map((user) => {
|
||||||
|
const isSelected = user.userId === selectedUserId;
|
||||||
|
const totalAtivas = Object.values(user.regras).filter(Boolean).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={user.userId}
|
||||||
|
onClick={() => setSelectedUserId(user.userId)}
|
||||||
|
className={`w-full text-left p-2.5 rounded-xl transition-all flex items-center gap-2.5 ${
|
||||||
|
isSelected
|
||||||
|
? `${corBadge[config.cor]} border shadow-sm`
|
||||||
|
: 'hover:bg-muted/60 text-muted-foreground hover:text-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Avatar className="h-8 w-8 rounded-lg border border-border shrink-0">
|
||||||
|
<AvatarImage src={user.profileImageUrl || ''} />
|
||||||
|
<AvatarFallback className="text-[10px] font-bold bg-primary/10 text-primary">
|
||||||
|
{user.fullName.substring(0, 2).toUpperCase()}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-center justify-between gap-1">
|
||||||
|
<span className="text-xs font-semibold text-foreground truncate">
|
||||||
|
{user.fullName}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] font-bold text-muted-foreground shrink-0">
|
||||||
|
{totalAtivas}/{config.regras.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground">
|
||||||
|
<span className="truncate max-w-[120px]">{user.functionName}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span className="truncate">{user.privilegeName}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ÁREA DE CONFIGURAÇÃO DO USUÁRIO */}
|
||||||
|
<div className="flex-1 flex flex-col bg-card overflow-hidden">
|
||||||
|
{selectedUser ? (
|
||||||
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
|
{/* CABEÇALHO DO USUÁRIO SELECIONADO */}
|
||||||
|
<div className="p-4 border-b border-border/60 bg-muted/20 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Avatar className="h-10 w-10 rounded-xl border border-border">
|
||||||
|
<AvatarImage src={selectedUser.profileImageUrl || ''} />
|
||||||
|
<AvatarFallback className="text-xs font-bold bg-primary/10 text-primary">
|
||||||
|
{selectedUser.fullName.substring(0, 2).toUpperCase()}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-bold text-foreground">
|
||||||
|
{selectedUser.fullName}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-muted-foreground flex items-center gap-2">
|
||||||
|
<span>{selectedUser.email}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<Badge variant="secondary" className="text-[10px] py-0 h-4">
|
||||||
|
{selectedUser.functionName}
|
||||||
|
</Badge>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={salvarConfiguracaoUsuario}
|
||||||
|
disabled={saving}
|
||||||
|
className="h-8 text-xs font-semibold bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-1.5 shadow-sm"
|
||||||
|
>
|
||||||
|
<Save className="h-3.5 w-3.5" />
|
||||||
|
{saving ? 'Gravando...' : 'Salvar Regras'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* TEMPLATES RÁPIDOS */}
|
||||||
|
<div className="px-6 py-3 border-b border-border/40 bg-muted/10">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<span className="text-[11px] font-semibold text-muted-foreground mr-1 flex items-center gap-1">
|
||||||
|
<Sparkles className="h-3.5 w-3.5 text-amber-400" />
|
||||||
|
Presets Rápidos:
|
||||||
|
</span>
|
||||||
|
{config.templates.map((tpl) => (
|
||||||
|
<Button
|
||||||
|
key={tpl.id}
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => aplicarTemplate(tpl.id)}
|
||||||
|
className="h-7 text-[11px] px-2.5 py-0 rounded-lg hover:border-primary/50 flex items-center gap-1.5 bg-background/80"
|
||||||
|
title={tpl.descricao}
|
||||||
|
>
|
||||||
|
{getIconComponent(tpl.icone)}
|
||||||
|
{tpl.nome}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* LISTAGEM DE REGRAS GRANULARES */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-6 space-y-3">
|
||||||
|
<div className="grid grid-cols-1 gap-3">
|
||||||
|
{config.regras.map((regra) => {
|
||||||
|
const ativo = Boolean(selectedUser.regras[regra.key]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={regra.key}
|
||||||
|
onClick={() => 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'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3.5 min-w-0">
|
||||||
|
<div
|
||||||
|
className={`p-2 rounded-lg border shrink-0 ${
|
||||||
|
ativo
|
||||||
|
? `${corBadge[config.cor]} border`
|
||||||
|
: 'bg-muted text-muted-foreground border-border'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{getIconComponent(regra.icone)}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-0.5 min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-bold text-foreground">
|
||||||
|
{regra.nome}
|
||||||
|
</span>
|
||||||
|
{ativo && (
|
||||||
|
<Badge className="bg-emerald-500/15 text-emerald-400 border-emerald-500/30 text-[9px] h-4 py-0">
|
||||||
|
LIBERADO
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-muted-foreground leading-snug">
|
||||||
|
{regra.descricao}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="shrink-0" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<Switch
|
||||||
|
checked={ativo}
|
||||||
|
onCheckedChange={() => toggleRegra(regra.key)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex-1 flex items-center justify-center text-muted-foreground text-xs">
|
||||||
|
Selecione um colaborador ao lado para configurar as regras avançadas.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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<string, boolean>; // regraKey -> true/false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGrupoAvancadoPermissions(grupoKey: string | null) {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [users, setUsers] = useState<UserGrupoAvancado[]>([]);
|
||||||
|
const [selectedUserId, setSelectedUserId] = useState<string | null>(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<string, Record<string, boolean>> = {};
|
||||||
|
(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<string, boolean> = {};
|
||||||
|
|
||||||
|
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<string, boolean> = {};
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ import { Users, UserPlus, Shield, Settings, Mail, Activity, LayoutGrid } from 'l
|
|||||||
import { UsersTable } from '@/components/users/UsersTable';
|
import { UsersTable } from '@/components/users/UsersTable';
|
||||||
import { PendingUsersTable } from '@/components/users/PendingUsersTable';
|
import { PendingUsersTable } from '@/components/users/PendingUsersTable';
|
||||||
import { FunctionsManager } from '@/components/users/FunctionsManager';
|
import { FunctionsManager } from '@/components/users/FunctionsManager';
|
||||||
import { PrivilegesManager } from '@/components/users/PrivilegesManager';
|
|
||||||
import { PasswordResetRequests } from '@/components/users/PasswordResetRequests';
|
import { PasswordResetRequests } from '@/components/users/PasswordResetRequests';
|
||||||
import { SessionLogsSimple } from '@/components/users/SessionLogsSimple';
|
import { SessionLogsSimple } from '@/components/users/SessionLogsSimple';
|
||||||
import { UserModal } from '@/components/users/UserModal';
|
import { UserModal } from '@/components/users/UserModal';
|
||||||
@@ -40,9 +39,6 @@ const UserManagement = () => {
|
|||||||
createFunction,
|
createFunction,
|
||||||
updateFunction,
|
updateFunction,
|
||||||
deleteFunction,
|
deleteFunction,
|
||||||
createPrivilege: createPrivilegeBase,
|
|
||||||
updatePrivilege: updatePrivilegeBase,
|
|
||||||
deletePrivilege,
|
|
||||||
updateUser,
|
updateUser,
|
||||||
toggleUserStatus,
|
toggleUserStatus,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
@@ -67,15 +63,6 @@ const UserManagement = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrapper functions to match expected signatures
|
|
||||||
const createPrivilege = async (data: { name: string; description?: string; permissions: Record<string, boolean> }): Promise<void> => {
|
|
||||||
await createPrivilegeBase(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatePrivilege = async (id: string, data: { name: string; description?: string; permissions: Record<string, boolean> }): Promise<void> => {
|
|
||||||
await updatePrivilegeBase(id, data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditUser = (user: UserProfile) => {
|
const handleEditUser = (user: UserProfile) => {
|
||||||
if (!canEdit()) return;
|
if (!canEdit()) return;
|
||||||
setSelectedUser(user);
|
setSelectedUser(user);
|
||||||
@@ -158,7 +145,7 @@ const UserManagement = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||||
<TabsList className="grid w-full grid-cols-2 md:grid-cols-7 bg-muted h-auto p-1">
|
<TabsList className="grid w-full grid-cols-2 sm:grid-cols-3 md:grid-cols-6 bg-muted h-auto p-1">
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="matriz"
|
value="matriz"
|
||||||
className="flex items-center gap-1 md:gap-2 text-xs md:text-sm p-2 md:p-3 font-semibold text-primary data-[state=active]:bg-primary data-[state=active]:text-primary-foreground"
|
className="flex items-center gap-1 md:gap-2 text-xs md:text-sm p-2 md:p-3 font-semibold text-primary data-[state=active]:bg-primary data-[state=active]:text-primary-foreground"
|
||||||
@@ -210,17 +197,6 @@ const UserManagement = () => {
|
|||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{canEdit() && (
|
|
||||||
<TabsTrigger
|
|
||||||
value="privileges"
|
|
||||||
className="flex items-center gap-1 md:gap-2 text-xs md:text-sm p-2 md:p-3"
|
|
||||||
>
|
|
||||||
<Shield className="h-3 w-3 md:h-4 md:w-4" />
|
|
||||||
<span className="hidden sm:inline">Privilégios</span>
|
|
||||||
<span className="sm:hidden">Priv</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{canView() && (
|
{canView() && (
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="session-logs"
|
value="session-logs"
|
||||||
@@ -311,27 +287,6 @@ const UserManagement = () => {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{canEdit() && (
|
|
||||||
<TabsContent value="privileges" className="space-y-4 mt-4">
|
|
||||||
<Card className="card-mobile">
|
|
||||||
<CardHeader className="card-header-mobile">
|
|
||||||
<CardTitle className="text-lg md:text-xl flex items-center gap-2">
|
|
||||||
<Shield className="h-4 w-4 md:h-5 md:w-5" />
|
|
||||||
Gerenciar Privilégios
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="card-content-mobile">
|
|
||||||
<PrivilegesManager
|
|
||||||
privileges={privileges as any}
|
|
||||||
onCreate={canCreate() ? createPrivilege : undefined}
|
|
||||||
onUpdate={canEdit() ? updatePrivilege : undefined}
|
|
||||||
onDelete={canDelete() ? deletePrivilege : undefined}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{canView() && (
|
{canView() && (
|
||||||
<TabsContent value="session-logs" className="space-y-4 mt-4">
|
<TabsContent value="session-logs" className="space-y-4 mt-4">
|
||||||
<SessionLogsSimple users={users} />
|
<SessionLogsSimple users={users} />
|
||||||
|
|||||||
@@ -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<string, GrupoAvancadoConfig> = {
|
||||||
|
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'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user