🚀 Initial commit: Versão atual do TrackSteel APP
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
Background,
|
||||
Controls,
|
||||
MiniMap,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
Node,
|
||||
Edge,
|
||||
ConnectionMode,
|
||||
} from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import { SystemModuleNode } from './nodes/SystemModuleNode';
|
||||
import { DataEntityNode } from './nodes/DataEntityNode';
|
||||
import { ProcessFlowNode } from './nodes/ProcessFlowNode';
|
||||
import { useSystemMapData } from './hooks/useSystemMapData';
|
||||
import { SystemMapFilters } from './SystemMapFilters';
|
||||
import { SystemMapLegend } from './SystemMapLegend';
|
||||
import { SystemMapTour } from './SystemMapTour';
|
||||
|
||||
const nodeTypes = {
|
||||
module: SystemModuleNode,
|
||||
entity: DataEntityNode,
|
||||
process: ProcessFlowNode,
|
||||
};
|
||||
|
||||
export function MapaInterativoFlow() {
|
||||
const { nodes: initialNodes, edges: initialEdges } = useSystemMapData();
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const [filteredView, setFilteredView] = React.useState<string>('all');
|
||||
const [showTour, setShowTour] = React.useState(false);
|
||||
|
||||
const onNodeClick = useCallback((event: React.MouseEvent, node: Node) => {
|
||||
if (node.data.url) {
|
||||
window.open(node.data.url as string, '_blank');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const filteredNodes = useMemo(() => {
|
||||
if (filteredView === 'all') return nodes;
|
||||
|
||||
return nodes.filter(node => {
|
||||
if (filteredView === 'permissions') {
|
||||
return node.data.hasAccess !== false;
|
||||
}
|
||||
if (filteredView === 'main-flow') {
|
||||
return node.data.isMainFlow;
|
||||
}
|
||||
if (filteredView === 'recent') {
|
||||
return node.data.recentlyUsed;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [nodes, filteredView]);
|
||||
|
||||
const filteredEdges = useMemo(() => {
|
||||
const nodeIds = new Set(filteredNodes.map(n => n.id));
|
||||
return edges.filter(edge =>
|
||||
nodeIds.has(edge.source) && nodeIds.has(edge.target)
|
||||
);
|
||||
}, [edges, filteredNodes]);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full relative bg-background">
|
||||
{/* Controles superiores */}
|
||||
<div className="absolute top-4 left-4 z-10 flex gap-4">
|
||||
<SystemMapFilters
|
||||
currentFilter={filteredView}
|
||||
onFilterChange={setFilteredView}
|
||||
/>
|
||||
<button
|
||||
onClick={() => setShowTour(true)}
|
||||
className="px-4 py-2 bg-primary text-primary-foreground rounded-md text-sm font-medium hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
Tour Guiado
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Legenda */}
|
||||
<div className="absolute top-4 right-4 z-10">
|
||||
<SystemMapLegend />
|
||||
</div>
|
||||
|
||||
{/* React Flow */}
|
||||
<ReactFlow
|
||||
nodes={filteredNodes}
|
||||
edges={filteredEdges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeClick={onNodeClick}
|
||||
nodeTypes={nodeTypes}
|
||||
connectionMode={ConnectionMode.Loose}
|
||||
fitView
|
||||
fitViewOptions={{ padding: 0.2 }}
|
||||
className="system-map-flow"
|
||||
proOptions={{ hideAttribution: true }}
|
||||
>
|
||||
<Background
|
||||
color="hsl(var(--muted-foreground))"
|
||||
size={1}
|
||||
gap={20}
|
||||
/>
|
||||
<Controls
|
||||
className="bg-card border border-border"
|
||||
showInteractive={false}
|
||||
/>
|
||||
<MiniMap
|
||||
className="bg-card border border-border"
|
||||
nodeColor="hsl(var(--primary))"
|
||||
maskColor="hsl(var(--background) / 0.8)"
|
||||
/>
|
||||
</ReactFlow>
|
||||
|
||||
{/* Tour Modal */}
|
||||
{showTour && (
|
||||
<SystemMapTour onClose={() => setShowTour(false)} />
|
||||
)}
|
||||
|
||||
<style>{`
|
||||
.system-map-flow .react-flow__node {
|
||||
font-family: inherit;
|
||||
}
|
||||
.system-map-flow .react-flow__edge-path {
|
||||
stroke: hsl(var(--border));
|
||||
stroke-width: 2;
|
||||
}
|
||||
.system-map-flow .react-flow__edge.animated .react-flow__edge-path {
|
||||
stroke-dasharray: 5;
|
||||
animation: dashdraw 0.5s linear infinite;
|
||||
}
|
||||
@keyframes dashdraw {
|
||||
to {
|
||||
stroke-dashoffset: -10;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Filter, Eye, Workflow, Clock, Shield } from 'lucide-react';
|
||||
|
||||
interface SystemMapFiltersProps {
|
||||
currentFilter: string;
|
||||
onFilterChange: (filter: string) => void;
|
||||
}
|
||||
|
||||
const filters = [
|
||||
{
|
||||
id: 'all',
|
||||
label: 'Ver Tudo',
|
||||
icon: Eye,
|
||||
description: 'Mostrar todos os módulos'
|
||||
},
|
||||
{
|
||||
id: 'main-flow',
|
||||
label: 'Fluxo Principal',
|
||||
icon: Workflow,
|
||||
description: 'Apenas o fluxo principal de trabalho'
|
||||
},
|
||||
{
|
||||
id: 'permissions',
|
||||
label: 'Meus Acessos',
|
||||
icon: Shield,
|
||||
description: 'Apenas módulos que tenho acesso'
|
||||
},
|
||||
{
|
||||
id: 'recent',
|
||||
label: 'Recentes',
|
||||
icon: Clock,
|
||||
description: 'Módulos utilizados recentemente'
|
||||
}
|
||||
];
|
||||
|
||||
export function SystemMapFilters({ currentFilter, onFilterChange }: SystemMapFiltersProps) {
|
||||
const currentFilterData = filters.find(f => f.id === currentFilter);
|
||||
const CurrentIcon = currentFilterData?.icon || Filter;
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-2 bg-card text-card-foreground border-border hover:bg-accent hover:text-accent-foreground">
|
||||
<CurrentIcon className="w-4 h-4" />
|
||||
{currentFilterData?.label || 'Filtros'}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-56 bg-card border-border">
|
||||
{filters.map((filter) => {
|
||||
const Icon = filter.icon;
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={filter.id}
|
||||
onClick={() => onFilterChange(filter.id)}
|
||||
className="flex items-start gap-3 p-3 text-card-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
<Icon className="w-4 h-4 mt-0.5 text-muted-foreground" />
|
||||
<div>
|
||||
<div className="font-medium text-sm">{filter.label}</div>
|
||||
<div className="text-xs text-muted-foreground">{filter.description}</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Square,
|
||||
Circle,
|
||||
Triangle,
|
||||
ArrowRight,
|
||||
MoreHorizontal
|
||||
} from 'lucide-react';
|
||||
|
||||
export function SystemMapLegend() {
|
||||
return (
|
||||
<Card className="w-64 bg-background/95 backdrop-blur-sm">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm">Legenda</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Tipos de Nodes */}
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold mb-2 text-muted-foreground">Tipos de Módulos</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Square className="w-4 h-4 text-primary" />
|
||||
<span>Módulos do Sistema</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Circle className="w-4 h-4 text-muted-foreground" />
|
||||
<span>Entidades de Dados</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Triangle className="w-4 h-4 text-green-500" />
|
||||
<span>Pontos de Processo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tipos de Conexões */}
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold mb-2 text-muted-foreground">Conexões</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<ArrowRight className="w-4 h-4 text-primary" />
|
||||
<span>Fluxo Principal</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<MoreHorizontal className="w-4 h-4 text-muted-foreground" />
|
||||
<span>Relação de Dados</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold mb-2 text-muted-foreground">Status</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Badge variant="default" className="h-4 text-xs px-1">Ativo</Badge>
|
||||
<span>Com acesso</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Badge variant="destructive" className="h-4 text-xs px-1">Sem Acesso</Badge>
|
||||
<span>Restrito</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Badge variant="secondary" className="h-4 text-xs px-1">Recente</Badge>
|
||||
<span>Usado recentemente</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
X,
|
||||
Play,
|
||||
FileText,
|
||||
Building2,
|
||||
Truck,
|
||||
Users
|
||||
} from 'lucide-react';
|
||||
|
||||
interface TourStep {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: React.ComponentType<any>;
|
||||
content: string[];
|
||||
}
|
||||
|
||||
const tourSteps: TourStep[] = [
|
||||
{
|
||||
title: 'Bem-vindo ao Mapa Interativo',
|
||||
description: 'Explore a arquitetura completa do sistema',
|
||||
icon: Play,
|
||||
content: [
|
||||
'Este mapa mostra todos os módulos do sistema e suas interações',
|
||||
'Clique nos módulos para navegar diretamente',
|
||||
'Use os filtros para focar em áreas específicas',
|
||||
'As linhas mostram o fluxo de dados e processos'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Fluxo Principal do Sistema',
|
||||
description: 'Jornada completa: do cadastro à instalação',
|
||||
icon: FileText,
|
||||
content: [
|
||||
'1. Cadastro de OF - Criação da ordem de fabricação',
|
||||
'2. Cadastro de Peças - Definição dos itens a produzir',
|
||||
'3. Gestão de Estoque - Controle de materiais necessários',
|
||||
'4. Produção - Fabricação das peças',
|
||||
'5. Expedição - Preparação para envio',
|
||||
'6. Obra - Instalação no canteiro'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Módulos de Produção',
|
||||
description: 'Gestão completa do processo produtivo',
|
||||
icon: Building2,
|
||||
content: [
|
||||
'Dashboard de Produção - Visão geral da fábrica',
|
||||
'Prioridades - Sequenciamento da produção',
|
||||
'Diário de Produção - Registro diário de atividades',
|
||||
'Apontamentos - Controle de quantidade produzida',
|
||||
'Painel Industrial - Monitoramento em tempo real'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Gestão de Expedição e Obra',
|
||||
description: 'Do produto acabado à instalação',
|
||||
icon: Truck,
|
||||
content: [
|
||||
'Romaneios - Documentos de expedição',
|
||||
'Apontamento Automático - Integração com produção',
|
||||
'RDO - Relatório Diário de Obra',
|
||||
'Controle de Peças em Canteiro',
|
||||
'Relatórios de Progresso'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Sistema de Apoio',
|
||||
description: 'Ferramentas auxiliares e administração',
|
||||
icon: Users,
|
||||
content: [
|
||||
'Sistema de Tarefas - Comunicação entre equipes',
|
||||
'Biblioteca - Catálogos e documentos técnicos',
|
||||
'Gestão de Usuários - Controle de acesso',
|
||||
'Configurações - Personalização do sistema',
|
||||
'Relatórios e Dashboards'
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
interface SystemMapTourProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function SystemMapTour({ onClose }: SystemMapTourProps) {
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const step = tourSteps[currentStep];
|
||||
const Icon = step.icon;
|
||||
|
||||
const nextStep = () => {
|
||||
if (currentStep < tourSteps.length - 1) {
|
||||
setCurrentStep(currentStep + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const prevStep = () => {
|
||||
if (currentStep > 0) {
|
||||
setCurrentStep(currentStep - 1);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={true} onOpenChange={() => onClose()}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<DialogTitle className="flex items-center gap-3">
|
||||
<Icon className="w-6 h-6 text-primary" />
|
||||
{step.title}
|
||||
</DialogTitle>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onClose}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Progress */}
|
||||
<div className="flex items-center gap-2">
|
||||
{tourSteps.map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`h-2 flex-1 rounded-full transition-colors ${
|
||||
index === currentStep
|
||||
? 'bg-primary'
|
||||
: index < currentStep
|
||||
? 'bg-primary/50'
|
||||
: 'bg-muted'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">{step.title}</h3>
|
||||
<p className="text-muted-foreground">{step.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{step.content.map((item, index) => (
|
||||
<div key={index} className="flex items-start gap-3">
|
||||
<Badge variant="outline" className="mt-0.5 min-w-fit">
|
||||
{index + 1}
|
||||
</Badge>
|
||||
<p className="text-sm leading-relaxed">{item}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={prevStep}
|
||||
disabled={currentStep === 0}
|
||||
className="gap-2"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
Anterior
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
{currentStep + 1} de {tourSteps.length}
|
||||
</div>
|
||||
|
||||
{currentStep < tourSteps.length - 1 ? (
|
||||
<Button onClick={nextStep} className="gap-2">
|
||||
Próximo
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={onClose} className="gap-2">
|
||||
Finalizar Tour
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Trash2, Plus, Users, AlertCircle, Loader2 } from 'lucide-react';
|
||||
import { useUserResourcePermissions } from '@/hooks/useUserResourcePermissions';
|
||||
|
||||
interface UserResourcePermissionsProps {
|
||||
resourceKey: string;
|
||||
resourceName: string;
|
||||
}
|
||||
|
||||
export function UserResourcePermissions({ resourceKey, resourceName }: UserResourcePermissionsProps) {
|
||||
const {
|
||||
users,
|
||||
resourcePermissions,
|
||||
loading,
|
||||
setUserPermission,
|
||||
removeUserPermission
|
||||
} = useUserResourcePermissions(resourceKey);
|
||||
|
||||
const [selectedUserId, setSelectedUserId] = useState<string>('');
|
||||
const [isAdding, setIsAdding] = useState(false);
|
||||
const [isUpdating, setIsUpdating] = useState<string | null>(null);
|
||||
|
||||
const handleAddPermission = async () => {
|
||||
if (!selectedUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsAdding(true);
|
||||
try {
|
||||
console.log('🎯 Adicionando usuário ao recurso:', {
|
||||
userId: selectedUserId,
|
||||
resourceKey,
|
||||
resourceName
|
||||
});
|
||||
|
||||
// Usando 'can_view_only' como padrão, mas as permissões reais virão dos privilégios
|
||||
const success = await setUserPermission(selectedUserId, 'can_view_only');
|
||||
if (success) {
|
||||
setSelectedUserId('');
|
||||
console.log('✅ Usuário adicionado com sucesso');
|
||||
} else {
|
||||
console.log('❌ Falha ao adicionar usuário');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao adicionar usuário:', error);
|
||||
} finally {
|
||||
setIsAdding(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemovePermission = async (userId: string) => {
|
||||
setIsUpdating(userId);
|
||||
try {
|
||||
console.log('🗑️ Removendo usuário do recurso:', { userId, resourceKey });
|
||||
await removeUserPermission(userId);
|
||||
} catch (error) {
|
||||
console.error('Error removing permission:', error);
|
||||
} finally {
|
||||
setIsUpdating(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Get users that don't have explicit permissions set
|
||||
const availableUsers = users.filter(user =>
|
||||
!resourcePermissions.some(perm => perm.user_id === user.id)
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<Loader2 className="w-6 h-6 animate-spin mr-3" />
|
||||
<div className="text-sm text-muted-foreground">Carregando usuários...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!resourceKey) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex items-center gap-3 text-amber-600 bg-amber-50 p-4 rounded-lg">
|
||||
<AlertCircle className="w-5 h-5" />
|
||||
<span>Recurso não identificado</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Resource Info */}
|
||||
<div className="p-4 bg-muted/30 rounded-lg">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Recurso: <code className="bg-muted px-2 py-1 rounded text-xs">{resourceKey}</code>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-2">
|
||||
As permissões específicas são definidas pelos privilégios de cada usuário
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add new user */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-lg font-semibold">Adicionar Usuário</h4>
|
||||
<div className="flex gap-3 flex-wrap">
|
||||
<Select
|
||||
value={selectedUserId}
|
||||
onValueChange={setSelectedUserId}
|
||||
disabled={isAdding}
|
||||
>
|
||||
<SelectTrigger className="flex-1 min-w-[200px]">
|
||||
<SelectValue placeholder="Selecionar usuário..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableUsers.length === 0 ? (
|
||||
<div className="p-3 text-sm text-muted-foreground">
|
||||
Todos os usuários já estão vinculados a este recurso
|
||||
</div>
|
||||
) : (
|
||||
availableUsers.map(user => (
|
||||
<SelectItem key={user.id} value={user.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="w-4 h-4" />
|
||||
<span>{user.full_name || user.email}</span>
|
||||
<span className="text-xs text-muted-foreground">({user.email})</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
onClick={handleAddPermission}
|
||||
disabled={!selectedUserId || isAdding || availableUsers.length === 0}
|
||||
size="default"
|
||||
>
|
||||
{isAdding ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
||||
) : (
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
Adicionar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current permissions */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-lg font-semibold">
|
||||
Usuários Vinculados ({resourcePermissions.length})
|
||||
</h4>
|
||||
{resourcePermissions.length === 0 ? (
|
||||
<div className="text-center p-8 bg-muted/20 rounded-lg border-2 border-dashed border-muted">
|
||||
<div className="text-muted-foreground">
|
||||
Nenhum usuário específico vinculado.<br />
|
||||
Usuários acessam baseado em seus privilégios funcionais.
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||
{resourcePermissions.map(permission => {
|
||||
const user = users.find(u => u.id === permission.user_id);
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<div key={permission.user_id} className="flex items-center justify-between p-4 bg-card border border-border rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<Users className="w-5 h-5 text-muted-foreground" />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium text-card-foreground">
|
||||
{user.full_name || user.email}
|
||||
</span>
|
||||
{user.full_name && (
|
||||
<span className="text-sm text-muted-foreground">{user.email}</span>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Permissões definidas pelo privilégio do usuário
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => handleRemovePermission(permission.user_id)}
|
||||
disabled={isUpdating === permission.user_id}
|
||||
>
|
||||
{isUpdating === permission.user_id ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Information note */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-base font-medium">Como Funciona</h4>
|
||||
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<div className="text-sm text-blue-800">
|
||||
<p className="font-medium mb-2">Sistema Simplificado de Permissões:</p>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
<li>Usuários são vinculados aos recursos que podem acessar</li>
|
||||
<li>As permissões específicas (criar, editar, excluir) são definidas pelos <strong>Privilégios</strong> de cada usuário</li>
|
||||
<li>Usuários não vinculados seguem as regras gerais do sistema</li>
|
||||
<li>Administradores sempre têm acesso total (exceto se explicitamente negado)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
|
||||
import React from 'react';
|
||||
import { ResponsiveModal } from '@/components/responsive/ResponsiveModal';
|
||||
import { UserResourcePermissions } from '@/components/mapa-interativo/UserResourcePermissions';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface UserResourcePermissionsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
resourceKey: string;
|
||||
resourceName: string;
|
||||
}
|
||||
|
||||
export function UserResourcePermissionsModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
resourceKey,
|
||||
resourceName
|
||||
}: UserResourcePermissionsModalProps) {
|
||||
return (
|
||||
<ResponsiveModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={`Controle de Acesso - ${resourceName}`}
|
||||
description={`Gerenciar permissões de usuários para ${resourceName}`}
|
||||
size="lg"
|
||||
footer={
|
||||
<Button onClick={onClose} variant="outline">
|
||||
Fechar
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<UserResourcePermissions
|
||||
resourceKey={resourceKey}
|
||||
resourceName={resourceName}
|
||||
/>
|
||||
</ResponsiveModal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Node, Edge, MarkerType } from '@xyflow/react';
|
||||
import {
|
||||
BarChart3,
|
||||
FileText,
|
||||
Warehouse,
|
||||
Building2,
|
||||
Truck,
|
||||
HardHat,
|
||||
CheckSquare,
|
||||
Book,
|
||||
Database,
|
||||
Users,
|
||||
Settings,
|
||||
Wrench
|
||||
} from 'lucide-react';
|
||||
|
||||
export function useSystemMapData() {
|
||||
const { nodes, edges } = useMemo(() => {
|
||||
const nodes: Node[] = [
|
||||
// Módulos Principais
|
||||
{
|
||||
id: 'dashboard',
|
||||
type: 'module',
|
||||
position: { x: 400, y: 50 },
|
||||
data: {
|
||||
title: 'Dashboard',
|
||||
icon: BarChart3,
|
||||
description: 'Visão geral do sistema',
|
||||
color: '#10b981',
|
||||
stats: { active: true, count: 1 },
|
||||
url: '/dashboard',
|
||||
hasAccess: true,
|
||||
isMainFlow: true,
|
||||
recentlyUsed: true
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cadastro-of',
|
||||
type: 'module',
|
||||
position: { x: 100, y: 150 },
|
||||
data: {
|
||||
title: 'Cadastro OF',
|
||||
icon: FileText,
|
||||
description: 'Gestão de Ordens de Fabricação',
|
||||
color: '#3b82f6',
|
||||
stats: { active: true, count: 25 },
|
||||
url: '/cadastro-of',
|
||||
hasAccess: true,
|
||||
isMainFlow: true,
|
||||
recentlyUsed: false
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cadastro-pecas',
|
||||
type: 'module',
|
||||
position: { x: 300, y: 150 },
|
||||
data: {
|
||||
title: 'Cadastro Peças',
|
||||
icon: FileText,
|
||||
description: 'Gestão de Peças e Componentes',
|
||||
color: '#3b82f6',
|
||||
stats: { active: true, count: 150 },
|
||||
url: '/seletor-of',
|
||||
hasAccess: true,
|
||||
isMainFlow: true,
|
||||
recentlyUsed: true
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'estoque',
|
||||
type: 'module',
|
||||
position: { x: 500, y: 150 },
|
||||
data: {
|
||||
title: 'Estoque',
|
||||
icon: Warehouse,
|
||||
description: 'Gestão de Materiais e Estoque',
|
||||
color: '#06b6d4',
|
||||
stats: { active: true, count: 320 },
|
||||
url: '/estoque',
|
||||
hasAccess: true,
|
||||
isMainFlow: true,
|
||||
recentlyUsed: true
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'producao',
|
||||
type: 'module',
|
||||
position: { x: 200, y: 300 },
|
||||
data: {
|
||||
title: 'Produção',
|
||||
icon: Building2,
|
||||
description: 'Gestão da Produção Industrial',
|
||||
color: '#f59e0b',
|
||||
stats: { active: true, count: 75 },
|
||||
url: '/producao',
|
||||
hasAccess: true,
|
||||
isMainFlow: true,
|
||||
recentlyUsed: true
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'expedicao',
|
||||
type: 'module',
|
||||
position: { x: 400, y: 300 },
|
||||
data: {
|
||||
title: 'Expedição',
|
||||
icon: Truck,
|
||||
description: 'Gestão de Expedição e Romaneios',
|
||||
color: '#ef4444',
|
||||
stats: { active: true, count: 45 },
|
||||
url: '/expedicao',
|
||||
hasAccess: true,
|
||||
isMainFlow: true,
|
||||
recentlyUsed: false
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'obra',
|
||||
type: 'module',
|
||||
position: { x: 600, y: 300 },
|
||||
data: {
|
||||
title: 'Obra',
|
||||
icon: HardHat,
|
||||
description: 'Gestão de Obras e RDO',
|
||||
color: '#ef4444',
|
||||
stats: { active: true, count: 12 },
|
||||
url: '/obra',
|
||||
hasAccess: true,
|
||||
isMainFlow: true,
|
||||
recentlyUsed: false
|
||||
},
|
||||
},
|
||||
// Entidades de Dados
|
||||
{
|
||||
id: 'data-ofs',
|
||||
type: 'entity',
|
||||
position: { x: 100, y: 250 },
|
||||
data: {
|
||||
title: 'OFs',
|
||||
icon: Database,
|
||||
description: 'Ordens de Fabricação',
|
||||
color: '#6b7280',
|
||||
count: 25,
|
||||
isMainFlow: true
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'data-pecas',
|
||||
type: 'entity',
|
||||
position: { x: 300, y: 250 },
|
||||
data: {
|
||||
title: 'Peças',
|
||||
icon: Database,
|
||||
description: 'Peças e Componentes',
|
||||
color: '#6b7280',
|
||||
count: 150,
|
||||
isMainFlow: true
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'data-materiais',
|
||||
type: 'entity',
|
||||
position: { x: 500, y: 250 },
|
||||
data: {
|
||||
title: 'Materiais',
|
||||
icon: Database,
|
||||
description: 'Materiais de Estoque',
|
||||
color: '#6b7280',
|
||||
count: 320,
|
||||
isMainFlow: true
|
||||
},
|
||||
},
|
||||
// Módulos de Apoio
|
||||
{
|
||||
id: 'tarefas',
|
||||
type: 'module',
|
||||
position: { x: 50, y: 450 },
|
||||
data: {
|
||||
title: 'Tarefas',
|
||||
icon: CheckSquare,
|
||||
description: 'Sistema de Tarefas',
|
||||
color: '#6b7280',
|
||||
stats: { active: true, count: 8 },
|
||||
url: '/tarefas',
|
||||
hasAccess: true,
|
||||
isMainFlow: false,
|
||||
recentlyUsed: false
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'biblioteca',
|
||||
type: 'module',
|
||||
position: { x: 250, y: 450 },
|
||||
data: {
|
||||
title: 'Biblioteca',
|
||||
icon: Book,
|
||||
description: 'Catálogos e Documentos',
|
||||
color: '#6b7280',
|
||||
stats: { active: true, count: 50 },
|
||||
url: '/biblioteca/catalogos',
|
||||
hasAccess: true,
|
||||
isMainFlow: false,
|
||||
recentlyUsed: false
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'usuarios',
|
||||
type: 'module',
|
||||
position: { x: 450, y: 450 },
|
||||
data: {
|
||||
title: 'Usuários',
|
||||
icon: Users,
|
||||
description: 'Gestão de Usuários',
|
||||
color: '#dc2626',
|
||||
stats: { active: true, count: 15 },
|
||||
url: '/user-management',
|
||||
hasAccess: false,
|
||||
isMainFlow: false,
|
||||
recentlyUsed: false
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'configuracoes',
|
||||
type: 'module',
|
||||
position: { x: 650, y: 450 },
|
||||
data: {
|
||||
title: 'Configurações',
|
||||
icon: Settings,
|
||||
description: 'Configurações do Sistema',
|
||||
color: '#64748b',
|
||||
stats: { active: true, count: 1 },
|
||||
url: '/configuracoes',
|
||||
hasAccess: true,
|
||||
isMainFlow: false,
|
||||
recentlyUsed: false
|
||||
},
|
||||
},
|
||||
// Processo de Início
|
||||
{
|
||||
id: 'start',
|
||||
type: 'process',
|
||||
position: { x: 400, y: -50 },
|
||||
data: {
|
||||
title: 'Início',
|
||||
description: 'Ponto de entrada do sistema',
|
||||
color: '#10b981',
|
||||
isMainFlow: true
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const edges: Edge[] = [
|
||||
// Fluxo Principal
|
||||
{
|
||||
id: 'start-dashboard',
|
||||
source: 'start',
|
||||
target: 'dashboard',
|
||||
type: 'smoothstep',
|
||||
animated: true,
|
||||
markerEnd: { type: MarkerType.ArrowClosed },
|
||||
style: { stroke: '#10b981', strokeWidth: 3 }
|
||||
},
|
||||
{
|
||||
id: 'dashboard-cadastro-of',
|
||||
source: 'dashboard',
|
||||
target: 'cadastro-of',
|
||||
type: 'smoothstep',
|
||||
animated: true,
|
||||
markerEnd: { type: MarkerType.ArrowClosed },
|
||||
label: '1. Criar OF'
|
||||
},
|
||||
{
|
||||
id: 'cadastro-of-data-ofs',
|
||||
source: 'cadastro-of',
|
||||
target: 'data-ofs',
|
||||
type: 'straight',
|
||||
markerEnd: { type: MarkerType.ArrowClosed }
|
||||
},
|
||||
{
|
||||
id: 'cadastro-of-cadastro-pecas',
|
||||
source: 'cadastro-of',
|
||||
target: 'cadastro-pecas',
|
||||
type: 'smoothstep',
|
||||
animated: true,
|
||||
markerEnd: { type: MarkerType.ArrowClosed },
|
||||
label: '2. Cadastrar Peças'
|
||||
},
|
||||
{
|
||||
id: 'cadastro-pecas-data-pecas',
|
||||
source: 'cadastro-pecas',
|
||||
target: 'data-pecas',
|
||||
type: 'straight',
|
||||
markerEnd: { type: MarkerType.ArrowClosed }
|
||||
},
|
||||
{
|
||||
id: 'cadastro-pecas-estoque',
|
||||
source: 'cadastro-pecas',
|
||||
target: 'estoque',
|
||||
type: 'smoothstep',
|
||||
animated: true,
|
||||
markerEnd: { type: MarkerType.ArrowClosed },
|
||||
label: '3. Gestão Materiais'
|
||||
},
|
||||
{
|
||||
id: 'estoque-data-materiais',
|
||||
source: 'estoque',
|
||||
target: 'data-materiais',
|
||||
type: 'straight',
|
||||
markerEnd: { type: MarkerType.ArrowClosed }
|
||||
},
|
||||
{
|
||||
id: 'data-pecas-producao',
|
||||
source: 'data-pecas',
|
||||
target: 'producao',
|
||||
type: 'smoothstep',
|
||||
animated: true,
|
||||
markerEnd: { type: MarkerType.ArrowClosed },
|
||||
label: '4. Produzir'
|
||||
},
|
||||
{
|
||||
id: 'data-materiais-producao',
|
||||
source: 'data-materiais',
|
||||
target: 'producao',
|
||||
type: 'smoothstep',
|
||||
markerEnd: { type: MarkerType.ArrowClosed }
|
||||
},
|
||||
{
|
||||
id: 'producao-expedicao',
|
||||
source: 'producao',
|
||||
target: 'expedicao',
|
||||
type: 'smoothstep',
|
||||
animated: true,
|
||||
markerEnd: { type: MarkerType.ArrowClosed },
|
||||
label: '5. Expedir'
|
||||
},
|
||||
{
|
||||
id: 'expedicao-obra',
|
||||
source: 'expedicao',
|
||||
target: 'obra',
|
||||
type: 'smoothstep',
|
||||
animated: true,
|
||||
markerEnd: { type: MarkerType.ArrowClosed },
|
||||
label: '6. Instalar'
|
||||
},
|
||||
// Conexões de Apoio
|
||||
{
|
||||
id: 'dashboard-tarefas',
|
||||
source: 'dashboard',
|
||||
target: 'tarefas',
|
||||
type: 'smoothstep',
|
||||
style: { strokeDasharray: '5,5' }
|
||||
},
|
||||
{
|
||||
id: 'dashboard-biblioteca',
|
||||
source: 'dashboard',
|
||||
target: 'biblioteca',
|
||||
type: 'smoothstep',
|
||||
style: { strokeDasharray: '5,5' }
|
||||
},
|
||||
{
|
||||
id: 'configuracoes-usuarios',
|
||||
source: 'configuracoes',
|
||||
target: 'usuarios',
|
||||
type: 'smoothstep',
|
||||
style: { strokeDasharray: '5,5' }
|
||||
},
|
||||
];
|
||||
|
||||
return { nodes, edges };
|
||||
}, []);
|
||||
|
||||
return { nodes, edges };
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import { Handle, Position, NodeProps } from '@xyflow/react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
interface DataEntityNodeData {
|
||||
title: string;
|
||||
icon: React.ComponentType<any>;
|
||||
description: string;
|
||||
color: string;
|
||||
count: number;
|
||||
isMainFlow: boolean;
|
||||
}
|
||||
|
||||
export function DataEntityNode({ data }: NodeProps<any>) {
|
||||
const Icon = data.icon;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
className="w-2 h-2 bg-border"
|
||||
/>
|
||||
|
||||
<Card
|
||||
className={`
|
||||
w-32 transition-all duration-300
|
||||
${data.isMainFlow ? 'ring-1 ring-primary/20' : ''}
|
||||
bg-muted/30
|
||||
`}
|
||||
>
|
||||
<CardContent className="p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Icon className="w-4 h-4 text-muted-foreground" />
|
||||
<h4 className="font-medium text-xs">{data.title}</h4>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{data.description}
|
||||
</p>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{data.count} registros
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
className="w-2 h-2 bg-border"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { Handle, Position, NodeProps } from '@xyflow/react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Play } from 'lucide-react';
|
||||
|
||||
interface ProcessFlowNodeData {
|
||||
title: string;
|
||||
description: string;
|
||||
color: string;
|
||||
isMainFlow: boolean;
|
||||
}
|
||||
|
||||
export function ProcessFlowNode({ data }: NodeProps<any>) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
className="w-2 h-2 bg-border"
|
||||
/>
|
||||
|
||||
<Card
|
||||
className="w-28 bg-gradient-to-br from-primary/10 to-primary/5 border-primary/30"
|
||||
>
|
||||
<CardContent className="p-3">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div
|
||||
className="p-1 rounded-full"
|
||||
style={{ backgroundColor: data.color }}
|
||||
>
|
||||
<Play className="w-3 h-3 text-white" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-xs">{data.title}</h4>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{data.description}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
className="w-2 h-2 bg-border"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import React from 'react';
|
||||
import { Handle, Position, NodeProps } from '@xyflow/react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ExternalLink, Users, Activity } from 'lucide-react';
|
||||
|
||||
interface SystemModuleNodeData {
|
||||
title: string;
|
||||
icon: React.ComponentType<any>;
|
||||
description: string;
|
||||
color: string;
|
||||
stats: {
|
||||
active: boolean;
|
||||
count: number;
|
||||
};
|
||||
url?: string;
|
||||
hasAccess: boolean;
|
||||
isMainFlow: boolean;
|
||||
recentlyUsed: boolean;
|
||||
}
|
||||
|
||||
export function SystemModuleNode({ data }: NodeProps<any>) {
|
||||
const Icon = data.icon;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
className="w-3 h-3 bg-border"
|
||||
/>
|
||||
|
||||
<Card
|
||||
className={`
|
||||
w-48 cursor-pointer transition-all duration-300 hover:shadow-lg
|
||||
${data.hasAccess ? 'hover:scale-105' : 'opacity-60'}
|
||||
${data.isMainFlow ? 'ring-2 ring-primary/30' : ''}
|
||||
${data.recentlyUsed ? 'bg-accent/5' : ''}
|
||||
`}
|
||||
style={{ borderColor: data.color }}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div
|
||||
className="p-2 rounded-lg"
|
||||
style={{ backgroundColor: `${data.color}20` }}
|
||||
>
|
||||
<Icon
|
||||
className="w-5 h-5"
|
||||
style={{ color: data.color }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
{data.recentlyUsed && (
|
||||
<Badge variant="secondary" className="text-xs px-1 py-0">
|
||||
<Activity className="w-3 h-3 mr-1" />
|
||||
Recente
|
||||
</Badge>
|
||||
)}
|
||||
{data.url && data.hasAccess && (
|
||||
<ExternalLink className="w-4 h-4 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold text-sm leading-tight">
|
||||
{data.title}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
{data.description}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Users className="w-3 h-3" />
|
||||
{data.stats.count}
|
||||
</div>
|
||||
|
||||
<Badge
|
||||
variant={data.stats.active ? "default" : "secondary"}
|
||||
className="text-xs"
|
||||
style={data.stats.active ? { backgroundColor: data.color } : {}}
|
||||
>
|
||||
{data.stats.active ? 'Ativo' : 'Inativo'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
className="w-3 h-3 bg-border"
|
||||
/>
|
||||
|
||||
{!data.hasAccess && (
|
||||
<div className="absolute inset-0 bg-background/80 rounded-lg flex items-center justify-center">
|
||||
<Badge variant="destructive" className="text-xs">
|
||||
Sem Acesso
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user