feat: sistema de deteccao de conflitos, conciliacao e ordem de intervencao fabril na importacao de pecas

This commit is contained in:
2026-10-16 15:27:30 -03:00
parent ac8bacc9e1
commit ec980cba05
10 changed files with 1975 additions and 11 deletions
@@ -26,6 +26,9 @@ import * as XLSX from 'xlsx';
import { useAppLabels } from '@/hooks/useAppLabels';
import { usePecas } from '@/hooks/usePecas';
import { supabase } from '@/integrations/supabase/client';
import { analisarConflitosImportacao, consolidarPecasEComponentes } from '@/utils/importDiffService';
import { ResultadoAnaliseImportacao } from '@/types/importDiff';
import { ImportarPecasDiffModal } from '@/components/pecas/ImportarPecasDiffModal';
export interface ComponentItem {
marca: string;
@@ -145,6 +148,9 @@ const AdvanceSteelConverterContent: React.FC = () => {
const [statusText, setStatusText] = useState<string>('Aguardando arquivo PDF...');
const [currentFileName, setCurrentFileName] = useState<string>('Lista_Pecas');
const [useIframeMode, setUseIframeMode] = useState<boolean>(false);
const [analiseDiff, setAnaliseDiff] = useState<ResultadoAnaliseImportacao | null>(null);
const [componentesMapDiff, setComponentesMapDiff] = useState<Map<string, any[]>>(new Map());
const [showDiffModal, setShowDiffModal] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const logEndRef = useRef<HTMLDivElement>(null);
@@ -872,6 +878,18 @@ const AdvanceSteelConverterContent: React.FC = () => {
}
});
toast.info('Verificando existência e status de produção das peças...');
const analise = await analisarConflitosImportacao(importacaoRows);
const { componentesMap } = consolidarPecasEComponentes(importacaoRows);
if (analise.totalComDivergencia > 0 || analise.totalIdenticas > 0 || analise.totalComApontamento > 0) {
setAnaliseDiff(analise);
setComponentesMapDiff(componentesMap);
setShowDiffModal(true);
setIsDirectImporting(false);
return;
}
await importPecas(importacaoRows);
toast.success(`${extractedData.length} peças e seus componentes foram importados com sucesso!`);
addLog(`✅ Importação concluída: ${extractedData.length} peças gravadas no banco.`);
@@ -1160,6 +1178,19 @@ const AdvanceSteelConverterContent: React.FC = () => {
)}
</CardContent>
</Card>
{/* Modal de Detecção de Conflitos e Conciliação */}
<ImportarPecasDiffModal
open={showDiffModal}
onOpenChange={setShowDiffModal}
analise={analiseDiff}
componentesMap={componentesMapDiff}
ofDefault={headerOf || ''}
onConcluido={() => {
setShowDiffModal(false);
addLog(`✅ Importação conciliada concluída com sucesso.`);
}}
/>
</div>
);
};
@@ -25,6 +25,9 @@ import {
import { toast } from 'sonner';
import * as XLSX from 'xlsx';
import { usePecas } from '@/hooks/usePecas';
import { analisarConflitosImportacao, consolidarPecasEComponentes } from '@/utils/importDiffService';
import { ResultadoAnaliseImportacao } from '@/types/importDiff';
import { ImportarPecasDiffModal } from '@/components/pecas/ImportarPecasDiffModal';
export interface BocadConvertedPiece {
of: string;
@@ -78,6 +81,9 @@ export const BocadConverter: React.FC = () => {
const [headerFase, setHeaderFase] = useState<string>('');
const [processingLogs, setProcessingLogs] = useState<string[]>([]);
const [showLogs, setShowLogs] = useState(false);
const [analiseDiff, setAnaliseDiff] = useState<ResultadoAnaliseImportacao | null>(null);
const [componentesMapDiff, setComponentesMapDiff] = useState<Map<string, any[]>>(new Map());
const [showDiffModal, setShowDiffModal] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
@@ -569,6 +575,18 @@ export const BocadConverter: React.FC = () => {
}
});
toast.info('Verificando existência e status de produção das peças...');
const analise = await analisarConflitosImportacao(importacaoRows);
const { componentesMap } = consolidarPecasEComponentes(importacaoRows);
if (analise.totalComDivergencia > 0 || analise.totalIdenticas > 0 || analise.totalComApontamento > 0) {
setAnaliseDiff(analise);
setComponentesMapDiff(componentesMap);
setShowDiffModal(true);
setIsDirectImporting(false);
return;
}
await importPecas(importacaoRows);
toast.success(`${extractedPieces.length} peças foram importadas com sucesso!`);
addLog(`✅ Importação concluída: ${extractedPieces.length} peças gravadas no banco.`);
@@ -866,6 +884,19 @@ export const BocadConverter: React.FC = () => {
)}
</CardContent>
</Card>
{/* Modal de Detecção de Conflitos e Conciliação */}
<ImportarPecasDiffModal
open={showDiffModal}
onOpenChange={setShowDiffModal}
analise={analiseDiff}
componentesMap={componentesMapDiff}
ofDefault={headerOf || ''}
onConcluido={() => {
setShowDiffModal(false);
addLog(`✅ Importação conciliada concluída com sucesso.`);
}}
/>
</div>
);
};
@@ -0,0 +1,692 @@
import React, { useState, useMemo } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Checkbox } from '@/components/ui/checkbox';
import {
AlertTriangle,
CheckCircle2,
XCircle,
Search,
Printer,
ChevronDown,
ChevronRight,
Factory,
Layers,
ArrowRight,
ShieldAlert,
Sparkles,
Info,
} from 'lucide-react';
import { toast } from 'sonner';
import {
ItemAnaliseImportacao,
NivelRiscoImportacao,
ResultadoAnaliseImportacao,
} from '@/types/importDiff';
import { IntervencaoFabrilPrintModal } from './IntervencaoFabrilPrintModal';
import { executarImportacaoConciliada } from '@/utils/importExecutionService';
import { useAuth } from '@/hooks/useAuth';
interface ImportarPecasDiffModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
analise: ResultadoAnaliseImportacao | null;
componentesMap: Map<string, any[]>;
onConcluido: () => void;
ofDefault?: string;
}
type TabFiltro = 'todas' | 'divergencias' | 'apontadas' | 'criticas' | 'novas' | 'identicas';
export const ImportarPecasDiffModal: React.FC<ImportarPecasDiffModalProps> = ({
open,
onOpenChange,
analise,
componentesMap,
onConcluido,
ofDefault = '',
}) => {
const { user } = useAuth();
const [itens, setItens] = useState<ItemAnaliseImportacao[]>([]);
const [tabAtiva, setTabFiltro] = useState<TabFiltro>('todas');
const [busca, setBusca] = useState('');
const [linhasExpandidas, setLinhasExpandidas] = useState<Set<string>>(new Set());
const [showPrintModal, setShowPrintModal] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
// Inicializar itens locais quando a análise for fornecida
React.useEffect(() => {
if (analise?.itens) {
setItens(analise.itens);
}
}, [analise]);
// Contadores dinâmicos baseados no estado atual dos itens
const counts = useMemo(() => {
return {
total: itens.length,
novas: itens.filter((i) => i.isNova).length,
divergencias: itens.filter((i) => i.temDivergencias).length,
apontadas: itens.filter((i) => i.apontamentoResumo.temApontamento).length,
criticas: itens.filter((i) => i.nivelRisco === 'critico').length,
identicas: itens.filter((i) => i.isIdentica).length,
sobreporTotal: itens.filter((i) => i.sobrepor).length,
atencaoEspecialTotal: itens.filter((i) => i.atencaoEspecial).length,
};
}, [itens]);
// Filtro dos itens exibidos
const itensFiltrados = useMemo(() => {
return itens.filter((item) => {
// Filtro de Busca
const matchBusca =
!busca ||
item.marca.toLowerCase().includes(busca.toLowerCase()) ||
item.of_number.toLowerCase().includes(busca.toLowerCase()) ||
item.descricao.toLowerCase().includes(busca.toLowerCase()) ||
(item.dadosImportados.perfil_principal &&
item.dadosImportados.perfil_principal.toLowerCase().includes(busca.toLowerCase()));
if (!matchBusca) return false;
// Filtro de Aba
if (tabAtiva === 'novas') return item.isNova;
if (tabAtiva === 'divergencias') return item.temDivergencias;
if (tabAtiva === 'apontadas') return item.apontamentoResumo.temApontamento;
if (tabAtiva === 'criticas') return item.nivelRisco === 'critico';
if (tabAtiva === 'identicas') return item.isIdentica;
return true;
});
}, [itens, busca, tabAtiva]);
// Toggle do checkbox Sobrepor por item
const handleToggleSobrepor = (key: string) => {
setItens((prev) =>
prev.map((item) => {
if (item.key === key) {
return { ...item, sobrepor: !item.sobrepor };
}
return item;
})
);
};
// Toggle do checkbox Atenção Especial por item
const handleToggleAtencaoEspecial = (key: string) => {
setItens((prev) =>
prev.map((item) => {
if (item.key === key) {
return { ...item, atencaoEspecial: !item.atencaoEspecial };
}
return item;
})
);
};
// Ações em massa
const handleMarcarTodasSobrepor = (valor: boolean) => {
const keysFiltradas = new Set(itensFiltrados.map((i) => i.key));
setItens((prev) =>
prev.map((item) => {
if (keysFiltradas.has(item.key)) {
return { ...item, sobrepor: valor };
}
return item;
})
);
};
const handleMarcarCriticasAtencaoEspecial = () => {
setItens((prev) =>
prev.map((item) => {
if (item.nivelRisco === 'critico' && item.sobrepor) {
return { ...item, atencaoEspecial: true };
}
return item;
})
);
toast.info('Todas as peças críticas marcadas para emissão de Ordem de Intervenção!');
};
const handleToggleExpandir = (key: string) => {
setLinhasExpandidas((prev) => {
const next = new Set(prev);
if (next.has(key)) {
next.delete(key);
} else {
next.add(key);
}
return next;
});
};
// Executar a importação conciliada
const handleConfirmarImportacao = async () => {
if (!user) {
toast.error('Usuário não autenticado');
return;
}
const sobreporCount = itens.filter((i) => i.sobrepor).length;
if (sobreporCount === 0) {
toast.warning('Nenhuma peça foi selecionada para importação ou sobreposição.');
return;
}
setIsProcessing(true);
try {
const resultado = await executarImportacaoConciliada({
itens,
componentesMap,
userId: user.id,
userNome: user.name || user.email || 'Operador',
userEmail: user.email || '',
motivoRevisao: 'Conciliação e atualização de lista técnica de peças',
});
if (resultado.sucesso) {
toast.success(resultado.mensagem);
// Se houver peças marcadas com atenção especial, perguntar ou abrir modal de impressão
if (resultado.totalComAtencaoEspecial > 0) {
toast.info(
`Atenção: ${resultado.totalComAtencaoEspecial} peça(s) requerem atenção do chão de fábrica! Abrindo Ordem de Intervenção...`
);
setShowPrintModal(true);
} else {
onOpenChange(false);
onConcluido();
}
} else {
toast.error(resultado.mensagem);
}
} catch (err: any) {
console.error('Erro na confirmação:', err);
toast.error(`Falha ao salvar dados: ${err?.message || 'Erro desconhecido'}`);
} finally {
setIsProcessing(false);
}
};
const itensParaImpressao = useMemo(() => {
return itens.filter((i) => i.atencaoEspecial);
}, [itens]);
return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-6xl max-h-[92vh] flex flex-col p-0 overflow-hidden bg-background border-border">
{/* Cabeçalho */}
<DialogHeader className="p-4 border-b border-border bg-card flex-shrink-0">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2.5">
<div className="p-2 rounded-xl bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/20">
<Layers className="h-6 w-6" />
</div>
<div>
<DialogTitle className="text-xl font-bold text-foreground flex items-center gap-2">
Detecção de Conflitos e Conciliação de Peças
<Badge variant="outline" className="text-xs font-semibold">
{ofDefault ? `OF ${ofDefault}` : 'Importação de Lista'}
</Badge>
</DialogTitle>
<p className="text-xs text-muted-foreground mt-0.5">
Foram identificadas peças já cadastradas. Analise as diferenças, selecione quais sobrepor e proteja os apontamentos fabris.
</p>
</div>
</div>
{/* Botão de emissão rápida da lista de atenção */}
{counts.atencaoEspecialTotal > 0 && (
<Button
variant="outline"
size="sm"
onClick={() => setShowPrintModal(true)}
className="bg-amber-500/10 border-amber-500/30 text-amber-700 dark:text-amber-400 hover:bg-amber-500/20 text-xs flex items-center gap-1.5 font-bold"
>
<Printer className="h-4 w-4 text-amber-600 dark:text-amber-400" />
Imprimir Lembretes Fabris ({counts.atencaoEspecialTotal})
</Button>
)}
</div>
{/* Cards de Métricas (KPIs) */}
<div className="grid grid-cols-2 sm:grid-cols-5 gap-2 mt-4">
<div
onClick={() => setTabFiltro('criticas')}
className={`p-2.5 rounded-lg border cursor-pointer transition-all ${
tabAtiva === 'criticas'
? 'bg-red-500/15 border-red-500/50 dark:bg-red-950/40 shadow-sm'
: 'bg-card hover:bg-muted/50 border-border'
}`}
>
<div className="flex items-center justify-between text-xs text-red-600 dark:text-red-400 font-bold">
<span className="flex items-center gap-1">
<ShieldAlert className="h-3.5 w-3.5" /> Atenção Crítica
</span>
<Badge className="bg-red-600 text-white font-extrabold text-[11px] h-5 px-1.5">
{counts.criticas}
</Badge>
</div>
<div className="text-[11px] text-muted-foreground mt-1">
Alteração em peças já iniciadas
</div>
</div>
<div
onClick={() => setTabFiltro('apontadas')}
className={`p-2.5 rounded-lg border cursor-pointer transition-all ${
tabAtiva === 'apontadas'
? 'bg-amber-500/15 border-amber-500/50 dark:bg-amber-950/40 shadow-sm'
: 'bg-card hover:bg-muted/50 border-border'
}`}
>
<div className="flex items-center justify-between text-xs text-amber-600 dark:text-amber-400 font-bold">
<span className="flex items-center gap-1">
<Factory className="h-3.5 w-3.5" /> Em Produção
</span>
<Badge variant="secondary" className="font-extrabold text-[11px] h-5 px-1.5">
{counts.apontadas}
</Badge>
</div>
<div className="text-[11px] text-muted-foreground mt-1">
Possuem apontamentos
</div>
</div>
<div
onClick={() => setTabFiltro('divergencias')}
className={`p-2.5 rounded-lg border cursor-pointer transition-all ${
tabAtiva === 'divergencias'
? 'bg-blue-500/15 border-blue-500/50 dark:bg-blue-950/40 shadow-sm'
: 'bg-card hover:bg-muted/50 border-border'
}`}
>
<div className="flex items-center justify-between text-xs text-blue-600 dark:text-blue-400 font-bold">
<span>Com Divergência</span>
<Badge variant="secondary" className="font-extrabold text-[11px] h-5 px-1.5">
{counts.divergencias}
</Badge>
</div>
<div className="text-[11px] text-muted-foreground mt-1">
Mudança em relação à base
</div>
</div>
<div
onClick={() => setTabFiltro('novas')}
className={`p-2.5 rounded-lg border cursor-pointer transition-all ${
tabAtiva === 'novas'
? 'bg-purple-500/15 border-purple-500/50 dark:bg-purple-950/40 shadow-sm'
: 'bg-card hover:bg-muted/50 border-border'
}`}
>
<div className="flex items-center justify-between text-xs text-purple-600 dark:text-purple-400 font-bold">
<span className="flex items-center gap-1">
<Sparkles className="h-3.5 w-3.5" /> Peças Novas
</span>
<Badge variant="secondary" className="font-extrabold text-[11px] h-5 px-1.5">
{counts.novas}
</Badge>
</div>
<div className="text-[11px] text-muted-foreground mt-1">
Inexistentes no cadastro
</div>
</div>
<div
onClick={() => setTabFiltro('identicas')}
className={`p-2.5 rounded-lg border cursor-pointer transition-all ${
tabAtiva === 'identicas'
? 'bg-emerald-500/15 border-emerald-500/50 dark:bg-emerald-950/40 shadow-sm'
: 'bg-card hover:bg-muted/50 border-border'
}`}
>
<div className="flex items-center justify-between text-xs text-emerald-600 dark:text-emerald-400 font-bold">
<span className="flex items-center gap-1">
<CheckCircle2 className="h-3.5 w-3.5" /> 100% Idênticas
</span>
<Badge variant="secondary" className="font-extrabold text-[11px] h-5 px-1.5">
{counts.identicas}
</Badge>
</div>
<div className="text-[11px] text-muted-foreground mt-1">
Sem alterações necessárias
</div>
</div>
</div>
</DialogHeader>
{/* Barra de Filtros e Ferramentas */}
<div className="p-3 border-b border-border bg-muted/20 flex flex-wrap items-center justify-between gap-2 text-xs">
{/* Input de Busca */}
<div className="relative w-72">
<Search className="absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground" />
<Input
placeholder="Buscar por marca, perfil, fase..."
value={busca}
onChange={(e) => setBusca(e.target.value)}
className="pl-8 h-8 text-xs bg-background"
/>
</div>
{/* Botões de Ação Rápida */}
<div className="flex items-center gap-1.5 flex-wrap">
<Button
variant="outline"
size="sm"
onClick={() => handleMarcarTodasSobrepor(true)}
className="h-7 text-[11px] px-2.5"
>
Sobrepor Todas Filtradas
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleMarcarTodasSobrepor(false)}
className="h-7 text-[11px] px-2.5"
>
Ignorar Todas (Manter Base)
</Button>
{counts.criticas > 0 && (
<Button
variant="outline"
size="sm"
onClick={handleMarcarCriticasAtencaoEspecial}
className="h-7 text-[11px] px-2.5 bg-red-500/10 text-red-600 border-red-500/30 hover:bg-red-500/20 font-semibold"
>
<ShieldAlert className="h-3 w-3 mr-1" />
Marcar Críticas p/ Lembrete
</Button>
)}
</div>
</div>
{/* Tabela de Conciliação e Diff */}
<ScrollArea className="flex-1">
<div className="p-4">
<table className="w-full text-left text-xs border-collapse">
<thead>
<tr className="border-b border-border text-muted-foreground font-semibold bg-muted/40">
<th className="p-2 w-10 text-center">Sobrepor</th>
<th className="p-2 w-28">Peça / Fase</th>
<th className="p-2 w-44">Perfil / Bitola</th>
<th className="p-2 w-40">Status de Produção</th>
<th className="p-2">Divergências Detectadas (Diff)</th>
<th className="p-2 w-28 text-center">Lembrete Fabril</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{itensFiltrados.length === 0 ? (
<tr>
<td colSpan={6} className="py-8 text-center text-muted-foreground">
Nenhuma peça encontrada com os filtros selecionados.
</td>
</tr>
) : (
itensFiltrados.map((item) => {
const isExpandida = linhasExpandidas.has(item.key);
const ultimoProc = item.apontamentoResumo.ultimoProcessoNome;
const totalApontado = item.apontamentoResumo.totalProduzido;
// Cor da linha de acordo com o risco
let linhaBg = 'hover:bg-muted/30';
if (item.nivelRisco === 'critico') {
linhaBg = 'bg-red-500/5 hover:bg-red-500/10 dark:bg-red-950/20';
} else if (item.isNova) {
linhaBg = 'bg-purple-500/5 hover:bg-purple-500/10 dark:bg-purple-950/20';
}
return (
<React.Fragment key={item.key}>
<tr className={`transition-colors ${linhaBg}`}>
{/* Checkbox de Sobreposição */}
<td className="p-2 text-center">
<Checkbox
checked={item.sobrepor}
onCheckedChange={() => handleToggleSobrepor(item.key)}
title={item.sobrepor ? 'Sobrepor no cadastro' : 'Ignorar e manter atual'}
/>
</td>
{/* Identificação da Peça */}
<td className="p-2">
<div className="flex items-center gap-1.5">
<span className="font-extrabold text-sm text-foreground">
{item.marca}
</span>
{item.nivelRisco === 'critico' && (
<Badge className="bg-red-600 text-white text-[9px] h-4 px-1 font-bold">
ALERTA
</Badge>
)}
{item.isNova && (
<Badge className="bg-purple-600 text-white text-[9px] h-4 px-1 font-bold">
NOVA
</Badge>
)}
</div>
<div className="text-[11px] text-muted-foreground">
OF {item.of_number} • Fase {item.etapa_fase}
</div>
</td>
{/* Perfil e Descrição */}
<td className="p-2">
<div className="font-semibold text-foreground">
{item.dadosImportados.perfil_principal || item.descricao || '-'}
</div>
<div className="text-[10px] text-muted-foreground">
{item.dadosImportados.comprimento
? `Comp: ${item.dadosImportados.comprimento} mm`
: 'Sem comp. ref.'}
</div>
</td>
{/* Status Fabril (Apontamentos) */}
<td className="p-2">
{item.apontamentoResumo.temApontamento ? (
<div className="space-y-0.5">
<div className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-[11px] font-bold bg-amber-500/15 text-amber-700 dark:text-amber-400 border border-amber-500/30">
<Factory className="h-3 w-3" />
{ultimoProc ? ultimoProc.toUpperCase() : 'PRODUÇÃO'}
</div>
<div className="text-[10px] text-muted-foreground">
{totalApontado} un apontadas
</div>
{item.apontamentoResumo.emRomaneio && (
<div className="text-[10px] font-bold text-red-600">
⚠️ Já expedida em Romaneio
</div>
)}
</div>
) : (
<span className="text-[11px] text-muted-foreground italic flex items-center gap-1">
<CheckCircle2 className="h-3 w-3 text-emerald-500" />
Não iniciada (0%)
</span>
)}
</td>
{/* Coluna de Divergências (Diff) */}
<td className="p-2">
{item.isNova ? (
<span className="text-[11px] text-purple-600 dark:text-purple-400 font-medium">
Peça nova para cadastro
</span>
) : item.isIdentica ? (
<span className="text-[11px] text-emerald-600 dark:text-emerald-400 font-medium flex items-center gap-1">
<CheckCircle2 className="h-3.5 w-3.5" /> Dados 100% idênticos à base
</span>
) : (
<div className="space-y-1">
<div className="flex flex-wrap gap-1.5">
{item.camposDivergentes.map((c) => (
<div
key={c.campo}
className={`px-2 py-0.5 rounded text-[11px] font-medium border flex items-center gap-1 ${
c.isCritico
? 'bg-red-500/10 border-red-500/30 text-red-700 dark:text-red-400'
: 'bg-amber-500/10 border-amber-500/30 text-amber-700 dark:text-amber-400'
}`}
>
<span className="font-bold">{c.label}:</span>
<span className="line-through opacity-75">{c.valorAntigo}</span>
<ArrowRight className="h-2.5 w-2.5" />
<span className="font-extrabold">{c.valorNovo}</span>
</div>
))}
</div>
{/* Resumo de componentes se houver */}
{item.componentesDivergentes.length > 0 && (
<button
type="button"
onClick={() => handleToggleExpandir(item.key)}
className="text-[11px] text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1 font-semibold"
>
{isExpandida ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
{item.componentesDivergentes.length} alteração(ões) em subcomponentes
</button>
)}
</div>
)}
</td>
{/* Checkbox de Lembrete Fabril (Atenção Especial) */}
<td className="p-2 text-center">
{item.apontamentoResumo.temApontamento || item.nivelRisco === 'critico' ? (
<div className="flex flex-col items-center justify-center">
<Checkbox
checked={item.atencaoEspecial}
onCheckedChange={() => handleToggleAtencaoEspecial(item.key)}
title="Incluir na Ordem de Intervenção Fabril Impressa"
/>
<span className="text-[9px] text-muted-foreground mt-0.5">
Lembrete
</span>
</div>
) : (
<span className="text-muted-foreground/40 text-[10px]">-</span>
)}
</td>
</tr>
{/* Linha Expansível para Detalhe de Componentes */}
{isExpandida && item.componentesDivergentes.length > 0 && (
<tr className="bg-muted/30">
<td colSpan={6} className="p-3 pl-12">
<div className="bg-background rounded-lg p-3 border border-border space-y-1.5">
<div className="font-bold text-xs text-foreground flex items-center gap-1.5">
<Layers className="h-3.5 w-3.5 text-blue-500" />
Detalhamento de Subcomponentes da Peça {item.marca}:
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-xs">
{item.componentesDivergentes.map((comp) => {
let badgeCor = 'bg-blue-500/10 text-blue-700 border-blue-300';
if (comp.tipo === 'adicionado') badgeCor = 'bg-emerald-500/10 text-emerald-700 border-emerald-300';
if (comp.tipo === 'removido') badgeCor = 'bg-red-500/10 text-red-700 border-red-300';
return (
<div
key={comp.marca_componente}
className={`p-2 rounded border flex items-start gap-2 ${badgeCor}`}
>
<Badge variant="outline" className="text-[10px] font-bold">
Pos. {comp.marca_componente}
</Badge>
<div className="text-[11px] space-y-0.5">
<div className="font-semibold">{comp.descricao || comp.perfil || 'Componente'}</div>
<div className="text-[10px] opacity-90">{comp.detalhe}</div>
</div>
</div>
);
})}
</div>
</div>
</td>
</tr>
)}
</React.Fragment>
);
})
)}
</tbody>
</table>
</div>
</ScrollArea>
{/* Rodapé da Janela com Decisão e Ações */}
<DialogFooter className="p-3 border-t border-border bg-card flex flex-row items-center justify-between flex-shrink-0">
<div className="text-xs text-muted-foreground flex items-center gap-3">
<span>
<strong className="text-foreground">{counts.sobreporTotal}</strong> a sobrepor
</span>
<span>•</span>
<span>
<strong className="text-foreground">{counts.novas}</strong> novas
</span>
<span>•</span>
<span>
<strong className="text-foreground">{counts.total - counts.sobreporTotal}</strong> mantidas originais
</span>
{counts.atencaoEspecialTotal > 0 && (
<>
<span>•</span>
<Badge variant="outline" className="bg-amber-500/10 text-amber-700 dark:text-amber-400 border-amber-500/30 text-[10px] font-bold">
{counts.atencaoEspecialTotal} na lista impressa
</Badge>
</>
)}
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => onOpenChange(false)}
disabled={isProcessing}
className="text-xs"
>
Cancelar
</Button>
<Button
size="sm"
onClick={handleConfirmarImportacao}
disabled={isProcessing || counts.sobreporTotal === 0}
className="bg-emerald-600 hover:bg-emerald-700 text-white text-xs font-bold shadow-sm"
>
{isProcessing ? 'Gravando e Conciliando...' : `Confirmar Importação (${counts.sobreporTotal})`}
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Modal de Impressão da Ordem de Intervenção */}
<IntervencaoFabrilPrintModal
open={showPrintModal}
onOpenChange={(openState) => {
setShowPrintModal(openState);
if (!openState) {
// Se fechou a janela de impressão após a importação, fecha o diff modal e conclui
onOpenChange(false);
onConcluido();
}
}}
itens={itensParaImpressao}
ofNumber={ofDefault || itens[0]?.of_number || ''}
usuarioNome={user?.name || user?.email || 'Operador'}
/>
</>
);
};
+39 -8
View File
@@ -9,6 +9,9 @@ import { ScrollArea } from '@/components/ui/scroll-area';
import { AlertTriangle, Check, X, Upload, FileSpreadsheet, Weight, AlertCircle, Edit3 } from 'lucide-react';
import { toast } from 'sonner';
import { parseCSV } from '@/utils/csvUtils';
import { analisarConflitosImportacao, consolidarPecasEComponentes } from '@/utils/importDiffService';
import { ResultadoAnaliseImportacao } from '@/types/importDiff';
import { ImportarPecasDiffModal } from '@/components/pecas/ImportarPecasDiffModal';
interface PecaCSVData {
of_number: string;
@@ -62,6 +65,9 @@ export function ImportarPecasModal({ open, onOpenChange, onImport, pecasExistent
const [isLoading, setIsLoading] = useState(false);
const [step, setStep] = useState<'upload' | 'preview'>('upload');
const [showErrors, setShowErrors] = useState(false);
const [analiseDiff, setAnaliseDiff] = useState<ResultadoAnaliseImportacao | null>(null);
const [componentesMapDiff, setComponentesMapDiff] = useState<Map<string, any[]>>(new Map());
const [showDiffModal, setShowDiffModal] = useState(false);
const requiredHeaders = [
'of_number', 'etapa_fase', 'marca', 'descricao', 'quantidade',
@@ -307,19 +313,29 @@ export function ImportarPecasModal({ open, onOpenChange, onImport, pecasExistent
};
const handleImport = async () => {
const validPecas = csvData.filter((_, index) => !duplicates[index]?.isDuplicate);
console.log('🚀 Iniciando importação de', validPecas.length, 'peças...');
if (validPecas.length === 0) {
if (csvData.length === 0) {
toast.error('Nenhuma peça válida para importar');
return;
}
setIsLoading(true);
try {
await onImport(validPecas);
console.log('✅ Importação concluída com sucesso!');
// Analisar conflitos e status na base de dados
toast.info('Verificando existência e status de produção das peças...');
const analise = await analisarConflitosImportacao(csvData);
const { componentesMap } = consolidarPecasEComponentes(csvData);
// Se houver peças já cadastradas na base
if (analise.totalComDivergencia > 0 || analise.totalIdenticas > 0 || analise.totalComApontamento > 0) {
setAnaliseDiff(analise);
setComponentesMapDiff(componentesMap);
setShowDiffModal(true);
setIsLoading(false);
return;
}
await onImport(csvData);
toast.success(`${csvData.length} novas peças importadas com sucesso!`);
handleClose();
} catch (error) {
console.error('❌ Erro na importação:', error);
@@ -352,7 +368,8 @@ export function ImportarPecasModal({ open, onOpenChange, onImport, pecasExistent
.reduce((sum, peca) => sum + peca.peso_total, 0);
return (
<Dialog open={open} onOpenChange={handleClose}>
<>
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-6xl max-h-[80vh]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
@@ -601,5 +618,19 @@ export function ImportarPecasModal({ open, onOpenChange, onImport, pecasExistent
</DialogFooter>
</DialogContent>
</Dialog>
{/* Modal de Detecção de Conflitos e Conciliação */}
<ImportarPecasDiffModal
open={showDiffModal}
onOpenChange={setShowDiffModal}
analise={analiseDiff}
componentesMap={componentesMapDiff}
ofDefault={csvData[0]?.of_number || ''}
onConcluido={() => {
setShowDiffModal(false);
handleClose();
}}
/>
</>
);
}
+38 -2
View File
@@ -15,6 +15,9 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@
import { FileSpreadsheet, Upload, Download, CheckCircle2, AlertCircle, Trash2, ArrowLeft, RefreshCw } from 'lucide-react';
import { toast } from 'sonner';
import * as XLSX from 'xlsx';
import { analisarConflitosImportacao, consolidarPecasEComponentes } from '@/utils/importDiffService';
import { ResultadoAnaliseImportacao } from '@/types/importDiff';
import { ImportarPecasDiffModal } from '@/components/pecas/ImportarPecasDiffModal';
export interface PecaXLSData {
of_number: string;
@@ -55,6 +58,9 @@ export function ImportarXLSModal({
const [isProcessing, setIsProcessing] = useState(false);
const [isImporting, setIsImporting] = useState(false);
const [step, setStep] = useState<'upload' | 'preview'>('upload');
const [analiseDiff, setAnaliseDiff] = useState<ResultadoAnaliseImportacao | null>(null);
const [componentesMapDiff, setComponentesMapDiff] = useState<Map<string, any[]>>(new Map());
const [showDiffModal, setShowDiffModal] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const resetState = () => {
@@ -478,8 +484,23 @@ export function ImportarXLSModal({
quantidade_por_peca: p.quantidade_por_peca,
}));
// Analisar conflitos com a base de dados
toast.info('Verificando existência e status de produção das peças...');
const analise = await analisarConflitosImportacao(pecasPayload);
const { componentesMap } = consolidarPecasEComponentes(pecasPayload);
// Se houver qualquer peça já existente na base (com divergência, idêntica ou com apontamento fabril)
if (analise.totalComDivergencia > 0 || analise.totalIdenticas > 0 || analise.totalComApontamento > 0) {
setAnaliseDiff(analise);
setComponentesMapDiff(componentesMap);
setShowDiffModal(true);
setIsImporting(false);
return;
}
// Se todas forem 100% novas
await onImport(pecasPayload);
toast.success(`${pecasProcessadas.length} registros importados com sucesso para a OF!`);
toast.success(`${pecasProcessadas.length} novas peças cadastradas com sucesso para a OF!`);
handleModalOpenChange(false);
} catch (err: unknown) {
console.error('Erro ao importar peças do Excel:', err);
@@ -544,7 +565,8 @@ export function ImportarXLSModal({
const totalQuantidade = pecasProcessadas.reduce((acc, p) => acc + (p.quantidade || 0), 0);
return (
<Dialog open={open} onOpenChange={handleModalOpenChange}>
<>
<Dialog open={open} onOpenChange={handleModalOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] flex flex-col p-6 bg-card text-card-foreground border-border">
<DialogHeader className="pb-3 border-b border-border">
<DialogTitle className="flex items-center justify-between">
@@ -767,5 +789,19 @@ export function ImportarXLSModal({
)}
</DialogContent>
</Dialog>
{/* Modal Comparador e Conciliador de Peças Repetidas */}
<ImportarPecasDiffModal
open={showDiffModal}
onOpenChange={setShowDiffModal}
analise={analiseDiff}
componentesMap={componentesMapDiff}
ofDefault={ofDefault}
onConcluido={() => {
setShowDiffModal(false);
handleModalOpenChange(false);
}}
/>
</>
);
}
@@ -0,0 +1,286 @@
import React, { useRef } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Printer, AlertTriangle, CheckCircle2, Copy, FileText, Factory } from 'lucide-react';
import { toast } from 'sonner';
import { ItemAnaliseImportacao } from '@/types/importDiff';
interface IntervencaoFabrilPrintModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
itens: ItemAnaliseImportacao[];
ofNumber?: string;
usuarioNome?: string;
}
export const IntervencaoFabrilPrintModal: React.FC<IntervencaoFabrilPrintModalProps> = ({
open,
onOpenChange,
itens,
ofNumber = '',
usuarioNome = 'Operador PCP',
}) => {
const printRef = useRef<HTMLDivElement>(null);
const handlePrint = () => {
window.print();
};
const handleCopyText = () => {
if (itens.length === 0) return;
const linhas = itens.map((item, idx) => {
const diffs = item.camposDivergentes.map((c) => `${c.label}: ${c.valorAntigo} ➔ ${c.valorNovo}`).join('; ');
const comps = item.componentesDivergentes.map((c) => c.detalhe).join('; ');
const ultimoProc = item.apontamentoResumo.ultimoProcessoNome || 'Não iniciado';
return `${idx + 1}. [${item.of_number}] Marca: ${item.marca} (Fase ${item.etapa_fase})
- Status Fabril: ${ultimoProc} (${item.apontamentoResumo.totalProduzido} un apontadas)
- Divergências: ${diffs || 'Nenhuma alteração de campo'} ${comps ? '| Subcomponentes: ' + comps : ''}
- Ação Recomendada: ${item.acaoRecomendadaFabril || item.motivoAlerta}
`;
});
const textoFinal = `ORDEM DE INTERVENÇÃO FABRIL - TRACKSTEEL
Data: ${new Date().toLocaleString('pt-BR')} | Responsável: ${usuarioNome}
OFs: ${ofNumber || 'Múltiplas'} | Total de Peças em Alerta: ${itens.length}
${linhas.join('\n')}
Aviso: Interromper avanço fabril das peças listadas até a conferência física.`;
navigator.clipboard.writeText(textoFinal);
toast.success('Lista de lembretes copiada para a área de transferência!');
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] flex flex-col p-0 overflow-hidden bg-background border-border">
{/* Cabeçalho da Janela */}
<DialogHeader className="p-4 border-b border-border bg-muted/40 flex-row items-center justify-between space-y-0">
<div className="flex items-center gap-2">
<div className="p-2 rounded-lg bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/20">
<AlertTriangle className="h-5 w-5" />
</div>
<div>
<DialogTitle className="text-lg font-bold text-foreground flex items-center gap-2">
Ordem de Intervenção Fabril & Lista de Lembretes
<Badge variant="outline" className="bg-red-500/10 text-red-600 border-red-500/30 text-xs font-bold">
{itens.length} peça(s) com atenção especial
</Badge>
</DialogTitle>
<p className="text-xs text-muted-foreground">
Documento de alerta para o chão de fábrica sobre peças alteradas que já possuem apontamentos de produção.
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={handleCopyText}
className="text-xs flex items-center gap-1.5"
>
<Copy className="h-3.5 w-3.5" />
Copiar Texto
</Button>
<Button
size="sm"
onClick={handlePrint}
className="bg-blue-600 hover:bg-blue-700 text-white text-xs flex items-center gap-1.5 shadow-sm"
>
<Printer className="h-3.5 w-3.5" />
Imprimir (A4 / Ficha)
</Button>
</div>
</DialogHeader>
{/* Conteúdo Imprimível */}
<ScrollArea className="flex-1 p-6">
<div
ref={printRef}
className="print-container bg-white text-slate-900 p-6 rounded-lg border border-slate-200 shadow-sm print:p-0 print:border-none print:shadow-none font-sans"
>
{/* Cabeçalho Oficial do Relatório */}
<div className="border-b-2 border-slate-900 pb-4 mb-4 flex justify-between items-start">
<div>
<div className="flex items-center gap-2">
<Factory className="h-6 w-6 text-slate-900" />
<span className="font-black text-xl tracking-tight text-slate-900">TRACKSTEEL</span>
<span className="text-xs uppercase bg-slate-100 font-bold px-2 py-0.5 rounded text-slate-700 border border-slate-300">
Industrial PCP
</span>
</div>
<h1 className="text-base font-extrabold uppercase mt-1 text-slate-900">
ORDEM DE INTERVENÇÃO FABRIL & ALERTA DE REVISÃO
</h1>
<p className="text-xs text-slate-600">
Atenção especial por atualização de engenharia em peças com processo fabril em andamento
</p>
</div>
<div className="text-right text-xs space-y-0.5 text-slate-700">
<p><span className="font-bold">OF:</span> {ofNumber || 'Geral / Múltiplas'}</p>
<p><span className="font-bold">Data/Hora:</span> {new Date().toLocaleString('pt-BR')}</p>
<p><span className="font-bold">Responsável:</span> {usuarioNome}</p>
<p><span className="font-bold text-red-600">Peças Afetadas:</span> {itens.length}</p>
</div>
</div>
{/* Aviso de Destaque */}
<div className="bg-amber-50 border-l-4 border-amber-500 p-3 mb-4 rounded text-xs text-amber-900 flex items-start gap-2">
<AlertTriangle className="h-4 w-4 text-amber-600 shrink-0 mt-0.5" />
<div>
<span className="font-bold">COMUNICADO URGENTE AOS LÍDERES DE PRODUÇÃO:</span> As peças abaixo sofreram alterações de projeto (dimensional, peso, componentes ou quantidade) mas já possuem avanço de fabricação registrado. Interrompa o avanço físico destas peças até a conferência técnica recomendada.
</div>
</div>
{/* Tabela de Ações e Lembretes */}
<div className="overflow-x-auto">
<table className="w-full text-left text-xs border-collapse">
<thead>
<tr className="bg-slate-100 border-b-2 border-slate-300 text-slate-800 font-bold uppercase">
<th className="py-2 px-2.5 w-12 text-center">Item</th>
<th className="py-2 px-2.5 w-28">OF / Marca</th>
<th className="py-2 px-2.5 w-40">Perfil / Descrição</th>
<th className="py-2 px-2.5 w-36">Status no Chão de Fábrica</th>
<th className="py-2 px-2.5">O Que Mudou (Engenharia)</th>
<th className="py-2 px-2.5 w-52">Ação Recomendada</th>
<th className="py-2 px-2.5 w-28 text-center">Visto do Líder</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-200">
{itens.map((item, index) => {
const ultimoProc = item.apontamentoResumo.ultimoProcessoNome || 'Não iniciado';
const totalApontado = item.apontamentoResumo.totalProduzido;
return (
<tr key={item.key} className={index % 2 === 1 ? 'bg-slate-50/50' : 'bg-white'}>
<td className="py-2 px-2.5 text-center font-bold text-slate-500">
{index + 1}
</td>
<td className="py-2 px-2.5">
<div className="font-extrabold text-slate-900 text-sm">{item.marca}</div>
<div className="text-[10px] text-slate-500">
{item.of_number} • Fase {item.etapa_fase}
</div>
</td>
<td className="py-2 px-2.5">
<div className="font-semibold text-slate-800">
{item.dadosImportados.perfil_principal || item.descricao || '-'}
</div>
{item.dadosImportados.comprimento && (
<div className="text-[10px] text-slate-500">
Comp: {item.dadosImportados.comprimento} mm
</div>
)}
</td>
<td className="py-2 px-2.5">
<div className="inline-block px-2 py-0.5 rounded text-[11px] font-bold bg-amber-100 text-amber-900 border border-amber-300">
{ultimoProc.toUpperCase()}
</div>
<div className="text-[10px] text-slate-600 mt-0.5">
{totalApontado} de {item.dadosExistentes?.quantidade || item.dadosImportados.quantidade} un apontadas
</div>
{item.apontamentoResumo.emRomaneio && (
<div className="text-[10px] font-bold text-red-600">
⚠️ EM ROMANEIO
</div>
)}
</td>
<td className="py-2 px-2.5 space-y-1">
{item.camposDivergentes.map((c) => (
<div key={c.campo} className="text-[11px]">
<span className="font-bold text-slate-700">{c.label}:</span>{' '}
<span className="line-through text-red-600">{c.valorAntigo}</span>{' '}
➔ <span className="font-bold text-emerald-700">{c.valorNovo}</span>
</div>
))}
{item.componentesDivergentes.map((comp) => (
<div key={comp.marca_componente} className="text-[10px] text-blue-700 font-medium">
• {comp.detalhe}
</div>
))}
</td>
<td className="py-2 px-2.5">
<div className="text-[11px] font-semibold text-slate-900 bg-red-50 p-1.5 rounded border border-red-200">
{item.acaoRecomendadaFabril || item.motivoAlerta || 'Verificar peça na fábrica.'}
</div>
</td>
<td className="py-2 px-2.5 text-center">
<div className="h-10 border-b border-dashed border-slate-400 mt-2"></div>
<div className="text-[9px] text-slate-400 mt-0.5">Assinatura / Data</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{/* Rodapé de Assinaturas */}
<div className="mt-8 pt-4 border-t border-slate-300 grid grid-cols-3 gap-6 text-center text-xs text-slate-600">
<div>
<div className="h-10 border-b border-slate-400 mb-1"></div>
<div className="font-bold text-slate-800">Engenharia / Projetista</div>
<div className="text-[10px]">Liberação de Revisão</div>
</div>
<div>
<div className="h-10 border-b border-slate-400 mb-1"></div>
<div className="font-bold text-slate-800">PCP / Planejamento</div>
<div className="text-[10px]">Ajuste de Fila & Diário</div>
</div>
<div>
<div className="h-10 border-b border-slate-400 mb-1"></div>
<div className="font-bold text-slate-800">Líder de Fabricação</div>
<div className="text-[10px]">Ciente & Intervenção Física</div>
</div>
</div>
</div>
</ScrollArea>
{/* Rodapé da Janela */}
<DialogFooter className="p-3 border-t border-border bg-muted/20 flex items-center justify-between">
<div className="text-xs text-muted-foreground flex items-center gap-1.5">
<FileText className="h-4 w-4" />
Esta folha pode ser impressa em qualquer impressora A4 ou salva em PDF pelo navegador.
</div>
<Button variant="outline" size="sm" onClick={() => onOpenChange(false)}>
Fechar Janela
</Button>
</DialogFooter>
{/* Estilo Global de Impressão (@media print) */}
<style dangerouslySetInnerHTML={{
__html: `
@media print {
body * {
visibility: hidden !important;
}
.print-container, .print-container * {
visibility: visible !important;
}
.print-container {
position: absolute !important;
left: 0 !important;
top: 0 !important;
width: 100% !important;
margin: 0 !important;
padding: 15mm !important;
background: white !important;
color: black !important;
}
}
`
}} />
</DialogContent>
</Dialog>
);
};
-1
View File
@@ -501,7 +501,6 @@ export function usePecas() {
const { data: pecasExistentes, error: queryExistentesError } = await supabase
.from('pecas')
.select('id, of_number, etapa_fase, marca')
.eq('user_id', user.id)
.in('of_number', ofsParaConsultar);
if (queryExistentesError) {
+76
View File
@@ -0,0 +1,76 @@
export type NivelRiscoImportacao = 'nenhum' | 'baixo' | 'medio' | 'critico';
export interface DiffCampo {
campo: string;
label: string;
valorAntigo: any;
valorNovo: any;
isCritico: boolean;
}
export interface DiffComponente {
marca_componente: string;
descricao?: string;
perfil?: string;
tipo: 'adicionado' | 'removido' | 'modificado' | 'inalterado';
detalhe?: string;
quantidadeAntiga?: number;
quantidadeNova?: number;
comprimentoAntigo?: number | null;
comprimentoNovo?: number | null;
pesoUnitAntigo?: number | null;
pesoUnitNovo?: number | null;
}
export interface ProcessoApontado {
processoId: string;
processoNome: string;
ordem: number;
quantidadeProduzida: number;
}
export interface ApontamentoResumoPeca {
totalProduzido: number;
processos: ProcessoApontado[];
ultimoProcessoNome?: string;
temApontamento: boolean;
emRomaneio: boolean;
}
export interface ItemAnaliseImportacao {
key: string; // `${of_number}-${etapa_fase}-${marca}`
of_number: string;
etapa_fase: string;
marca: string;
descricao: string;
isNova: boolean;
isIdentica: boolean;
temDivergencias: boolean;
camposDivergentes: DiffCampo[];
componentesDivergentes: DiffComponente[];
dadosImportados: Record<string, any>;
dadosExistentes?: Record<string, any>;
pecaIdExistente?: string;
apontamentoResumo: ApontamentoResumoPeca;
nivelRisco: NivelRiscoImportacao;
// Ações de usuário selecionadas
sobrepor: boolean; // true para atualizar no banco, false para ignorar
atencaoEspecial: boolean; // true se deve gerar ordem de intervenção impressa
motivoAlerta?: string;
acaoRecomendadaFabril?: string;
}
export interface ResultadoAnaliseImportacao {
totalImportadas: number;
totalNovas: number;
totalIdenticas: number;
totalComDivergencia: number;
totalComApontamento: number;
totalCriticas: number;
itens: ItemAnaliseImportacao[];
}
+531
View File
@@ -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,
};
}
+251
View File
@@ -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'}`,
};
}
}