🚀 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
+314
View File
@@ -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>
);
};