🚀 Initial commit: Versão atual do TrackSteel APP

This commit is contained in:
2026-03-18 21:17:53 +00:00
commit bde410c9ad
633 changed files with 108150 additions and 0 deletions
@@ -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 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>
);
}