🚀 Initial commit: Versão atual do TrackSteel APP
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user