feat: Balões duplos [ V ] (Visualizar) e [ C ] (Criar/Editar) na Matriz Central de Acessos
This commit is contained in:
@@ -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)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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
|
||||||
|
? 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>
|
</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>
|
||||||
|
|||||||
+127
-75
@@ -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 {
|
} else {
|
||||||
novoSet.add(resourceKey);
|
novoNivel = 'visualizar'; // liga V
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 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)
|
||||||
.eq('resource_key', resourceKey);
|
.in('resource_key', [resourceKey, `${resourceKey}:edit`]);
|
||||||
} else {
|
|
||||||
// Adicionar
|
// Gravar novo estado
|
||||||
await supabase
|
if (novoNivel === 'visualizar') {
|
||||||
.from('privilege_interface_resources')
|
await supabase.from('privilege_interface_resources').insert({
|
||||||
.upsert(
|
privilege_id: user.privilegeId,
|
||||||
|
resource_key: resourceKey,
|
||||||
|
});
|
||||||
|
} 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 },
|
||||||
{ onConflict: 'privilege_id,resource_key' }
|
{ 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,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user