🚀 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,136 @@
import React, { useMemo } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import { format, parseISO, eachDayOfInterval } from 'date-fns';
import { ptBR } from 'date-fns/locale';
import { DashboardProcesso } from '@/hooks/useDashboardProducaoOtimizado';
interface GraficoMestreProps {
processos: DashboardProcesso[];
onProcessoClick?: (processoNome: string) => void;
processoSelecionado?: string | null;
}
export const GraficoMestre: React.FC<GraficoMestreProps> = ({
processos,
onProcessoClick,
processoSelecionado
}) => {
// Processar dados dos gráficos individuais para criar um gráfico sobreposto
const dadosGraficoSobreposto = useMemo(() => {
if (!processos || processos.length === 0) return [];
// Coletar todas as datas dos gráficos individuais
const todasAsDatas = new Set<string>();
processos.forEach(processo => {
processo.dadosGrafico.forEach(ponto => {
todasAsDatas.add(ponto.data);
});
});
// Ordenar as datas
const datasOrdenadas = Array.from(todasAsDatas).sort();
// Construir dados do gráfico sobreposto
return datasOrdenadas.map(data => {
const pontoGrafico: any = {
data: format(parseISO(data), 'dd/MM', { locale: ptBR }),
dataCompleta: data
};
// Para cada processo, buscar o valor realizado na data
processos.forEach(processo => {
const pontoProcesso = processo.dadosGrafico.find(ponto => ponto.data === data);
// Converter para toneladas (dividir por 1000)
pontoGrafico[processo.nome] = pontoProcesso ? Math.round(pontoProcesso.realizado / 1000 * 100) / 100 : 0;
});
return pontoGrafico;
});
}, [processos]);
// Obter cores dos processos baseado no status
const obterCorProcesso = (status: string) => {
switch (status) {
case 'verde':
return '#10b981';
case 'amarelo':
return '#f59e0b';
case 'vermelho':
return '#ef4444';
case 'azul':
return '#3b82f6';
default:
return '#8884d8';
}
};
const handleProcessoClick = (processoNome: string) => {
if (onProcessoClick) {
onProcessoClick(processoNome);
}
};
const formatTooltipValue = (value: number, name: string) => [
`${value.toFixed(2)} t`,
name
];
return (
<div className="w-full h-96">
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={dadosGraficoSobreposto}
margin={{ top: 20, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis
dataKey="data"
stroke="#9ca3af"
fontSize={12}
tick={{ fill: '#9ca3af' }}
/>
<YAxis
stroke="#9ca3af"
fontSize={12}
tick={{ fill: '#9ca3af' }}
label={{
value: 'Peso Acumulado (t)',
angle: -90,
position: 'insideLeft',
style: { textAnchor: 'middle', fill: '#9ca3af' }
}}
/>
<Tooltip
contentStyle={{
backgroundColor: '#1f2937',
border: '1px solid #374151',
borderRadius: '6px',
color: '#f3f4f6'
}}
formatter={formatTooltipValue}
labelFormatter={(label) => `Data: ${label}`}
/>
<Legend
wrapperStyle={{ color: '#9ca3af' }}
onClick={(e) => handleProcessoClick(e.value)}
/>
{processos.map((processo) => (
<Line
key={processo.nome}
type="monotone"
dataKey={processo.nome}
stroke={obterCorProcesso(processo.status)}
strokeWidth={2}
dot={{ fill: obterCorProcesso(processo.status), strokeWidth: 2, r: 3 }}
activeDot={{ r: 5, fill: obterCorProcesso(processo.status) }}
opacity={processoSelecionado ? (processoSelecionado === processo.nome ? 1 : 0.3) : 1}
style={{ cursor: 'pointer' }}
/>
))}
</LineChart>
</ResponsiveContainer>
</div>
);
};
@@ -0,0 +1,119 @@
import React from 'react';
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { DashboardProcesso } from '@/hooks/useDashboardProducaoOtimizado';
import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale';
interface GraficoProgressoIndividualProps {
processos: DashboardProcesso[];
}
export const GraficoProgressoIndividual: React.FC<GraficoProgressoIndividualProps> = ({ processos }) => {
const formatTooltipValue = (value: number, name: string) => [
`${(value / 1000).toFixed(2)} t`,
name === 'planejado' ? 'Planejado' : 'Realizado'
];
const formatAxisValue = (value: number) => `${(value / 1000).toFixed(1)}t`;
const formatDateLabel = (tickItem: string) => {
try {
return format(new Date(tickItem), 'dd/MM', { locale: ptBR });
} catch {
return tickItem;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'verde': return '#10b981'; // emerald-500
case 'amarelo': return '#f59e0b'; // amber-500
case 'vermelho': return '#ef4444'; // red-500
case 'azul': return '#3b82f6'; // blue-500
default: return '#6b7280'; // gray-500
}
};
if (!processos.length) {
return (
<div className="flex items-center justify-center h-96 text-muted-foreground">
<p>Nenhum processo disponível para exibir gráficos</p>
</div>
);
}
return (
<div className="space-y-6">
{processos.map((processo) => (
<div key={processo.id} className="bg-card rounded-lg border p-4">
<div className="flex items-center gap-2 mb-4">
<div
className="w-3 h-3 rounded-full animate-pulse"
style={{ backgroundColor: getStatusColor(processo.status) }}
/>
<h3 className="text-lg font-semibold text-card-foreground">
Progresso - {processo.nome}
</h3>
<div className="ml-auto text-sm text-muted-foreground">
{processo.progressoReal.toFixed(1)}% realizado
</div>
</div>
<div className="h-64 w-full">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={processo.dadosGrafico}>
<CartesianGrid strokeDasharray="3 3" className="opacity-30" />
<XAxis
dataKey="data"
tickFormatter={formatDateLabel}
className="text-xs"
/>
<YAxis
tickFormatter={formatAxisValue}
className="text-xs"
/>
<Tooltip
formatter={formatTooltipValue}
labelFormatter={(label) => `Data: ${formatDateLabel(label)}`}
contentStyle={{
backgroundColor: 'hsl(var(--card))',
border: '1px solid hsl(var(--border))',
borderRadius: '6px'
}}
/>
<Legend />
<defs>
<linearGradient id={`colorPlanejado-${processo.id}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#8884d8" stopOpacity={0.8}/>
<stop offset="95%" stopColor="#8884d8" stopOpacity={0}/>
</linearGradient>
<linearGradient id={`colorRealizado-${processo.id}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={getStatusColor(processo.status)} stopOpacity={0.8}/>
<stop offset="95%" stopColor={getStatusColor(processo.status)} stopOpacity={0}/>
</linearGradient>
</defs>
<Area
type="monotone"
dataKey="planejado"
stroke="#8884d8"
fillOpacity={1}
fill={`url(#colorPlanejado-${processo.id})`}
name="planejado"
/>
<Area
type="monotone"
dataKey="realizado"
stroke={getStatusColor(processo.status)}
fillOpacity={1}
fill={`url(#colorRealizado-${processo.id})`}
name="realizado"
/>
</AreaChart>
</ResponsiveContainer>
</div>
</div>
))}
</div>
);
};
@@ -0,0 +1,82 @@
import React from 'react';
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { DashboardProcesso } from '@/hooks/useDashboardProducaoOtimizado';
import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale';
interface GraficoProgressoProcessoProps {
processo: DashboardProcesso;
}
export const GraficoProgressoProcesso: React.FC<GraficoProgressoProcessoProps> = ({ processo }) => {
const formatTooltipValue = (value: number, name: string) => [
`${(value / 1000).toFixed(2)} t`,
name === 'planejado' ? 'Planejado' : 'Realizado'
];
const formatAxisValue = (value: number) => `${(value / 1000).toFixed(1)}t`;
const formatDateLabel = (tickItem: string) => {
try {
return format(new Date(tickItem), 'dd/MM', { locale: ptBR });
} catch {
return tickItem;
}
};
return (
<div className="h-80 w-full">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={processo.dadosGrafico}>
<CartesianGrid strokeDasharray="3 3" className="opacity-30" />
<XAxis
dataKey="data"
tickFormatter={formatDateLabel}
className="text-xs"
/>
<YAxis
tickFormatter={formatAxisValue}
className="text-xs"
/>
<Tooltip
formatter={formatTooltipValue}
labelFormatter={(label) => `Data: ${formatDateLabel(label)}`}
contentStyle={{
backgroundColor: 'hsl(var(--card))',
border: '1px solid hsl(var(--border))',
borderRadius: '6px'
}}
/>
<Legend />
<defs>
<linearGradient id="colorPlanejado" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#8884d8" stopOpacity={0.8}/>
<stop offset="95%" stopColor="#8884d8" stopOpacity={0}/>
</linearGradient>
<linearGradient id="colorRealizado" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#82ca9d" stopOpacity={0.8}/>
<stop offset="95%" stopColor="#82ca9d" stopOpacity={0}/>
</linearGradient>
</defs>
<Area
type="monotone"
dataKey="planejado"
stroke="#8884d8"
fillOpacity={1}
fill="url(#colorPlanejado)"
name="planejado"
/>
<Area
type="monotone"
dataKey="realizado"
stroke="#82ca9d"
fillOpacity={1}
fill="url(#colorRealizado)"
name="realizado"
/>
</AreaChart>
</ResponsiveContainer>
</div>
);
};
@@ -0,0 +1,101 @@
import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { Badge } from '@/components/ui/badge';
import { Package, TrendingUp, Weight, Calendar } from 'lucide-react';
import { DashboardDataOtimizado } from '@/hooks/useDashboardProducaoOtimizado';
interface ResumoOFProps {
of: string;
data: DashboardDataOtimizado | null;
loading: boolean;
}
export const ResumoOF: React.FC<ResumoOFProps> = ({ of, data, loading }) => {
if (loading) {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{[...Array(3)].map((_, i) => (
<Card key={i} className="bg-card border-border">
<CardContent className="p-6">
<div className="animate-pulse">
<div className="h-4 bg-muted rounded w-3/4 mb-2"></div>
<div className="h-8 bg-muted rounded w-1/2"></div>
</div>
</CardContent>
</Card>
))}
</div>
);
}
if (!data) return null;
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-card-foreground">
Progresso Geral da OF
</CardTitle>
<TrendingUp className="h-4 w-4 text-blue-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-card-foreground mb-2">
{data.progressoGeral.toFixed(1)}%
</div>
<Progress value={data.progressoGeral} className="h-2 mb-2" />
<p className="text-xs text-muted-foreground">
OF: {data.of}
</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-card-foreground">
Peso Total Fabricado
</CardTitle>
<Weight className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-card-foreground">
{(data.pesoTotalFabricado / 1000).toFixed(2)} t
</div>
<p className="text-xs text-muted-foreground">
de {(data.tonelagem / 1000).toFixed(2)} t contratadas
</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-card-foreground">
Status dos Processos
</CardTitle>
<Package className="h-4 w-4 text-purple-500" />
</CardHeader>
<CardContent>
<div className="flex gap-2 mb-2">
<Badge variant="default" className="bg-blue-500">
{data.processos.filter(p => p.status === 'azul').length}
</Badge>
<Badge variant="default" className="bg-green-500">
{data.processos.filter(p => p.status === 'verde').length}
</Badge>
<Badge variant="secondary" className="bg-yellow-500">
{data.processos.filter(p => p.status === 'amarelo').length}
</Badge>
<Badge variant="destructive">
{data.processos.filter(p => p.status === 'vermelho').length}
</Badge>
</div>
<p className="text-xs text-muted-foreground">
Adiantado / No Prazo / Atenção / Atrasado
</p>
</CardContent>
</Card>
</div>
);
};
@@ -0,0 +1,94 @@
import React from 'react';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import { DashboardDataOtimizado } from '@/hooks/useDashboardProducaoOtimizado';
interface TabelaResumoProcessosProps {
data: DashboardDataOtimizado | null;
loading: boolean;
}
export const TabelaResumoProcessos: React.FC<TabelaResumoProcessosProps> = ({ data, loading }) => {
if (loading) {
return (
<div className="space-y-3">
{[...Array(3)].map((_, i) => (
<div key={i} className="animate-pulse">
<div className="h-4 bg-muted rounded w-full"></div>
</div>
))}
</div>
);
}
if (!data || !data.processos.length) {
return (
<div className="text-center py-8 text-muted-foreground">
Nenhum processo encontrado para esta OF
</div>
);
}
const getStatusBadge = (status: string) => {
switch (status) {
case 'azul':
return <Badge className="bg-blue-500">Adiantado</Badge>;
case 'verde':
return <Badge className="bg-green-500">No Prazo</Badge>;
case 'amarelo':
return <Badge className="bg-yellow-500">Atenção</Badge>;
case 'vermelho':
return <Badge variant="destructive">Atrasado</Badge>;
default:
return <Badge variant="secondary">Indefinido</Badge>;
}
};
return (
<div className="w-full overflow-x-auto">
<Table disableOverflow>
<TableHeader>
<TableRow className="h-10">
<TableHead>Processo</TableHead>
<TableHead>Progresso Real</TableHead>
<TableHead>Progresso Esperado</TableHead>
<TableHead>Status</TableHead>
<TableHead>Peso Processado</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.processos.map((processo) => (
<TableRow key={processo.id} className="h-12">
<TableCell className="font-medium py-2">{processo.nome}</TableCell>
<TableCell className="py-2">
<div className="space-y-1">
<div className="flex justify-between text-sm">
<span>{processo.progressoReal.toFixed(1)}%</span>
</div>
<Progress value={processo.progressoReal} className="h-2" />
</div>
</TableCell>
<TableCell className="py-2">
<div className="space-y-1">
<div className="flex justify-between text-sm">
<span>{processo.progressoEsperado.toFixed(1)}%</span>
</div>
<Progress value={processo.progressoEsperado} className="h-2 opacity-50" />
</div>
</TableCell>
<TableCell className="py-2">
{getStatusBadge(processo.status)}
</TableCell>
<TableCell className="text-right py-2">
{(processo.pesoFabricado / 1000).toFixed(3)} t
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
};