diff --git a/src/components/conversores/AdvanceSteelConverter.tsx b/src/components/conversores/AdvanceSteelConverter.tsx index 47ed144..07710d1 100644 --- a/src/components/conversores/AdvanceSteelConverter.tsx +++ b/src/components/conversores/AdvanceSteelConverter.tsx @@ -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('Aguardando arquivo PDF...'); const [currentFileName, setCurrentFileName] = useState('Lista_Pecas'); const [useIframeMode, setUseIframeMode] = useState(false); + const [analiseDiff, setAnaliseDiff] = useState(null); + const [componentesMapDiff, setComponentesMapDiff] = useState>(new Map()); + const [showDiffModal, setShowDiffModal] = useState(false); const fileInputRef = useRef(null); const logEndRef = useRef(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 = () => { )} + + {/* Modal de Detecção de Conflitos e Conciliação */} + { + setShowDiffModal(false); + addLog(`✅ Importação conciliada concluída com sucesso.`); + }} + /> ); }; diff --git a/src/components/conversores/BocadConverter.tsx b/src/components/conversores/BocadConverter.tsx index 917a150..3ffeded 100644 --- a/src/components/conversores/BocadConverter.tsx +++ b/src/components/conversores/BocadConverter.tsx @@ -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(''); const [processingLogs, setProcessingLogs] = useState([]); const [showLogs, setShowLogs] = useState(false); + const [analiseDiff, setAnaliseDiff] = useState(null); + const [componentesMapDiff, setComponentesMapDiff] = useState>(new Map()); + const [showDiffModal, setShowDiffModal] = useState(false); const fileInputRef = useRef(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 = () => { )} + + {/* Modal de Detecção de Conflitos e Conciliação */} + { + setShowDiffModal(false); + addLog(`✅ Importação conciliada concluída com sucesso.`); + }} + /> ); }; diff --git a/src/components/pecas/ImportarPecasDiffModal.tsx b/src/components/pecas/ImportarPecasDiffModal.tsx new file mode 100644 index 0000000..5338363 --- /dev/null +++ b/src/components/pecas/ImportarPecasDiffModal.tsx @@ -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; + onConcluido: () => void; + ofDefault?: string; +} + +type TabFiltro = 'todas' | 'divergencias' | 'apontadas' | 'criticas' | 'novas' | 'identicas'; + +export const ImportarPecasDiffModal: React.FC = ({ + open, + onOpenChange, + analise, + componentesMap, + onConcluido, + ofDefault = '', +}) => { + const { user } = useAuth(); + const [itens, setItens] = useState([]); + const [tabAtiva, setTabFiltro] = useState('todas'); + const [busca, setBusca] = useState(''); + const [linhasExpandidas, setLinhasExpandidas] = useState>(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 ( + <> + + + {/* Cabeçalho */} + +
+
+
+ +
+
+ + Detecção de Conflitos e Conciliação de Peças + + {ofDefault ? `OF ${ofDefault}` : 'Importação de Lista'} + + +

+ Foram identificadas peças já cadastradas. Analise as diferenças, selecione quais sobrepor e proteja os apontamentos fabris. +

+
+
+ + {/* Botão de emissão rápida da lista de atenção */} + {counts.atencaoEspecialTotal > 0 && ( + + )} +
+ + {/* Cards de Métricas (KPIs) */} +
+
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' + }`} + > +
+ + Atenção Crítica + + + {counts.criticas} + +
+
+ Alteração em peças já iniciadas +
+
+ +
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' + }`} + > +
+ + Em Produção + + + {counts.apontadas} + +
+
+ Possuem apontamentos +
+
+ +
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' + }`} + > +
+ Com Divergência + + {counts.divergencias} + +
+
+ Mudança em relação à base +
+
+ +
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' + }`} + > +
+ + Peças Novas + + + {counts.novas} + +
+
+ Inexistentes no cadastro +
+
+ +
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' + }`} + > +
+ + 100% Idênticas + + + {counts.identicas} + +
+
+ Sem alterações necessárias +
+
+
+
+ + {/* Barra de Filtros e Ferramentas */} +
+ {/* Input de Busca */} +
+ + setBusca(e.target.value)} + className="pl-8 h-8 text-xs bg-background" + /> +
+ + {/* Botões de Ação Rápida */} +
+ + + {counts.criticas > 0 && ( + + )} +
+
+ + {/* Tabela de Conciliação e Diff */} + +
+ + + + + + + + + + + + + {itensFiltrados.length === 0 ? ( + + + + ) : ( + 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 ( + + + {/* Checkbox de Sobreposição */} + + + {/* Identificação da Peça */} + + + {/* Perfil e Descrição */} + + + {/* Status Fabril (Apontamentos) */} + + + {/* Coluna de Divergências (Diff) */} + + + {/* Checkbox de Lembrete Fabril (Atenção Especial) */} + + + + {/* Linha Expansível para Detalhe de Componentes */} + {isExpandida && item.componentesDivergentes.length > 0 && ( + + + + )} + + ); + }) + )} + +
SobreporPeça / FasePerfil / BitolaStatus de ProduçãoDivergências Detectadas (Diff)Lembrete Fabril
+ Nenhuma peça encontrada com os filtros selecionados. +
+ handleToggleSobrepor(item.key)} + title={item.sobrepor ? 'Sobrepor no cadastro' : 'Ignorar e manter atual'} + /> + +
+ + {item.marca} + + {item.nivelRisco === 'critico' && ( + + ALERTA + + )} + {item.isNova && ( + + NOVA + + )} +
+
+ OF {item.of_number} • Fase {item.etapa_fase} +
+
+
+ {item.dadosImportados.perfil_principal || item.descricao || '-'} +
+
+ {item.dadosImportados.comprimento + ? `Comp: ${item.dadosImportados.comprimento} mm` + : 'Sem comp. ref.'} +
+
+ {item.apontamentoResumo.temApontamento ? ( +
+
+ + {ultimoProc ? ultimoProc.toUpperCase() : 'PRODUÇÃO'} +
+
+ {totalApontado} un apontadas +
+ {item.apontamentoResumo.emRomaneio && ( +
+ ⚠️ Já expedida em Romaneio +
+ )} +
+ ) : ( + + + Não iniciada (0%) + + )} +
+ {item.isNova ? ( + + Peça nova para cadastro + + ) : item.isIdentica ? ( + + Dados 100% idênticos à base + + ) : ( +
+
+ {item.camposDivergentes.map((c) => ( +
+ {c.label}: + {c.valorAntigo} + + {c.valorNovo} +
+ ))} +
+ + {/* Resumo de componentes se houver */} + {item.componentesDivergentes.length > 0 && ( + + )} +
+ )} +
+ {item.apontamentoResumo.temApontamento || item.nivelRisco === 'critico' ? ( +
+ handleToggleAtencaoEspecial(item.key)} + title="Incluir na Ordem de Intervenção Fabril Impressa" + /> + + Lembrete + +
+ ) : ( + - + )} +
+
+
+ + Detalhamento de Subcomponentes da Peça {item.marca}: +
+
+ {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 ( +
+ + Pos. {comp.marca_componente} + +
+
{comp.descricao || comp.perfil || 'Componente'}
+
{comp.detalhe}
+
+
+ ); + })} +
+
+
+
+
+ + {/* Rodapé da Janela com Decisão e Ações */} + +
+ + {counts.sobreporTotal} a sobrepor + + • + + {counts.novas} novas + + • + + {counts.total - counts.sobreporTotal} mantidas originais + + {counts.atencaoEspecialTotal > 0 && ( + <> + • + + {counts.atencaoEspecialTotal} na lista impressa + + + )} +
+ +
+ + +
+
+
+
+ + {/* Modal de Impressão da Ordem de Intervenção */} + { + 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'} + /> + + ); +}; diff --git a/src/components/pecas/ImportarPecasModal.tsx b/src/components/pecas/ImportarPecasModal.tsx index 534a18c..0b8d750 100644 --- a/src/components/pecas/ImportarPecasModal.tsx +++ b/src/components/pecas/ImportarPecasModal.tsx @@ -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(null); + const [componentesMapDiff, setComponentesMapDiff] = useState>(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 ( - + <> + @@ -601,5 +618,19 @@ export function ImportarPecasModal({ open, onOpenChange, onImport, pecasExistent + + {/* Modal de Detecção de Conflitos e Conciliação */} + { + setShowDiffModal(false); + handleClose(); + }} + /> + ); } diff --git a/src/components/pecas/ImportarXLSModal.tsx b/src/components/pecas/ImportarXLSModal.tsx index 250b4cb..d58b19d 100644 --- a/src/components/pecas/ImportarXLSModal.tsx +++ b/src/components/pecas/ImportarXLSModal.tsx @@ -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(null); + const [componentesMapDiff, setComponentesMapDiff] = useState>(new Map()); + const [showDiffModal, setShowDiffModal] = useState(false); const fileInputRef = useRef(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 ( - + <> + @@ -767,5 +789,19 @@ export function ImportarXLSModal({ )} + + {/* Modal Comparador e Conciliador de Peças Repetidas */} + { + setShowDiffModal(false); + handleModalOpenChange(false); + }} + /> + ); } diff --git a/src/components/pecas/IntervencaoFabrilPrintModal.tsx b/src/components/pecas/IntervencaoFabrilPrintModal.tsx new file mode 100644 index 0000000..9a45444 --- /dev/null +++ b/src/components/pecas/IntervencaoFabrilPrintModal.tsx @@ -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 = ({ + open, + onOpenChange, + itens, + ofNumber = '', + usuarioNome = 'Operador PCP', +}) => { + const printRef = useRef(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 ( + + + {/* Cabeçalho da Janela */} + +
+
+ +
+
+ + Ordem de Intervenção Fabril & Lista de Lembretes + + {itens.length} peça(s) com atenção especial + + +

+ Documento de alerta para o chão de fábrica sobre peças alteradas que já possuem apontamentos de produção. +

+
+
+
+ + +
+
+ + {/* Conteúdo Imprimível */} + +
+ {/* Cabeçalho Oficial do Relatório */} +
+
+
+ + TRACKSTEEL + + Industrial PCP + +
+

+ ORDEM DE INTERVENÇÃO FABRIL & ALERTA DE REVISÃO +

+

+ Atenção especial por atualização de engenharia em peças com processo fabril em andamento +

+
+ +
+

OF: {ofNumber || 'Geral / Múltiplas'}

+

Data/Hora: {new Date().toLocaleString('pt-BR')}

+

Responsável: {usuarioNome}

+

Peças Afetadas: {itens.length}

+
+
+ + {/* Aviso de Destaque */} +
+ +
+ COMUNICADO URGENTE AOS LÍDERES DE PRODUÇÃO: 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. +
+
+ + {/* Tabela de Ações e Lembretes */} +
+ + + + + + + + + + + + + + {itens.map((item, index) => { + const ultimoProc = item.apontamentoResumo.ultimoProcessoNome || 'Não iniciado'; + const totalApontado = item.apontamentoResumo.totalProduzido; + + return ( + + + + + + + + + + ); + })} + +
ItemOF / MarcaPerfil / DescriçãoStatus no Chão de FábricaO Que Mudou (Engenharia)Ação RecomendadaVisto do Líder
+ {index + 1} + +
{item.marca}
+
+ {item.of_number} • Fase {item.etapa_fase} +
+
+
+ {item.dadosImportados.perfil_principal || item.descricao || '-'} +
+ {item.dadosImportados.comprimento && ( +
+ Comp: {item.dadosImportados.comprimento} mm +
+ )} +
+
+ {ultimoProc.toUpperCase()} +
+
+ {totalApontado} de {item.dadosExistentes?.quantidade || item.dadosImportados.quantidade} un apontadas +
+ {item.apontamentoResumo.emRomaneio && ( +
+ ⚠️ EM ROMANEIO +
+ )} +
+ {item.camposDivergentes.map((c) => ( +
+ {c.label}:{' '} + {c.valorAntigo}{' '} + ➔ {c.valorNovo} +
+ ))} + {item.componentesDivergentes.map((comp) => ( +
+ • {comp.detalhe} +
+ ))} +
+
+ {item.acaoRecomendadaFabril || item.motivoAlerta || 'Verificar peça na fábrica.'} +
+
+
+
Assinatura / Data
+
+
+ + {/* Rodapé de Assinaturas */} +
+
+
+
Engenharia / Projetista
+
Liberação de Revisão
+
+
+
+
PCP / Planejamento
+
Ajuste de Fila & Diário
+
+
+
+
Líder de Fabricação
+
Ciente & Intervenção Física
+
+
+
+
+ + {/* Rodapé da Janela */} + +
+ + Esta folha pode ser impressa em qualquer impressora A4 ou salva em PDF pelo navegador. +
+ +
+ + {/* Estilo Global de Impressão (@media print) */} +