fix(producao): liberacao automatica de pecas na solda por kits completos no corte

This commit is contained in:
2026-10-26 11:59:05 -03:00
parent 1e191fe562
commit ea447df298
6 changed files with 580 additions and 146 deletions
+112 -59
View File
@@ -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<typeof criarApontamento>[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<typeof criarApontamento>[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<boolean> => {
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<typeof criarApontamento>[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<typeof criarApontamento>[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!');
+45 -76
View File
@@ -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 <T,>(
@@ -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<string, number>();
(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
};
};
+35 -6
View File
@@ -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
};
};
@@ -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<SmartComponentesCorteModalProp
const { error } = await supabase.from('apontamentos_producao').insert(batchPayloads);
if (error) throw error;
// Sincronizar liberação de kits completos da peça mestre no Corte para a Solda
const resSync = await sincronizarPecaKitNoCorte({
ofNumber,
pecaId: pecaKit.peca_id,
processoCorteId,
userId: user?.id,
usuarioNome: nomeUsuario,
dataApontamento: hoje,
});
smartAudio.playSuccess();
const infoLiberado =
resSync && resSync.delta_liberado > 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<SmartComponentesCorteModalProp
const { error } = await supabase.from('apontamentos_producao').insert(payload);
if (error) throw error;
// Sincronizar se este corte completou novos kits completos (respeitando estritamente o gargalo de todos os componentes)
const resSync = await sincronizarPecaKitNoCorte({
ofNumber,
pecaId: pecaKit.peca_id,
processoCorteId,
userId: user?.id,
usuarioNome: nomeUsuario,
dataApontamento: hoje,
});
const pesoKg = qtd * (comp.peso_unitario || 0);
smartAudio.playSuccess();
toast.success(
`✂️ Apontado no Corte: ${qtd}x ${comp.marca_componente} (+${pesoKg.toFixed(1)} kg)`
);
if (resSync && resSync.delta_liberado > 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) => {
@@ -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<SmartProducaoFlowProps> = ({
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<SmartProducaoFlowProps> = ({
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) => {
+334
View File
@@ -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<string, number>
): { kitsCompletos: number; menorComponenteId?: string; razaoPorComp: Map<string, number> } {
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<string, number>();
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<ResultadoSincronizacaoPeca | null> {
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<string, number>();
(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<ResultadoSincronizacaoPeca[]> {
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<string, ComponenteNecessario[]>();
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<string, Map<string, number>>();
(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<string, number>();
(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 [];
}
}