diff --git a/src/components/dashboard-producao/GraficoMestre.tsx b/src/components/dashboard-producao/GraficoMestre.tsx index 0f1d951..2b7a710 100644 --- a/src/components/dashboard-producao/GraficoMestre.tsx +++ b/src/components/dashboard-producao/GraficoMestre.tsx @@ -1,9 +1,19 @@ - -import React, { useMemo } from 'react'; -import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts'; -import { format, parseISO, eachDayOfInterval } from 'date-fns'; +import React, { useState, useMemo } from 'react'; +import { + ResponsiveContainer, + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend +} from 'recharts'; +import { format, parseISO } from 'date-fns'; import { ptBR } from 'date-fns/locale'; import { DashboardProcesso } from '@/hooks/useDashboardProducaoOtimizado'; +import { Button } from '@/components/ui/button'; +import { Scale, Percent, FilterX } from 'lucide-react'; interface GraficoMestreProps { processos: DashboardProcesso[]; @@ -11,126 +21,296 @@ interface GraficoMestreProps { processoSelecionado?: string | null; } +// Ordem sequencial padrão de produção para ordenação linear +const ORDEM_PROCESSOS_LINEAR = [ + 'detalhamento', + 'corte', + 'solda', + 'pintura/galv', + 'pintura', + 'galvanizacao', + 'expedicao', + 'expedição', + 'montagem', + 'aceite/db', + 'aceite', + 'concluido', + 'concluído' +]; + +// Paleta de cores vibrantes e distintas para cada processo +const CORES_PROCESSOS: Record = { + 'detalhamento': '#a855f7', // Roxo / Purple + 'corte': '#3b82f6', // Azul Royal + 'solda': '#f97316', // Laranja Vibrante + 'pintura/galv': '#ec4899', // Rosa / Magenta + 'pintura': '#ec4899', // Rosa + 'galvanizacao': '#ec4899', // Rosa + 'expedicao': '#06b6d4', // Ciano + 'expedição': '#06b6d4', // Ciano + 'montagem': '#14b8a6', // Verde Água / Teal + 'aceite/db': '#6366f1', // Índigo + 'aceite': '#6366f1', // Índigo + 'concluido': '#10b981', // Verde Esmeralda + 'concluído': '#10b981' // Verde Esmeralda +}; + +// Paleta de fallback se houver processo fora do padrão +const CORES_FALLBACK = [ + '#a855f7', '#3b82f6', '#f97316', '#ec4899', + '#06b6d4', '#14b8a6', '#6366f1', '#10b981', '#eab308' +]; + export const GraficoMestre: React.FC = ({ processos, onProcessoClick, - processoSelecionado + processoSelecionado: processoSelecionadoProp }) => { - // Processar dados dos gráficos individuais para criar um gráfico sobreposto - const dadosGraficoSobreposto = useMemo(() => { - if (!processos || processos.length === 0) return []; + const [modoExibicao, setModoExibicao] = useState<'kg' | 'percent'>('kg'); + const [selectedProcessoInternal, setSelectedProcessoInternal] = useState(null); + + const selectedProcesso = processoSelecionadoProp !== undefined ? processoSelecionadoProp : selectedProcessoInternal; + + const handleSelectProcesso = (nome: string) => { + const nextSelected = selectedProcesso === nome ? null : nome; + setSelectedProcessoInternal(nextSelected); + if (onProcessoClick) { + onProcessoClick(nextSelected || ''); + } + }; + + // 1. Ordenar processos na sequência linear real da produção + const processosOrdenados = useMemo(() => { + if (!processos) return []; + + return [...processos].sort((a, b) => { + const nomeA = a.nome.toLowerCase().trim(); + const nomeB = b.nome.toLowerCase().trim(); + + const indexA = ORDEM_PROCESSOS_LINEAR.findIndex(p => nomeA.includes(p)); + const indexB = ORDEM_PROCESSOS_LINEAR.findIndex(p => nomeB.includes(p)); + + const posA = indexA !== -1 ? indexA : 99; + const posB = indexB !== -1 ? indexB : 99; + + return posA - posB; + }); + }, [processos]); + + // Função utilitária para pegar a cor de um processo + const getCorProcesso = (nomeProcesso: string, index: number) => { + const nomeLower = nomeProcesso.toLowerCase().trim(); + for (const [key, color] of Object.entries(CORES_PROCESSOS)) { + if (nomeLower.includes(key)) return color; + } + return CORES_FALLBACK[index % CORES_FALLBACK.length]; + }; + + // 2. Processar dados agregados por data + const dadosGrafico = useMemo(() => { + if (!processosOrdenados || processosOrdenados.length === 0) return []; - // Coletar todas as datas dos gráficos individuais const todasAsDatas = new Set(); - processos.forEach(processo => { + processosOrdenados.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 dataFormatada = format(parseISO(data), 'dd/MM', { locale: ptBR }); const pontoGrafico: any = { - data: format(parseISO(data), 'dd/MM', { locale: ptBR }), + data: dataFormatada, 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; + processosOrdenados.forEach(processo => { + const pontoProcesso = processo.dadosGrafico.find(p => p.data === data); + const valorKg = pontoProcesso ? pontoProcesso.realizado : 0; + + if (modoExibicao === 'percent') { + const pesoTotal = processo.pesoTotal > 0 ? processo.pesoTotal : 1; + const percentual = Math.min(100, (valorKg / pesoTotal) * 100); + pontoGrafico[processo.nome] = Math.round(percentual * 10) / 10; + } else { + pontoGrafico[processo.nome] = Math.round(valorKg * 10) / 10; + } }); return pontoGrafico; }); - }, [processos]); + }, [processosOrdenados, modoExibicao]); - // 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'; - } + // Formatação customizada para o Tooltip estilo Glassmorphism + const CustomTooltip = ({ active, payload, label }: any) => { + if (!active || !payload || !payload.length) return null; + + return ( +
+
+ Data + {label} +
+
+ {payload.map((entry: any, index: number) => { + const isHighlight = !selectedProcesso || selectedProcesso === entry.name; + return ( +
+
+ + {entry.name} +
+ + {modoExibicao === 'kg' + ? `${entry.value.toLocaleString('pt-BR')} kg` + : `${entry.value.toFixed(1)}%` + } + +
+ ); + })} +
+
+ ); }; - const handleProcessoClick = (processoNome: string) => { - if (onProcessoClick) { - onProcessoClick(processoNome); - } - }; - - const formatTooltipValue = (value: number, name: string) => [ - `${value.toFixed(2)} t`, - name - ]; - return ( -
- - - - - - `Data: ${label}`} - /> - handleProcessoClick(e.value)} - /> - - {processos.map((processo) => ( - + {/* Barra Superior de Controles */} +
+
+ Métrica: +
+ + +
+
+ + {selectedProcesso && ( + + )} +
+ + {/* Legenda Customizada em Ordem Linear Sequencial */} +
+ {processosOrdenados.map((processo, idx) => { + const color = getCorProcesso(processo.nome, idx); + const isSelected = selectedProcesso === processo.nome; + const isDimmed = selectedProcesso && !isSelected; + + return ( + + ); + })} +
+ + {/* Container de Altura Fixa com minWidth=0 para EVITAR Avisos no Console */} +
+ + + + - ))} - - + modoExibicao === 'kg' && val >= 1000 ? `${(val / 1000).toFixed(1)}k` : `${val}`} + /> + } /> + + {processosOrdenados.map((processo, idx) => { + const color = getCorProcesso(processo.nome, idx); + const isSelected = selectedProcesso === processo.nome; + const isDimmed = selectedProcesso && !isSelected; + + return ( + + ); + })} + + +
); }; diff --git a/src/components/dashboard-producao/GraficoProgressoIndividual.tsx b/src/components/dashboard-producao/GraficoProgressoIndividual.tsx index 983c4f8..3242b12 100644 --- a/src/components/dashboard-producao/GraficoProgressoIndividual.tsx +++ b/src/components/dashboard-producao/GraficoProgressoIndividual.tsx @@ -1,4 +1,3 @@ - import React from 'react'; import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; import { DashboardProcesso } from '@/hooks/useDashboardProducaoOtimizado'; @@ -11,11 +10,16 @@ interface GraficoProgressoIndividualProps { export const GraficoProgressoIndividual: React.FC = ({ processos }) => { const formatTooltipValue = (value: number, name: string) => [ - `${(value / 1000).toFixed(2)} t`, + `${Math.round(value).toLocaleString('pt-BR')} kg`, name === 'planejado' ? 'Planejado' : 'Realizado' ]; - const formatAxisValue = (value: number) => `${(value / 1000).toFixed(1)}t`; + const formatAxisValue = (value: number) => { + if (value >= 1000) { + return `${(value / 1000).toFixed(1)}k kg`; + } + return `${Math.round(value)} kg`; + }; const formatDateLabel = (tickItem: string) => { try { @@ -55,13 +59,13 @@ export const GraficoProgressoIndividual: React.FC Progresso - {processo.nome} -
- {processo.progressoReal.toFixed(1)}% realizado +
+ {processo.progressoReal.toFixed(1)}% realizado ({Math.round(processo.pesoFabricado)} kg)
-
- +
+ { console.log(`PROGRESSO GERAL: ${pesoTotalFabricado} kg / ${pesoTotalPlanejado} kg = ${progressoGeral.toFixed(2)}%`); - // 7. Buscar range de datas - const { data: dateRangeData, error: dateRangeError } = await supabase - .rpc('get_dashboard_date_range', { of_number_param: ofNumber }); - - if (dateRangeError) { - console.error('Erro ao buscar range de datas:', dateRangeError); + // 7. Buscar range de datas (opcional, ignora erro se a RPC não existir no Supabase) + let dateRangeData = null; + try { + const { data, error } = await supabase + .rpc('get_dashboard_date_range', { of_number_param: ofNumber }); + if (!error) dateRangeData = data; + } catch (e) { + // Ignora caso RPC não exista } - // 8. Buscar dados consolidados - const { data: consolidatedData, error: consolidatedError } = await supabase - .rpc('get_dashboard_consolidated_data', { of_number_param: ofNumber }); - - if (consolidatedError) { - console.error('Erro ao buscar dados consolidados:', consolidatedError); + // 8. Buscar dados consolidados (opcional, ignora erro se a RPC não existir no Supabase) + let consolidatedData = null; + try { + const { data, error } = await supabase + .rpc('get_dashboard_consolidated_data', { of_number_param: ofNumber }); + if (!error) consolidatedData = data; + } catch (e) { + // Ignora caso RPC não exista } // 9. Buscar processos