feat: sistema de deteccao de conflitos, conciliacao e ordem de intervencao fabril na importacao de pecas
This commit is contained in:
@@ -26,6 +26,9 @@ import * as XLSX from 'xlsx';
|
|||||||
import { useAppLabels } from '@/hooks/useAppLabels';
|
import { useAppLabels } from '@/hooks/useAppLabels';
|
||||||
import { usePecas } from '@/hooks/usePecas';
|
import { usePecas } from '@/hooks/usePecas';
|
||||||
import { supabase } from '@/integrations/supabase/client';
|
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 {
|
export interface ComponentItem {
|
||||||
marca: string;
|
marca: string;
|
||||||
@@ -145,6 +148,9 @@ const AdvanceSteelConverterContent: React.FC = () => {
|
|||||||
const [statusText, setStatusText] = useState<string>('Aguardando arquivo PDF...');
|
const [statusText, setStatusText] = useState<string>('Aguardando arquivo PDF...');
|
||||||
const [currentFileName, setCurrentFileName] = useState<string>('Lista_Pecas');
|
const [currentFileName, setCurrentFileName] = useState<string>('Lista_Pecas');
|
||||||
const [useIframeMode, setUseIframeMode] = useState<boolean>(false);
|
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 fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const logEndRef = useRef<HTMLDivElement>(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);
|
await importPecas(importacaoRows);
|
||||||
toast.success(`${extractedData.length} peças e seus componentes foram importados com sucesso!`);
|
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.`);
|
addLog(`✅ Importação concluída: ${extractedData.length} peças gravadas no banco.`);
|
||||||
@@ -1160,6 +1178,19 @@ const AdvanceSteelConverterContent: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ import {
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import { usePecas } from '@/hooks/usePecas';
|
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 {
|
export interface BocadConvertedPiece {
|
||||||
of: string;
|
of: string;
|
||||||
@@ -78,6 +81,9 @@ export const BocadConverter: React.FC = () => {
|
|||||||
const [headerFase, setHeaderFase] = useState<string>('');
|
const [headerFase, setHeaderFase] = useState<string>('');
|
||||||
const [processingLogs, setProcessingLogs] = useState<string[]>([]);
|
const [processingLogs, setProcessingLogs] = useState<string[]>([]);
|
||||||
const [showLogs, setShowLogs] = useState(false);
|
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);
|
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);
|
await importPecas(importacaoRows);
|
||||||
toast.success(`${extractedPieces.length} peças foram importadas com sucesso!`);
|
toast.success(`${extractedPieces.length} peças foram importadas com sucesso!`);
|
||||||
addLog(`✅ Importação concluída: ${extractedPieces.length} peças gravadas no banco.`);
|
addLog(`✅ Importação concluída: ${extractedPieces.length} peças gravadas no banco.`);
|
||||||
@@ -866,6 +884,19 @@ export const BocadConverter: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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>
|
</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'}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -9,6 +9,9 @@ import { ScrollArea } from '@/components/ui/scroll-area';
|
|||||||
import { AlertTriangle, Check, X, Upload, FileSpreadsheet, Weight, AlertCircle, Edit3 } from 'lucide-react';
|
import { AlertTriangle, Check, X, Upload, FileSpreadsheet, Weight, AlertCircle, Edit3 } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { parseCSV } from '@/utils/csvUtils';
|
import { parseCSV } from '@/utils/csvUtils';
|
||||||
|
import { analisarConflitosImportacao, consolidarPecasEComponentes } from '@/utils/importDiffService';
|
||||||
|
import { ResultadoAnaliseImportacao } from '@/types/importDiff';
|
||||||
|
import { ImportarPecasDiffModal } from '@/components/pecas/ImportarPecasDiffModal';
|
||||||
|
|
||||||
interface PecaCSVData {
|
interface PecaCSVData {
|
||||||
of_number: string;
|
of_number: string;
|
||||||
@@ -62,6 +65,9 @@ export function ImportarPecasModal({ open, onOpenChange, onImport, pecasExistent
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [step, setStep] = useState<'upload' | 'preview'>('upload');
|
const [step, setStep] = useState<'upload' | 'preview'>('upload');
|
||||||
const [showErrors, setShowErrors] = useState(false);
|
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 = [
|
const requiredHeaders = [
|
||||||
'of_number', 'etapa_fase', 'marca', 'descricao', 'quantidade',
|
'of_number', 'etapa_fase', 'marca', 'descricao', 'quantidade',
|
||||||
@@ -307,19 +313,29 @@ export function ImportarPecasModal({ open, onOpenChange, onImport, pecasExistent
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleImport = async () => {
|
const handleImport = async () => {
|
||||||
const validPecas = csvData.filter((_, index) => !duplicates[index]?.isDuplicate);
|
if (csvData.length === 0) {
|
||||||
|
|
||||||
console.log('🚀 Iniciando importação de', validPecas.length, 'peças...');
|
|
||||||
|
|
||||||
if (validPecas.length === 0) {
|
|
||||||
toast.error('Nenhuma peça válida para importar');
|
toast.error('Nenhuma peça válida para importar');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
await onImport(validPecas);
|
// Analisar conflitos e status na base de dados
|
||||||
console.log('✅ Importação concluída com sucesso!');
|
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();
|
handleClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Erro na importação:', error);
|
console.error('❌ Erro na importação:', error);
|
||||||
@@ -352,6 +368,7 @@ export function ImportarPecasModal({ open, onOpenChange, onImport, pecasExistent
|
|||||||
.reduce((sum, peca) => sum + peca.peso_total, 0);
|
.reduce((sum, peca) => sum + peca.peso_total, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Dialog open={open} onOpenChange={handleClose}>
|
<Dialog open={open} onOpenChange={handleClose}>
|
||||||
<DialogContent className="max-w-6xl max-h-[80vh]">
|
<DialogContent className="max-w-6xl max-h-[80vh]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -601,5 +618,19 @@ export function ImportarPecasModal({ open, onOpenChange, onImport, pecasExistent
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { FileSpreadsheet, Upload, Download, CheckCircle2, AlertCircle, Trash2, ArrowLeft, RefreshCw } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import * as XLSX from 'xlsx';
|
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 {
|
export interface PecaXLSData {
|
||||||
of_number: string;
|
of_number: string;
|
||||||
@@ -55,6 +58,9 @@ export function ImportarXLSModal({
|
|||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
const [isImporting, setIsImporting] = useState(false);
|
const [isImporting, setIsImporting] = useState(false);
|
||||||
const [step, setStep] = useState<'upload' | 'preview'>('upload');
|
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 fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const resetState = () => {
|
const resetState = () => {
|
||||||
@@ -478,8 +484,23 @@ export function ImportarXLSModal({
|
|||||||
quantidade_por_peca: p.quantidade_por_peca,
|
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);
|
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);
|
handleModalOpenChange(false);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
console.error('Erro ao importar peças do Excel:', err);
|
console.error('Erro ao importar peças do Excel:', err);
|
||||||
@@ -544,6 +565,7 @@ export function ImportarXLSModal({
|
|||||||
const totalQuantidade = pecasProcessadas.reduce((acc, p) => acc + (p.quantidade || 0), 0);
|
const totalQuantidade = pecasProcessadas.reduce((acc, p) => acc + (p.quantidade || 0), 0);
|
||||||
|
|
||||||
return (
|
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">
|
<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">
|
<DialogHeader className="pb-3 border-b border-border">
|
||||||
@@ -767,5 +789,19 @@ export function ImportarXLSModal({
|
|||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -501,7 +501,6 @@ export function usePecas() {
|
|||||||
const { data: pecasExistentes, error: queryExistentesError } = await supabase
|
const { data: pecasExistentes, error: queryExistentesError } = await supabase
|
||||||
.from('pecas')
|
.from('pecas')
|
||||||
.select('id, of_number, etapa_fase, marca')
|
.select('id, of_number, etapa_fase, marca')
|
||||||
.eq('user_id', user.id)
|
|
||||||
.in('of_number', ofsParaConsultar);
|
.in('of_number', ofsParaConsultar);
|
||||||
|
|
||||||
if (queryExistentesError) {
|
if (queryExistentesError) {
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
|
import {
|
||||||
|
DiffCampo,
|
||||||
|
DiffComponente,
|
||||||
|
ItemAnaliseImportacao,
|
||||||
|
NivelRiscoImportacao,
|
||||||
|
ProcessoApontado,
|
||||||
|
ResultadoAnaliseImportacao,
|
||||||
|
} from '@/types/importDiff';
|
||||||
|
|
||||||
|
interface CampoConfig {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
isCritico: boolean;
|
||||||
|
isNumeric?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CAMPOS_MONITORADOS: CampoConfig[] = [
|
||||||
|
{ key: 'quantidade', label: 'Quantidade (un)', isCritico: true, isNumeric: true },
|
||||||
|
{ key: 'comprimento', label: 'Comprimento (mm)', isCritico: true, isNumeric: true },
|
||||||
|
{ key: 'perfil_principal', label: 'Perfil / Bitola', isCritico: true },
|
||||||
|
{ key: 'peso_unitario', label: 'Peso Unitário (kg)', isCritico: true, isNumeric: true },
|
||||||
|
{ key: 'peso_total', label: 'Peso Total (kg)', isCritico: true, isNumeric: true },
|
||||||
|
{ key: 'material', label: 'Material', isCritico: false },
|
||||||
|
{ key: 'tratamento_superficial', label: 'Tratamento', isCritico: false },
|
||||||
|
{ key: 'descricao', label: 'Descrição', isCritico: false },
|
||||||
|
{ key: 'tem_componentes', label: 'Tem Componentes', isCritico: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normaliza e agrupa linhas de peças e subcomponentes
|
||||||
|
*/
|
||||||
|
export function consolidarPecasEComponentes(rawItems: any[]): {
|
||||||
|
pecasMap: Map<string, any>;
|
||||||
|
componentesMap: Map<string, any[]>;
|
||||||
|
} {
|
||||||
|
const pecasMap = new Map<string, any>();
|
||||||
|
const componentesMap = new Map<string, any[]>();
|
||||||
|
|
||||||
|
rawItems.forEach((item) => {
|
||||||
|
const ofNumber = String(item.of_number || '').trim();
|
||||||
|
const etapaFase = String(item.etapa_fase || '1').trim();
|
||||||
|
const marca = String(item.marca || '').trim();
|
||||||
|
|
||||||
|
if (!marca) return;
|
||||||
|
|
||||||
|
const pecaKey = `${ofNumber}-${etapaFase}-${marca}`;
|
||||||
|
|
||||||
|
const pecaData = {
|
||||||
|
of_number: ofNumber,
|
||||||
|
etapa_fase: etapaFase,
|
||||||
|
marca: marca,
|
||||||
|
descricao: item.descricao || item.perfil_principal || '',
|
||||||
|
quantidade: Number(item.quantidade) || 0,
|
||||||
|
peso_unitario: Number(item.peso_unitario) || 0,
|
||||||
|
peso_total: Number(item.peso_total) || 0,
|
||||||
|
comprimento:
|
||||||
|
item.comprimento !== undefined && item.comprimento !== null
|
||||||
|
? Number(item.comprimento)
|
||||||
|
: item.comprimento_ref
|
||||||
|
? Number(item.comprimento_ref)
|
||||||
|
: null,
|
||||||
|
tratamento_superficial: item.tratamento_superficial || 'pintura',
|
||||||
|
material: item.material || 'Aço A36',
|
||||||
|
perfil_principal: item.perfil_principal || item.descricao || '',
|
||||||
|
tem_componentes: Boolean(item.tem_componentes),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!pecasMap.has(pecaKey)) {
|
||||||
|
pecasMap.set(pecaKey, pecaData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se o item contém um subcomponente
|
||||||
|
if (item.marca_componente && String(item.marca_componente).trim()) {
|
||||||
|
const rawMarca = String(item.marca_componente).trim();
|
||||||
|
let cleanMarca = rawMarca;
|
||||||
|
const matchSuffix = rawMarca.match(/-?(\d{4,})$/) || rawMarca.match(/(\d{4,})/);
|
||||||
|
if (matchSuffix) {
|
||||||
|
cleanMarca = matchSuffix[1];
|
||||||
|
} else {
|
||||||
|
const digits = rawMarca.replace(/\D/g, '');
|
||||||
|
if (digits.length >= 4) {
|
||||||
|
cleanMarca = digits.slice(-4);
|
||||||
|
} else {
|
||||||
|
cleanMarca = String(1000 + (componentesMap.get(pecaKey)?.length || 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let numVal = parseInt(cleanMarca, 10);
|
||||||
|
if (isNaN(numVal) || numVal < 1000 || numVal > 9999) {
|
||||||
|
numVal = 1000 + (componentesMap.get(pecaKey)?.length || 0);
|
||||||
|
}
|
||||||
|
cleanMarca = String(numVal);
|
||||||
|
|
||||||
|
const compData = {
|
||||||
|
marca_componente: cleanMarca,
|
||||||
|
descricao: item.descricao_componente || item.descricao || '',
|
||||||
|
perfil: item.perfil_componente || item.perfil_principal || '',
|
||||||
|
peso_unitario: Number(item.peso_unitario_componente) || 0,
|
||||||
|
comprimento: item.comprimento_componente ? Number(item.comprimento_componente) : null,
|
||||||
|
quantidade_por_peca: Math.max(1, Math.round(Number(item.quantidade_por_peca) || 1)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!componentesMap.has(pecaKey)) {
|
||||||
|
componentesMap.set(pecaKey, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evita duplicatas idênticas no mesmo lote
|
||||||
|
const comps = componentesMap.get(pecaKey)!;
|
||||||
|
const exists = comps.some((c) => c.marca_componente === cleanMarca);
|
||||||
|
if (!exists) {
|
||||||
|
comps.push(compData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { pecasMap, componentesMap };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analisa a lista de importação e cruza com a base de dados
|
||||||
|
*/
|
||||||
|
export async function analisarConflitosImportacao(
|
||||||
|
rawItems: any[]
|
||||||
|
): Promise<ResultadoAnaliseImportacao> {
|
||||||
|
const { pecasMap, componentesMap } = consolidarPecasEComponentes(rawItems);
|
||||||
|
const pecasImportadas = Array.from(pecasMap.values());
|
||||||
|
|
||||||
|
if (pecasImportadas.length === 0) {
|
||||||
|
return {
|
||||||
|
totalImportadas: 0,
|
||||||
|
totalNovas: 0,
|
||||||
|
totalIdenticas: 0,
|
||||||
|
totalComDivergencia: 0,
|
||||||
|
totalComApontamento: 0,
|
||||||
|
totalCriticas: 0,
|
||||||
|
itens: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Extrair OFs únicas
|
||||||
|
const ofsUnicas = Array.from(
|
||||||
|
new Set(pecasImportadas.map((p) => p.of_number).filter(Boolean))
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Buscar peças existentes no Supabase
|
||||||
|
let pecasExistentes: any[] = [];
|
||||||
|
if (ofsUnicas.length > 0) {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('pecas')
|
||||||
|
.select('*')
|
||||||
|
.in('of_number', ofsUnicas);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('❌ Erro ao consultar peças existentes para diff:', error);
|
||||||
|
} else {
|
||||||
|
pecasExistentes = data || [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapear peças existentes por chave
|
||||||
|
const existingMap = new Map<string, any>();
|
||||||
|
const existingIds: string[] = [];
|
||||||
|
pecasExistentes.forEach((p) => {
|
||||||
|
const key = `${p.of_number || ''}-${p.etapa_fase || ''}-${p.marca || ''}`;
|
||||||
|
existingMap.set(key, p);
|
||||||
|
existingIds.push(p.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Buscar componentes existentes
|
||||||
|
const existingComponentsByPecaId = new Map<string, any[]>();
|
||||||
|
if (existingIds.length > 0) {
|
||||||
|
const { data: compData, error: compErr } = await supabase
|
||||||
|
.from('componentes_peca')
|
||||||
|
.select('*')
|
||||||
|
.in('peca_id', existingIds);
|
||||||
|
|
||||||
|
if (!compErr && compData) {
|
||||||
|
compData.forEach((c) => {
|
||||||
|
if (!existingComponentsByPecaId.has(c.peca_id)) {
|
||||||
|
existingComponentsByPecaId.set(c.peca_id, []);
|
||||||
|
}
|
||||||
|
existingComponentsByPecaId.get(c.peca_id)!.push(c);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Buscar apontamentos de produção existentes
|
||||||
|
const apontamentosByPecaId = new Map<string, any[]>();
|
||||||
|
if (existingIds.length > 0) {
|
||||||
|
const { data: aptData, error: aptErr } = await supabase
|
||||||
|
.from('apontamentos_producao')
|
||||||
|
.select(`
|
||||||
|
id,
|
||||||
|
peca_id,
|
||||||
|
processo_id,
|
||||||
|
quantidade_produzida,
|
||||||
|
data_apontamento,
|
||||||
|
processo:processos_fabricacao(id, nome, ordem)
|
||||||
|
`)
|
||||||
|
.in('peca_id', existingIds);
|
||||||
|
|
||||||
|
if (!aptErr && aptData) {
|
||||||
|
aptData.forEach((a) => {
|
||||||
|
if (!apontamentosByPecaId.has(a.peca_id)) {
|
||||||
|
apontamentosByPecaId.set(a.peca_id, []);
|
||||||
|
}
|
||||||
|
apontamentosByPecaId.get(a.peca_id)!.push(a);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Buscar romaneios de expedição
|
||||||
|
const pecaIdsEmRomaneio = new Set<string>();
|
||||||
|
if (existingIds.length > 0) {
|
||||||
|
const { data: romData, error: romErr } = await supabase
|
||||||
|
.from('itens_romaneio_pecas')
|
||||||
|
.select('peca_id')
|
||||||
|
.in('peca_id', existingIds);
|
||||||
|
|
||||||
|
if (!romErr && romData) {
|
||||||
|
romData.forEach((r) => pecaIdsEmRomaneio.add(r.peca_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Construir a análise detalhada item a item
|
||||||
|
const itensResultado: ItemAnaliseImportacao[] = [];
|
||||||
|
|
||||||
|
for (const pecaImportada of pecasImportadas) {
|
||||||
|
const key = `${pecaImportada.of_number || ''}-${pecaImportada.etapa_fase || ''}-${pecaImportada.marca || ''}`;
|
||||||
|
const pecaExistente = existingMap.get(key);
|
||||||
|
const incomingComps = componentesMap.get(key) || [];
|
||||||
|
|
||||||
|
if (!pecaExistente) {
|
||||||
|
// Peça NOVA
|
||||||
|
itensResultado.push({
|
||||||
|
key,
|
||||||
|
of_number: pecaImportada.of_number,
|
||||||
|
etapa_fase: pecaImportada.etapa_fase,
|
||||||
|
marca: pecaImportada.marca,
|
||||||
|
descricao: pecaImportada.descricao || pecaImportada.perfil_principal || '',
|
||||||
|
isNova: true,
|
||||||
|
isIdentica: false,
|
||||||
|
temDivergencias: false,
|
||||||
|
camposDivergentes: [],
|
||||||
|
componentesDivergentes: incomingComps.map((c) => ({
|
||||||
|
marca_componente: c.marca_componente,
|
||||||
|
descricao: c.descricao,
|
||||||
|
perfil: c.perfil,
|
||||||
|
tipo: 'adicionado',
|
||||||
|
quantidadeNova: c.quantidade_por_peca,
|
||||||
|
comprimentoNovo: c.comprimento,
|
||||||
|
detalhe: 'Novo componente na peça',
|
||||||
|
})),
|
||||||
|
dadosImportados: pecaImportada,
|
||||||
|
apontamentoResumo: {
|
||||||
|
totalProduzido: 0,
|
||||||
|
processos: [],
|
||||||
|
temApontamento: false,
|
||||||
|
emRomaneio: false,
|
||||||
|
},
|
||||||
|
nivelRisco: 'nenhum',
|
||||||
|
sobrepor: true,
|
||||||
|
atencaoEspecial: false,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Peça EXISTENTE: Comparar campos
|
||||||
|
const pecaId = pecaExistente.id;
|
||||||
|
const camposDivergentes: DiffCampo[] = [];
|
||||||
|
|
||||||
|
CAMPOS_MONITORADOS.forEach(({ key: campoKey, label, isCritico, isNumeric }) => {
|
||||||
|
let valAntigo = pecaExistente[campoKey];
|
||||||
|
let valNovo = pecaImportada[campoKey];
|
||||||
|
|
||||||
|
if (isNumeric) {
|
||||||
|
valAntigo = valAntigo !== null && valAntigo !== undefined ? Number(valAntigo) : null;
|
||||||
|
valNovo = valNovo !== null && valNovo !== undefined ? Number(valNovo) : null;
|
||||||
|
|
||||||
|
if (valAntigo !== null && valNovo !== null) {
|
||||||
|
if (Math.abs(valAntigo - valNovo) > 0.001) {
|
||||||
|
camposDivergentes.push({
|
||||||
|
campo: campoKey,
|
||||||
|
label,
|
||||||
|
valorAntigo: valAntigo,
|
||||||
|
valorNovo: valNovo,
|
||||||
|
isCritico,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (valAntigo !== valNovo) {
|
||||||
|
camposDivergentes.push({
|
||||||
|
campo: campoKey,
|
||||||
|
label,
|
||||||
|
valorAntigo: valAntigo,
|
||||||
|
valorNovo: valNovo,
|
||||||
|
isCritico,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const strAntigo = String(valAntigo || '').trim().toLowerCase();
|
||||||
|
const strNovo = String(valNovo || '').trim().toLowerCase();
|
||||||
|
|
||||||
|
if (strAntigo !== strNovo) {
|
||||||
|
camposDivergentes.push({
|
||||||
|
campo: campoKey,
|
||||||
|
label,
|
||||||
|
valorAntigo: pecaExistente[campoKey] || '-',
|
||||||
|
valorNovo: pecaImportada[campoKey] || '-',
|
||||||
|
isCritico,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Comparar componentes existentes vs importados
|
||||||
|
const existingComps = existingComponentsByPecaId.get(pecaId) || [];
|
||||||
|
const componentesDivergentes: DiffComponente[] = [];
|
||||||
|
|
||||||
|
const existingCompsMap = new Map<string, any>();
|
||||||
|
existingComps.forEach((c) => existingCompsMap.set(String(c.marca_componente).trim(), c));
|
||||||
|
|
||||||
|
const incomingCompsMap = new Map<string, any>();
|
||||||
|
incomingComps.forEach((c) => incomingCompsMap.set(String(c.marca_componente).trim(), c));
|
||||||
|
|
||||||
|
// Verificar adicionados ou modificados
|
||||||
|
incomingComps.forEach((inc) => {
|
||||||
|
const matchMarca = String(inc.marca_componente).trim();
|
||||||
|
const ext = existingCompsMap.get(matchMarca);
|
||||||
|
|
||||||
|
if (!ext) {
|
||||||
|
componentesDivergentes.push({
|
||||||
|
marca_componente: matchMarca,
|
||||||
|
descricao: inc.descricao,
|
||||||
|
perfil: inc.perfil,
|
||||||
|
tipo: 'adicionado',
|
||||||
|
quantidadeNova: inc.quantidade_por_peca,
|
||||||
|
comprimentoNovo: inc.comprimento,
|
||||||
|
detalhe: `Novo componente Pos. ${matchMarca} adicionado`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const mudouQtd = Number(ext.quantidade_por_peca || 1) !== Number(inc.quantidade_por_peca || 1);
|
||||||
|
const mudouCompr =
|
||||||
|
ext.comprimento !== null && inc.comprimento !== null
|
||||||
|
? Math.abs(Number(ext.comprimento) - Number(inc.comprimento)) > 0.1
|
||||||
|
: ext.comprimento !== inc.comprimento;
|
||||||
|
const mudouPerfil =
|
||||||
|
String(ext.perfil || '').trim().toLowerCase() !== String(inc.perfil || '').trim().toLowerCase();
|
||||||
|
|
||||||
|
if (mudouQtd || mudouCompr || mudouPerfil) {
|
||||||
|
const detalhes: string[] = [];
|
||||||
|
if (mudouQtd) detalhes.push(`Qtd: ${ext.quantidade_por_peca} ➔ ${inc.quantidade_por_peca}`);
|
||||||
|
if (mudouCompr) detalhes.push(`Compr: ${ext.comprimento || 0} ➔ ${inc.comprimento || 0} mm`);
|
||||||
|
if (mudouPerfil) detalhes.push(`Perfil: ${ext.perfil} ➔ ${inc.perfil}`);
|
||||||
|
|
||||||
|
componentesDivergentes.push({
|
||||||
|
marca_componente: matchMarca,
|
||||||
|
descricao: inc.descricao || ext.descricao,
|
||||||
|
perfil: inc.perfil || ext.perfil,
|
||||||
|
tipo: 'modificado',
|
||||||
|
quantidadeAntiga: ext.quantidade_por_peca,
|
||||||
|
quantidadeNova: inc.quantidade_por_peca,
|
||||||
|
comprimentoAntigo: ext.comprimento,
|
||||||
|
comprimentoNovo: inc.comprimento,
|
||||||
|
detalhe: detalhes.join(' | '),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verificar componentes removidos
|
||||||
|
existingComps.forEach((ext) => {
|
||||||
|
const matchMarca = String(ext.marca_componente).trim();
|
||||||
|
if (!incomingCompsMap.has(matchMarca)) {
|
||||||
|
componentesDivergentes.push({
|
||||||
|
marca_componente: matchMarca,
|
||||||
|
descricao: ext.descricao,
|
||||||
|
perfil: ext.perfil,
|
||||||
|
tipo: 'removido',
|
||||||
|
quantidadeAntiga: ext.quantidade_por_peca,
|
||||||
|
comprimentoAntigo: ext.comprimento,
|
||||||
|
detalhe: `Componente Pos. ${matchMarca} removido na nova lista`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Analisar Apontamentos Fabris
|
||||||
|
const apontamentosPeca = apontamentosByPecaId.get(pecaId) || [];
|
||||||
|
const processosMap = new Map<string, ProcessoApontado>();
|
||||||
|
|
||||||
|
apontamentosPeca.forEach((apt) => {
|
||||||
|
const procNome = apt.processo?.nome || 'Geral';
|
||||||
|
const procOrdem = apt.processo?.ordem || 99;
|
||||||
|
const procId = apt.processo_id;
|
||||||
|
|
||||||
|
if (!processosMap.has(procId)) {
|
||||||
|
processosMap.set(procId, {
|
||||||
|
processoId: procId,
|
||||||
|
processoNome: procNome,
|
||||||
|
ordem: procOrdem,
|
||||||
|
quantidadeProduzida: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
processosMap.get(procId)!.quantidadeProduzida += Number(apt.quantidade_produzida || 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const processosApontados = Array.from(processosMap.values()).sort(
|
||||||
|
(a, b) => a.ordem - b.ordem
|
||||||
|
);
|
||||||
|
const totalProduzidoMax = processosApontados.reduce(
|
||||||
|
(max, p) => Math.max(max, p.quantidadeProduzida),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const ultimoProc =
|
||||||
|
processosApontados.length > 0
|
||||||
|
? processosApontados[processosApontados.length - 1].processoNome
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const temApontamento = apontamentosPeca.length > 0;
|
||||||
|
const emRomaneio = pecaIdsEmRomaneio.has(pecaId);
|
||||||
|
|
||||||
|
const temDivergencias = camposDivergentes.length > 0 || componentesDivergentes.length > 0;
|
||||||
|
const isIdentica = !temDivergencias;
|
||||||
|
|
||||||
|
// Calcular nível de risco
|
||||||
|
let nivelRisco: NivelRiscoImportacao = 'nenhum';
|
||||||
|
let motivoAlerta = '';
|
||||||
|
let acaoRecomendadaFabril = '';
|
||||||
|
|
||||||
|
if (temDivergencias) {
|
||||||
|
const temAlteracaoCritica =
|
||||||
|
camposDivergentes.some((c) => c.isCritico) || componentesDivergentes.length > 0;
|
||||||
|
|
||||||
|
if (!temApontamento && !emRomaneio) {
|
||||||
|
nivelRisco = 'baixo';
|
||||||
|
motivoAlerta = 'Peça ainda não iniciada na fábrica. Atualização cadastral limpa.';
|
||||||
|
acaoRecomendadaFabril = 'Nenhuma ação necessária no galpão.';
|
||||||
|
} else {
|
||||||
|
// Já tem apontamento ou romaneio
|
||||||
|
if (emRomaneio) {
|
||||||
|
nivelRisco = 'critico';
|
||||||
|
motivoAlerta = `ALERTA MÁXIMO: Peça já foi expedida ou carregada em romaneio!`;
|
||||||
|
acaoRecomendadaFabril =
|
||||||
|
'Acionar PCP e Engenharia de Obra imediatamente para conferência do lote expedido.';
|
||||||
|
} else if (temAlteracaoCritica) {
|
||||||
|
nivelRisco = 'critico';
|
||||||
|
const etapaStr = ultimoProc ? `no setor de ${ultimoProc.toUpperCase()}` : 'na produção';
|
||||||
|
motivoAlerta = `Peça em andamento ${etapaStr} (${totalProduzidoMax} un produzidas) com alterações físicas/dimensionais.`;
|
||||||
|
|
||||||
|
const compReduzido = camposDivergentes.find(
|
||||||
|
(c) => c.campo === 'comprimento' && Number(c.valorNovo) < Number(c.valorAntigo)
|
||||||
|
);
|
||||||
|
const compAumentado = camposDivergentes.find(
|
||||||
|
(c) => c.campo === 'comprimento' && Number(c.valorNovo) > Number(c.valorAntigo)
|
||||||
|
);
|
||||||
|
const qtdReduzida = camposDivergentes.find(
|
||||||
|
(c) => c.campo === 'quantidade' && Number(c.valorNovo) < Number(c.valorAntigo)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (compReduzido) {
|
||||||
|
acaoRecomendadaFabril = `Retornar peça para corte/serra: comprimento deve ser reduzido em ${Math.abs(
|
||||||
|
Number(compReduzido.valorAntigo) - Number(compReduzido.valorNovo)
|
||||||
|
)} mm.`;
|
||||||
|
} else if (compAumentado) {
|
||||||
|
acaoRecomendadaFabril = `ATENÇÃO: Peça cresceu ${
|
||||||
|
Number(compAumentado.valorNovo) - Number(compAumentado.valorAntigo)
|
||||||
|
} mm. Descartar/reaproveitar corte antigo e cortar nova barra.`;
|
||||||
|
} else if (qtdReduzida) {
|
||||||
|
acaoRecomendadaFabril = `Redução de lote: verificar se excedente já fabricado deve ser estornado ou enviado para estoque de peças prontas.`;
|
||||||
|
} else if (componentesDivergentes.length > 0) {
|
||||||
|
acaoRecomendadaFabril = `Interromper solda/pintura: verificar adição/remoção de componentes na montagem antes de avançar.`;
|
||||||
|
} else {
|
||||||
|
acaoRecomendadaFabril = `Inspecionar peça na bancada de trabalho perante os novos dados técnicos.`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nivelRisco = 'medio';
|
||||||
|
motivoAlerta = 'Alteração apenas cadastral/superficial em peça com apontamento.';
|
||||||
|
acaoRecomendadaFabril = 'Verificar etiquetas e conferir especificação na pintura.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configura ações padrão
|
||||||
|
itensResultado.push({
|
||||||
|
key,
|
||||||
|
of_number: pecaImportada.of_number,
|
||||||
|
etapa_fase: pecaImportada.etapa_fase,
|
||||||
|
marca: pecaImportada.marca,
|
||||||
|
descricao: pecaImportada.descricao || pecaExistente.descricao || '',
|
||||||
|
isNova: false,
|
||||||
|
isIdentica,
|
||||||
|
temDivergencias,
|
||||||
|
camposDivergentes,
|
||||||
|
componentesDivergentes,
|
||||||
|
dadosImportados: pecaImportada,
|
||||||
|
dadosExistentes: pecaExistente,
|
||||||
|
pecaIdExistente: pecaId,
|
||||||
|
apontamentoResumo: {
|
||||||
|
totalProduzido: totalProduzidoMax,
|
||||||
|
processos: processosApontados,
|
||||||
|
ultimoProcessoNome: ultimoProc,
|
||||||
|
temApontamento,
|
||||||
|
emRomaneio,
|
||||||
|
},
|
||||||
|
nivelRisco,
|
||||||
|
sobrepor: temDivergencias, // Sobrepor marcado por padrão se tem divergência
|
||||||
|
atencaoEspecial: nivelRisco === 'critico', // Atenção especial marcada automaticamente se crítico
|
||||||
|
motivoAlerta,
|
||||||
|
acaoRecomendadaFabril,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordenar: críticas primeiro, depois com divergência, depois novas, depois idênticas
|
||||||
|
itensResultado.sort((a, b) => {
|
||||||
|
const pesoRisco = { critico: 4, medio: 3, baixo: 2, nenhum: 1 };
|
||||||
|
const diffRisco = pesoRisco[b.nivelRisco] - pesoRisco[a.nivelRisco];
|
||||||
|
if (diffRisco !== 0) return diffRisco;
|
||||||
|
if (b.temDivergencias !== a.temDivergencias) return b.temDivergencias ? 1 : -1;
|
||||||
|
return a.marca.localeCompare(b.marca, undefined, { numeric: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalImportadas: itensResultado.length,
|
||||||
|
totalNovas: itensResultado.filter((i) => i.isNova).length,
|
||||||
|
totalIdenticas: itensResultado.filter((i) => i.isIdentica).length,
|
||||||
|
totalComDivergencia: itensResultado.filter((i) => i.temDivergencias).length,
|
||||||
|
totalComApontamento: itensResultado.filter((i) => i.apontamentoResumo.temApontamento).length,
|
||||||
|
totalCriticas: itensResultado.filter((i) => i.nivelRisco === 'critico').length,
|
||||||
|
itens: itensResultado,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
|
import { ItemAnaliseImportacao } from '@/types/importDiff';
|
||||||
|
|
||||||
|
export interface ExecucaoImportacaoParams {
|
||||||
|
itens: ItemAnaliseImportacao[];
|
||||||
|
componentesMap: Map<string, any[]>;
|
||||||
|
userId: string;
|
||||||
|
userNome?: string;
|
||||||
|
userEmail?: string;
|
||||||
|
motivoRevisao?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResultadoExecucaoImportacao {
|
||||||
|
sucesso: boolean;
|
||||||
|
totalInseridas: number;
|
||||||
|
totalAtualizadas: number;
|
||||||
|
totalIgnoradas: number;
|
||||||
|
totalComAtencaoEspecial: number;
|
||||||
|
itensAtencaoEspecial: ItemAnaliseImportacao[];
|
||||||
|
mensagem: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executa a importação conciliada respeitando as decisões tomadas pelo usuário no modal.
|
||||||
|
* Preserva 100% dos IDs existentes, garantindo que nenhum apontamento de produção ou romaneio seja afetado.
|
||||||
|
*/
|
||||||
|
export async function executarImportacaoConciliada(
|
||||||
|
params: ExecucaoImportacaoParams
|
||||||
|
): Promise<ResultadoExecucaoImportacao> {
|
||||||
|
const {
|
||||||
|
itens,
|
||||||
|
componentesMap,
|
||||||
|
userId,
|
||||||
|
userNome = 'Operador',
|
||||||
|
userEmail = '',
|
||||||
|
motivoRevisao = 'Atualização de projeto via importação de lista de peças',
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
console.log('🚀 Executando importação conciliada para', itens.length, 'itens analisados');
|
||||||
|
|
||||||
|
let totalInseridas = 0;
|
||||||
|
let totalAtualizadas = 0;
|
||||||
|
let totalIgnoradas = 0;
|
||||||
|
const itensAtencaoEspecial: ItemAnaliseImportacao[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Separar itens por ação
|
||||||
|
const pecasNovas = itens.filter((i) => i.isNova && i.sobrepor);
|
||||||
|
const pecasParaAtualizar = itens.filter(
|
||||||
|
(i) => !i.isNova && i.sobrepor && (i.temDivergencias || i.isIdentica)
|
||||||
|
);
|
||||||
|
const pecasIgnoradas = itens.filter((i) => !i.sobrepor);
|
||||||
|
|
||||||
|
totalIgnoradas = pecasIgnoradas.length;
|
||||||
|
|
||||||
|
// 2. Atualizar peças existentes selecionadas mantendo o UUID
|
||||||
|
for (const item of pecasParaAtualizar) {
|
||||||
|
if (!item.pecaIdExistente) continue;
|
||||||
|
|
||||||
|
const pecaId = item.pecaIdExistente;
|
||||||
|
const importado = item.dadosImportados;
|
||||||
|
const existente = item.dadosExistentes || {};
|
||||||
|
|
||||||
|
// Se é idêntica e estava marcada, apenas pula a escrita desnecessária
|
||||||
|
if (item.isIdentica) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se marcada para atenção especial, memorizar para a lista impressa
|
||||||
|
if (item.atencaoEspecial) {
|
||||||
|
itensAtencaoEspecial.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
const qtdFinal = Number(importado.quantidade || existente.quantidade || 0);
|
||||||
|
const pesoUnitFinal = Number(importado.peso_unitario || existente.peso_unitario || 0);
|
||||||
|
const pesoTotalCalculado = Number((qtdFinal * pesoUnitFinal).toFixed(3)) || Number(importado.peso_total || 0);
|
||||||
|
|
||||||
|
const payloadUpdate: any = {
|
||||||
|
descricao: importado.descricao || existente.descricao || '',
|
||||||
|
quantidade: qtdFinal,
|
||||||
|
peso_unitario: pesoUnitFinal,
|
||||||
|
peso_total: pesoTotalCalculado,
|
||||||
|
comprimento: importado.comprimento !== undefined ? importado.comprimento : existente.comprimento,
|
||||||
|
tratamento_superficial: importado.tratamento_superficial || existente.tratamento_superficial || 'pintura',
|
||||||
|
material: importado.material || existente.material || 'Aço A36',
|
||||||
|
perfil_principal: importado.perfil_principal || existente.perfil_principal || '',
|
||||||
|
tem_componentes: importado.tem_componentes !== undefined ? importado.tem_componentes : existente.tem_componentes,
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { error: updateErr } = await supabase
|
||||||
|
.from('pecas')
|
||||||
|
.update(payloadUpdate)
|
||||||
|
.eq('id', pecaId);
|
||||||
|
|
||||||
|
if (updateErr) {
|
||||||
|
console.error(`❌ Erro ao atualizar peça ${item.marca}:`, updateErr);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalAtualizadas++;
|
||||||
|
|
||||||
|
// Atualizar componentes da peça existente
|
||||||
|
const novosComponentes = componentesMap.get(item.key) || [];
|
||||||
|
if (novosComponentes.length > 0) {
|
||||||
|
// Limpar componentes antigos e inserir os novos
|
||||||
|
await supabase.from('componentes_peca').delete().eq('peca_id', pecaId);
|
||||||
|
|
||||||
|
const componentesFormatados = novosComponentes.map((comp) => ({
|
||||||
|
...comp,
|
||||||
|
peca_id: pecaId,
|
||||||
|
user_id: userId,
|
||||||
|
}));
|
||||||
|
|
||||||
|
await supabase.from('componentes_peca').insert(componentesFormatados);
|
||||||
|
} else if (item.camposDivergentes.some((c) => c.campo === 'tem_componentes' && !c.valorNovo)) {
|
||||||
|
// Peça virou simples sem componentes: limpar antigos
|
||||||
|
await supabase.from('componentes_peca').delete().eq('peca_id', pecaId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registrar Histórico de Revisão na tabela 'revisoes' se houve divergência
|
||||||
|
if (item.temDivergencias) {
|
||||||
|
const camposAlterados = item.camposDivergentes.map((c) => c.campo);
|
||||||
|
if (item.componentesDivergentes.length > 0) {
|
||||||
|
camposAlterados.push('componentes');
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxNumeroRevisao = existente.revisao_atual
|
||||||
|
? `R${(Number(existente.revisao_atual.replace(/\D/g, '')) || 0) + 1}`
|
||||||
|
: 'R1';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await supabase.from('revisoes' as any).insert({
|
||||||
|
of_number: item.of_number,
|
||||||
|
peca_id: pecaId,
|
||||||
|
numero_revisao: proxNumeroRevisao,
|
||||||
|
tipo_revisao: item.nivelRisco === 'critico' ? 'critica' : 'simples',
|
||||||
|
descricao_motivo: motivoRevisao,
|
||||||
|
observacoes: item.motivoAlerta || item.acaoRecomendadaFabril || null,
|
||||||
|
usuario_id: userId || null,
|
||||||
|
usuario_nome: userNome,
|
||||||
|
usuario_email: userEmail,
|
||||||
|
dados_anteriores: existente,
|
||||||
|
dados_novos: importado,
|
||||||
|
campos_alterados: camposAlterados,
|
||||||
|
afeta_apontamentos: item.apontamentoResumo.temApontamento,
|
||||||
|
impacto_processos: {
|
||||||
|
divergencias: item.camposDivergentes.map((c) => ({
|
||||||
|
campo: c.label,
|
||||||
|
antes: c.valorAntigo,
|
||||||
|
depois: c.valorNovo,
|
||||||
|
})),
|
||||||
|
componentes_alterados: item.componentesDivergentes,
|
||||||
|
ultimo_processo_afetado: item.apontamentoResumo.ultimoProcessoNome || null,
|
||||||
|
total_produzido: item.apontamentoResumo.totalProduzido,
|
||||||
|
atencao_especial_marcada: item.atencaoEspecial,
|
||||||
|
},
|
||||||
|
data_revisao: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
} catch (revError) {
|
||||||
|
console.warn('Aviso: Não foi possível gravar histórico em revisoes:', revError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Inserir peças novas
|
||||||
|
if (pecasNovas.length > 0) {
|
||||||
|
const pecasParaInsert = pecasNovas.map((p) => {
|
||||||
|
const imp = p.dadosImportados;
|
||||||
|
const qtd = Number(imp.quantidade) || 0;
|
||||||
|
const pesoU = Number(imp.peso_unitario) || 0;
|
||||||
|
const pesoT = Number(imp.peso_total) || Number((qtd * pesoU).toFixed(3));
|
||||||
|
|
||||||
|
return {
|
||||||
|
of_number: imp.of_number,
|
||||||
|
etapa_fase: imp.etapa_fase,
|
||||||
|
marca: imp.marca,
|
||||||
|
descricao: imp.descricao || imp.perfil_principal || '',
|
||||||
|
quantidade: qtd,
|
||||||
|
peso_unitario: pesoU,
|
||||||
|
peso_total: pesoT,
|
||||||
|
comprimento: imp.comprimento || null,
|
||||||
|
tratamento_superficial: imp.tratamento_superficial || 'pintura',
|
||||||
|
material: imp.material || 'Aço A36',
|
||||||
|
perfil_principal: imp.perfil_principal || '',
|
||||||
|
tem_componentes: Boolean(imp.tem_componentes),
|
||||||
|
user_id: userId,
|
||||||
|
prioridade: 'P4',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: pecasInseridas, error: insertError } = await supabase
|
||||||
|
.from('pecas')
|
||||||
|
.insert(pecasParaInsert)
|
||||||
|
.select('id, of_number, etapa_fase, marca');
|
||||||
|
|
||||||
|
if (insertError) {
|
||||||
|
console.error('❌ Erro ao inserir peças novas:', insertError);
|
||||||
|
throw insertError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pecasInseridas) {
|
||||||
|
totalInseridas = pecasInseridas.length;
|
||||||
|
|
||||||
|
// Inserir componentes das peças novas
|
||||||
|
const componentesParaInsert: any[] = [];
|
||||||
|
pecasInseridas.forEach((p) => {
|
||||||
|
const key = `${p.of_number || ''}-${p.etapa_fase || ''}-${p.marca || ''}`;
|
||||||
|
const comps = componentesMap.get(key) || [];
|
||||||
|
comps.forEach((c) => {
|
||||||
|
componentesParaInsert.push({
|
||||||
|
...c,
|
||||||
|
peca_id: p.id,
|
||||||
|
user_id: userId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (componentesParaInsert.length > 0) {
|
||||||
|
await supabase.from('componentes_peca').insert(componentesParaInsert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mensagem = `✅ Importação concluída!`;
|
||||||
|
if (totalInseridas > 0) mensagem += ` ${totalInseridas} nova(s) peça(s) inserida(s).`;
|
||||||
|
if (totalAtualizadas > 0) mensagem += ` ${totalAtualizadas} peça(s) sobreposta(s)/atualizada(s).`;
|
||||||
|
if (totalIgnoradas > 0) mensagem += ` ${totalIgnoradas} peça(s) mantida(s) no cadastro original.`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
sucesso: true,
|
||||||
|
totalInseridas,
|
||||||
|
totalAtualizadas,
|
||||||
|
totalIgnoradas,
|
||||||
|
totalComAtencaoEspecial: itensAtencaoEspecial.length,
|
||||||
|
itensAtencaoEspecial,
|
||||||
|
mensagem,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ Falha na execução da importação conciliada:', error);
|
||||||
|
return {
|
||||||
|
sucesso: false,
|
||||||
|
totalInseridas: 0,
|
||||||
|
totalAtualizadas: 0,
|
||||||
|
totalIgnoradas: 0,
|
||||||
|
totalComAtencaoEspecial: 0,
|
||||||
|
itensAtencaoEspecial: [],
|
||||||
|
mensagem: `Erro na importação: ${error?.message || 'Falha de comunicação com o banco'}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user