feat: sistema de deteccao de conflitos, conciliacao e ordem de intervencao fabril na importacao de pecas
This commit is contained in:
@@ -0,0 +1,531 @@
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import {
|
||||
DiffCampo,
|
||||
DiffComponente,
|
||||
ItemAnaliseImportacao,
|
||||
NivelRiscoImportacao,
|
||||
ProcessoApontado,
|
||||
ResultadoAnaliseImportacao,
|
||||
} from '@/types/importDiff';
|
||||
|
||||
interface CampoConfig {
|
||||
key: string;
|
||||
label: string;
|
||||
isCritico: boolean;
|
||||
isNumeric?: boolean;
|
||||
}
|
||||
|
||||
const CAMPOS_MONITORADOS: CampoConfig[] = [
|
||||
{ key: 'quantidade', label: 'Quantidade (un)', isCritico: true, isNumeric: true },
|
||||
{ key: 'comprimento', label: 'Comprimento (mm)', isCritico: true, isNumeric: true },
|
||||
{ key: 'perfil_principal', label: 'Perfil / Bitola', isCritico: true },
|
||||
{ key: 'peso_unitario', label: 'Peso Unitário (kg)', isCritico: true, isNumeric: true },
|
||||
{ key: 'peso_total', label: 'Peso Total (kg)', isCritico: true, isNumeric: true },
|
||||
{ key: 'material', label: 'Material', isCritico: false },
|
||||
{ key: 'tratamento_superficial', label: 'Tratamento', isCritico: false },
|
||||
{ key: 'descricao', label: 'Descrição', isCritico: false },
|
||||
{ key: 'tem_componentes', label: 'Tem Componentes', isCritico: true },
|
||||
];
|
||||
|
||||
/**
|
||||
* Normaliza e agrupa linhas de peças e subcomponentes
|
||||
*/
|
||||
export function consolidarPecasEComponentes(rawItems: any[]): {
|
||||
pecasMap: Map<string, any>;
|
||||
componentesMap: Map<string, any[]>;
|
||||
} {
|
||||
const pecasMap = new Map<string, any>();
|
||||
const componentesMap = new Map<string, any[]>();
|
||||
|
||||
rawItems.forEach((item) => {
|
||||
const ofNumber = String(item.of_number || '').trim();
|
||||
const etapaFase = String(item.etapa_fase || '1').trim();
|
||||
const marca = String(item.marca || '').trim();
|
||||
|
||||
if (!marca) return;
|
||||
|
||||
const pecaKey = `${ofNumber}-${etapaFase}-${marca}`;
|
||||
|
||||
const pecaData = {
|
||||
of_number: ofNumber,
|
||||
etapa_fase: etapaFase,
|
||||
marca: marca,
|
||||
descricao: item.descricao || item.perfil_principal || '',
|
||||
quantidade: Number(item.quantidade) || 0,
|
||||
peso_unitario: Number(item.peso_unitario) || 0,
|
||||
peso_total: Number(item.peso_total) || 0,
|
||||
comprimento:
|
||||
item.comprimento !== undefined && item.comprimento !== null
|
||||
? Number(item.comprimento)
|
||||
: item.comprimento_ref
|
||||
? Number(item.comprimento_ref)
|
||||
: null,
|
||||
tratamento_superficial: item.tratamento_superficial || 'pintura',
|
||||
material: item.material || 'Aço A36',
|
||||
perfil_principal: item.perfil_principal || item.descricao || '',
|
||||
tem_componentes: Boolean(item.tem_componentes),
|
||||
};
|
||||
|
||||
if (!pecasMap.has(pecaKey)) {
|
||||
pecasMap.set(pecaKey, pecaData);
|
||||
}
|
||||
|
||||
// Se o item contém um subcomponente
|
||||
if (item.marca_componente && String(item.marca_componente).trim()) {
|
||||
const rawMarca = String(item.marca_componente).trim();
|
||||
let cleanMarca = rawMarca;
|
||||
const matchSuffix = rawMarca.match(/-?(\d{4,})$/) || rawMarca.match(/(\d{4,})/);
|
||||
if (matchSuffix) {
|
||||
cleanMarca = matchSuffix[1];
|
||||
} else {
|
||||
const digits = rawMarca.replace(/\D/g, '');
|
||||
if (digits.length >= 4) {
|
||||
cleanMarca = digits.slice(-4);
|
||||
} else {
|
||||
cleanMarca = String(1000 + (componentesMap.get(pecaKey)?.length || 0));
|
||||
}
|
||||
}
|
||||
|
||||
let numVal = parseInt(cleanMarca, 10);
|
||||
if (isNaN(numVal) || numVal < 1000 || numVal > 9999) {
|
||||
numVal = 1000 + (componentesMap.get(pecaKey)?.length || 0);
|
||||
}
|
||||
cleanMarca = String(numVal);
|
||||
|
||||
const compData = {
|
||||
marca_componente: cleanMarca,
|
||||
descricao: item.descricao_componente || item.descricao || '',
|
||||
perfil: item.perfil_componente || item.perfil_principal || '',
|
||||
peso_unitario: Number(item.peso_unitario_componente) || 0,
|
||||
comprimento: item.comprimento_componente ? Number(item.comprimento_componente) : null,
|
||||
quantidade_por_peca: Math.max(1, Math.round(Number(item.quantidade_por_peca) || 1)),
|
||||
};
|
||||
|
||||
if (!componentesMap.has(pecaKey)) {
|
||||
componentesMap.set(pecaKey, []);
|
||||
}
|
||||
|
||||
// Evita duplicatas idênticas no mesmo lote
|
||||
const comps = componentesMap.get(pecaKey)!;
|
||||
const exists = comps.some((c) => c.marca_componente === cleanMarca);
|
||||
if (!exists) {
|
||||
comps.push(compData);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { pecasMap, componentesMap };
|
||||
}
|
||||
|
||||
/**
|
||||
* Analisa a lista de importação e cruza com a base de dados
|
||||
*/
|
||||
export async function analisarConflitosImportacao(
|
||||
rawItems: any[]
|
||||
): Promise<ResultadoAnaliseImportacao> {
|
||||
const { pecasMap, componentesMap } = consolidarPecasEComponentes(rawItems);
|
||||
const pecasImportadas = Array.from(pecasMap.values());
|
||||
|
||||
if (pecasImportadas.length === 0) {
|
||||
return {
|
||||
totalImportadas: 0,
|
||||
totalNovas: 0,
|
||||
totalIdenticas: 0,
|
||||
totalComDivergencia: 0,
|
||||
totalComApontamento: 0,
|
||||
totalCriticas: 0,
|
||||
itens: [],
|
||||
};
|
||||
}
|
||||
|
||||
// 1. Extrair OFs únicas
|
||||
const ofsUnicas = Array.from(
|
||||
new Set(pecasImportadas.map((p) => p.of_number).filter(Boolean))
|
||||
);
|
||||
|
||||
// 2. Buscar peças existentes no Supabase
|
||||
let pecasExistentes: any[] = [];
|
||||
if (ofsUnicas.length > 0) {
|
||||
const { data, error } = await supabase
|
||||
.from('pecas')
|
||||
.select('*')
|
||||
.in('of_number', ofsUnicas);
|
||||
|
||||
if (error) {
|
||||
console.error('❌ Erro ao consultar peças existentes para diff:', error);
|
||||
} else {
|
||||
pecasExistentes = data || [];
|
||||
}
|
||||
}
|
||||
|
||||
// Mapear peças existentes por chave
|
||||
const existingMap = new Map<string, any>();
|
||||
const existingIds: string[] = [];
|
||||
pecasExistentes.forEach((p) => {
|
||||
const key = `${p.of_number || ''}-${p.etapa_fase || ''}-${p.marca || ''}`;
|
||||
existingMap.set(key, p);
|
||||
existingIds.push(p.id);
|
||||
});
|
||||
|
||||
// 3. Buscar componentes existentes
|
||||
const existingComponentsByPecaId = new Map<string, any[]>();
|
||||
if (existingIds.length > 0) {
|
||||
const { data: compData, error: compErr } = await supabase
|
||||
.from('componentes_peca')
|
||||
.select('*')
|
||||
.in('peca_id', existingIds);
|
||||
|
||||
if (!compErr && compData) {
|
||||
compData.forEach((c) => {
|
||||
if (!existingComponentsByPecaId.has(c.peca_id)) {
|
||||
existingComponentsByPecaId.set(c.peca_id, []);
|
||||
}
|
||||
existingComponentsByPecaId.get(c.peca_id)!.push(c);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Buscar apontamentos de produção existentes
|
||||
const apontamentosByPecaId = new Map<string, any[]>();
|
||||
if (existingIds.length > 0) {
|
||||
const { data: aptData, error: aptErr } = await supabase
|
||||
.from('apontamentos_producao')
|
||||
.select(`
|
||||
id,
|
||||
peca_id,
|
||||
processo_id,
|
||||
quantidade_produzida,
|
||||
data_apontamento,
|
||||
processo:processos_fabricacao(id, nome, ordem)
|
||||
`)
|
||||
.in('peca_id', existingIds);
|
||||
|
||||
if (!aptErr && aptData) {
|
||||
aptData.forEach((a) => {
|
||||
if (!apontamentosByPecaId.has(a.peca_id)) {
|
||||
apontamentosByPecaId.set(a.peca_id, []);
|
||||
}
|
||||
apontamentosByPecaId.get(a.peca_id)!.push(a);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Buscar romaneios de expedição
|
||||
const pecaIdsEmRomaneio = new Set<string>();
|
||||
if (existingIds.length > 0) {
|
||||
const { data: romData, error: romErr } = await supabase
|
||||
.from('itens_romaneio_pecas')
|
||||
.select('peca_id')
|
||||
.in('peca_id', existingIds);
|
||||
|
||||
if (!romErr && romData) {
|
||||
romData.forEach((r) => pecaIdsEmRomaneio.add(r.peca_id));
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Construir a análise detalhada item a item
|
||||
const itensResultado: ItemAnaliseImportacao[] = [];
|
||||
|
||||
for (const pecaImportada of pecasImportadas) {
|
||||
const key = `${pecaImportada.of_number || ''}-${pecaImportada.etapa_fase || ''}-${pecaImportada.marca || ''}`;
|
||||
const pecaExistente = existingMap.get(key);
|
||||
const incomingComps = componentesMap.get(key) || [];
|
||||
|
||||
if (!pecaExistente) {
|
||||
// Peça NOVA
|
||||
itensResultado.push({
|
||||
key,
|
||||
of_number: pecaImportada.of_number,
|
||||
etapa_fase: pecaImportada.etapa_fase,
|
||||
marca: pecaImportada.marca,
|
||||
descricao: pecaImportada.descricao || pecaImportada.perfil_principal || '',
|
||||
isNova: true,
|
||||
isIdentica: false,
|
||||
temDivergencias: false,
|
||||
camposDivergentes: [],
|
||||
componentesDivergentes: incomingComps.map((c) => ({
|
||||
marca_componente: c.marca_componente,
|
||||
descricao: c.descricao,
|
||||
perfil: c.perfil,
|
||||
tipo: 'adicionado',
|
||||
quantidadeNova: c.quantidade_por_peca,
|
||||
comprimentoNovo: c.comprimento,
|
||||
detalhe: 'Novo componente na peça',
|
||||
})),
|
||||
dadosImportados: pecaImportada,
|
||||
apontamentoResumo: {
|
||||
totalProduzido: 0,
|
||||
processos: [],
|
||||
temApontamento: false,
|
||||
emRomaneio: false,
|
||||
},
|
||||
nivelRisco: 'nenhum',
|
||||
sobrepor: true,
|
||||
atencaoEspecial: false,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Peça EXISTENTE: Comparar campos
|
||||
const pecaId = pecaExistente.id;
|
||||
const camposDivergentes: DiffCampo[] = [];
|
||||
|
||||
CAMPOS_MONITORADOS.forEach(({ key: campoKey, label, isCritico, isNumeric }) => {
|
||||
let valAntigo = pecaExistente[campoKey];
|
||||
let valNovo = pecaImportada[campoKey];
|
||||
|
||||
if (isNumeric) {
|
||||
valAntigo = valAntigo !== null && valAntigo !== undefined ? Number(valAntigo) : null;
|
||||
valNovo = valNovo !== null && valNovo !== undefined ? Number(valNovo) : null;
|
||||
|
||||
if (valAntigo !== null && valNovo !== null) {
|
||||
if (Math.abs(valAntigo - valNovo) > 0.001) {
|
||||
camposDivergentes.push({
|
||||
campo: campoKey,
|
||||
label,
|
||||
valorAntigo: valAntigo,
|
||||
valorNovo: valNovo,
|
||||
isCritico,
|
||||
});
|
||||
}
|
||||
} else if (valAntigo !== valNovo) {
|
||||
camposDivergentes.push({
|
||||
campo: campoKey,
|
||||
label,
|
||||
valorAntigo: valAntigo,
|
||||
valorNovo: valNovo,
|
||||
isCritico,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const strAntigo = String(valAntigo || '').trim().toLowerCase();
|
||||
const strNovo = String(valNovo || '').trim().toLowerCase();
|
||||
|
||||
if (strAntigo !== strNovo) {
|
||||
camposDivergentes.push({
|
||||
campo: campoKey,
|
||||
label,
|
||||
valorAntigo: pecaExistente[campoKey] || '-',
|
||||
valorNovo: pecaImportada[campoKey] || '-',
|
||||
isCritico,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Comparar componentes existentes vs importados
|
||||
const existingComps = existingComponentsByPecaId.get(pecaId) || [];
|
||||
const componentesDivergentes: DiffComponente[] = [];
|
||||
|
||||
const existingCompsMap = new Map<string, any>();
|
||||
existingComps.forEach((c) => existingCompsMap.set(String(c.marca_componente).trim(), c));
|
||||
|
||||
const incomingCompsMap = new Map<string, any>();
|
||||
incomingComps.forEach((c) => incomingCompsMap.set(String(c.marca_componente).trim(), c));
|
||||
|
||||
// Verificar adicionados ou modificados
|
||||
incomingComps.forEach((inc) => {
|
||||
const matchMarca = String(inc.marca_componente).trim();
|
||||
const ext = existingCompsMap.get(matchMarca);
|
||||
|
||||
if (!ext) {
|
||||
componentesDivergentes.push({
|
||||
marca_componente: matchMarca,
|
||||
descricao: inc.descricao,
|
||||
perfil: inc.perfil,
|
||||
tipo: 'adicionado',
|
||||
quantidadeNova: inc.quantidade_por_peca,
|
||||
comprimentoNovo: inc.comprimento,
|
||||
detalhe: `Novo componente Pos. ${matchMarca} adicionado`,
|
||||
});
|
||||
} else {
|
||||
const mudouQtd = Number(ext.quantidade_por_peca || 1) !== Number(inc.quantidade_por_peca || 1);
|
||||
const mudouCompr =
|
||||
ext.comprimento !== null && inc.comprimento !== null
|
||||
? Math.abs(Number(ext.comprimento) - Number(inc.comprimento)) > 0.1
|
||||
: ext.comprimento !== inc.comprimento;
|
||||
const mudouPerfil =
|
||||
String(ext.perfil || '').trim().toLowerCase() !== String(inc.perfil || '').trim().toLowerCase();
|
||||
|
||||
if (mudouQtd || mudouCompr || mudouPerfil) {
|
||||
const detalhes: string[] = [];
|
||||
if (mudouQtd) detalhes.push(`Qtd: ${ext.quantidade_por_peca} ➔ ${inc.quantidade_por_peca}`);
|
||||
if (mudouCompr) detalhes.push(`Compr: ${ext.comprimento || 0} ➔ ${inc.comprimento || 0} mm`);
|
||||
if (mudouPerfil) detalhes.push(`Perfil: ${ext.perfil} ➔ ${inc.perfil}`);
|
||||
|
||||
componentesDivergentes.push({
|
||||
marca_componente: matchMarca,
|
||||
descricao: inc.descricao || ext.descricao,
|
||||
perfil: inc.perfil || ext.perfil,
|
||||
tipo: 'modificado',
|
||||
quantidadeAntiga: ext.quantidade_por_peca,
|
||||
quantidadeNova: inc.quantidade_por_peca,
|
||||
comprimentoAntigo: ext.comprimento,
|
||||
comprimentoNovo: inc.comprimento,
|
||||
detalhe: detalhes.join(' | '),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Verificar componentes removidos
|
||||
existingComps.forEach((ext) => {
|
||||
const matchMarca = String(ext.marca_componente).trim();
|
||||
if (!incomingCompsMap.has(matchMarca)) {
|
||||
componentesDivergentes.push({
|
||||
marca_componente: matchMarca,
|
||||
descricao: ext.descricao,
|
||||
perfil: ext.perfil,
|
||||
tipo: 'removido',
|
||||
quantidadeAntiga: ext.quantidade_por_peca,
|
||||
comprimentoAntigo: ext.comprimento,
|
||||
detalhe: `Componente Pos. ${matchMarca} removido na nova lista`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Analisar Apontamentos Fabris
|
||||
const apontamentosPeca = apontamentosByPecaId.get(pecaId) || [];
|
||||
const processosMap = new Map<string, ProcessoApontado>();
|
||||
|
||||
apontamentosPeca.forEach((apt) => {
|
||||
const procNome = apt.processo?.nome || 'Geral';
|
||||
const procOrdem = apt.processo?.ordem || 99;
|
||||
const procId = apt.processo_id;
|
||||
|
||||
if (!processosMap.has(procId)) {
|
||||
processosMap.set(procId, {
|
||||
processoId: procId,
|
||||
processoNome: procNome,
|
||||
ordem: procOrdem,
|
||||
quantidadeProduzida: 0,
|
||||
});
|
||||
}
|
||||
processosMap.get(procId)!.quantidadeProduzida += Number(apt.quantidade_produzida || 0);
|
||||
});
|
||||
|
||||
const processosApontados = Array.from(processosMap.values()).sort(
|
||||
(a, b) => a.ordem - b.ordem
|
||||
);
|
||||
const totalProduzidoMax = processosApontados.reduce(
|
||||
(max, p) => Math.max(max, p.quantidadeProduzida),
|
||||
0
|
||||
);
|
||||
const ultimoProc =
|
||||
processosApontados.length > 0
|
||||
? processosApontados[processosApontados.length - 1].processoNome
|
||||
: undefined;
|
||||
|
||||
const temApontamento = apontamentosPeca.length > 0;
|
||||
const emRomaneio = pecaIdsEmRomaneio.has(pecaId);
|
||||
|
||||
const temDivergencias = camposDivergentes.length > 0 || componentesDivergentes.length > 0;
|
||||
const isIdentica = !temDivergencias;
|
||||
|
||||
// Calcular nível de risco
|
||||
let nivelRisco: NivelRiscoImportacao = 'nenhum';
|
||||
let motivoAlerta = '';
|
||||
let acaoRecomendadaFabril = '';
|
||||
|
||||
if (temDivergencias) {
|
||||
const temAlteracaoCritica =
|
||||
camposDivergentes.some((c) => c.isCritico) || componentesDivergentes.length > 0;
|
||||
|
||||
if (!temApontamento && !emRomaneio) {
|
||||
nivelRisco = 'baixo';
|
||||
motivoAlerta = 'Peça ainda não iniciada na fábrica. Atualização cadastral limpa.';
|
||||
acaoRecomendadaFabril = 'Nenhuma ação necessária no galpão.';
|
||||
} else {
|
||||
// Já tem apontamento ou romaneio
|
||||
if (emRomaneio) {
|
||||
nivelRisco = 'critico';
|
||||
motivoAlerta = `ALERTA MÁXIMO: Peça já foi expedida ou carregada em romaneio!`;
|
||||
acaoRecomendadaFabril =
|
||||
'Acionar PCP e Engenharia de Obra imediatamente para conferência do lote expedido.';
|
||||
} else if (temAlteracaoCritica) {
|
||||
nivelRisco = 'critico';
|
||||
const etapaStr = ultimoProc ? `no setor de ${ultimoProc.toUpperCase()}` : 'na produção';
|
||||
motivoAlerta = `Peça em andamento ${etapaStr} (${totalProduzidoMax} un produzidas) com alterações físicas/dimensionais.`;
|
||||
|
||||
const compReduzido = camposDivergentes.find(
|
||||
(c) => c.campo === 'comprimento' && Number(c.valorNovo) < Number(c.valorAntigo)
|
||||
);
|
||||
const compAumentado = camposDivergentes.find(
|
||||
(c) => c.campo === 'comprimento' && Number(c.valorNovo) > Number(c.valorAntigo)
|
||||
);
|
||||
const qtdReduzida = camposDivergentes.find(
|
||||
(c) => c.campo === 'quantidade' && Number(c.valorNovo) < Number(c.valorAntigo)
|
||||
);
|
||||
|
||||
if (compReduzido) {
|
||||
acaoRecomendadaFabril = `Retornar peça para corte/serra: comprimento deve ser reduzido em ${Math.abs(
|
||||
Number(compReduzido.valorAntigo) - Number(compReduzido.valorNovo)
|
||||
)} mm.`;
|
||||
} else if (compAumentado) {
|
||||
acaoRecomendadaFabril = `ATENÇÃO: Peça cresceu ${
|
||||
Number(compAumentado.valorNovo) - Number(compAumentado.valorAntigo)
|
||||
} mm. Descartar/reaproveitar corte antigo e cortar nova barra.`;
|
||||
} else if (qtdReduzida) {
|
||||
acaoRecomendadaFabril = `Redução de lote: verificar se excedente já fabricado deve ser estornado ou enviado para estoque de peças prontas.`;
|
||||
} else if (componentesDivergentes.length > 0) {
|
||||
acaoRecomendadaFabril = `Interromper solda/pintura: verificar adição/remoção de componentes na montagem antes de avançar.`;
|
||||
} else {
|
||||
acaoRecomendadaFabril = `Inspecionar peça na bancada de trabalho perante os novos dados técnicos.`;
|
||||
}
|
||||
} else {
|
||||
nivelRisco = 'medio';
|
||||
motivoAlerta = 'Alteração apenas cadastral/superficial em peça com apontamento.';
|
||||
acaoRecomendadaFabril = 'Verificar etiquetas e conferir especificação na pintura.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Configura ações padrão
|
||||
itensResultado.push({
|
||||
key,
|
||||
of_number: pecaImportada.of_number,
|
||||
etapa_fase: pecaImportada.etapa_fase,
|
||||
marca: pecaImportada.marca,
|
||||
descricao: pecaImportada.descricao || pecaExistente.descricao || '',
|
||||
isNova: false,
|
||||
isIdentica,
|
||||
temDivergencias,
|
||||
camposDivergentes,
|
||||
componentesDivergentes,
|
||||
dadosImportados: pecaImportada,
|
||||
dadosExistentes: pecaExistente,
|
||||
pecaIdExistente: pecaId,
|
||||
apontamentoResumo: {
|
||||
totalProduzido: totalProduzidoMax,
|
||||
processos: processosApontados,
|
||||
ultimoProcessoNome: ultimoProc,
|
||||
temApontamento,
|
||||
emRomaneio,
|
||||
},
|
||||
nivelRisco,
|
||||
sobrepor: temDivergencias, // Sobrepor marcado por padrão se tem divergência
|
||||
atencaoEspecial: nivelRisco === 'critico', // Atenção especial marcada automaticamente se crítico
|
||||
motivoAlerta,
|
||||
acaoRecomendadaFabril,
|
||||
});
|
||||
}
|
||||
|
||||
// Ordenar: críticas primeiro, depois com divergência, depois novas, depois idênticas
|
||||
itensResultado.sort((a, b) => {
|
||||
const pesoRisco = { critico: 4, medio: 3, baixo: 2, nenhum: 1 };
|
||||
const diffRisco = pesoRisco[b.nivelRisco] - pesoRisco[a.nivelRisco];
|
||||
if (diffRisco !== 0) return diffRisco;
|
||||
if (b.temDivergencias !== a.temDivergencias) return b.temDivergencias ? 1 : -1;
|
||||
return a.marca.localeCompare(b.marca, undefined, { numeric: true });
|
||||
});
|
||||
|
||||
return {
|
||||
totalImportadas: itensResultado.length,
|
||||
totalNovas: itensResultado.filter((i) => i.isNova).length,
|
||||
totalIdenticas: itensResultado.filter((i) => i.isIdentica).length,
|
||||
totalComDivergencia: itensResultado.filter((i) => i.temDivergencias).length,
|
||||
totalComApontamento: itensResultado.filter((i) => i.apontamentoResumo.temApontamento).length,
|
||||
totalCriticas: itensResultado.filter((i) => i.nivelRisco === 'critico').length,
|
||||
itens: itensResultado,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { ItemAnaliseImportacao } from '@/types/importDiff';
|
||||
|
||||
export interface ExecucaoImportacaoParams {
|
||||
itens: ItemAnaliseImportacao[];
|
||||
componentesMap: Map<string, any[]>;
|
||||
userId: string;
|
||||
userNome?: string;
|
||||
userEmail?: string;
|
||||
motivoRevisao?: string;
|
||||
}
|
||||
|
||||
export interface ResultadoExecucaoImportacao {
|
||||
sucesso: boolean;
|
||||
totalInseridas: number;
|
||||
totalAtualizadas: number;
|
||||
totalIgnoradas: number;
|
||||
totalComAtencaoEspecial: number;
|
||||
itensAtencaoEspecial: ItemAnaliseImportacao[];
|
||||
mensagem: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executa a importação conciliada respeitando as decisões tomadas pelo usuário no modal.
|
||||
* Preserva 100% dos IDs existentes, garantindo que nenhum apontamento de produção ou romaneio seja afetado.
|
||||
*/
|
||||
export async function executarImportacaoConciliada(
|
||||
params: ExecucaoImportacaoParams
|
||||
): Promise<ResultadoExecucaoImportacao> {
|
||||
const {
|
||||
itens,
|
||||
componentesMap,
|
||||
userId,
|
||||
userNome = 'Operador',
|
||||
userEmail = '',
|
||||
motivoRevisao = 'Atualização de projeto via importação de lista de peças',
|
||||
} = params;
|
||||
|
||||
console.log('🚀 Executando importação conciliada para', itens.length, 'itens analisados');
|
||||
|
||||
let totalInseridas = 0;
|
||||
let totalAtualizadas = 0;
|
||||
let totalIgnoradas = 0;
|
||||
const itensAtencaoEspecial: ItemAnaliseImportacao[] = [];
|
||||
|
||||
try {
|
||||
// 1. Separar itens por ação
|
||||
const pecasNovas = itens.filter((i) => i.isNova && i.sobrepor);
|
||||
const pecasParaAtualizar = itens.filter(
|
||||
(i) => !i.isNova && i.sobrepor && (i.temDivergencias || i.isIdentica)
|
||||
);
|
||||
const pecasIgnoradas = itens.filter((i) => !i.sobrepor);
|
||||
|
||||
totalIgnoradas = pecasIgnoradas.length;
|
||||
|
||||
// 2. Atualizar peças existentes selecionadas mantendo o UUID
|
||||
for (const item of pecasParaAtualizar) {
|
||||
if (!item.pecaIdExistente) continue;
|
||||
|
||||
const pecaId = item.pecaIdExistente;
|
||||
const importado = item.dadosImportados;
|
||||
const existente = item.dadosExistentes || {};
|
||||
|
||||
// Se é idêntica e estava marcada, apenas pula a escrita desnecessária
|
||||
if (item.isIdentica) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Se marcada para atenção especial, memorizar para a lista impressa
|
||||
if (item.atencaoEspecial) {
|
||||
itensAtencaoEspecial.push(item);
|
||||
}
|
||||
|
||||
const qtdFinal = Number(importado.quantidade || existente.quantidade || 0);
|
||||
const pesoUnitFinal = Number(importado.peso_unitario || existente.peso_unitario || 0);
|
||||
const pesoTotalCalculado = Number((qtdFinal * pesoUnitFinal).toFixed(3)) || Number(importado.peso_total || 0);
|
||||
|
||||
const payloadUpdate: any = {
|
||||
descricao: importado.descricao || existente.descricao || '',
|
||||
quantidade: qtdFinal,
|
||||
peso_unitario: pesoUnitFinal,
|
||||
peso_total: pesoTotalCalculado,
|
||||
comprimento: importado.comprimento !== undefined ? importado.comprimento : existente.comprimento,
|
||||
tratamento_superficial: importado.tratamento_superficial || existente.tratamento_superficial || 'pintura',
|
||||
material: importado.material || existente.material || 'Aço A36',
|
||||
perfil_principal: importado.perfil_principal || existente.perfil_principal || '',
|
||||
tem_componentes: importado.tem_componentes !== undefined ? importado.tem_componentes : existente.tem_componentes,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const { error: updateErr } = await supabase
|
||||
.from('pecas')
|
||||
.update(payloadUpdate)
|
||||
.eq('id', pecaId);
|
||||
|
||||
if (updateErr) {
|
||||
console.error(`❌ Erro ao atualizar peça ${item.marca}:`, updateErr);
|
||||
continue;
|
||||
}
|
||||
|
||||
totalAtualizadas++;
|
||||
|
||||
// Atualizar componentes da peça existente
|
||||
const novosComponentes = componentesMap.get(item.key) || [];
|
||||
if (novosComponentes.length > 0) {
|
||||
// Limpar componentes antigos e inserir os novos
|
||||
await supabase.from('componentes_peca').delete().eq('peca_id', pecaId);
|
||||
|
||||
const componentesFormatados = novosComponentes.map((comp) => ({
|
||||
...comp,
|
||||
peca_id: pecaId,
|
||||
user_id: userId,
|
||||
}));
|
||||
|
||||
await supabase.from('componentes_peca').insert(componentesFormatados);
|
||||
} else if (item.camposDivergentes.some((c) => c.campo === 'tem_componentes' && !c.valorNovo)) {
|
||||
// Peça virou simples sem componentes: limpar antigos
|
||||
await supabase.from('componentes_peca').delete().eq('peca_id', pecaId);
|
||||
}
|
||||
|
||||
// Registrar Histórico de Revisão na tabela 'revisoes' se houve divergência
|
||||
if (item.temDivergencias) {
|
||||
const camposAlterados = item.camposDivergentes.map((c) => c.campo);
|
||||
if (item.componentesDivergentes.length > 0) {
|
||||
camposAlterados.push('componentes');
|
||||
}
|
||||
|
||||
const proxNumeroRevisao = existente.revisao_atual
|
||||
? `R${(Number(existente.revisao_atual.replace(/\D/g, '')) || 0) + 1}`
|
||||
: 'R1';
|
||||
|
||||
try {
|
||||
await supabase.from('revisoes' as any).insert({
|
||||
of_number: item.of_number,
|
||||
peca_id: pecaId,
|
||||
numero_revisao: proxNumeroRevisao,
|
||||
tipo_revisao: item.nivelRisco === 'critico' ? 'critica' : 'simples',
|
||||
descricao_motivo: motivoRevisao,
|
||||
observacoes: item.motivoAlerta || item.acaoRecomendadaFabril || null,
|
||||
usuario_id: userId || null,
|
||||
usuario_nome: userNome,
|
||||
usuario_email: userEmail,
|
||||
dados_anteriores: existente,
|
||||
dados_novos: importado,
|
||||
campos_alterados: camposAlterados,
|
||||
afeta_apontamentos: item.apontamentoResumo.temApontamento,
|
||||
impacto_processos: {
|
||||
divergencias: item.camposDivergentes.map((c) => ({
|
||||
campo: c.label,
|
||||
antes: c.valorAntigo,
|
||||
depois: c.valorNovo,
|
||||
})),
|
||||
componentes_alterados: item.componentesDivergentes,
|
||||
ultimo_processo_afetado: item.apontamentoResumo.ultimoProcessoNome || null,
|
||||
total_produzido: item.apontamentoResumo.totalProduzido,
|
||||
atencao_especial_marcada: item.atencaoEspecial,
|
||||
},
|
||||
data_revisao: new Date().toISOString(),
|
||||
});
|
||||
} catch (revError) {
|
||||
console.warn('Aviso: Não foi possível gravar histórico em revisoes:', revError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Inserir peças novas
|
||||
if (pecasNovas.length > 0) {
|
||||
const pecasParaInsert = pecasNovas.map((p) => {
|
||||
const imp = p.dadosImportados;
|
||||
const qtd = Number(imp.quantidade) || 0;
|
||||
const pesoU = Number(imp.peso_unitario) || 0;
|
||||
const pesoT = Number(imp.peso_total) || Number((qtd * pesoU).toFixed(3));
|
||||
|
||||
return {
|
||||
of_number: imp.of_number,
|
||||
etapa_fase: imp.etapa_fase,
|
||||
marca: imp.marca,
|
||||
descricao: imp.descricao || imp.perfil_principal || '',
|
||||
quantidade: qtd,
|
||||
peso_unitario: pesoU,
|
||||
peso_total: pesoT,
|
||||
comprimento: imp.comprimento || null,
|
||||
tratamento_superficial: imp.tratamento_superficial || 'pintura',
|
||||
material: imp.material || 'Aço A36',
|
||||
perfil_principal: imp.perfil_principal || '',
|
||||
tem_componentes: Boolean(imp.tem_componentes),
|
||||
user_id: userId,
|
||||
prioridade: 'P4',
|
||||
};
|
||||
});
|
||||
|
||||
const { data: pecasInseridas, error: insertError } = await supabase
|
||||
.from('pecas')
|
||||
.insert(pecasParaInsert)
|
||||
.select('id, of_number, etapa_fase, marca');
|
||||
|
||||
if (insertError) {
|
||||
console.error('❌ Erro ao inserir peças novas:', insertError);
|
||||
throw insertError;
|
||||
}
|
||||
|
||||
if (pecasInseridas) {
|
||||
totalInseridas = pecasInseridas.length;
|
||||
|
||||
// Inserir componentes das peças novas
|
||||
const componentesParaInsert: any[] = [];
|
||||
pecasInseridas.forEach((p) => {
|
||||
const key = `${p.of_number || ''}-${p.etapa_fase || ''}-${p.marca || ''}`;
|
||||
const comps = componentesMap.get(key) || [];
|
||||
comps.forEach((c) => {
|
||||
componentesParaInsert.push({
|
||||
...c,
|
||||
peca_id: p.id,
|
||||
user_id: userId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (componentesParaInsert.length > 0) {
|
||||
await supabase.from('componentes_peca').insert(componentesParaInsert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mensagem = `✅ Importação concluída!`;
|
||||
if (totalInseridas > 0) mensagem += ` ${totalInseridas} nova(s) peça(s) inserida(s).`;
|
||||
if (totalAtualizadas > 0) mensagem += ` ${totalAtualizadas} peça(s) sobreposta(s)/atualizada(s).`;
|
||||
if (totalIgnoradas > 0) mensagem += ` ${totalIgnoradas} peça(s) mantida(s) no cadastro original.`;
|
||||
|
||||
return {
|
||||
sucesso: true,
|
||||
totalInseridas,
|
||||
totalAtualizadas,
|
||||
totalIgnoradas,
|
||||
totalComAtencaoEspecial: itensAtencaoEspecial.length,
|
||||
itensAtencaoEspecial,
|
||||
mensagem,
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error('❌ Falha na execução da importação conciliada:', error);
|
||||
return {
|
||||
sucesso: false,
|
||||
totalInseridas: 0,
|
||||
totalAtualizadas: 0,
|
||||
totalIgnoradas: 0,
|
||||
totalComAtencaoEspecial: 0,
|
||||
itensAtencaoEspecial: [],
|
||||
mensagem: `Erro na importação: ${error?.message || 'Falha de comunicação com o banco'}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user