🚀 Initial commit: Versão atual do TrackSteel APP
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useDashboardProducaoOtimizado } from '@/hooks/useDashboardProducaoOtimizado';
|
||||
import { useProcessChartData } from '@/hooks/useProcessChartData';
|
||||
import { ProcessChart } from './ProcessChart';
|
||||
|
||||
interface OFData {
|
||||
num_of: string;
|
||||
descritivo?: string;
|
||||
peso_total?: number;
|
||||
data_prazo?: string;
|
||||
}
|
||||
|
||||
interface OFCardProps {
|
||||
ofData: OFData;
|
||||
}
|
||||
|
||||
export const OFCard: React.FC<OFCardProps> = ({ ofData }) => {
|
||||
const { dashboardData, loading } = useDashboardProducaoOtimizado(ofData.num_of);
|
||||
const { chartData, loading: chartLoading } = useProcessChartData(ofData.num_of);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card className="h-[580px] border-2">
|
||||
<CardContent className="p-3 flex items-center justify-center h-full">
|
||||
<div className="text-center space-y-3">
|
||||
<div className="animate-spin rounded-full h-10 w-10 border-b-2 border-primary mx-auto"></div>
|
||||
<p className="text-muted-foreground text-sm">Carregando dados...</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (!dashboardData) {
|
||||
return (
|
||||
<Card className="h-[580px] border-2">
|
||||
<CardContent className="p-3 flex items-center justify-center h-full">
|
||||
<p className="text-muted-foreground text-sm">Dados não disponíveis</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const getStatusInfo = () => {
|
||||
const progressoReal = dashboardData.progressoGeral;
|
||||
|
||||
// Calcular dias restantes se houver data de prazo
|
||||
let diasRestantes = null;
|
||||
if (ofData.data_prazo) {
|
||||
const hoje = new Date();
|
||||
const prazo = new Date(ofData.data_prazo);
|
||||
const diffTime = prazo.getTime() - hoje.getTime();
|
||||
diasRestantes = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
// Determinar status baseado no progresso e prazo
|
||||
if (progressoReal >= 95) {
|
||||
return {
|
||||
status: 'Concluído',
|
||||
variant: 'default' as const,
|
||||
color: 'bg-green-500',
|
||||
borderColor: 'border-green-500'
|
||||
};
|
||||
} else if (diasRestantes !== null && diasRestantes < 0) {
|
||||
return {
|
||||
status: 'Atrasado',
|
||||
variant: 'destructive' as const,
|
||||
color: 'bg-red-500',
|
||||
borderColor: 'border-red-500'
|
||||
};
|
||||
} else if (progressoReal > 80) {
|
||||
return {
|
||||
status: 'Adiantado',
|
||||
variant: 'secondary' as const,
|
||||
color: 'bg-blue-500',
|
||||
borderColor: 'border-blue-500'
|
||||
};
|
||||
} else if (diasRestantes !== null && diasRestantes <= 7) {
|
||||
return {
|
||||
status: 'Muito Atrasado',
|
||||
variant: 'destructive' as const,
|
||||
color: 'bg-orange-500',
|
||||
borderColor: 'border-orange-500'
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 'No Prazo',
|
||||
variant: 'outline' as const,
|
||||
color: 'bg-green-500',
|
||||
borderColor: 'border-green-500'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Função para obter a cor da borda baseada no status do processo "Concluído"
|
||||
const getCardBorderColor = () => {
|
||||
const processoConcluido = dashboardData.processos.find(p =>
|
||||
p.nome.toLowerCase().includes('concluído') || p.nome.toLowerCase().includes('concluido')
|
||||
);
|
||||
|
||||
if (!processoConcluido) return 'border-gray-500';
|
||||
|
||||
switch (processoConcluido.status) {
|
||||
case 'verde':
|
||||
return 'border-green-500';
|
||||
case 'amarelo':
|
||||
return 'border-yellow-500';
|
||||
case 'vermelho':
|
||||
return 'border-red-500';
|
||||
case 'azul':
|
||||
return 'border-blue-500';
|
||||
default:
|
||||
return 'border-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
const getProcessStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'verde':
|
||||
return '#10b981'; // Verde - Adiantado/No Prazo
|
||||
case 'amarelo':
|
||||
return '#f59e0b'; // Amarelo - Atenção
|
||||
case 'vermelho':
|
||||
return '#ef4444'; // Vermelho - Atrasado
|
||||
case 'azul':
|
||||
return '#3b82f6'; // Azul - No Prazo/Atenção
|
||||
default:
|
||||
return '#6b7280'; // Cinza - Padrão
|
||||
}
|
||||
};
|
||||
|
||||
const getProcessBorderColor = (processName: string) => {
|
||||
const processo = dashboardData.processos.find(p =>
|
||||
p.nome.toLowerCase().includes(processName.toLowerCase())
|
||||
);
|
||||
|
||||
if (!processo) return 'border-gray-500';
|
||||
|
||||
switch (processo.status) {
|
||||
case 'verde':
|
||||
return 'border-green-500';
|
||||
case 'amarelo':
|
||||
return 'border-yellow-500';
|
||||
case 'vermelho':
|
||||
return 'border-red-500';
|
||||
case 'azul':
|
||||
return 'border-blue-500';
|
||||
default:
|
||||
return 'border-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
const statusInfo = getStatusInfo();
|
||||
const progressoReal = dashboardData.progressoGeral;
|
||||
const pesoFabricado = (dashboardData.pesoTotalFabricado / 1000).toFixed(1);
|
||||
const pesoTotal = (dashboardData.tonelagem / 1000).toFixed(1);
|
||||
const cardBorderColor = getCardBorderColor();
|
||||
|
||||
// Pegar os principais processos para mostrar (incluindo Montagem - 6 processos)
|
||||
const processosVisiveis = dashboardData.processos
|
||||
.filter(p => !p.nome.toLowerCase().includes('aceite') &&
|
||||
!p.nome.toLowerCase().includes('db') &&
|
||||
!p.nome.toLowerCase().includes('concluído') &&
|
||||
!p.nome.toLowerCase().includes('concluido'))
|
||||
.slice(0, 6); // Mostrar 6 processos
|
||||
|
||||
console.log('Dados dos gráficos no OFCard:', chartData);
|
||||
console.log('Loading dos gráficos:', chartLoading);
|
||||
|
||||
return (
|
||||
<Card className={`h-[580px] border-2 ${cardBorderColor} transition-all hover:shadow-lg`}>
|
||||
<CardContent className="p-3 h-full flex flex-col">
|
||||
{/* Header do Card - Mais compactado */}
|
||||
<div className="mb-2">
|
||||
<div className="flex justify-between items-start mb-1">
|
||||
<h3 className="text-lg font-bold text-foreground">{ofData.num_of}</h3>
|
||||
<Badge variant={statusInfo.variant} className="text-xs px-2 py-0.5">
|
||||
{statusInfo.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-semibold text-foreground">
|
||||
{progressoReal.toFixed(1)}%
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{pesoFabricado}t de {pesoTotal}t fabricado
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-secondary rounded-full h-1.5">
|
||||
<div
|
||||
className={`h-1.5 rounded-full ${statusInfo.color} transition-all duration-300`}
|
||||
style={{ width: `${Math.min(progressoReal, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Resumo dos Processos - Layout Grid 2x3 mais compacto */}
|
||||
<div className="mb-2">
|
||||
<h4 className="text-xs font-semibold text-foreground mb-1">
|
||||
Resumo Comparativo dos Processos
|
||||
</h4>
|
||||
|
||||
{/* Grid 2x3 para os processos - mais compacto */}
|
||||
<div className="grid grid-cols-2 gap-x-2 gap-y-1">
|
||||
{processosVisiveis.map((processo) => (
|
||||
<div key={processo.id} className="space-y-0.5">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-[8px] font-medium text-foreground truncate pr-1">
|
||||
{processo.nome}
|
||||
</span>
|
||||
<span className="text-[7px] text-muted-foreground whitespace-nowrap">
|
||||
{processo.progressoReal.toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-0.5">
|
||||
{/* Barra Planejado */}
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-[6px] text-muted-foreground w-5">Plan</span>
|
||||
<div className="flex-1 bg-secondary rounded-full h-0.5">
|
||||
<div
|
||||
className="h-0.5 rounded-full bg-blue-500 transition-all duration-300"
|
||||
style={{ width: `${Math.min(processo.progressoEsperado, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Barra Real */}
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-[6px] text-muted-foreground w-5">Real</span>
|
||||
<div className="flex-1 bg-secondary rounded-full h-0.5">
|
||||
<div
|
||||
className="h-0.5 rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.min(processo.progressoReal, 100)}%`,
|
||||
backgroundColor: getProcessStatusColor(processo.status)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Título dos Gráficos */}
|
||||
<div className="mb-2">
|
||||
<h4 className="text-xs font-semibold text-foreground text-center">
|
||||
Gráfico de evolução/metas da quinzena
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
{/* Gráficos nos Retângulos com Bordas Coloridas */}
|
||||
<div className="flex-1 space-y-2 min-h-0">
|
||||
{/* Retângulo Corte - Borda vermelha */}
|
||||
<div className={`flex-1 bg-background rounded border-2 ${getProcessBorderColor('corte')} p-2`}
|
||||
style={{ minHeight: '90px', maxHeight: '110px' }}>
|
||||
{chartLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<span className="text-foreground text-xs">Carregando...</span>
|
||||
</div>
|
||||
) : (
|
||||
<ProcessChart
|
||||
processName="Corte (Kg)"
|
||||
data={chartData.corte || []}
|
||||
color="#dc2626"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Retângulo Solda - Borda azul */}
|
||||
<div className={`flex-1 bg-background rounded border-2 ${getProcessBorderColor('solda')} p-2`}
|
||||
style={{ minHeight: '90px', maxHeight: '110px' }}>
|
||||
{chartLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<span className="text-foreground text-xs">Carregando...</span>
|
||||
</div>
|
||||
) : (
|
||||
<ProcessChart
|
||||
processName="Solda (Kg)"
|
||||
data={chartData.solda || []}
|
||||
color="#2563eb"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Retângulo Montagem - Borda rosa */}
|
||||
<div className={`flex-1 bg-background rounded border-2 ${getProcessBorderColor('montagem')} p-2`}
|
||||
style={{ minHeight: '90px', maxHeight: '110px' }}>
|
||||
{chartLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<span className="text-foreground text-xs">Carregando...</span>
|
||||
</div>
|
||||
) : (
|
||||
<ProcessChart
|
||||
processName="Mont.Obra (Kg)"
|
||||
data={chartData.montagem || []}
|
||||
color="#db2777"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,260 @@
|
||||
import React from 'react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useDashboardProducaoOtimizado } from '@/hooks/useDashboardProducaoOtimizado';
|
||||
import { useProcessChartData } from '@/hooks/useProcessChartData';
|
||||
import { ProcessChartOptimized } from './ProcessChartOptimized';
|
||||
|
||||
interface OFData {
|
||||
num_of: string;
|
||||
descritivo?: string;
|
||||
peso_total?: number;
|
||||
data_prazo?: string;
|
||||
}
|
||||
|
||||
interface OFCardOptimizedProps {
|
||||
ofData: OFData;
|
||||
}
|
||||
|
||||
export const OFCardOptimized: React.FC<OFCardOptimizedProps> = ({ ofData }) => {
|
||||
const { dashboardData, loading } = useDashboardProducaoOtimizado(ofData.num_of);
|
||||
const { chartData, loading: chartLoading } = useProcessChartData(ofData.num_of);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card className="h-auto min-h-[700px] border-2">
|
||||
<CardContent className="p-6 flex items-center justify-center h-full">
|
||||
<div className="text-center space-y-3">
|
||||
<div className="animate-spin rounded-full h-10 w-10 border-b-2 border-primary mx-auto"></div>
|
||||
<p className="text-muted-foreground">Carregando dados...</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (!dashboardData) {
|
||||
return (
|
||||
<Card className="h-auto min-h-[700px] border-2">
|
||||
<CardContent className="p-6 flex items-center justify-center h-full">
|
||||
<p className="text-muted-foreground">Dados não disponíveis</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const getStatusInfo = () => {
|
||||
const progressoReal = dashboardData.progressoGeral;
|
||||
|
||||
let diasRestantes = null;
|
||||
if (ofData.data_prazo) {
|
||||
const hoje = new Date();
|
||||
const prazo = new Date(ofData.data_prazo);
|
||||
const diffTime = prazo.getTime() - hoje.getTime();
|
||||
diasRestantes = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
if (progressoReal >= 95) {
|
||||
return {
|
||||
status: 'Concluído',
|
||||
variant: 'default' as const,
|
||||
color: 'bg-green-500',
|
||||
borderColor: 'border-green-500'
|
||||
};
|
||||
} else if (diasRestantes !== null && diasRestantes < 0) {
|
||||
return {
|
||||
status: 'Atrasado',
|
||||
variant: 'destructive' as const,
|
||||
color: 'bg-red-500',
|
||||
borderColor: 'border-red-500'
|
||||
};
|
||||
} else if (progressoReal > 80) {
|
||||
return {
|
||||
status: 'Adiantado',
|
||||
variant: 'secondary' as const,
|
||||
color: 'bg-blue-500',
|
||||
borderColor: 'border-blue-500'
|
||||
};
|
||||
} else if (diasRestantes !== null && diasRestantes <= 7) {
|
||||
return {
|
||||
status: 'Muito Atrasado',
|
||||
variant: 'destructive' as const,
|
||||
color: 'bg-orange-500',
|
||||
borderColor: 'border-orange-500'
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 'No Prazo',
|
||||
variant: 'outline' as const,
|
||||
color: 'bg-green-500',
|
||||
borderColor: 'border-green-500'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getCardBorderColor = () => {
|
||||
const processoConcluido = dashboardData.processos.find(p =>
|
||||
p.nome.toLowerCase().includes('concluído') || p.nome.toLowerCase().includes('concluido')
|
||||
);
|
||||
|
||||
if (!processoConcluido) return 'border-gray-500';
|
||||
|
||||
switch (processoConcluido.status) {
|
||||
case 'verde':
|
||||
return 'border-green-500';
|
||||
case 'amarelo':
|
||||
return 'border-yellow-500';
|
||||
case 'vermelho':
|
||||
return 'border-red-500';
|
||||
case 'azul':
|
||||
return 'border-blue-500';
|
||||
default:
|
||||
return 'border-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
const getProcessStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'verde':
|
||||
return '#10b981';
|
||||
case 'amarelo':
|
||||
return '#f59e0b';
|
||||
case 'vermelho':
|
||||
return '#ef4444';
|
||||
case 'azul':
|
||||
return '#3b82f6';
|
||||
default:
|
||||
return '#6b7280';
|
||||
}
|
||||
};
|
||||
|
||||
// Função para abreviar nomes de processos
|
||||
const abreviarNomeProcesso = (nome: string) => {
|
||||
return nome
|
||||
.replace(/Detalhamento/gi, 'Detalham')
|
||||
.replace(/Pintura\/Galv/gi, 'Pint/Galv');
|
||||
};
|
||||
|
||||
const statusInfo = getStatusInfo();
|
||||
const progressoReal = dashboardData.progressoGeral;
|
||||
const pesoFabricado = (dashboardData.pesoTotalFabricado).toFixed(0);
|
||||
const pesoTotal = (dashboardData.tonelagem).toFixed(0);
|
||||
const cardBorderColor = getCardBorderColor();
|
||||
|
||||
// Pegar os principais processos para mostrar
|
||||
const processosVisiveis = dashboardData.processos
|
||||
.filter(p => !p.nome.toLowerCase().includes('aceite') &&
|
||||
!p.nome.toLowerCase().includes('db') &&
|
||||
!p.nome.toLowerCase().includes('concluído') &&
|
||||
!p.nome.toLowerCase().includes('concluido'))
|
||||
.slice(0, 6);
|
||||
|
||||
return (
|
||||
<Card className={`h-auto min-h-[720px] border-2 ${cardBorderColor} transition-all hover:shadow-lg bg-card text-card-foreground shadow-lg ${cardBorderColor.replace('border-', 'shadow-')}/10`}>
|
||||
<CardContent className="p-4 sm:p-6 h-full flex flex-col gap-3 sm:gap-4">
|
||||
{/* Header do Card - Responsivo */}
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start gap-2 sm:gap-0">
|
||||
<div className="w-full sm:w-auto">
|
||||
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-black text-foreground" style={{ fontSize: 'clamp(2rem, 4vw, 3rem)' }}>
|
||||
{ofData.num_of}
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-1 text-sm sm:text-base">
|
||||
{pesoFabricado} Kg de {pesoTotal} Kg fabricado
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant={statusInfo.variant} className="text-xs px-3 py-1 self-start sm:self-auto">
|
||||
{statusInfo.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Barra de Progresso - Espaçamento reduzido */}
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm sm:text-base font-medium text-foreground" style={{ fontSize: 'clamp(0.875rem, 2vw, 1rem)' }}>
|
||||
Progresso
|
||||
</span>
|
||||
<span className="text-sm font-medium text-foreground">{progressoReal.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-muted rounded-full h-2.5">
|
||||
<div
|
||||
className={`h-2.5 rounded-full ${statusInfo.color} transition-all duration-300`}
|
||||
style={{ width: `${Math.min(progressoReal, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Resumo Comparativo dos Processos - Layout Responsivo com espaçamento reduzido */}
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-base sm:text-lg font-semibold text-foreground border-b border-border pb-1" style={{ fontSize: 'clamp(1rem, 2.5vw, 1.125rem)' }}>
|
||||
Resumo Comparativo dos Processos
|
||||
</h3>
|
||||
{/* Grid responsivo: 1 coluna em mobile, 2 colunas em telas maiores */}
|
||||
<div className="
|
||||
grid grid-cols-1
|
||||
sm:grid-cols-2
|
||||
gap-x-4 sm:gap-x-6
|
||||
gap-y-2 sm:gap-y-3
|
||||
text-sm
|
||||
">
|
||||
{processosVisiveis.map((processo) => (
|
||||
<div key={processo.id} className="w-full">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span
|
||||
className="text-muted-foreground text-xs sm:text-sm break-words"
|
||||
style={{
|
||||
overflowWrap: 'break-word',
|
||||
wordBreak: 'break-word',
|
||||
fontSize: 'clamp(0.75rem, 1.8vw, 0.875rem)'
|
||||
}}
|
||||
>
|
||||
{abreviarNomeProcesso(processo.nome)}
|
||||
</span>
|
||||
<span
|
||||
className={`font-bold ml-2 ${
|
||||
processo.status === 'verde' ? 'text-green-400' :
|
||||
processo.status === 'amarelo' ? 'text-yellow-400' :
|
||||
processo.status === 'vermelho' ? 'text-red-400' :
|
||||
processo.status === 'azul' ? 'text-blue-400' : 'text-gray-400'
|
||||
}`}
|
||||
style={{ fontSize: 'clamp(0.55rem, 1.2vw, 0.65rem)' }}
|
||||
>
|
||||
{processo.progressoReal.toFixed(0)}% ({(processo.pesoFabricado).toFixed(0)} Kg)
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-muted rounded-full h-2 mt-1">
|
||||
<div
|
||||
className="h-2 rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.min(processo.progressoReal, 100)}%`,
|
||||
backgroundColor: getProcessStatusColor(processo.status)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Gráfico de Evolução - Responsivo com espaçamento reduzido */}
|
||||
<div className="flex-1 min-h-0">
|
||||
<h3 className="text-base sm:text-lg font-semibold text-foreground mb-1" style={{ fontSize: 'clamp(1rem, 2.5vw, 1.125rem)' }}>
|
||||
Evolução/Metas da Quinzena (Kg)
|
||||
</h3>
|
||||
<div className="h-full min-h-[200px] sm:min-h-[250px] border border-border rounded-lg bg-background/50 mb-1">
|
||||
{chartLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<span className="text-muted-foreground text-sm">Carregando gráfico...</span>
|
||||
</div>
|
||||
) : (
|
||||
<ProcessChartOptimized
|
||||
corteData={chartData.corte || []}
|
||||
soldaData={chartData.solda || []}
|
||||
montagemData={chartData.montagem || []}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { X, ChevronLeft, ChevronRight, Play, Pause } from 'lucide-react';
|
||||
|
||||
interface PainelHeaderProps {
|
||||
totalPages: number;
|
||||
currentPage: number;
|
||||
autoRotate: boolean;
|
||||
onToggleAutoRotate: () => void;
|
||||
onPrevPage: () => void;
|
||||
onNextPage: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const PainelHeader: React.FC<PainelHeaderProps> = ({
|
||||
totalPages,
|
||||
currentPage,
|
||||
autoRotate,
|
||||
onToggleAutoRotate,
|
||||
onPrevPage,
|
||||
onNextPage,
|
||||
onClose
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-foreground mb-2">Painel Industrial</h1>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Acompanhamento comparativo em tempo real dos contratos.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{totalPages > 1 && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
onClick={onToggleAutoRotate}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{autoRotate ? <Pause className="h-5 w-5" /> : <Play className="h-5 w-5" />}
|
||||
{autoRotate ? 'Pausar' : 'Auto'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
onClick={onPrevPage}
|
||||
>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
|
||||
<span className="text-lg font-medium text-foreground">
|
||||
{currentPage + 1} de {totalPages}
|
||||
</span>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
onClick={onNextPage}
|
||||
>
|
||||
<ChevronRight className="h-5 w-5" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="lg"
|
||||
onClick={onClose}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
Fechar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
|
||||
import React from 'react';
|
||||
import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Tooltip } from 'recharts';
|
||||
|
||||
interface ProcessChartProps {
|
||||
processName: string;
|
||||
data: {
|
||||
name: string;
|
||||
weight: number;
|
||||
}[];
|
||||
color: string;
|
||||
}
|
||||
|
||||
export const ProcessChart: React.FC<ProcessChartProps> = ({ processName, data, color }) => {
|
||||
const formatTooltip = (value: number, name: string) => {
|
||||
return [`${value.toFixed(3)}t`, name];
|
||||
};
|
||||
|
||||
// Se não há dados ou todos os valores são zero
|
||||
if (!data || data.length === 0 || data.every(item => item.weight === 0)) {
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<h4 className="text-[9px] font-semibold text-foreground mb-1 text-center">
|
||||
{processName}
|
||||
</h4>
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<span className="text-[8px] text-muted-foreground">Sem dados</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Renderizando gráfico ${processName} com dados:`, data);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col">
|
||||
<h4 className="text-[9px] font-semibold text-foreground mb-1 text-center truncate">
|
||||
{processName}
|
||||
</h4>
|
||||
<div className="flex-1 w-full" style={{ minHeight: '60px' }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
data={data}
|
||||
margin={{ top: 5, right: 5, left: 5, bottom: 20 }}
|
||||
barCategoryGap="10%"
|
||||
>
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tick={{ fontSize: 6, fill: 'hsl(var(--foreground))' }}
|
||||
interval={0}
|
||||
angle={-45}
|
||||
textAnchor="end"
|
||||
height={15}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 6, fill: 'hsl(var(--foreground))' }}
|
||||
tickFormatter={(value) => `${value}t`}
|
||||
width={20}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={formatTooltip}
|
||||
labelStyle={{ color: 'black', fontSize: '8px' }}
|
||||
contentStyle={{
|
||||
backgroundColor: 'rgba(0,0,0,0.8)',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
fontSize: '8px',
|
||||
padding: '4px 8px'
|
||||
}}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="weight"
|
||||
fill={color}
|
||||
radius={[2, 2, 0, 0]}
|
||||
maxBarSize={25}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,216 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface ChartData {
|
||||
name: string;
|
||||
weight: number;
|
||||
}
|
||||
|
||||
interface ProcessChartOptimizedProps {
|
||||
corteData: ChartData[];
|
||||
soldaData: ChartData[];
|
||||
montagemData: ChartData[];
|
||||
}
|
||||
|
||||
export const ProcessChartOptimized: React.FC<ProcessChartOptimizedProps> = ({
|
||||
corteData,
|
||||
soldaData,
|
||||
montagemData
|
||||
}) => {
|
||||
const formatValue = (value: number) => {
|
||||
if (value >= 1000) {
|
||||
return `${(value / 1000).toFixed(1)}k`;
|
||||
}
|
||||
return value.toString();
|
||||
};
|
||||
|
||||
const getMaxValue = () => {
|
||||
const allValues = [
|
||||
...corteData.map(d => d.weight),
|
||||
...soldaData.map(d => d.weight),
|
||||
...montagemData.map(d => d.weight)
|
||||
];
|
||||
return Math.max(...allValues, 1);
|
||||
};
|
||||
|
||||
const maxValue = getMaxValue();
|
||||
|
||||
const calculateHeight = (value: number) => {
|
||||
return Math.max((value / maxValue) * 100, 5);
|
||||
};
|
||||
|
||||
// Garantir que todos os processos tenham 3 barras
|
||||
const normalizeData = (data: ChartData[]) => {
|
||||
const labels = ['7-15d', 'ult.7d', 'prev.7d'];
|
||||
return labels.map((label, index) => ({
|
||||
name: label,
|
||||
weight: data[index]?.weight || 0
|
||||
}));
|
||||
};
|
||||
|
||||
const normalizedCorteData = normalizeData(corteData);
|
||||
const normalizedSoldaData = normalizeData(soldaData);
|
||||
const normalizedMontagemData = normalizeData(montagemData);
|
||||
|
||||
return (
|
||||
<div style={{ fontFamily: 'Inter, sans-serif' }} className="h-full">
|
||||
<div
|
||||
className="flex justify-evenly items-end h-full p-4 pt-6 pb-6 rounded-lg"
|
||||
style={{ height: '200px' }}
|
||||
>
|
||||
{/* Grupo Corte */}
|
||||
<div className="flex flex-col justify-end items-center h-full flex-1">
|
||||
<div
|
||||
className="flex items-end justify-center w-full gap-1"
|
||||
style={{ height: '120px' }}
|
||||
>
|
||||
{normalizedCorteData.map((data, index) => {
|
||||
const colors = ['#f97316', '#10b981', '#ec4899']; // orange, green, pink
|
||||
const labels = ['7-15d', 'ult.7d', 'prev.7d'];
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="relative transition-all duration-500"
|
||||
style={{
|
||||
width: '20px',
|
||||
height: `${calculateHeight(data.weight)}%`,
|
||||
backgroundColor: colors[index],
|
||||
borderRadius: '4px 4px 0 0'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute w-full text-center text-xs font-semibold text-foreground"
|
||||
style={{
|
||||
top: '-20px',
|
||||
fontSize: '10px',
|
||||
lineHeight: '1.2'
|
||||
}}
|
||||
>
|
||||
{formatValue(data.weight)}
|
||||
</div>
|
||||
<div
|
||||
className="absolute w-full text-center text-foreground whitespace-nowrap"
|
||||
style={{
|
||||
bottom: '-18px',
|
||||
fontSize: '9px'
|
||||
}}
|
||||
>
|
||||
{labels[index]}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className="mt-4 text-muted-foreground font-medium"
|
||||
style={{ fontSize: '11px' }}
|
||||
>
|
||||
Corte
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grupo Solda */}
|
||||
<div className="flex flex-col justify-end items-center h-full flex-1">
|
||||
<div
|
||||
className="flex items-end justify-center w-full gap-1"
|
||||
style={{ height: '120px' }}
|
||||
>
|
||||
{normalizedSoldaData.map((data, index) => {
|
||||
const colors = ['#f97316', '#10b981', '#ec4899']; // orange, green, pink
|
||||
const labels = ['7-15d', 'ult.7d', 'prev.7d'];
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="relative transition-all duration-500"
|
||||
style={{
|
||||
width: '20px',
|
||||
height: `${calculateHeight(data.weight)}%`,
|
||||
backgroundColor: colors[index],
|
||||
borderRadius: '4px 4px 0 0'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute w-full text-center text-xs font-semibold text-foreground"
|
||||
style={{
|
||||
top: '-20px',
|
||||
fontSize: '10px',
|
||||
lineHeight: '1.2'
|
||||
}}
|
||||
>
|
||||
{formatValue(data.weight)}
|
||||
</div>
|
||||
<div
|
||||
className="absolute w-full text-center text-foreground whitespace-nowrap"
|
||||
style={{
|
||||
bottom: '-18px',
|
||||
fontSize: '9px'
|
||||
}}
|
||||
>
|
||||
{labels[index]}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className="mt-4 text-muted-foreground font-medium"
|
||||
style={{ fontSize: '11px' }}
|
||||
>
|
||||
Solda
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grupo Montagem */}
|
||||
<div className="flex flex-col justify-end items-center h-full flex-1">
|
||||
<div
|
||||
className="flex items-end justify-center w-full gap-1"
|
||||
style={{ height: '120px' }}
|
||||
>
|
||||
{normalizedMontagemData.map((data, index) => {
|
||||
const colors = ['#f97316', '#10b981', '#ec4899']; // orange, green, pink
|
||||
const labels = ['7-15d', 'ult.7d', 'prev.7d'];
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="relative transition-all duration-500"
|
||||
style={{
|
||||
width: '20px',
|
||||
height: `${calculateHeight(data.weight)}%`,
|
||||
backgroundColor: colors[index],
|
||||
borderRadius: '4px 4px 0 0'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute w-full text-center text-xs font-semibold text-foreground"
|
||||
style={{
|
||||
top: '-20px',
|
||||
fontSize: '10px',
|
||||
lineHeight: '1.2'
|
||||
}}
|
||||
>
|
||||
{formatValue(data.weight)}
|
||||
</div>
|
||||
<div
|
||||
className="absolute w-full text-center text-foreground whitespace-nowrap"
|
||||
style={{
|
||||
bottom: '-18px',
|
||||
fontSize: '9px'
|
||||
}}
|
||||
>
|
||||
{labels[index]}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className="mt-4 text-muted-foreground font-medium"
|
||||
style={{ fontSize: '11px' }}
|
||||
>
|
||||
Mont. Obra
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user