import { useState, useEffect, useCallback } from 'react'; import { supabase } from '@/integrations/supabase/client'; import { toast } from 'sonner'; export interface ProcessoConsolidado { processo_id: string; processo_nome: string; processo_cor: string; data_apontamento: string; peso_acumulado: number; } export interface DateRange { data_inicio_grafico: string; data_fim_grafico: string; } export interface DashboardProcesso { id: string; nome: string; pesoTotal: number; pesoFabricado: number; progressoReal: number; progressoEsperado: number; status: 'verde' | 'amarelo' | 'vermelho' | 'azul'; dadosGrafico: { data: string; planejado: number; realizado: number; }[]; } export interface DashboardDataOtimizado { of: string; progressoGeral: number; pesoTotalFabricado: number; tonelagem: number; processos: DashboardProcesso[]; dadosConsolidados: ProcessoConsolidado[]; dateRange: DateRange; } export const useDashboardProducaoOtimizado = (ofNumber: string) => { const [dashboardData, setDashboardData] = useState(null); const [loading, setLoading] = useState(false); const fetchDashboardData = useCallback(async () => { if (!ofNumber) { setDashboardData(null); return; } setLoading(true); try { console.log('Buscando dados do dashboard para OF:', ofNumber); // 1. Buscar dados da OF para obter o peso total planejado const { data: ofData, error: ofError } = await supabase .from('ordens_fabricacao') .select('*') .eq('num_of', ofNumber) .single(); if (ofError) { console.error('Erro ao buscar OF:', ofError); throw ofError; } console.log('Dados da OF encontrados:', ofData); // 2. Buscar ficha técnica para obter peso total se não estiver na OF let pesoTotalPlanejado = ofData.peso_total || 0; if (!pesoTotalPlanejado) { const { data: fichaTecnica, error: fichaError } = await supabase .from('ficha_tecnica_contratos') .select('quantidade') .eq('of_number', ofNumber) .maybeSingle(); if (!fichaError && fichaTecnica?.quantidade) { pesoTotalPlanejado = fichaTecnica.quantidade; } } // 3. Buscar peças cadastradas para calcular peso total e peso soldável (peças com componentes) const { data: pecasData, error: pecasError } = await supabase .from('pecas') .select('id, peso_unitario, quantidade, tem_componentes') .eq('of_number', ofNumber); if (pecasError) { console.error('Erro ao buscar peças:', pecasError); throw pecasError; } const pesoBrutoPecas = (pecasData || []).reduce((total, peca) => { const pesoUnitario = peca.peso_unitario || 0; const quantidade = peca.quantidade || 0; return total + (pesoUnitario * quantidade); }, 0); // Peso soldável: apenas peças que possuem componentes (peças soltas não vão para solda) let pesoTotalSoldavel = (pecasData || []).filter(p => p.tem_componentes === true).reduce((total, peca) => { const pesoUnitario = peca.peso_unitario || 0; const quantidade = peca.quantidade || 0; return total + (pesoUnitario * quantidade); }, 0); if (!pesoTotalPlanejado) { pesoTotalPlanejado = pesoBrutoPecas; } // CORREÇÃO AUTOMÁTICA: Evita que erros de digitação (ex: digitar 1.856 ao invés de 1856 kg na OF) // causem porcentagens irreais (ex: 100000%). Compara com a soma real das peças. if (pesoTotalPlanejado > 0 && pesoBrutoPecas > 0) { if (pesoBrutoPecas > pesoTotalPlanejado * 50) { console.log(`Corrigindo erro de digitação de peso da OF. Planejado original: ${pesoTotalPlanejado} kg, Soma das peças: ${pesoBrutoPecas} kg`); if (pesoBrutoPecas <= pesoTotalPlanejado * 3000 && pesoBrutoPecas >= pesoTotalPlanejado * 500) { pesoTotalPlanejado = pesoTotalPlanejado * 1000; } else { pesoTotalPlanejado = pesoBrutoPecas; } } } // Se todas as peças têm componentes ou nenhuma tem flag marcada, fallback para o peso total if (pesoTotalSoldavel <= 0 && pesoTotalPlanejado > 0) { pesoTotalSoldavel = pesoTotalPlanejado; } console.log('Peso total planejado (BASE DE TODOS OS CÁLCULOS):', pesoTotalPlanejado, 'kg'); console.log('Peso total soldável (apenas peças com componentes):', pesoTotalSoldavel, 'kg'); // 4. Buscar apontamentos da OF com joins corretos const { data: apontamentosData, error: apontamentosError } = await supabase .from('apontamentos_producao') .select(` *, peca:pecas!apontamentos_producao_peca_id_fkey(peso_unitario, marca), processo:processos_fabricacao!apontamentos_producao_processo_id_fkey(nome, ordem, cor), componente:componentes_peca!apontamentos_producao_componente_id_fkey(peso_unitario, marca_componente) `) .eq('of_number', ofNumber); if (apontamentosError) { console.error('Erro ao buscar apontamentos:', apontamentosError); throw apontamentosError; } console.log('Apontamentos encontrados:', apontamentosData); // 5. Calcular peso total fabricado: // - Para peças com componentes (tem_componentes = true): são finalizadas na SOLDA // - Para peças soltas (tem_componentes = false): são finalizadas no CORTE (não passam por solda) let pesoTotalFabricado = 0; const processoSolda = await supabase .from('processos_fabricacao') .select('id') .ilike('nome', '%solda%') .single(); const processoCorte = await supabase .from('processos_fabricacao') .select('id') .ilike('nome', '%corte%') .single(); const processoSoldaId = processoSolda.data?.id; const processoCorteId = processoCorte.data?.id; const pecasInfoMap = new Map((pecasData || []).map(p => [p.id, p])); // 1. Somar peças com componentes apontadas na Solda (apenas tipo peca) if (processoSoldaId) { const apontamentosSolda = apontamentosData.filter(a => a.processo_id === processoSoldaId && a.tipo_apontamento === 'peca'); apontamentosSolda.forEach(apontamento => { const pesoUnitario = Number(apontamento.peca?.peso_unitario || 0); const pesoApontamento = pesoUnitario * Number(apontamento.quantidade_produzida || 0); pesoTotalFabricado += pesoApontamento; }); } // 2. Somar peças SOLTAS (sem componentes) apontadas no Corte if (processoCorteId) { const apontamentosCorte = apontamentosData.filter(a => a.processo_id === processoCorteId); apontamentosCorte.forEach(apontamento => { if (apontamento.tipo_apontamento === 'peca' && apontamento.peca_id) { const peca = pecasInfoMap.get(apontamento.peca_id); const temComponentes = peca?.tem_componentes === true; if (!temComponentes && apontamento.peca?.peso_unitario) { const pesoUnitario = Number(apontamento.peca.peso_unitario); const pesoApontamento = pesoUnitario * Number(apontamento.quantidade_produzida || 0); pesoTotalFabricado += pesoApontamento; } } }); } // Evita que ultrapasse o planejado por arredondamento if (pesoTotalPlanejado > 0) { pesoTotalFabricado = Math.min(pesoTotalFabricado, pesoTotalPlanejado); } console.log('Peso total fabricado (Solda p/ compostas + Corte p/ soltas):', pesoTotalFabricado, 'kg'); // 6. Calcular progresso geral - será recalculado após processar todos os processos let progressoGeral = 0; console.log(`PROGRESSO GERAL: ${pesoTotalFabricado} kg / ${pesoTotalPlanejado} kg = ${progressoGeral.toFixed(2)}%`); // 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 (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 const { data: processosData, error: processosError } = await supabase .from('processos_fabricacao') .select('*') .eq('ativo', true) .order('ordem'); if (processosError) { console.error('Erro ao buscar processos:', processosError); throw processosError; } // 10. Buscar cronograma da OF para calcular progresso esperado correto const { data: cronogramaRaw } = await supabase .from('cronogramas_of') .select(` id, defasagem_solda, processos_cronograma:processos_cronograma( nome_processo, data_inicio, data_fim ) `) .eq('of_id', ofData.id) .maybeSingle(); const cronogramaOf = cronogramaRaw as any; console.log('Cronograma da OF encontrado:', cronogramaOf); // Função auxiliar para calcular progresso esperado baseado no cronograma const calcularProgressoEsperado = (nomeProcesso: string): number => { if (!cronogramaOf?.processos_cronograma || cronogramaOf.processos_cronograma.length === 0) { console.log(`Cronograma não encontrado para processo ${nomeProcesso}`); return 0; } const hoje = new Date(); let processoCronograma; // Mapear nomes dos processos para os do cronograma if (nomeProcesso.toLowerCase().includes('corte') || nomeProcesso.toLowerCase().includes('solda')) { processoCronograma = cronogramaOf.processos_cronograma.find((p: any) => p.nome_processo.toLowerCase().includes('fabricação') || p.nome_processo.toLowerCase().includes('fabricacao') ); } else if (nomeProcesso.toLowerCase().includes('pint') || nomeProcesso.toLowerCase().includes('galv') || nomeProcesso.toLowerCase().includes('expedição') || nomeProcesso.toLowerCase().includes('expedicao') || nomeProcesso.toLowerCase().includes('montagem')) { // Usar datas do processo "Instalação" (incluindo "montagem") processoCronograma = cronogramaOf.processos_cronograma.find((p: any) => p.nome_processo.toLowerCase().includes('instalação') || p.nome_processo.toLowerCase().includes('instalacao') ); } else { // Buscar processo com nome exato ou similar processoCronograma = cronogramaOf.processos_cronograma.find((p: any) => p.nome_processo.toLowerCase().includes(nomeProcesso.toLowerCase()) || nomeProcesso.toLowerCase().includes(p.nome_processo.toLowerCase()) ); } if (!processoCronograma) { console.log(`Processo ${nomeProcesso} não encontrado no cronograma`); return 0; } const dataInicio = new Date(processoCronograma.data_inicio + 'T00:00:00'); const dataFim = new Date(processoCronograma.data_fim + 'T23:59:59'); console.log(`Processo ${nomeProcesso} - Inicio: ${dataInicio.toDateString()}, Fim: ${dataFim.toDateString()}, Hoje: ${hoje.toDateString()}`); // Se ainda não chegou na data de início, progresso esperado é 0% if (hoje < dataInicio) { console.log(`Data atual (${hoje.toDateString()}) é menor que data de início (${dataInicio.toDateString()}) - Progresso esperado: 0%`); return 0; } const MS_PER_DAY = 1000 * 60 * 60 * 24; const diasTotais = Math.max(1, Math.floor((dataFim.getTime() - dataInicio.getTime()) / MS_PER_DAY) + 1); const diasDecorridos = Math.floor((hoje.getTime() - dataInicio.getTime()) / MS_PER_DAY) + 1; // Calcular percentual baseado nos dias let percentualEsperado = (diasDecorridos / diasTotais) * 100; // Se passou da data fim, manter em 100% if (hoje > dataFim) { percentualEsperado = 100; console.log(`Data atual passou da data fim - Progresso esperado mantido em: 100%`); } else { console.log(`Processo ${nomeProcesso}: ${diasDecorridos}/${diasTotais} dias = ${percentualEsperado.toFixed(2)}%`); } // Se for o processo de Solda, aplicar o fator de defasagem configurado em relação ao Corte if (nomeProcesso.toLowerCase().includes('solda')) { const defasagem = cronogramaOf?.defasagem_solda !== undefined && cronogramaOf?.defasagem_solda !== null ? Number(cronogramaOf.defasagem_solda) : 10; if (percentualEsperado <= defasagem) { console.log(`Processo Solda: Fabricação esperada (${percentualEsperado.toFixed(2)}%) <= defasagem (${defasagem}%) -> Progresso esperado Solda: 0%`); return 0; } if (hoje > dataFim) { return 100; } // Escalonamento suave proporcional: de defasagem% até 100% const progressoSolda = ((percentualEsperado - defasagem) / (100 - defasagem)) * 100; console.log(`Processo Solda: Fabricação (${percentualEsperado.toFixed(2)}%) com defasagem (${defasagem}%) -> Solda esperada: ${progressoSolda.toFixed(2)}%`); return Math.min(100, Math.max(0, progressoSolda)); } return Math.min(100, Math.max(0, percentualEsperado)); }; // 11. Processar dados por processo individual const processos: DashboardProcesso[] = await Promise.all(processosData.map(async (processo) => { const apontamentosProcesso = apontamentosData.filter(a => a.processo_id === processo.id); let pesoFabricadoProcesso = 0; apontamentosProcesso.forEach(a => { // Apenas apontamentos de peça mestre somam peso no processo para evitar dupla contagem com subcomponentes if (a.tipo_apontamento === 'peca' && a.peca?.peso_unitario) { pesoFabricadoProcesso += Number(a.peca.peso_unitario) * Number(a.quantidade_produzida); } }); const isProcessoSolda = processo.nome.toLowerCase().includes('solda'); const pesoBaseProcesso = isProcessoSolda && pesoTotalSoldavel > 0 ? pesoTotalSoldavel : pesoTotalPlanejado; const progressoReal = pesoBaseProcesso > 0 ? (pesoFabricadoProcesso / pesoBaseProcesso) * 100 : 0; console.log(`PROCESSO ${processo.nome}: ${pesoFabricadoProcesso} kg fabricado / ${pesoBaseProcesso} kg planejado (base ${isProcessoSolda ? 'soldável' : 'total'}) = ${progressoReal.toFixed(2)}%`); // Calcular progresso esperado usando as datas do cronograma const progressoEsperado = calcularProgressoEsperado(processo.nome); // Determinar status baseado na comparação entre progresso real e esperado let status: 'verde' | 'amarelo' | 'vermelho' | 'azul'; // Tolerância de conclusão: 99.5% ou mais é considerado 100% concluído (evita erros por dízimas em somas de peso) const isConcluido = progressoReal >= 99.5; if (isConcluido) { // Se já concluiu 100%: // Se concluiu antes do prazo final (progressoEsperado < 100), fica azul (Adiantado), senão verde (Concluído) if (progressoEsperado < 100) { status = 'azul'; // Adiantado } else { status = 'verde'; // Concluído no prazo } } else if (progressoEsperado >= 100) { // Passou da data fim e ainda não concluiu: Atrasado status = 'vermelho'; } else if (progressoEsperado > 0) { // Comparar progresso real com esperado if (progressoReal > progressoEsperado) { status = 'azul'; // Adiantado } else { const percentualDoEsperado = (progressoReal / progressoEsperado) * 100; if (percentualDoEsperado < 60) { status = 'vermelho'; // Muito atrasado } else if (percentualDoEsperado < 80) { status = 'amarelo'; // Atenção } else { status = 'verde'; // No prazo } } } else { // Se não há progresso esperado ainda (antes da data de início), usar verde status = 'verde'; } console.log(`STATUS ${processo.nome}: ${progressoReal.toFixed(2)}% real vs ${progressoEsperado.toFixed(2)}% esperado -> ${status}`); // Gerar dados do gráfico usando as datas específicas do cronograma do processo const dadosGrafico = []; // Buscar as datas específicas deste processo no cronograma let dataInicioProcesso: Date | null = null; let dataFimProcesso: Date | null = null; if (cronogramaOf?.processos_cronograma) { let processoCronograma; // Mapear nomes dos processos para os do cronograma if (processo.nome.toLowerCase().includes('corte') || processo.nome.toLowerCase().includes('solda')) { processoCronograma = cronogramaOf.processos_cronograma.find((p: any) => p.nome_processo.toLowerCase().includes('fabricação') || p.nome_processo.toLowerCase().includes('fabricacao') ); } else if (processo.nome.toLowerCase().includes('pint') || processo.nome.toLowerCase().includes('galv') || processo.nome.toLowerCase().includes('expedição') || processo.nome.toLowerCase().includes('expedicao') || processo.nome.toLowerCase().includes('montagem')) { processoCronograma = cronogramaOf.processos_cronograma.find((p: any) => p.nome_processo.toLowerCase().includes('instalação') || p.nome_processo.toLowerCase().includes('instalacao') ); } else { processoCronograma = cronogramaOf.processos_cronograma.find((p: any) => p.nome_processo.toLowerCase().includes(processo.nome.toLowerCase()) || processo.nome.toLowerCase().includes(p.nome_processo.toLowerCase()) ); } if (processoCronograma) { dataInicioProcesso = new Date(processoCronograma.data_inicio + 'T00:00:00'); dataFimProcesso = new Date(processoCronograma.data_fim + 'T23:59:59'); } } // Se não há cronograma específico, usar as datas da OF como fallback if (!dataInicioProcesso || !dataFimProcesso) { dataInicioProcesso = ofData.data_abertura ? new Date(ofData.data_abertura + 'T00:00:00') : new Date(); dataFimProcesso = ofData.data_prazo ? new Date(ofData.data_prazo + 'T23:59:59') : new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); } // Verificar se há apontamentos fora do período planejado e ajustar o range se necessário let dataInicioGrafico = dataInicioProcesso; let dataFimGrafico = dataFimProcesso; if (apontamentosProcesso.length > 0) { const datasMaisAntigas = apontamentosProcesso.map(a => new Date(a.data_apontamento + 'T00:00:00')); const datasMaisRecentes = apontamentosProcesso.map(a => new Date(a.data_apontamento + 'T23:59:59')); const menorDataApontamento = new Date(Math.min(...datasMaisAntigas.map(d => d.getTime()))); const maiorDataApontamento = new Date(Math.max(...datasMaisRecentes.map(d => d.getTime()))); // Estender o período se houver apontamentos fora do planejado if (menorDataApontamento < dataInicioGrafico) { dataInicioGrafico = menorDataApontamento; } if (maiorDataApontamento > dataFimGrafico) { dataFimGrafico = maiorDataApontamento; } } const MS_PER_DAY = 1000 * 60 * 60 * 24; const diasTotaisProcesso = Math.max(1, Math.floor((dataFimProcesso.getTime() - dataInicioProcesso.getTime()) / MS_PER_DAY) + 1); const diasRange = Math.floor((dataFimGrafico.getTime() - dataInicioGrafico.getTime()) / MS_PER_DAY); for (let i = 0; i <= diasRange; i++) { const data = new Date(dataInicioGrafico); data.setDate(data.getDate() + i); const dataStr = data.toISOString().split('T')[0]; // Calcular progresso planejado baseado no cronograma específico do processo (percentual dos dias) let planejado = 0; if (data >= dataInicioProcesso && data <= dataFimProcesso) { const diasDecorridos = Math.floor((Math.min(data.getTime(), dataFimProcesso.getTime()) - dataInicioProcesso.getTime()) / MS_PER_DAY) + 1; const percentualDias = diasDecorridos / diasTotaisProcesso; planejado = percentualDias * pesoBaseProcesso; } else if (data > dataFimProcesso) { // Após o fim planejado, manter o peso base do processo planejado = pesoBaseProcesso; } // Antes do início planejado, o planejado fica 0 // Calcular progresso real até esta data - soma dos pesos apontados const apontamentosAteData = apontamentosProcesso.filter(a => { if (!a.data_apontamento) return false; const dataAptStr = typeof a.data_apontamento === 'string' ? a.data_apontamento.substring(0, 10) : ''; return dataAptStr <= dataStr; }); let realizado = 0; apontamentosAteData.forEach(a => { // Apenas peças mestres computam peso realizado no gráfico if (a.tipo_apontamento === 'peca' && a.peca?.peso_unitario) { realizado += Number(a.peca.peso_unitario) * Number(a.quantidade_produzida); } }); dadosGrafico.push({ data: dataStr, planejado, realizado }); } return { id: processo.id, nome: processo.nome, pesoTotal: pesoBaseProcesso, // Peso alvo do processo (soldável para solda, total para os demais) pesoFabricado: pesoFabricadoProcesso, progressoReal, progressoEsperado, status, dadosGrafico }; })); // Calcular progresso geral como média dos progressos reais, excluindo "Aceite/DB" e "Concluído" const processosParaMedia = processos.filter(p => !p.nome.toLowerCase().includes('aceite') && !p.nome.toLowerCase().includes('db') && !p.nome.toLowerCase().includes('concluído') && !p.nome.toLowerCase().includes('concluido') ); if (processosParaMedia.length > 0) { const somaProgressos = processosParaMedia.reduce((soma, processo) => soma + processo.progressoReal, 0); progressoGeral = somaProgressos / processosParaMedia.length; } else { progressoGeral = 0; } // Atualizar progresso real e esperado do processo "Concluído" com o progresso geral processos.forEach(processo => { if (processo.nome.toLowerCase().includes('concluído') || processo.nome.toLowerCase().includes('concluido')) { processo.progressoReal = progressoGeral; // Calcular progresso esperado do processo "Concluído" como média dos outros processos // (exceto Aceite/DB e o próprio Concluído) const processosParaMediaEsperada = processos.filter(p => !p.nome.toLowerCase().includes('aceite') && !p.nome.toLowerCase().includes('db') && !p.nome.toLowerCase().includes('concluído') && !p.nome.toLowerCase().includes('concluido') ); if (processosParaMediaEsperada.length > 0) { const somaProgressosEsperados = processosParaMediaEsperada.reduce((soma, proc) => soma + proc.progressoEsperado, 0); processo.progressoEsperado = somaProgressosEsperados / processosParaMediaEsperada.length; } else { processo.progressoEsperado = 0; } // Recalcular status do processo "Concluído" if (processo.progressoReal >= 99.5) { processo.status = 'verde'; // Concluído } else if (processo.progressoEsperado >= 100) { processo.status = 'vermelho'; // Atrasado (prazo expirou e ainda não finalizou) } else if (processo.progressoEsperado > 0) { if (processo.progressoReal > processo.progressoEsperado) { processo.status = 'azul'; // Adiantado } else { const percentualDoEsperado = (processo.progressoReal / processo.progressoEsperado) * 100; if (percentualDoEsperado < 60) { processo.status = 'vermelho'; // Atrasado } else if (percentualDoEsperado < 80) { processo.status = 'amarelo'; // Atenção } else { processo.status = 'verde'; // No prazo } } } console.log(`PROCESSO CONCLUÍDO atualizado: progresso real = ${progressoGeral.toFixed(2)}%, esperado = ${processo.progressoEsperado.toFixed(2)}%, status = ${processo.status}`); } }); console.log(`PROGRESSO GERAL CORRIGIDO: Média de ${processosParaMedia.length} processos (excluindo Aceite/DB e Concluído) = ${progressoGeral.toFixed(2)}%`); console.log('Processos incluídos na média:', processosParaMedia.map(p => `${p.nome}: ${p.progressoReal.toFixed(1)}%`).join(', ')); const result = { of: ofNumber, progressoGeral, pesoTotalFabricado, tonelagem: pesoTotalPlanejado, // SEMPRE o peso total da OF/ficha técnica processos, dadosConsolidados: consolidatedData || [], dateRange: dateRangeData?.[0] || { data_inicio_grafico: '', data_fim_grafico: '' } }; console.log('=== RESULTADO FINAL DO DASHBOARD ==='); console.log('OF:', ofNumber); console.log('Peso total planejado (base):', pesoTotalPlanejado, 'kg =', (pesoTotalPlanejado / 1000).toFixed(2), 't'); console.log('Peso total fabricado:', pesoTotalFabricado, 'kg =', (pesoTotalFabricado / 1000).toFixed(2), 't'); console.log('Progresso geral:', progressoGeral.toFixed(2) + '%'); console.log('====================================='); setDashboardData(result); } catch (error) { console.error('Erro ao buscar dados do dashboard:', error); toast.error('Erro ao carregar dados do dashboard'); } finally { setLoading(false); } }, [ofNumber]); useEffect(() => { fetchDashboardData(); }, [fetchDashboardData]); return { dashboardData, loading, refetch: fetchDashboardData }; };