import React, { useState } 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 { Label } from '@/components/ui/label'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Badge } from '@/components/ui/badge'; 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; etapa_fase: string; marca: string; descricao: string; quantidade: number; peso_unitario: number; peso_total: number; tratamento_superficial: string; material: string; perfil_principal: string; tem_componentes: boolean; marca_componente?: string; descricao_componente?: string; perfil_componente?: string; peso_unitario_componente?: number; quantidade_por_peca?: number; } interface DuplicateInfo { index: number; marca: string; of_number: string; etapa_fase: string; isDuplicate: boolean; } interface ValidationError { row: number; column: string; value: any; error: string; suggestion: string; canAutoFix: boolean; } interface ImportarPecasModalProps { open: boolean; onOpenChange: (open: boolean) => void; onImport: (pecas: PecaCSVData[]) => Promise; pecasExistentes: Array<{ marca: string; of_number: string; etapa_fase: string }>; } export function ImportarPecasModal({ open, onOpenChange, onImport, pecasExistentes }: ImportarPecasModalProps) { const [csvFile, setCsvFile] = useState(null); const [csvData, setCsvData] = useState([]); const [rawData, setRawData] = useState([]); const [duplicates, setDuplicates] = useState([]); const [validationErrors, setValidationErrors] = useState([]); const [isLoading, setIsLoading] = useState(false); const [step, setStep] = useState<'upload' | 'preview'>('upload'); const [showErrors, setShowErrors] = useState(false); const [analiseDiff, setAnaliseDiff] = useState(null); const [componentesMapDiff, setComponentesMapDiff] = useState>(new Map()); const [showDiffModal, setShowDiffModal] = useState(false); const requiredHeaders = [ 'of_number', 'etapa_fase', 'marca', 'descricao', 'quantidade', 'peso_unitario', 'peso_total', 'tratamento_superficial', 'material', 'perfil_principal', 'tem_componentes', 'marca_componente', 'descricao_componente', 'perfil_componente', 'peso_unitario_componente', 'quantidade_por_peca' ]; const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file && file.type === 'text/csv') { setCsvFile(file); console.log('📁 Arquivo CSV selecionado:', file.name, 'Tamanho:', file.size); } else { toast.error('Por favor, selecione um arquivo CSV válido'); } }; const validateAndCleanData = (rawRow: any, rowIndex: number): { data: PecaCSVData | null; errors: ValidationError[] } => { const errors: ValidationError[] = []; console.log(`🔍 Validando linha ${rowIndex + 1}:`, rawRow); // Verificar campos obrigatórios const requiredStringFields = ['of_number', 'marca', 'descricao']; requiredStringFields.forEach(field => { if (!rawRow[field] || rawRow[field].toString().trim() === '') { errors.push({ row: rowIndex + 1, column: field, value: rawRow[field], error: 'Campo obrigatório vazio', suggestion: 'Preencha este campo obrigatório', canAutoFix: false }); } }); // Validar e converter campos numéricos const numericFields = ['quantidade', 'peso_unitario', 'peso_total', 'peso_unitario_componente', 'quantidade_por_peca']; const convertedData: any = { ...rawRow }; numericFields.forEach(field => { const value = rawRow[field]; if (value !== null && value !== undefined && value !== '') { // Limpar e converter string para número let cleanValue = value.toString().replace(',', '.'); const numValue = parseFloat(cleanValue); if (isNaN(numValue)) { errors.push({ row: rowIndex + 1, column: field, value: value, error: 'Valor numérico inválido', suggestion: `Corrigir para um número válido (ex: ${field === 'quantidade' ? '10' : '152.5'})`, canAutoFix: /^[\d,\.]+$/.test(value.toString()) }); convertedData[field] = 0; } else { convertedData[field] = numValue; } } else { convertedData[field] = field.includes('componente') ? 0 : (field === 'quantidade' ? 1 : 0); } }); // Validar campo booleano const boolValue = rawRow.tem_componentes; if (boolValue !== null && boolValue !== undefined) { const strValue = boolValue.toString().toLowerCase().trim(); if (['true', '1', 'sim', 'yes'].includes(strValue)) { convertedData.tem_componentes = true; } else if (['false', '0', 'não', 'nao', 'no'].includes(strValue)) { convertedData.tem_componentes = false; } else { errors.push({ row: rowIndex + 1, column: 'tem_componentes', value: boolValue, error: 'Valor booleano inválido', suggestion: 'Use: true/false, 1/0, sim/não', canAutoFix: true }); convertedData.tem_componentes = false; } } else { convertedData.tem_componentes = false; } if (errors.length > 0) { console.log(`❌ Erros na linha ${rowIndex + 1}:`, errors); return { data: null, errors }; } // Criar objeto final const processedData: PecaCSVData = { of_number: convertedData.of_number?.toString().trim() || '', etapa_fase: convertedData.etapa_fase?.toString().trim() || '', marca: convertedData.marca?.toString().trim() || '', descricao: convertedData.descricao?.toString().trim() || '', quantidade: convertedData.quantidade || 0, peso_unitario: convertedData.peso_unitario || 0, peso_total: convertedData.peso_total || 0, tratamento_superficial: convertedData.tratamento_superficial?.toString().trim() || '', material: convertedData.material?.toString().trim() || '', perfil_principal: convertedData.perfil_principal?.toString().trim() || '', tem_componentes: convertedData.tem_componentes, marca_componente: convertedData.marca_componente?.toString().trim() || '', descricao_componente: convertedData.descricao_componente?.toString().trim() || '', perfil_componente: convertedData.perfil_componente?.toString().trim() || '', peso_unitario_componente: convertedData.peso_unitario_componente || 0, quantidade_por_peca: convertedData.quantidade_por_peca || 0 }; console.log(`✅ Linha ${rowIndex + 1} processada:`, processedData); return { data: processedData, errors: [] }; }; const processCSV = async () => { if (!csvFile) return; console.log('🔄 Processando arquivo CSV...'); setIsLoading(true); try { const text = await csvFile.text(); const parsed = parseCSV(text); console.log('📊 Dados parseados:', parsed.length, 'linhas'); if (parsed.length === 0) { toast.error('Arquivo CSV vazio ou inválido'); return; } setRawData(parsed); // Verificar headers const firstRow = parsed[0]; const missingHeaders = requiredHeaders.filter(header => !(header in firstRow)); if (missingHeaders.length > 0) { console.error('❌ Headers obrigatórios ausentes:', missingHeaders); toast.error(`Colunas obrigatórias ausentes: ${missingHeaders.join(', ')}`); return; } // Processar e validar cada linha const validData: PecaCSVData[] = []; const allErrors: ValidationError[] = []; parsed.forEach((row, index) => { const { data, errors } = validateAndCleanData(row, index); if (data) { validData.push(data); } allErrors.push(...errors); }); console.log('✅ Linhas válidas processadas:', validData.length); console.log('❌ Erros encontrados:', allErrors.length); setValidationErrors(allErrors); // Verificar duplicatas para dados válidos const duplicateInfo: DuplicateInfo[] = validData.map((peca, index) => { const isDuplicate = pecasExistentes.some(existente => existente.marca === peca.marca && existente.of_number === peca.of_number && existente.etapa_fase === peca.etapa_fase ); return { index, marca: peca.marca, of_number: peca.of_number, etapa_fase: peca.etapa_fase, isDuplicate }; }); setCsvData(validData); setDuplicates(duplicateInfo); setStep('preview'); // Mostrar resumo if (allErrors.length > 0) { toast.warning(`${parsed.length - validData.length} linha(s) com problemas encontradas. Verifique os detalhes.`); } else { toast.success(`${validData.length} peça(s) válida(s) encontrada(s) para importação!`); } } catch (error) { console.error('❌ Erro ao processar CSV:', error); toast.error('Erro ao processar arquivo CSV'); } finally { setIsLoading(false); } }; const handleAutoFix = () => { const fixableErrors = validationErrors.filter(err => err.canAutoFix); if (fixableErrors.length === 0) { toast.info('Não há erros que possam ser corrigidos automaticamente'); return; } // Aplicar correções automáticas e reprocessar const correctedRawData = rawData.map((row, index) => { const rowErrors = fixableErrors.filter(err => err.row === index + 1); const correctedRow = { ...row }; rowErrors.forEach(error => { if (error.column === 'tem_componentes') { correctedRow[error.column] = 'false'; } else if (['quantidade', 'peso_unitario', 'peso_total', 'peso_unitario_componente', 'quantidade_por_peca'].includes(error.column)) { const cleaned = error.value.toString().replace(/[^\d,\.]/g, '').replace(',', '.'); correctedRow[error.column] = cleaned; } }); return correctedRow; }); // Reprocessar com dados corrigidos const validData: PecaCSVData[] = []; const remainingErrors: ValidationError[] = []; correctedRawData.forEach((row, index) => { const { data, errors } = validateAndCleanData(row, index); if (data) { validData.push(data); } remainingErrors.push(...errors); }); setValidationErrors(remainingErrors); setCsvData(validData); toast.success(`${fixableErrors.length} erro(s) corrigido(s) automaticamente`); }; const handleImport = async () => { if (csvData.length === 0) { toast.error('Nenhuma peça válida para importar'); return; } setIsLoading(true); try { // 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); toast.error(`Erro ao importar peças: ${error instanceof Error ? error.message : 'Erro desconhecido'}`); } finally { setIsLoading(false); } }; const handleClose = () => { console.log('🔒 Fechando modal de importação'); setCsvFile(null); setCsvData([]); setRawData([]); setDuplicates([]); setValidationErrors([]); setStep('upload'); setShowErrors(false); onOpenChange(false); }; const duplicateCount = duplicates.filter(d => d.isDuplicate).length; const validCount = csvData.length - duplicateCount; const totalRows = rawData.length; const errorCount = validationErrors.length; const rowsWithErrors = new Set(validationErrors.map(err => err.row)).size; const totalWeight = csvData .filter((_, index) => !duplicates[index]?.isDuplicate) .reduce((sum, peca) => sum + peca.peso_total, 0); return ( <> Importar Peças via CSV {step === 'upload' && (
{csvFile && ( Arquivo selecionado: {csvFile.name} ({(csvFile.size / 1024).toFixed(1)} KB) )}

Formato do arquivo CSV:

Colunas obrigatórias (em ordem):

of_number, etapa_fase, marca, descricao,
quantidade, peso_unitario, peso_total,
tratamento_superficial, material,
perfil_principal, tem_componentes,
marca_componente, descricao_componente,
perfil_componente, peso_unitario_componente,
quantidade_por_peca
)} {step === 'preview' && (
{totalRows} total {validCount} válidas {duplicateCount > 0 && ( {duplicateCount} duplicatas )} {rowsWithErrors > 0 && ( {rowsWithErrors} com erros )}
{rowsWithErrors > 0 && ( )}
{duplicateCount > 0 && ( {duplicateCount} peça(s) já existem no banco de dados e serão ignoradas na importação. )} {rowsWithErrors > 0 && !showErrors && ( {rowsWithErrors} linha(s) não puderam ser processadas devido a erros nos dados.
{validationErrors.some(err => err.canAutoFix) && ( )}
)} {showErrors && rowsWithErrors > 0 && (

Erros Encontrados:

{validationErrors.some(err => err.canAutoFix) && ( )}
{validationErrors.map((error, index) => (
Linha {error.row}, Coluna "{error.column}"
Valor: "{error.value}" - {error.error}
Sugestão: {error.suggestion} {error.canAutoFix && (Pode ser corrigido automaticamente)}
))}
)}
Status
OF
Fase
Marca
Descrição
Qtd
Peso Unit.
Peso Total
Material
Perfil
{csvData.map((peca, index) => { const duplicate = duplicates.find(d => d.index === index); return (
{duplicate?.isDuplicate ? ( ) : ( )}
{peca.of_number}
{peca.etapa_fase}
{peca.marca}
{peca.descricao}
{peca.quantidade}
{peca.peso_unitario}
{peca.peso_total}
{peca.material}
{peca.perfil_principal}
); })}
)}
{step === 'preview' && validCount > 0 && (
Peso Total: {totalWeight.toFixed(2)} kg
)}
{step === 'upload' ? ( <> ) : ( <> )}
{/* Modal de Detecção de Conflitos e Conciliação */} { setShowDiffModal(false); handleClose(); }} /> ); }