From ea447df298ecc10747208e4382b4c22dc01962a8 Mon Sep 17 00:00:00 2001 From: admbrainsteel Date: Mon, 26 Oct 2026 11:59:05 -0300 Subject: [PATCH] fix(producao): liberacao automatica de pecas na solda por kits completos no corte --- .../apontamento/ApontamentoForm.tsx | 171 +++++---- src/hooks/useApontamentosProducao.tsx | 121 +++---- src/hooks/useComponentesAgrupados.tsx | 41 ++- .../components/SmartComponentesCorteModal.tsx | 39 +- .../smart/components/SmartProducaoFlow.tsx | 20 +- src/utils/sincronizacaoKitsCorte.ts | 334 ++++++++++++++++++ 6 files changed, 580 insertions(+), 146 deletions(-) create mode 100644 src/utils/sincronizacaoKitsCorte.ts diff --git a/src/components/apontamento/ApontamentoForm.tsx b/src/components/apontamento/ApontamentoForm.tsx index c683dec..4698eb2 100644 --- a/src/components/apontamento/ApontamentoForm.tsx +++ b/src/components/apontamento/ApontamentoForm.tsx @@ -49,10 +49,10 @@ export const ApontamentoForm = () => { const [saving, setSaving] = useState(false); const [cacheValido, setCacheValido] = useState(false); - const { criarApontamento, refetch, processos, apontamentos, loading: loadingApontamentos } = useApontamentosProducao(); + const { criarApontamento, refetch, processos, apontamentos, loading: loadingApontamentos, sincronizarKitsOF } = useApontamentosProducao(); const { pecas, loading: loadingPecas } = usePecas(); const { ofs } = useOFs(); - const { componentesAgrupados, loading: loadingComponentes } = useComponentesAgrupados(formData.of_number, formData.fase, pecas); + const { componentesAgrupados, componentesRaw, loading: loadingComponentes } = useComponentesAgrupados(formData.of_number, formData.fase, pecas); // Buscar fases únicas da OF selecionada const fasesDisponiveis = useMemo(() => { @@ -166,17 +166,25 @@ export const ApontamentoForm = () => { .filter(a => a.tipo_apontamento === 'peca' && a.peca_id === peca.id && a.processo_id === processoAnteriorValido.id) .reduce((sum, a) => sum + (Number(a.quantidade_produzida) || 0), 0); - // Fallback de retrocompatibilidade para peças com componentes cortados antes da automação - if (temComponentesCadastrados && qtdApontadaAnterior === 0 && processoAnteriorValido.nome.toLowerCase().includes('corte')) { - if (compsDaPeca.length > 0) { - const todosCortados = compsDaPeca.every(c => { - const aptsComp = apontamentos - .filter(a => a.tipo_apontamento === 'componente' && c.componente_ids.includes(a.componente_id || '') && a.processo_id === processoAnteriorValido.id) + // Se a peça tem componentes e o processo anterior for Corte: + // Calcula estritamente os kits completos disponíveis baseados no corte de todos os componentes (regra do gargalo) + if (temComponentesCadastrados && processoAnteriorValido.nome.toLowerCase().includes('corte')) { + const compsDestaPeca = (componentesRaw || []).filter(c => c.peca_id === peca.id); + if (compsDestaPeca.length > 0) { + let menorKits = Number(peca.quantidade) || 0; + compsDestaPeca.forEach(comp => { + const cortado = apontamentos + .filter(a => a.tipo_apontamento === 'componente' && a.componente_id === comp.id && a.processo_id === processoAnteriorValido.id) .reduce((sum, a) => sum + (Number(a.quantidade_produzida) || 0), 0); - return aptsComp >= c.quantidade_total; + const porPeca = Math.max(1, Number(comp.quantidade_por_peca) || 1); + const kitsDeste = Math.floor(cortado / porPeca); + if (kitsDeste < menorKits) { + menorKits = kitsDeste; + } }); - if (todosCortados) { - qtdApontadaAnterior = Number(peca.quantidade) || 0; + const kitsCompletosCalculados = Math.max(0, Math.min(Number(peca.quantidade) || 0, menorKits)); + if (kitsCompletosCalculados > qtdApontadaAnterior) { + qtdApontadaAnterior = kitsCompletosCalculados; } } } @@ -285,23 +293,12 @@ export const ApontamentoForm = () => { try { for (const item of items) { try { - const apontamentoData: Parameters[0] = { - of_number: formData.of_number, - tipo_apontamento: item.tipo, - processo_id: formData.processo_id, - quantidade_produzida: item.quantidade_disponivel, - data_apontamento: formData.data_apontamento, - observacoes: `Registro em lote - ${tipoTexto}` - }; + const result = await registrarItemComDistribuicao( + item, + item.quantidade_disponivel, + `Registro em lote - ${tipoTexto}` + ); - if (item.tipo === 'componente') { - apontamentoData.componente_id = item.id; - } else { - apontamentoData.peca_id = item.id; - } - - const result = await criarApontamento(apontamentoData); - if (result.success) { sucessos++; } else { @@ -363,6 +360,13 @@ export const ApontamentoForm = () => { setFormData(prev => ({ ...prev, ...updates })); updateCache({ processo_id: processoId }); setItemSelecionado(null); + + // Ao selecionar Solda, acionar sincronização de kits da OF em segundo plano + const proc = processos.find(p => p.id === processoId); + const procNome = (proc?.nome || '').toLowerCase(); + if ((procNome.includes('solda') || procNome.includes('montag')) && formData.of_number) { + sincronizarKitsOF(formData.of_number); + } }; const handleItemSelect = (item: ItemDisponivel) => { @@ -389,6 +393,81 @@ export const ApontamentoForm = () => { })); }; + // Função auxiliar para registrar apontamento distribuindo componentes entre peças irmãs se necessário + const registrarItemComDistribuicao = async ( + item: ItemDisponivel, + quantidade: number, + observacoes?: string + ): Promise<{ success: boolean }> => { + if (item.tipo === 'componente') { + const grupo = componentesAgrupados.find(g => g.marca_componente === item.marca); + if (grupo && grupo.vinculos && grupo.vinculos.length > 1) { + let restante = quantidade; + let sucessos = 0; + + for (const v of grupo.vinculos) { + if (restante <= 0) break; + + const jaCortado = apontamentos + .filter(a => a.tipo_apontamento === 'componente' && a.componente_id === v.componente_id && a.processo_id === formData.processo_id) + .reduce((sum, a) => sum + (Number(a.quantidade_produzida) || 0), 0); + + const faltaParaPeca = Math.max(0, v.quantidade_necessaria - jaCortado); + if (faltaParaPeca <= 0) continue; + + const qtdParaV = Math.min(restante, faltaParaPeca); + restante -= qtdParaV; + + const res = await criarApontamento({ + of_number: formData.of_number, + tipo_apontamento: 'componente', + processo_id: formData.processo_id, + quantidade_produzida: qtdParaV, + data_apontamento: formData.data_apontamento, + componente_id: v.componente_id, + observacoes: observacoes || undefined + }); + + if (res.success) sucessos++; + } + + // Se sobrou saldo além das necessidades das peças vinculadas, aplica ao primeiro vínculo + if (restante > 0 && grupo.vinculos.length > 0) { + const res = await criarApontamento({ + of_number: formData.of_number, + tipo_apontamento: 'componente', + processo_id: formData.processo_id, + quantidade_produzida: restante, + data_apontamento: formData.data_apontamento, + componente_id: grupo.vinculos[0].componente_id, + observacoes: observacoes || undefined + }); + if (res.success) sucessos++; + } + + return { success: sucessos > 0 }; + } + } + + // Caso padrão (peça ou componente monovinculado) + const apontamentoData: Parameters[0] = { + of_number: formData.of_number, + tipo_apontamento: item.tipo, + processo_id: formData.processo_id, + quantidade_produzida: quantidade, + data_apontamento: formData.data_apontamento, + observacoes: observacoes || undefined + }; + + if (item.tipo === 'componente') { + apontamentoData.componente_id = item.id; + } else { + apontamentoData.peca_id = item.id; + } + + return await criarApontamento(apontamentoData); + }; + const handleApontarItemDireto = async (item: ItemDisponivel, quantidade: number): Promise => { if (!formData.processo_id) { toast.error('Selecione um processo antes de apontar'); @@ -402,22 +481,7 @@ export const ApontamentoForm = () => { setSaving(true); try { - const apontamentoData: Parameters[0] = { - of_number: formData.of_number, - tipo_apontamento: item.tipo, - processo_id: formData.processo_id, - quantidade_produzida: quantidade, - data_apontamento: formData.data_apontamento, - observacoes: formData.observacoes || undefined - }; - - if (item.tipo === 'componente') { - apontamentoData.componente_id = item.id; - } else { - apontamentoData.peca_id = item.id; - } - - const result = await criarApontamento(apontamentoData); + const result = await registrarItemComDistribuicao(item, quantidade, formData.observacoes || undefined); if (result.success) { toast.success(`${quantidade} un de ${item.marca} apontada(s) com sucesso!`); @@ -465,22 +529,11 @@ export const ApontamentoForm = () => { setSaving(true); try { - const apontamentoData: Parameters[0] = { - of_number: formData.of_number, - tipo_apontamento: itemSelecionado.tipo, - processo_id: formData.processo_id, - quantidade_produzida: quantidade, - data_apontamento: formData.data_apontamento, - observacoes: formData.observacoes || undefined - }; - - if (itemSelecionado.tipo === 'componente') { - apontamentoData.componente_id = itemSelecionado.id; - } else { - apontamentoData.peca_id = itemSelecionado.id; - } - - const result = await criarApontamento(apontamentoData); + const result = await registrarItemComDistribuicao( + itemSelecionado, + quantidade, + formData.observacoes || undefined + ); if (result.success) { toast.success('Apontamento registrado com sucesso!'); diff --git a/src/hooks/useApontamentosProducao.tsx b/src/hooks/useApontamentosProducao.tsx index 36e184e..4aa65b7 100644 --- a/src/hooks/useApontamentosProducao.tsx +++ b/src/hooks/useApontamentosProducao.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react'; import { supabase } from '@/integrations/supabase/client'; import { useAuth } from '@/hooks/useAuth'; import { toast } from 'sonner'; +import { sincronizarPecaKitNoCorte, sincronizarTodosKitsOFCorte } from '@/utils/sincronizacaoKitsCorte'; // Função utilitária para retry de requisições const retryRequest = async ( @@ -378,7 +379,7 @@ interface InsertApontamentoData { throw error; } - // REGRA DO KIT COMPLETO (Passo 3 + Opção 1): + // REGRA DO KIT COMPLETO: // Se apontou componente no processo de Corte, verificar se todos os componentes da peça pai completaram o kit. // Se completou, cria automaticamente o apontamento da Peça Principal no Corte (tipo_apontamento = 'peca'), // computando o peso integral da peça mestre no Corte e liberando-a para a Solda! @@ -390,82 +391,17 @@ interface InsertApontamentoData { : true; if (isProcessoCorte) { - const pecaId = insertData.peca_id; - - // 1. Buscar todos os componentes requeridos da peça pai - const { data: compsPeca } = await supabase - .from('componentes_peca') - .select('id, quantidade_por_peca') - .eq('peca_id', pecaId); + const resSync = await sincronizarPecaKitNoCorte({ + ofNumber: insertData.of_number, + pecaId: insertData.peca_id, + processoCorteId: insertData.processo_id, + userId: user.id, + usuarioNome: user.name || user.username || user.email?.split('@')[0] || 'Operador', + dataApontamento: insertData.data_apontamento + }); - // 2. Buscar a peça pai - const { data: pecaPai } = await supabase - .from('pecas') - .select('id, marca, quantidade, of_number') - .eq('id', pecaId) - .single(); - - if (pecaPai && compsPeca && compsPeca.length > 0) { - // 3. Buscar todos os apontamentos de componentes já feitos para essa peça pai neste processo de corte - const { data: todosAptsComp } = await supabase - .from('apontamentos_producao') - .select('componente_id, quantidade_produzida') - .eq('peca_id', pecaId) - .eq('processo_id', insertData.processo_id) - .eq('tipo_apontamento', 'componente'); - - const cortadoPorComp = new Map(); - (todosAptsComp || []).forEach(a => { - if (a.componente_id) { - cortadoPorComp.set(a.componente_id, (cortadoPorComp.get(a.componente_id) || 0) + Number(a.quantidade_produzida || 0)); - } - }); - - let kitsCompletos = Number(pecaPai.quantidade) || 0; - for (const comp of compsPeca) { - const cortado = cortadoPorComp.get(comp.id) || 0; - const porPeca = Number(comp.quantidade_por_peca) || 1; - const kitsDeste = Math.floor(cortado / porPeca); - if (kitsDeste < kitsCompletos) { - kitsCompletos = kitsDeste; - } - } - - // 4. Verificar quanto já foi apontado como 'peca' mestre no corte - const { data: aptsPecaJaRegistrados } = await supabase - .from('apontamentos_producao') - .select('quantidade_produzida') - .eq('peca_id', pecaId) - .eq('processo_id', insertData.processo_id) - .eq('tipo_apontamento', 'peca'); - - const totalPecaJaApontada = (aptsPecaJaRegistrados || []).reduce( - (sum, a) => sum + Number(a.quantidade_produzida || 0), 0 - ); - - // 5. Se há novos kits completos, registrar apontamento da peça mestre no Corte - const qtdLiberar = kitsCompletos - totalPecaJaApontada; - if (qtdLiberar > 0) { - const { error: errPeca } = await supabase - .from('apontamentos_producao') - .insert({ - of_number: pecaPai.of_number, - peca_id: pecaPai.id, - processo_id: insertData.processo_id, - tipo_apontamento: 'peca', - quantidade_produzida: qtdLiberar, - data_apontamento: insertData.data_apontamento, - observacoes: `Liberação automática de Kit 100% cortado (${kitsCompletos}/${pecaPai.quantidade} un)`, - created_by: user.id - }); - - if (!errPeca) { - console.log(`✅ Kit 100% completo! ${qtdLiberar} un da peça mestre ${pecaPai.marca} registradas no Corte.`); - toast.success(`Kit 100% cortado! ${qtdLiberar} un da peça ${pecaPai.marca} liberada(s) para Solda.`); - } else { - console.warn('Aviso ao registrar liberação automática de peça:', errPeca); - } - } + if (resSync && resSync.delta_liberado > 0) { + toast.success(`Kit 100% cortado! +${resSync.delta_liberado} un da peça ${resSync.marca} liberada(s) para Solda (${resSync.kits_completos}/${resSync.quantidade_total}).`); } } } catch (kitError) { @@ -602,6 +538,38 @@ interface InsertApontamentoData { } }, [user, fetchApontamentos, fetchProcessos]); + const sincronizarKitsOF = useCallback(async (ofNumber: string, processoCorteId?: string) => { + try { + const procCorte = processoCorteId || processos.find(p => { + const n = (p.nome || '').toLowerCase(); + return n.includes('corte') || n.includes('prepar') || p.ordem === 1; + })?.id; + + if (!procCorte) { + console.warn('Processo de corte não encontrado para sincronização de kits.'); + return []; + } + + const resultados = await sincronizarTodosKitsOFCorte({ + ofNumber, + processoCorteId: procCorte, + userId: user?.id, + usuarioNome: user?.name || user?.username || user?.email?.split('@')[0] || 'Operador' + }); + + const totalLiberadas = resultados.reduce((sum, r) => sum + r.delta_liberado, 0); + if (totalLiberadas > 0) { + toast.success(`Sincronização: +${totalLiberadas} kit(s) completos liberados para Solda!`); + await fetchApontamentos(); + } + + return resultados; + } catch (e) { + console.error('Erro ao sincronizar kits da OF:', e); + return []; + } + }, [processos, user, fetchApontamentos]); + return { apontamentos, processos, @@ -611,6 +579,7 @@ interface InsertApontamentoData { atualizarProcesso, exportarModeloCSV, buscarQuantidadeProcessada, + sincronizarKitsOF, refetch }; }; diff --git a/src/hooks/useComponentesAgrupados.tsx b/src/hooks/useComponentesAgrupados.tsx index cdd5f27..b439cab 100644 --- a/src/hooks/useComponentesAgrupados.tsx +++ b/src/hooks/useComponentesAgrupados.tsx @@ -3,6 +3,14 @@ import { useState, useEffect, useMemo } from 'react'; import { useComponentesPeca } from '@/hooks/useComponentesPeca'; import { useDebounce } from '@/hooks/useDebounce'; +export interface ComponenteVinculoDetalhe { + componente_id: string; + peca_id: string; + peca_marca: string; + quantidade_por_peca: number; + quantidade_necessaria: number; +} + export interface ComponenteAgrupado { marca_componente: string; descricao: string; @@ -12,6 +20,7 @@ export interface ComponenteAgrupado { peca_pai_info: string; componente_ids: string[]; peca_ids: string[]; + vinculos: ComponenteVinculoDetalhe[]; } // Cache local para evitar buscas repetitivas @@ -36,7 +45,7 @@ export const useComponentesAgrupados = (ofNumber: string, fase: string, pecasDat peca && peca.of_number === debouncedOfNumber && peca.etapa_fase === debouncedFase && - peca.tem_componentes === true // Apenas peças com componentes + peca.tem_componentes !== false // Peças com componentes (ou não marcadas como S/M) ); }, [pecasData, debouncedOfNumber, debouncedFase]); @@ -101,22 +110,40 @@ export const useComponentesAgrupados = (ofNumber: string, fase: string, pecasDat if (!pecaPai) return; + const porPeca = Math.max(1, Number(comp.quantidade_por_peca) || 1); + const qtdTotalPeca = Number(pecaPai.quantidade) || 0; + const qtdNecessaria = porPeca * qtdTotalPeca; + + const vinculo: ComponenteVinculoDetalhe = { + componente_id: comp.id, + peca_id: comp.peca_id, + peca_marca: pecaPai.marca, + quantidade_por_peca: porPeca, + quantidade_necessaria: qtdNecessaria, + }; + if (grupos.has(marca)) { const grupo = grupos.get(marca)!; - grupo.quantidade_total += comp.quantidade_por_peca * (pecaPai.quantidade || 0); - grupo.peca_pai_info += `, ${pecaPai.marca}`; + grupo.quantidade_total += qtdNecessaria; + if (!grupo.peca_pai_info.includes(pecaPai.marca)) { + grupo.peca_pai_info += `, ${pecaPai.marca}`; + } grupo.componente_ids.push(comp.id); - grupo.peca_ids.push(comp.peca_id); + if (!grupo.peca_ids.includes(comp.peca_id)) { + grupo.peca_ids.push(comp.peca_id); + } + grupo.vinculos.push(vinculo); } else { grupos.set(marca, { marca_componente: marca, descricao: comp.descricao || '', perfil: comp.perfil || '', peso_unitario: comp.peso_unitario, - quantidade_total: comp.quantidade_por_peca * (pecaPai.quantidade || 0), + quantidade_total: qtdNecessaria, peca_pai_info: pecaPai.marca, componente_ids: [comp.id], - peca_ids: [comp.peca_id] + peca_ids: [comp.peca_id], + vinculos: [vinculo], }); } }); @@ -133,6 +160,8 @@ export const useComponentesAgrupados = (ofNumber: string, fase: string, pecasDat return { componentesAgrupados, + componentesRaw: componentes || [], loading: loading || componentesLoading }; }; + diff --git a/src/pages/smart/components/SmartComponentesCorteModal.tsx b/src/pages/smart/components/SmartComponentesCorteModal.tsx index fe4ed14..dfbc6ab 100644 --- a/src/pages/smart/components/SmartComponentesCorteModal.tsx +++ b/src/pages/smart/components/SmartComponentesCorteModal.tsx @@ -24,6 +24,7 @@ import { useAuth } from '@/hooks/useAuth'; import { smartAudio } from '@/utils/smartAudio'; import { toast } from 'sonner'; import { PecaKitInfo, ComponenteKitInfo } from '@/hooks/useKitsProducao'; +import { sincronizarPecaKitNoCorte } from '@/utils/sincronizacaoKitsCorte'; interface SmartComponentesCorteModalProps { open: boolean; @@ -122,9 +123,23 @@ export const SmartComponentesCorteModal: React.FC 0 + ? ` (+${resSync.delta_liberado} peça(s) liberada(s) para Solda)` + : ''; toast.success( - `✅ ${multiplicadorKits} Kit Completo da peça ${pecaKit.marca} cortado com sucesso! (+${pesoTotalKg.toFixed(1)} kg)` + `✅ ${multiplicadorKits} Kit Completo da peça ${pecaKit.marca} cortado com sucesso! (+${pesoTotalKg.toFixed(1)} kg)${infoLiberado}` ); onApontamentoSalvo(); @@ -165,11 +180,27 @@ export const SmartComponentesCorteModal: React.FC 0) { + toast.success( + `✂️ Apontado no Corte: ${qtd}x ${comp.marca_componente} (+${pesoKg.toFixed(1)} kg) • ⚡ Kit completo formado: +${resSync.delta_liberado} un de ${pecaKit.marca} liberada(s) para Solda!` + ); + } else { + toast.success( + `✂️ Apontado no Corte: ${qtd}x ${comp.marca_componente} (+${pesoKg.toFixed(1)} kg)` + ); + } // Limpar valor digitado setQuantidadesManual((prev) => { diff --git a/src/pages/smart/components/SmartProducaoFlow.tsx b/src/pages/smart/components/SmartProducaoFlow.tsx index 7d2eb69..a14c604 100644 --- a/src/pages/smart/components/SmartProducaoFlow.tsx +++ b/src/pages/smart/components/SmartProducaoFlow.tsx @@ -29,6 +29,7 @@ import { SmartForcarApontamentoModal } from './SmartForcarApontamentoModal'; import { calcularKitsOF, PecaKitInfo } from '@/hooks/useKitsProducao'; import { MapaTermicoKitsModal } from '@/components/apontamento/kits/MapaTermicoKitsModal'; import { SmartComponentesCorteModal } from './SmartComponentesCorteModal'; +import { sincronizarTodosKitsOFCorte } from '@/utils/sincronizacaoKitsCorte'; interface SmartProducaoFlowProps { obra: OFAtiva; @@ -384,7 +385,7 @@ export const SmartProducaoFlow: React.FC = ({ if (a.saldo === 0 && b.saldo > 0) return 1; return a.marca.localeCompare(b.marca, undefined, { numeric: true, sensitivity: 'base' }); }); - }, [pecas, faseSelecionada, processoSelecionado, mapaProducao, processos, pecasPendentesMap]); + }, [pecas, faseSelecionada, processoSelecionado, mapaProducao, processos, pecasPendentesMap, mapaKits]); // Helper de ícone por processo const getIconeProcesso = (nome: string) => { @@ -403,6 +404,23 @@ export const SmartProducaoFlow: React.FC = ({ smartAudio.playClick(); setProcessoSelecionado(proc); setEtapaAtual('fase'); + + // Ao selecionar Solda ou Montagem, rodar sincronização preventiva de kits da OF + const nomeP = (proc.nome || '').toLowerCase(); + if ((nomeP.includes('solda') || nomeP.includes('montag')) && procCorteId && obra.of_number) { + sincronizarTodosKitsOFCorte({ + ofNumber: obra.of_number, + processoCorteId: procCorteId, + userId: user?.id, + usuarioNome: user?.name || user?.username || 'Operador', + }) + .then((res) => { + if (res.some((r) => r.delta_liberado > 0)) { + carregarDadosOF(); + } + }) + .catch((e) => console.warn('Erro na sincronização preventiva de kits:', e)); + } }; const handleEscolherFase = (fase: string) => { diff --git a/src/utils/sincronizacaoKitsCorte.ts b/src/utils/sincronizacaoKitsCorte.ts new file mode 100644 index 0000000..cc4ded3 --- /dev/null +++ b/src/utils/sincronizacaoKitsCorte.ts @@ -0,0 +1,334 @@ +import { supabase } from '@/integrations/supabase/client'; + +export interface ComponenteNecessario { + id: string; + quantidade_por_peca: number; + marca_componente?: string; +} + +export interface ResultadoSincronizacaoPeca { + peca_id: string; + marca: string; + quantidade_total: number; + kits_completos: number; + ja_apontado_anteriormente: number; + delta_liberado: number; +} + +/** + * Calcula estritamente a quantidade de kits completos cortados para uma peça mestre. + * Respeita a regra do GARGALO (bottleneck): + * Se um ou mais componentes têm corte para N peças, mas qualquer outro componente + * tem corte para apenas M peças (onde M < N), APENAS M peças são consideradas completas. + * A sobra (N - M) NÃO é liberada para Solda! + */ +export function calcularKitsCompletosPeca( + pecaQuantidade: number, + componentes: ComponenteNecessario[], + cortadoPorComponenteId: Map +): { kitsCompletos: number; menorComponenteId?: string; razaoPorComp: Map } { + const qtdTotal = Number(pecaQuantidade) || 0; + if (qtdTotal <= 0 || !componentes || componentes.length === 0) { + return { kitsCompletos: 0, razaoPorComp: new Map() }; + } + + let menorKits = qtdTotal; + let menorComponenteId: string | undefined = undefined; + const razaoPorComp = new Map(); + + for (const comp of componentes) { + const cortado = cortadoPorComponenteId.get(comp.id) || 0; + const porPeca = Math.max(1, Number(comp.quantidade_por_peca) || 1); + const kitsDeste = Math.floor(cortado / porPeca); + razaoPorComp.set(comp.id, kitsDeste); + + if (kitsDeste < menorKits) { + menorKits = kitsDeste; + menorComponenteId = comp.id; + } + } + + const kitsCompletos = Math.max(0, Math.min(qtdTotal, menorKits)); + return { kitsCompletos, menorComponenteId, razaoPorComp }; +} + +/** + * Sincroniza uma peça específica: + * Verifica o corte de todos os seus componentes, calcula os kits 100% completos, + * e gera o apontamento da peça mestre no processo de Corte para o delta liberado. + */ +export async function sincronizarPecaKitNoCorte(params: { + ofNumber: string; + pecaId: string; + processoCorteId: string; + userId?: string; + usuarioNome?: string; + dataApontamento?: string; +}): Promise { + const { ofNumber, pecaId, processoCorteId, userId, usuarioNome, dataApontamento } = params; + + try { + // 1. Buscar a peça mestre + const { data: peca, error: pecaError } = await supabase + .from('pecas') + .select('id, marca, quantidade, of_number, tem_componentes') + .eq('id', pecaId) + .single(); + + if (pecaError || !peca) { + console.warn('Peça não encontrada para sincronização de kit:', pecaId); + return null; + } + + // Se for peça S/M (sem componentes), ela não depende de kit + if (peca.tem_componentes === false) { + return null; + } + + // 2. Buscar componentes cadastrados para esta peça + const { data: comps, error: compsError } = await supabase + .from('componentes_peca') + .select('id, marca_componente, quantidade_por_peca') + .eq('peca_id', pecaId); + + if (compsError || !comps || comps.length === 0) { + return null; + } + + // 3. Buscar todos os apontamentos de componentes já feitos para essa peça no processo de Corte + const { data: aptsComp, error: aptsCompError } = await supabase + .from('apontamentos_producao') + .select('componente_id, quantidade_produzida') + .eq('peca_id', pecaId) + .eq('processo_id', processoCorteId) + .eq('tipo_apontamento', 'componente'); + + if (aptsCompError) { + console.error('Erro ao buscar apontamentos de componente:', aptsCompError); + return null; + } + + const cortadoPorComp = new Map(); + (aptsComp || []).forEach((a) => { + if (a.componente_id) { + cortadoPorComp.set( + a.componente_id, + (cortadoPorComp.get(a.componente_id) || 0) + Number(a.quantidade_produzida || 0) + ); + } + }); + + // 4. Calcular kits completos estritos + const { kitsCompletos } = calcularKitsCompletosPeca(peca.quantidade, comps, cortadoPorComp); + + // 5. Buscar quanto já foi apontado como peça mestre no corte + const { data: aptsPeca, error: aptsPecaError } = await supabase + .from('apontamentos_producao') + .select('quantidade_produzida') + .eq('peca_id', pecaId) + .eq('processo_id', processoCorteId) + .eq('tipo_apontamento', 'peca'); + + if (aptsPecaError) { + console.error('Erro ao buscar apontamentos de peça mestre:', aptsPecaError); + return null; + } + + const totalJaApontadoPeca = (aptsPeca || []).reduce( + (sum, a) => sum + Number(a.quantidade_produzida || 0), + 0 + ); + + const delta = kitsCompletos - totalJaApontadoPeca; + + // 6. Se houver delta positivo de kits completos formados, registrar apontamento no Corte + if (delta > 0) { + const hoje = new Date().toISOString().split('T')[0]; + const payload = { + of_number: peca.of_number, + peca_id: peca.id, + processo_id: processoCorteId, + tipo_apontamento: 'peca' as const, + quantidade_produzida: delta, + data_apontamento: dataApontamento || hoje, + created_by: userId || null, + usuario_nome: usuarioNome || 'Sistema (Liberação de Kits)', + observacoes: `Liberação automática de Kit 100% cortado (${kitsCompletos}/${peca.quantidade} un)`, + status_confirmacao: 'confirmado', + is_forcado: false, + }; + + const { error: insertError } = await supabase.from('apontamentos_producao').insert(payload); + if (insertError) { + console.error('Erro ao inserir apontamento da peça mestre no corte:', insertError); + throw insertError; + } + + console.log(`✅ Peça ${peca.marca}: +${delta} kit(s) completos liberados para Solda (${kitsCompletos}/${peca.quantidade})`); + } + + return { + peca_id: peca.id, + marca: peca.marca, + quantidade_total: peca.quantidade, + kits_completos: kitsCompletos, + ja_apontado_anteriormente: totalJaApontadoPeca, + delta_liberado: Math.max(0, delta), + }; + } catch (error) { + console.error('Erro ao sincronizar kits da peça no corte:', error); + return null; + } +} + +/** + * Sincroniza em lote todas as peças com componentes de uma OF. + * Útil ao entrar no processo de Solda ou ao finalizar apontamentos em lote. + */ +export async function sincronizarTodosKitsOFCorte(params: { + ofNumber: string; + processoCorteId: string; + userId?: string; + usuarioNome?: string; +}): Promise { + const { ofNumber, processoCorteId, userId, usuarioNome } = params; + + try { + // 1. Buscar peças com componentes da OF + const { data: pecas, error: pecasError } = await supabase + .from('pecas') + .select('id, marca, quantidade, of_number, tem_componentes') + .eq('of_number', ofNumber); + + if (pecasError || !pecas || pecas.length === 0) return []; + + const pecasComComponentes = pecas.filter((p) => p.tem_componentes !== false); + if (pecasComComponentes.length === 0) return []; + + const pecaIds = pecasComComponentes.map((p) => p.id); + + // 2. Buscar componentes de todas as peças + const { data: todosComponentes, error: compsError } = await supabase + .from('componentes_peca') + .select('id, peca_id, marca_componente, quantidade_por_peca') + .in('peca_id', pecaIds); + + if (compsError || !todosComponentes || todosComponentes.length === 0) return []; + + const compsPorPeca = new Map(); + todosComponentes.forEach((c) => { + if (!compsPorPeca.has(c.peca_id)) { + compsPorPeca.set(c.peca_id, []); + } + compsPorPeca.get(c.peca_id)!.push({ + id: c.id, + quantidade_por_peca: c.quantidade_por_peca, + marca_componente: c.marca_componente, + }); + }); + + // 3. Buscar todos os apontamentos de componentes no processo de Corte + const { data: todosAptsComp, error: aptsCompError } = await supabase + .from('apontamentos_producao') + .select('peca_id, componente_id, quantidade_produzida') + .eq('of_number', ofNumber) + .eq('processo_id', processoCorteId) + .eq('tipo_apontamento', 'componente'); + + if (aptsCompError) { + console.error('Erro ao buscar apontamentos de componentes da OF:', aptsCompError); + return []; + } + + const cortadoPorPecaEComp = new Map>(); + (todosAptsComp || []).forEach((a) => { + if (a.peca_id && a.componente_id) { + if (!cortadoPorPecaEComp.has(a.peca_id)) { + cortadoPorPecaEComp.set(a.peca_id, new Map()); + } + const mapComp = cortadoPorPecaEComp.get(a.peca_id)!; + mapComp.set( + a.componente_id, + (mapComp.get(a.componente_id) || 0) + Number(a.quantidade_produzida || 0) + ); + } + }); + + // 4. Buscar apontamentos de peças mestres existentes no Corte + const { data: todosAptsPeca, error: aptsPecaError } = await supabase + .from('apontamentos_producao') + .select('peca_id, quantidade_produzida') + .eq('of_number', ofNumber) + .eq('processo_id', processoCorteId) + .eq('tipo_apontamento', 'peca'); + + if (aptsPecaError) { + console.error('Erro ao buscar apontamentos de peça da OF:', aptsPecaError); + return []; + } + + const jaApontadoPorPeca = new Map(); + (todosAptsPeca || []).forEach((a) => { + if (a.peca_id) { + jaApontadoPorPeca.set( + a.peca_id, + (jaApontadoPorPeca.get(a.peca_id) || 0) + Number(a.quantidade_produzida || 0) + ); + } + }); + + // 5. Identificar deltas e preparar inserções em lote + const hoje = new Date().toISOString().split('T')[0]; + const insertsNovos: any[] = []; + const resultados: ResultadoSincronizacaoPeca[] = []; + + for (const peca of pecasComComponentes) { + const comps = compsPorPeca.get(peca.id) || []; + if (comps.length === 0) continue; + + const cortados = cortadoPorPecaEComp.get(peca.id) || new Map(); + const { kitsCompletos } = calcularKitsCompletosPeca(peca.quantidade, comps, cortados); + const jaApontado = jaApontadoPorPeca.get(peca.id) || 0; + const delta = kitsCompletos - jaApontado; + + if (delta > 0) { + insertsNovos.push({ + of_number: peca.of_number, + peca_id: peca.id, + processo_id: processoCorteId, + tipo_apontamento: 'peca', + quantidade_produzida: delta, + data_apontamento: hoje, + created_by: userId || null, + usuario_nome: usuarioNome || 'Sistema (Liberação de Kits)', + observacoes: `Liberação automática de Kit 100% cortado (${kitsCompletos}/${peca.quantidade} un)`, + status_confirmacao: 'confirmado', + is_forcado: false, + }); + } + + resultados.push({ + peca_id: peca.id, + marca: peca.marca, + quantidade_total: peca.quantidade, + kits_completos: kitsCompletos, + ja_apontado_anteriormente: jaApontado, + delta_liberado: Math.max(0, delta), + }); + } + + if (insertsNovos.length > 0) { + const { error: insertError } = await supabase.from('apontamentos_producao').insert(insertsNovos); + if (insertError) { + console.error('Erro ao sincronizar peças mestre em lote:', insertError); + } else { + console.log(`✅ Sincronização de kits concluída: ${insertsNovos.length} peças atualizadas no Corte.`); + } + } + + return resultados; + } catch (err) { + console.error('Erro na sincronização geral de kits da OF:', err); + return []; + } +}