fix(producao): liberacao automatica de pecas na solda por kits completos no corte
This commit is contained in:
@@ -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!');
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user