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