feat: Balões duplos [ V ] (Visualizar) e [ C ] (Criar/Editar) na Matriz Central de Acessos

This commit is contained in:
2026-09-22 14:48:51 +00:00
parent f095737d53
commit 7d13d2b19e
4 changed files with 244 additions and 112 deletions
@@ -21,7 +21,7 @@ export const MatrizAcessosPainel: React.FC<MatrizAcessosPainelProps> = ({
gruposModulos, gruposModulos,
usuarioSimuladoId, usuarioSimuladoId,
setUsuarioSimuladoId, setUsuarioSimuladoId,
togglePermissao, toggleNivel,
aplicarTemplate, aplicarTemplate,
recarregar, recarregar,
} = useMatrizAcessos(); } = useMatrizAcessos();
@@ -41,9 +41,10 @@ export const MatrizAcessosPainel: React.FC<MatrizAcessosPainelProps> = ({
// Métricas rápidas // Métricas rápidas
const totalUsuarios = usuarios.length; const totalUsuarios = usuarios.length;
const totalSmartExclusivo = usuarios.filter( const totalSmartExclusivo = usuarios.filter((u) => {
(u) => u.recursosPermitidos.has('modo-smart') && u.recursosPermitidos.size <= 4 const ativos = Object.entries(u.permissoesMap).filter(([, v]) => v !== 'nenhum');
).length; return u.permissoesMap['modo-smart'] !== 'nenhum' && ativos.length <= 4;
}).length;
const totalAdmins = usuarios.filter((u) => const totalAdmins = usuarios.filter((u) =>
u.privilegeName.toLowerCase().includes('admin') u.privilegeName.toLowerCase().includes('admin')
).length; ).length;
@@ -136,7 +137,7 @@ export const MatrizAcessosPainel: React.FC<MatrizAcessosPainelProps> = ({
<MatrizGrid <MatrizGrid
usuarios={usuariosFiltrados} usuarios={usuariosFiltrados}
grupos={gruposModulos} grupos={gruposModulos}
onTogglePermissao={togglePermissao} onToggleNivel={toggleNivel}
onOpenTemplates={(user) => setSelectedUserForTemplate(user)} onOpenTemplates={(user) => setSelectedUserForTemplate(user)}
/> />
)} )}
+97 -23
View File
@@ -1,22 +1,22 @@
import React from 'react'; import React from 'react';
import { UsuarioMatriz, GrupoRecurso } from '@/hooks/useMatrizAcessos'; import { UsuarioMatriz, GrupoRecurso, NivelAcesso } from '@/hooks/useMatrizAcessos';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Check, X, Shield, Sparkles, User, HelpCircle } from 'lucide-react'; import { Sparkles, Eye, Edit3, ShieldAlert } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
interface MatrizGridProps { interface MatrizGridProps {
usuarios: UsuarioMatriz[]; usuarios: UsuarioMatriz[];
grupos: GrupoRecurso[]; grupos: GrupoRecurso[];
onTogglePermissao: (userId: string, resourceKey: string) => void; onToggleNivel: (userId: string, resourceKey: string, tipo: 'V' | 'C') => void;
onOpenTemplates: (user: UsuarioMatriz) => void; onOpenTemplates: (user: UsuarioMatriz) => void;
} }
export const MatrizGrid: React.FC<MatrizGridProps> = ({ export const MatrizGrid: React.FC<MatrizGridProps> = ({
usuarios, usuarios,
grupos, grupos,
onTogglePermissao, onToggleNivel,
onOpenTemplates, onOpenTemplates,
}) => { }) => {
// Cores de fundo e borda para os grupos // Cores de fundo e borda para os grupos
@@ -32,7 +32,7 @@ export const MatrizGrid: React.FC<MatrizGridProps> = ({
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">
<table className="w-full text-left border-collapse min-w-[900px]"> <table className="w-full text-left border-collapse min-w-[950px]">
{/* CABEÇALHO DE GRUPOS DE MÓDULOS */} {/* CABEÇALHO DE GRUPOS DE MÓDULOS */}
<thead> <thead>
<tr className="border-b border-border/80 bg-muted/40"> <tr className="border-b border-border/80 bg-muted/40">
@@ -68,10 +68,10 @@ export const MatrizGrid: React.FC<MatrizGridProps> = ({
grupo.recursos.map((rec) => ( grupo.recursos.map((rec) => (
<th <th
key={rec.key} key={rec.key}
className="p-2 text-center border-r border-border/30 font-medium whitespace-nowrap min-w-[110px]" className="p-2 text-center border-r border-border/30 font-medium whitespace-nowrap min-w-[120px]"
title={rec.nome} title={rec.nome}
> >
<span className="truncate block max-w-[120px] mx-auto"> <span className="truncate block max-w-[125px] mx-auto">
{rec.nome} {rec.nome}
</span> </span>
</th> </th>
@@ -128,10 +128,12 @@ export const MatrizGrid: React.FC<MatrizGridProps> = ({
</div> </div>
</td> </td>
{/* Células Clicáveis da Matriz */} {/* Células da Matriz com o Balão Duplo [ V ] [ C ] */}
{grupos.map((grupo) => {grupos.map((grupo) =>
grupo.recursos.map((rec) => { grupo.recursos.map((rec) => {
const temAcesso = user.recursosPermitidos.has(rec.key); const nivel: NivelAcesso = user.permissoesMap[rec.key] || 'nenhum';
const isVAtivo = nivel === 'visualizar' || nivel === 'criar';
const isCAtivo = nivel === 'criar';
return ( return (
<td <td
@@ -141,25 +143,59 @@ export const MatrizGrid: React.FC<MatrizGridProps> = ({
<TooltipProvider delayDuration={150}> <TooltipProvider delayDuration={150}>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<button {/* O Balão Principal com 2 metades internas */}
onClick={() => onTogglePermissao(user.id, rec.key)} <div
className={`w-full py-1.5 rounded-lg font-medium text-xs flex items-center justify-center transition-all ${ className={`inline-flex items-center p-0.5 rounded-lg border transition-all ${
temAcesso isCAtivo
? 'bg-emerald-500/15 text-emerald-400 hover:bg-emerald-500/25 border border-emerald-500/30' ? 'border-emerald-500/40 bg-emerald-500/10 shadow-sm shadow-emerald-500/10'
: 'bg-muted/40 text-muted-foreground/40 hover:bg-muted/70 hover:text-muted-foreground border border-transparent' : isVAtivo
? 'border-sky-500/40 bg-sky-500/10 shadow-sm shadow-sky-500/10'
: 'border-border/60 bg-muted/30 opacity-70'
}`} }`}
> >
{temAcesso ? ( {/* Botão Interno V (Visualizar) */}
<Check className="h-4 w-4 stroke-[2.5]" /> <button
) : ( onClick={() => onToggleNivel(user.id, rec.key, 'V')}
<X className="h-3.5 w-3.5 opacity-40" /> className={`px-2 py-1 rounded-md text-[11px] font-bold transition-all flex items-center gap-1 ${
)} isVAtivo
</button> ? isCAtivo
? 'bg-emerald-500 text-white shadow-xs'
: 'bg-sky-500 text-white shadow-xs'
: 'text-muted-foreground/50 hover:text-muted-foreground hover:bg-muted'
}`}
title="V: Apenas Visualizar"
>
V
</button>
{/* Separador sutil */}
<div className="w-[1px] h-3.5 bg-border/60 mx-0.5" />
{/* Botão Interno C (Criar/Editar) */}
<button
onClick={() => onToggleNivel(user.id, rec.key, 'C')}
className={`px-2 py-1 rounded-md text-[11px] font-bold transition-all flex items-center gap-1 ${
isCAtivo
? 'bg-emerald-500 text-white shadow-xs'
: 'text-muted-foreground/50 hover:text-muted-foreground hover:bg-muted'
}`}
title="C: Criar e Editar (ativa ambos)"
>
C
</button>
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top" className="text-xs"> <TooltipContent side="top" className="text-xs">
<p className="font-bold">{rec.nome}</p> <p className="font-bold">{rec.nome}</p>
<p className="text-muted-foreground"> <p className="text-muted-foreground mt-0.5">
{temAcesso ? 'Clique para Bloquear' : 'Clique para Liberar'} {nivel === 'criar'
? '🟢 Permissão Total: Criação, Edição e Apontamento (V + C)'
: nivel === 'visualizar'
? '🔵 Apenas Leitura / Consulta (V)'
: '⚪ Bloqueado / Oculto'}
</p>
<p className="text-[10px] text-muted-foreground/80 mt-1 italic">
* Clique em V para apenas ver | Clique em C para liberar tudo
</p> </p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@@ -174,6 +210,44 @@ export const MatrizGrid: React.FC<MatrizGridProps> = ({
</tbody> </tbody>
</table> </table>
</div> </div>
{/* Legenda Explicativa do Balão no Rodapé */}
<div className="p-3 bg-muted/20 border-t border-border/50 flex flex-wrap items-center justify-between text-xs text-muted-foreground gap-3">
<div className="flex items-center gap-4">
<span className="font-semibold text-foreground">Legenda dos Balões:</span>
<div className="flex items-center gap-1.5">
<span className="inline-flex items-center p-0.5 rounded border border-border/60 bg-muted/40">
<span className="px-1.5 py-0.5 text-[10px] font-bold text-muted-foreground/50">V</span>
<span className="w-[1px] h-3 bg-border/60 mx-0.5" />
<span className="px-1.5 py-0.5 text-[10px] font-bold text-muted-foreground/50">C</span>
</span>
<span>Bloqueado (Nem visualiza)</span>
</div>
<div className="flex items-center gap-1.5">
<span className="inline-flex items-center p-0.5 rounded border border-sky-500/40 bg-sky-500/10">
<span className="px-1.5 py-0.5 text-[10px] font-bold bg-sky-500 text-white rounded">V</span>
<span className="w-[1px] h-3 bg-border/60 mx-0.5" />
<span className="px-1.5 py-0.5 text-[10px] font-bold text-muted-foreground/50">C</span>
</span>
<span>Apenas Visualização (Leitura)</span>
</div>
<div className="flex items-center gap-1.5">
<span className="inline-flex items-center p-0.5 rounded border border-emerald-500/40 bg-emerald-500/10">
<span className="px-1.5 py-0.5 text-[10px] font-bold bg-emerald-500 text-white rounded">V</span>
<span className="w-[1px] h-3 bg-border/60 mx-0.5" />
<span className="px-1.5 py-0.5 text-[10px] font-bold bg-emerald-500 text-white rounded">C</span>
</span>
<span>Criação / Edição Total</span>
</div>
</div>
<div className="text-[11px] italic">
* Clicar em C ativa ambos em verde | Clicar em V desativa o C.
</div>
</div>
</div> </div>
); );
}; };
@@ -22,7 +22,12 @@ export const SimuladorVisaoBar: React.FC<SimuladorVisaoBarProps> = ({
// Total de módulos disponíveis // Total de módulos disponíveis
const totalModulos = grupos.reduce((acc, g) => acc + g.recursos.length, 0); const totalModulos = grupos.reduce((acc, g) => acc + g.recursos.length, 0);
const totalPermitidos = usuarioSimulado ? usuarioSimulado.recursosPermitidos.size : 0; const totalPermitidos = usuarioSimulado
? Object.values(usuarioSimulado.permissoesMap).filter((v) => v !== 'nenhum').length
: 0;
const isSmartExclusivo = usuarioSimulado
? usuarioSimulado.permissoesMap['modo-smart'] !== 'nenhum' && totalPermitidos <= 4
: false;
return ( return (
<div className="rounded-xl border border-border/80 bg-muted/30 p-3 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 text-xs"> <div className="rounded-xl border border-border/80 bg-muted/30 p-3 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 text-xs">
@@ -63,13 +68,13 @@ export const SimuladorVisaoBar: React.FC<SimuladorVisaoBarProps> = ({
<Badge <Badge
variant="outline" variant="outline"
className={`h-7 px-2 border font-semibold flex items-center gap-1 ${ className={`h-7 px-2 border font-semibold flex items-center gap-1 ${
usuarioSimulado.recursosPermitidos.has('modo-smart') && totalPermitidos <= 4 isSmartExclusivo
? 'border-emerald-500/40 bg-emerald-500/10 text-emerald-400' ? 'border-emerald-500/40 bg-emerald-500/10 text-emerald-400'
: 'border-sky-500/40 bg-sky-500/10 text-sky-400' : 'border-sky-500/40 bg-sky-500/10 text-sky-400'
}`} }`}
> >
<CheckCircle2 className="h-3 w-3" /> <CheckCircle2 className="h-3 w-3" />
{usuarioSimulado.recursosPermitidos.has('modo-smart') && totalPermitidos <= 4 {isSmartExclusivo
? 'Modo Smart Exclusivo' ? 'Modo Smart Exclusivo'
: `${totalPermitidos}/${totalModulos} Módulos`} : `${totalPermitidos}/${totalModulos} Módulos`}
</Badge> </Badge>
+133 -81
View File
@@ -2,6 +2,8 @@ import { useState, useEffect, useMemo } from 'react';
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner'; import { toast } from 'sonner';
export type NivelAcesso = 'nenhum' | 'visualizar' | 'criar';
export interface UsuarioMatriz { export interface UsuarioMatriz {
id: string; id: string;
email: string; email: string;
@@ -12,13 +14,13 @@ export interface UsuarioMatriz {
functionId: string | null; functionId: string | null;
functionName: string; functionName: string;
profileImageUrl: string | null; profileImageUrl: string | null;
recursosPermitidos: Set<string>; permissoesMap: Record<string, NivelAcesso>;
} }
export interface GrupoRecurso { export interface GrupoRecurso {
key: string; key: string;
titulo: string; titulo: string;
cor: string; // Ex: 'emerald', 'sky', 'indigo', 'amber', 'rose' cor: string; // 'emerald', 'sky', 'indigo', 'amber', 'teal', 'rose'
descricao: string; descricao: string;
recursos: { recursos: {
key: string; key: string;
@@ -107,7 +109,7 @@ export interface TemplateAcesso {
descricao: string; descricao: string;
icone: string; icone: string;
badge: string; badge: string;
recursosPermitidos: string[]; permissoesPadrao: Record<string, NivelAcesso>;
} }
export const TEMPLATES_ACESSO: TemplateAcesso[] = [ export const TEMPLATES_ACESSO: TemplateAcesso[] = [
@@ -117,12 +119,12 @@ export const TEMPLATES_ACESSO: TemplateAcesso[] = [
descricao: 'Acesso 100% restrito ao Modo Smart touch. Não enxerga nenhum menu corporativo do ERP.', descricao: 'Acesso 100% restrito ao Modo Smart touch. Não enxerga nenhum menu corporativo do ERP.',
icone: 'Factory', icone: 'Factory',
badge: 'Chão de Fábrica', badge: 'Chão de Fábrica',
recursosPermitidos: [ permissoesPadrao: {
'modo-smart', 'modo-smart': 'visualizar',
'smart-apontamento', 'smart-apontamento': 'criar', // pode apontar produção
'smart-metricas', 'smart-metricas': 'visualizar', // pode ver suas métricas
'smart-pdf', 'smart-pdf': 'criar', // pode gerar checklist/PDF
], },
}, },
{ {
id: 'pcp_planejamento', id: 'pcp_planejamento',
@@ -130,17 +132,17 @@ export const TEMPLATES_ACESSO: TemplateAcesso[] = [
descricao: 'Acesso completo à Mesa Tática do PCP, Dashboards, Cronogramas, OFs e Visualizador 3D.', descricao: 'Acesso completo à Mesa Tática do PCP, Dashboards, Cronogramas, OFs e Visualizador 3D.',
icone: 'Layers', icone: 'Layers',
badge: 'Planejamento', badge: 'Planejamento',
recursosPermitidos: [ permissoesPadrao: {
'producao-pcp', 'producao-pcp': 'criar',
'producao-dashboard', 'producao-dashboard': 'visualizar',
'diario-producao', 'diario-producao': 'visualizar',
'prioridades-fabricacao', 'prioridades-fabricacao': 'criar',
'visualizador-3d', 'visualizador-3d': 'visualizar',
'ofs-lista', 'ofs-lista': 'criar',
'ofs-cronograma', 'ofs-cronograma': 'criar',
'cadastro-pecas', 'cadastro-pecas': 'criar',
'modo-smart', 'modo-smart': 'visualizar',
], },
}, },
{ {
id: 'expedicao_logistica', id: 'expedicao_logistica',
@@ -148,12 +150,12 @@ export const TEMPLATES_ACESSO: TemplateAcesso[] = [
descricao: 'Controle de romaneios, pesagens, expedição de peças e solicitações de compras.', descricao: 'Controle de romaneios, pesagens, expedição de peças e solicitações de compras.',
icone: 'Truck', icone: 'Truck',
badge: 'Logística', badge: 'Logística',
recursosPermitidos: [ permissoesPadrao: {
'expedicao', 'expedicao': 'criar',
'estoque', 'estoque': 'criar',
'estoque-solicitacao-compras', 'estoque-solicitacao-compras': 'criar',
'modo-smart', 'modo-smart': 'visualizar',
], },
}, },
{ {
id: 'fiscal_obra', id: 'fiscal_obra',
@@ -161,20 +163,23 @@ export const TEMPLATES_ACESSO: TemplateAcesso[] = [
descricao: 'Acompanhamento do canteiro de obras, romaneios recebidos e Modo Smart de montagem.', descricao: 'Acompanhamento do canteiro de obras, romaneios recebidos e Modo Smart de montagem.',
icone: 'HardHat', icone: 'HardHat',
badge: 'Obra', badge: 'Obra',
recursosPermitidos: [ permissoesPadrao: {
'obra-dashboard', 'obra-dashboard': 'visualizar',
'obra-configuracoes', 'obra-configuracoes': 'visualizar',
'visualizador-3d', 'visualizador-3d': 'visualizar',
'modo-smart', 'modo-smart': 'visualizar',
], },
}, },
{ {
id: 'administrador_total', id: 'administrador_total',
nome: 'Administrador / Gestor Total', nome: 'Administrador / Gestor Total',
descricao: 'Acesso irrestrito a todas as telas, configurações, usuários e ferramentas do sistema.', descricao: 'Acesso irrestrito com criação e edição em todas as telas e configurações.',
icone: 'Crown', icone: 'Crown',
badge: 'Controle Total', badge: 'Controle Total',
recursosPermitidos: GRUPOS_MODULOS.flatMap((g) => g.recursos.map((r) => r.key)), permissoesPadrao: GRUPOS_MODULOS.flatMap((g) => g.recursos).reduce((acc, r) => {
acc[r.key] = 'criar';
return acc;
}, {} as Record<string, NivelAcesso>),
}, },
]; ];
@@ -230,15 +235,28 @@ export const useMatrizAcessos = () => {
// Mapear usuários para a Matriz // Mapear usuários para a Matriz
const mapped: UsuarioMatriz[] = (profiles || []).map((p: any) => { const mapped: UsuarioMatriz[] = (profiles || []).map((p: any) => {
const privId = p.privilege_id; const privId = p.privilege_id;
const recursos = privId && recursosPorPrivilege[privId] const privNome = p.privileges?.name || 'Padrão';
? new Set(recursosPorPrivilege[privId]) const isAdmin = privNome.toLowerCase().includes('admin');
const recursosSet = privId && recursosPorPrivilege[privId]
? recursosPorPrivilege[privId]
: new Set<string>(); : new Set<string>();
// Se o privilégio tiver nome "Admin" ou similar, já inclui todos const permissoesMap: Record<string, NivelAcesso> = {};
const privNome = p.privileges?.name || 'Padrão';
if (privNome.toLowerCase().includes('admin')) { GRUPOS_MODULOS.forEach((g) => {
GRUPOS_MODULOS.forEach((g) => g.recursos.forEach((r) => recursos.add(r.key))); g.recursos.forEach((r) => {
} if (isAdmin) {
permissoesMap[r.key] = 'criar';
} else if (recursosSet.has(`${r.key}:edit`) || recursosSet.has(`${r.key}_edit`)) {
permissoesMap[r.key] = 'criar';
} else if (recursosSet.has(r.key)) {
// Se tiver o recurso base, verificar se o perfil é apenas visualizador ou editor
permissoesMap[r.key] = 'criar'; // padrão ativo é criar
} else {
permissoesMap[r.key] = 'nenhum';
}
});
});
return { return {
id: p.id, id: p.id,
@@ -250,7 +268,7 @@ export const useMatrizAcessos = () => {
functionId: p.function_id, functionId: p.function_id,
functionName: p.user_functions?.name || 'Sem Cargo', functionName: p.user_functions?.name || 'Sem Cargo',
profileImageUrl: p.profile_image_url, profileImageUrl: p.profile_image_url,
recursosPermitidos: recursos, permissoesMap,
}; };
}); });
@@ -267,54 +285,78 @@ export const useMatrizAcessos = () => {
carregarDados(); carregarDados();
}, []); }, []);
// TOGGLE DIRETO DE UMA PERMISSÃO NA MATRIZ // LÓGICA SOLICITADA PELO USUÁRIO PARA BOTÕES V E C:
const togglePermissao = async (userId: string, resourceKey: string) => { // - Clicar em V (Visualizar):
// * Se o nível atual for 'criar' -> rebaixa para 'visualizar' (desativa o C)
// * Se o nível atual for 'visualizar' -> desativa ambos ('nenhum')
// * Se o nível atual for 'nenhum' -> ativa 'visualizar' (V verde, C desativado)
// - Clicar em C (Criar/Editar):
// * Se o nível for 'nenhum' ou 'visualizar' -> ativa 'criar' (ambos V e C ficam verdes!)
// * Se o nível já for 'criar' -> desliga C (rebaixa para 'visualizar')
const toggleNivel = async (userId: string, resourceKey: string, tipo: 'V' | 'C') => {
const user = usuarios.find((u) => u.id === userId); const user = usuarios.find((u) => u.id === userId);
if (!user) return; if (!user) return;
const temAcesso = user.recursosPermitidos.has(resourceKey); const nivelAtual = user.permissoesMap[resourceKey] || 'nenhum';
const novoSet = new Set(user.recursosPermitidos); let novoNivel: NivelAcesso = 'nenhum';
if (temAcesso) { if (tipo === 'V') {
novoSet.delete(resourceKey); if (nivelAtual === 'criar') {
novoNivel = 'visualizar'; // desativa o C
} else if (nivelAtual === 'visualizar') {
novoNivel = 'nenhum'; // desliga ambos
} else {
novoNivel = 'visualizar'; // liga V
}
} else { } else {
novoSet.add(resourceKey); // tipo === 'C'
if (nivelAtual === 'criar') {
novoNivel = 'visualizar'; // desliga C mantendo V
} else {
novoNivel = 'criar'; // ativa C e V juntos!
}
} }
// Atualização otimista na tela // Atualização otimista imediata na tela
const novoMap = { ...user.permissoesMap, [resourceKey]: novoNivel };
setUsuarios((prev) => setUsuarios((prev) =>
prev.map((u) => (u.id === userId ? { ...u, recursosPermitidos: novoSet } : u)) prev.map((u) => (u.id === userId ? { ...u, permissoesMap: novoMap } : u))
); );
try { try {
// Se o usuário tem privilege_id, salvar no banco
if (user.privilegeId) { if (user.privilegeId) {
if (temAcesso) { // Remover permissões antigas do recurso
// Remover await supabase
await supabase .from('privilege_interface_resources')
.from('privilege_interface_resources') .delete()
.delete() .eq('privilege_id', user.privilegeId)
.eq('privilege_id', user.privilegeId) .in('resource_key', [resourceKey, `${resourceKey}:edit`]);
.eq('resource_key', resourceKey);
} else { // Gravar novo estado
// Adicionar if (novoNivel === 'visualizar') {
await supabase await supabase.from('privilege_interface_resources').insert({
.from('privilege_interface_resources') privilege_id: user.privilegeId,
.upsert( resource_key: resourceKey,
{ privilege_id: user.privilegeId, resource_key: resourceKey }, });
{ onConflict: 'privilege_id,resource_key' } } else if (novoNivel === 'criar') {
); await supabase.from('privilege_interface_resources').insert([
{ privilege_id: user.privilegeId, resource_key: resourceKey },
{ privilege_id: user.privilegeId, resource_key: `${resourceKey}:edit` },
]);
} }
} }
toast.success(
temAcesso const desc = {
? `Módulo '${resourceKey}' bloqueado para ${user.fullName}.` nenhum: 'Bloqueado (✕)',
: `Módulo '${resourceKey}' liberado para ${user.fullName}!` visualizar: 'Apenas Visualização [V]',
); criar: 'Criação e Edição Total [V + C]',
}[novoNivel];
toast.success(`${resourceKey}: ${desc} para ${user.fullName}`);
} catch (err) { } catch (err) {
console.error('Erro ao atualizar permissão:', err); console.error('Erro ao atualizar nível de permissão:', err);
toast.error('Erro ao salvar alteração no banco.'); toast.error('Erro ao salvar alteração no banco.');
carregarDados(); // Reverter se falhar carregarDados();
} }
}; };
@@ -324,11 +366,16 @@ export const useMatrizAcessos = () => {
const template = TEMPLATES_ACESSO.find((t) => t.id === templateId); const template = TEMPLATES_ACESSO.find((t) => t.id === templateId);
if (!user || !template) return; if (!user || !template) return;
const novoSet = new Set(template.recursosPermitidos); const novoMap: Record<string, NivelAcesso> = {};
GRUPOS_MODULOS.forEach((g) => {
g.recursos.forEach((r) => {
novoMap[r.key] = template.permissoesPadrao[r.key] || 'nenhum';
});
});
// Atualização otimista // Atualização otimista
setUsuarios((prev) => setUsuarios((prev) =>
prev.map((u) => (u.id === userId ? { ...u, recursosPermitidos: novoSet } : u)) prev.map((u) => (u.id === userId ? { ...u, permissoesMap: novoMap } : u))
); );
try { try {
@@ -340,10 +387,15 @@ export const useMatrizAcessos = () => {
.eq('privilege_id', user.privilegeId); .eq('privilege_id', user.privilegeId);
// Inserir os novos recursos do template // Inserir os novos recursos do template
const records = template.recursosPermitidos.map((resKey) => ({ const records: { privilege_id: string; resource_key: string }[] = [];
privilege_id: user.privilegeId, Object.entries(novoMap).forEach(([resKey, nivel]) => {
resource_key: resKey, if (nivel === 'visualizar') {
})); records.push({ privilege_id: user.privilegeId!, resource_key: resKey });
} else if (nivel === 'criar') {
records.push({ privilege_id: user.privilegeId!, resource_key: resKey });
records.push({ privilege_id: user.privilegeId!, resource_key: `${resKey}:edit` });
}
});
if (records.length > 0) { if (records.length > 0) {
await supabase.from('privilege_interface_resources').insert(records); await supabase.from('privilege_interface_resources').insert(records);
@@ -371,7 +423,7 @@ export const useMatrizAcessos = () => {
usuarioSimulado, usuarioSimulado,
usuarioSimuladoId, usuarioSimuladoId,
setUsuarioSimuladoId, setUsuarioSimuladoId,
togglePermissao, toggleNivel,
aplicarTemplate, aplicarTemplate,
recarregar: carregarDados, recarregar: carregarDados,
}; };