🚀 Initial commit: Versão atual do TrackSteel APP
This commit is contained in:
326
src/components/conversores/AdvanceSteelConverter.tsx
Normal file
326
src/components/conversores/AdvanceSteelConverter.tsx
Normal file
@@ -0,0 +1,326 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Upload, FileSpreadsheet } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
const AdvanceSteelConverter: React.FC = () => {
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [divideBy1000, setDivideBy1000] = useState(false);
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
handleFile(files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
handleFile(e.target.files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFile = (file: File) => {
|
||||
const validTypes = [
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-excel'
|
||||
];
|
||||
|
||||
if (!validTypes.includes(file.type)) {
|
||||
toast.error('Por favor, selecione um arquivo Excel (.xlsx ou .xls)');
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedFile(file);
|
||||
toast.success('Arquivo selecionado com sucesso!');
|
||||
};
|
||||
|
||||
const processFile = () => {
|
||||
if (!selectedFile) {
|
||||
toast.error('Por favor, selecione um arquivo primeiro.');
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const data = new Uint8Array(e.target?.result as ArrayBuffer);
|
||||
const workbook = XLSX.read(data, { type: 'array' });
|
||||
const firstSheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[firstSheetName];
|
||||
const json = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][];
|
||||
|
||||
const newSheetData: any[][] = [];
|
||||
newSheetData.push(['Marca', 'Qtde', 'Descrição', 'Mat.', 'Comp.', 'Larg.', 'P.Un.', 'P.Tot.']);
|
||||
|
||||
let fileNamePrefix: string | null = null;
|
||||
const processedMarks = new Set<number>();
|
||||
|
||||
for (let i = 0; i < json.length; i++) {
|
||||
const row = json[i];
|
||||
if (!row || row.length === 0) continue;
|
||||
|
||||
const marcaCompleta = String(row[0] || '');
|
||||
|
||||
// Primeiro tenta o formato com fase: B118-4-2
|
||||
let mainMarkMatch = marcaCompleta.match(/^(B\d+)-(\d+)-(\d+)$/);
|
||||
let prefixo: string;
|
||||
let numeroMarca: number;
|
||||
|
||||
if (mainMarkMatch) {
|
||||
// Formato com fase: B118-4-2
|
||||
const ofNumber = mainMarkMatch[1]; // B118
|
||||
const faseNumber = mainMarkMatch[2]; // 4
|
||||
const marcaNumber = mainMarkMatch[3]; // 2
|
||||
prefixo = `${ofNumber}-`; // B118-
|
||||
numeroMarca = parseInt(marcaNumber, 10); // 2
|
||||
} else {
|
||||
// Tenta o formato sem fase: B118-2
|
||||
mainMarkMatch = marcaCompleta.match(/^(B\d+-)(\d+)$/);
|
||||
if (mainMarkMatch) {
|
||||
prefixo = mainMarkMatch[1]; // B118-
|
||||
numeroMarca = parseInt(mainMarkMatch[2], 10); // 2
|
||||
}
|
||||
}
|
||||
|
||||
if (mainMarkMatch) {
|
||||
|
||||
if (numeroMarca >= 999 || processedMarks.has(numeroMarca)) {
|
||||
continue;
|
||||
}
|
||||
processedMarks.add(numeroMarca);
|
||||
|
||||
if (!fileNamePrefix) {
|
||||
fileNamePrefix = prefixo;
|
||||
}
|
||||
|
||||
const qtde = row[5];
|
||||
const descricao = row[6];
|
||||
const larg = row[14];
|
||||
|
||||
let mat = '';
|
||||
let compRaw = '';
|
||||
let maxComp = 0;
|
||||
|
||||
// Busca o maior comprimento entre todas as sub-linhas da marca
|
||||
for (let j = i + 1; j < json.length; j++) {
|
||||
const subRow = json[j];
|
||||
if (!subRow || !subRow[0]) continue;
|
||||
|
||||
const subMarkCompleta = String(subRow[0]);
|
||||
|
||||
// Verifica formato com fase: B118-4-2
|
||||
let subMarkMatch = subMarkCompleta.match(/^(B\d+)-(\d+)-(\d+)$/);
|
||||
let subNumeroMarca: number | null = null;
|
||||
|
||||
if (subMarkMatch) {
|
||||
// Formato com fase
|
||||
subNumeroMarca = parseInt(subMarkMatch[3], 10);
|
||||
} else {
|
||||
// Verifica formato sem fase: B118-2
|
||||
subMarkMatch = subMarkCompleta.match(/^(B\d+-)(\d+)$/);
|
||||
if (subMarkMatch) {
|
||||
subNumeroMarca = parseInt(subMarkMatch[2], 10);
|
||||
}
|
||||
}
|
||||
|
||||
// Se encontrou uma nova marca principal, para a busca
|
||||
if (subMarkMatch && subNumeroMarca !== null && subNumeroMarca < 999 && !processedMarks.has(subNumeroMarca)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Se é uma sub-linha da marca atual, verifica o comprimento
|
||||
if (subMarkMatch) {
|
||||
const currentComp = Number(String(subRow[13] || 0).replace(',', '.'));
|
||||
if (!isNaN(currentComp) && currentComp > maxComp) {
|
||||
maxComp = currentComp;
|
||||
mat = subRow[12];
|
||||
compRaw = subRow[13];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let pTotSum = 0;
|
||||
for (let j = i + 1; j < json.length; j++) {
|
||||
const subRow = json[j];
|
||||
if (!subRow || !subRow[0]) continue;
|
||||
|
||||
const subMarkCompleta = String(subRow[0]);
|
||||
|
||||
// Verifica formato com fase: B118-4-2
|
||||
let subMarkMatch = subMarkCompleta.match(/^(B\d+)-(\d+)-(\d+)$/);
|
||||
let subNumeroMarca: number | null = null;
|
||||
|
||||
if (subMarkMatch) {
|
||||
// Formato com fase
|
||||
subNumeroMarca = parseInt(subMarkMatch[3], 10);
|
||||
} else {
|
||||
// Verifica formato sem fase: B118-2
|
||||
subMarkMatch = subMarkCompleta.match(/^(B\d+-)(\d+)$/);
|
||||
if (subMarkMatch) {
|
||||
subNumeroMarca = parseInt(subMarkMatch[2], 10);
|
||||
}
|
||||
}
|
||||
|
||||
if (subMarkMatch && subNumeroMarca !== null && subNumeroMarca < 999 && !processedMarks.has(subNumeroMarca)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (subMarkMatch) {
|
||||
const pTotSubItemRaw = Number(String(subRow[16] || 0).replace(',', '.'));
|
||||
const pTotSubItemValue = divideBy1000 ? pTotSubItemRaw / 1000 : pTotSubItemRaw;
|
||||
if (!isNaN(pTotSubItemValue)) {
|
||||
pTotSum += pTotSubItemValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const qtdeValue = Number(String(qtde || 0).replace(',', '.'));
|
||||
const compValue = Number(String(compRaw || 0).replace(',', '.'));
|
||||
|
||||
let pUnCalculated = 0;
|
||||
if (qtdeValue !== 0) {
|
||||
pUnCalculated = pTotSum / qtdeValue;
|
||||
}
|
||||
|
||||
const compRounded = Math.round(compValue);
|
||||
const pUnRounded = Math.round(pUnCalculated);
|
||||
const pTotRounded = Math.round(pTotSum);
|
||||
|
||||
newSheetData.push([
|
||||
numeroMarca,
|
||||
qtde,
|
||||
descricao,
|
||||
mat,
|
||||
isNaN(compRounded) ? '' : compRounded,
|
||||
larg,
|
||||
isNaN(pUnRounded) ? '' : pUnRounded,
|
||||
isNaN(pTotRounded) ? '' : pTotRounded
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (newSheetData.length <= 1) {
|
||||
throw new Error("Nenhuma linha válida foi encontrada para conversão.");
|
||||
}
|
||||
if (!fileNamePrefix) {
|
||||
throw new Error("Não foi possível determinar o prefixo para o nome do arquivo.");
|
||||
}
|
||||
|
||||
const newWorksheet = XLSX.utils.aoa_to_sheet(newSheetData);
|
||||
const newWorkbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(newWorkbook, newWorksheet, 'Lista de Peças');
|
||||
|
||||
const newFileName = `${fileNamePrefix}Lista de Peças.xlsx`;
|
||||
XLSX.writeFile(newWorkbook, newFileName);
|
||||
|
||||
toast.success('Arquivo convertido e baixado com sucesso!');
|
||||
setSelectedFile(null);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro no processamento:', error);
|
||||
toast.error(`Erro ao processar o arquivo: ${(error as Error).message}`);
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = () => {
|
||||
toast.error('Não foi possível ler o arquivo.');
|
||||
};
|
||||
|
||||
reader.readAsArrayBuffer(selectedFile);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="max-w-2xl mx-auto">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileSpreadsheet className="h-5 w-5" />
|
||||
Conversor Advance Steel
|
||||
</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Converta a "Lista de Peças - Estruturada" para o formato final.
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div
|
||||
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors ${
|
||||
isDragging
|
||||
? 'border-primary bg-primary/10'
|
||||
: 'border-muted-foreground/25 hover:border-primary hover:bg-accent'
|
||||
}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => document.getElementById('advance-file-input')?.click()}
|
||||
>
|
||||
<input
|
||||
id="advance-file-input"
|
||||
type="file"
|
||||
className="hidden"
|
||||
accept=".xlsx,.xls"
|
||||
onChange={handleFileInput}
|
||||
/>
|
||||
<Upload className="mx-auto h-12 w-12 text-muted-foreground mb-4" />
|
||||
{selectedFile ? (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground mb-2">
|
||||
Arquivo selecionado:
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">{selectedFile.name}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
<span className="font-semibold text-primary">Clique para carregar</span> ou arraste e solte a planilha
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Formatos suportados: .xlsx, .xls
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="divide-by-1000"
|
||||
checked={divideBy1000}
|
||||
onCheckedChange={(checked) => setDivideBy1000(checked === true)}
|
||||
/>
|
||||
<label
|
||||
htmlFor="divide-by-1000"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Dividir peso por 1000?
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={processFile}
|
||||
disabled={!selectedFile}
|
||||
className="w-full"
|
||||
>
|
||||
Converter e Baixar
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvanceSteelConverter;
|
||||
236
src/components/conversores/BocadConverter.tsx
Normal file
236
src/components/conversores/BocadConverter.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Upload, FileText, RefreshCw } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
const BocadConverter: React.FC = () => {
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [uploadText, setUploadText] = useState(true);
|
||||
|
||||
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
handleFile(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFile = (file: File) => {
|
||||
const isExcel = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
|
||||
file.type === 'application/vnd.ms-excel' ||
|
||||
file.name.endsWith('.xlsx') ||
|
||||
file.name.endsWith('.xls');
|
||||
|
||||
if (isExcel) {
|
||||
setSelectedFile(file);
|
||||
setUploadText(false);
|
||||
toast.success('Planilha selecionada com sucesso!');
|
||||
} else {
|
||||
toast.error('Por favor, selecione apenas arquivos de planilha (.xlsx ou .xls).');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
const files = event.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
handleFile(files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const processFile = async () => {
|
||||
if (!selectedFile) {
|
||||
toast.error('Por favor, selecione uma planilha para processar.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProcessing(true);
|
||||
|
||||
try {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const data = new Uint8Array(e.target?.result as ArrayBuffer);
|
||||
const workbook = XLSX.read(data, { type: 'array' });
|
||||
const firstSheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[firstSheetName];
|
||||
|
||||
const json = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][];
|
||||
|
||||
// 1. Extração e formatação dos dados para o nome do arquivo
|
||||
const obraCompleta = String(json[3][1] || 'OBRA_NAO_ENCONTRADA').trim();
|
||||
const fase = String(json[3][5] || 'FASE_NAO_ENCONTRADA').trim();
|
||||
|
||||
const match = obraCompleta.match(/B\d+/);
|
||||
const obra = match ? match[0] : obraCompleta;
|
||||
|
||||
// 2. Localização da tabela de dados
|
||||
let dataStartIndex = -1;
|
||||
let dataEndIndex = -1;
|
||||
for (let i = 0; i < json.length; i++) {
|
||||
if (json[i][0] === 'Marca' && json[i][1] === 'Quant.') {
|
||||
dataStartIndex = i + 1;
|
||||
}
|
||||
if (String(json[i][0]).trim().toLowerCase() === 'total') {
|
||||
dataEndIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (dataStartIndex === -1) {
|
||||
throw new Error("Não foi possível encontrar o cabeçalho da tabela de peças ('Marca', 'Quant.').");
|
||||
}
|
||||
if (dataEndIndex === -1) {
|
||||
dataEndIndex = json.length;
|
||||
}
|
||||
|
||||
// 3. Processamento das linhas da tabela
|
||||
const newSheetData: any[][] = [];
|
||||
newSheetData.push(['Marca', 'Pos.', 'Descrição', 'Qtde', 'Lar.', 'Esp.', 'Comp.', 'Mat.', 'P.Un.', 'P.Tot.']);
|
||||
|
||||
for (let i = dataStartIndex; i < dataEndIndex; i++) {
|
||||
const row = json[i];
|
||||
if (!row[0]) continue;
|
||||
|
||||
const marca = row[0];
|
||||
const marcaAsNumber = Number(marca);
|
||||
if (isNaN(marcaAsNumber) || marcaAsNumber >= 1000) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const quant = row[1];
|
||||
const perfil = row[2];
|
||||
const qualid = row[3];
|
||||
const compr = row[4];
|
||||
const pesoTot = row[6];
|
||||
const nota = row[8] || '';
|
||||
|
||||
const comprRounded = Math.round(Number(String(compr).replace(',', '.')));
|
||||
const pesoTotRounded = Math.round(Number(String(pesoTot).replace(',', '.')));
|
||||
|
||||
const pos = String(nota).split(' ').pop();
|
||||
const mat = (qualid === 'ASTM-A572') ? 'A572GR50' : String(qualid).replace('ASTM-', '');
|
||||
|
||||
newSheetData.push([
|
||||
marca,
|
||||
pos,
|
||||
perfil,
|
||||
quant,
|
||||
'',
|
||||
'',
|
||||
isNaN(comprRounded) ? '' : comprRounded,
|
||||
mat,
|
||||
isNaN(pesoTotRounded) ? '' : pesoTotRounded,
|
||||
isNaN(pesoTotRounded) ? '' : pesoTotRounded
|
||||
]);
|
||||
}
|
||||
|
||||
if (newSheetData.length <= 1) {
|
||||
throw new Error("Nenhuma linha válida (com Marca < 1000) foi encontrada para conversão.");
|
||||
}
|
||||
|
||||
// 4. Criação e download do novo arquivo Excel
|
||||
const newWorksheet = XLSX.utils.aoa_to_sheet(newSheetData);
|
||||
const newWorkbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(newWorkbook, newWorksheet, 'Lista de Peças');
|
||||
|
||||
// *** CORRIGIDO: Nome do arquivo com "peças" em minúsculo ***
|
||||
const newFileName = `${obra}-${fase}-Lista de peças.xlsx`;
|
||||
XLSX.writeFile(newWorkbook, newFileName);
|
||||
|
||||
toast.success('Arquivo convertido com sucesso!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro ao processar planilha:', error);
|
||||
toast.error(`Erro ao processar o arquivo: ${error instanceof Error ? error.message : 'Erro desconhecido'}`);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = () => {
|
||||
toast.error('Não foi possível ler o arquivo.');
|
||||
setIsProcessing(false);
|
||||
};
|
||||
|
||||
reader.readAsArrayBuffer(selectedFile);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro geral:', error);
|
||||
toast.error('Erro ao processar a planilha. Verifique o arquivo e tente novamente.');
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 flex items-center justify-center p-4">
|
||||
<div className="bg-white p-8 rounded-xl shadow-lg w-full max-w-lg mx-4">
|
||||
<div className="text-center mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-800">Conversor de Planilhas BSI</h1>
|
||||
<p className="text-gray-500 mt-2">Transforme a "Lista de Materiais" em uma "Lista de Peças" pronta para uso.</p>
|
||||
</div>
|
||||
|
||||
{/* Área de Upload */}
|
||||
<div className="relative">
|
||||
<input
|
||||
type="file"
|
||||
accept=".xls,.xlsx"
|
||||
onChange={handleFileSelect}
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10"
|
||||
/>
|
||||
<div
|
||||
className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center cursor-pointer hover:border-blue-500 hover:bg-gray-50 transition-colors"
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<Upload className="mx-auto h-12 w-12 text-gray-400 mb-4" />
|
||||
{uploadText ? (
|
||||
<div>
|
||||
<p className="mt-2 text-sm text-gray-600">
|
||||
<span className="font-semibold text-blue-600">Clique para carregar</span> ou arraste e solte a planilha.
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">Apenas arquivos .xlsx e .xls</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-800 mb-2">{selectedFile?.name}</p>
|
||||
<p className="text-xs text-gray-500">Planilha selecionada</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Botão de Conversão */}
|
||||
<div className="mt-6">
|
||||
<Button
|
||||
onClick={processFile}
|
||||
disabled={!selectedFile || isProcessing}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-lg transition-transform transform active:scale-95 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
|
||||
Processando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
Converter e Baixar
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BocadConverter;
|
||||
447
src/components/conversores/ConversaoGenericaModal.tsx
Normal file
447
src/components/conversores/ConversaoGenericaModal.tsx
Normal file
@@ -0,0 +1,447 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Upload, FileText, Download, X, Check, Eye, AlertCircle } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { usePrompts } from '@/hooks/usePrompts';
|
||||
import * as XLSX from 'xlsx';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
|
||||
interface ConversaoGenericaModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
interface ProcessedData {
|
||||
headers: string[];
|
||||
rows: any[][];
|
||||
totalRows: number;
|
||||
totalColumns: number;
|
||||
rawData: any[];
|
||||
}
|
||||
|
||||
const ConversaoGenericaModal: React.FC<ConversaoGenericaModalProps> = ({
|
||||
open,
|
||||
onOpenChange
|
||||
}) => {
|
||||
const { prompts, loading: promptsLoading } = usePrompts();
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [processedData, setProcessedData] = useState<ProcessedData | null>(null);
|
||||
const [selectedPrompt, setSelectedPrompt] = useState<string>('');
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
|
||||
// Função para verificar se um valor é numérico decimal
|
||||
const isDecimalNumber = (value: any): boolean => {
|
||||
if (typeof value === 'number') {
|
||||
return value % 1 !== 0;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const num = parseFloat(value);
|
||||
return !isNaN(num) && num % 1 !== 0 && value.includes('.');
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Função para arredondar valores decimais
|
||||
const roundDecimalValue = (value: any): any => {
|
||||
if (typeof value === 'number' && value % 1 !== 0) {
|
||||
return Math.round(value);
|
||||
}
|
||||
if (typeof value === 'string' && value.includes('.')) {
|
||||
const num = parseFloat(value);
|
||||
if (!isNaN(num) && num % 1 !== 0) {
|
||||
return Math.round(num).toString();
|
||||
}
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
// Função para processar e arredondar dados
|
||||
const processAndRoundData = (data: any[]): any[] => {
|
||||
return data.map(row => {
|
||||
const processedRow: any = {};
|
||||
Object.keys(row).forEach(key => {
|
||||
const value = row[key];
|
||||
processedRow[key] = roundDecimalValue(value);
|
||||
});
|
||||
return processedRow;
|
||||
});
|
||||
};
|
||||
|
||||
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const selectedFile = event.target.files?.[0];
|
||||
if (selectedFile) {
|
||||
const validTypes = [
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
|
||||
'application/vnd.ms-excel', // .xls
|
||||
];
|
||||
|
||||
if (!validTypes.includes(selectedFile.type)) {
|
||||
toast.error('Formato de arquivo inválido. Selecione um arquivo .xlsx ou .xls');
|
||||
return;
|
||||
}
|
||||
|
||||
setFile(selectedFile);
|
||||
setProcessedData(null);
|
||||
setShowPreview(false);
|
||||
}
|
||||
};
|
||||
|
||||
const processFile = async () => {
|
||||
if (!file) return;
|
||||
|
||||
setProcessing(true);
|
||||
try {
|
||||
const buffer = await file.arrayBuffer();
|
||||
const workbook = XLSX.read(buffer, { type: 'array' });
|
||||
|
||||
// Pegar a primeira planilha
|
||||
const firstSheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[firstSheetName];
|
||||
|
||||
// Converter para JSON com chaves como nomes das colunas
|
||||
const jsonDataWithHeaders = XLSX.utils.sheet_to_json(worksheet) as any[];
|
||||
|
||||
// Converter para formato de array para visualização
|
||||
const jsonArrayData = XLSX.utils.sheet_to_json(worksheet, {
|
||||
header: 1,
|
||||
defval: '',
|
||||
raw: false
|
||||
}) as any[][];
|
||||
|
||||
if (jsonDataWithHeaders.length === 0) {
|
||||
throw new Error('Nenhum dado encontrado no arquivo');
|
||||
}
|
||||
|
||||
// Processar e arredondar dados decimais
|
||||
const roundedJsonData = processAndRoundData(jsonDataWithHeaders);
|
||||
|
||||
// Determinar o número máximo de colunas
|
||||
const maxColumns = Math.max(...jsonArrayData.map(row => row.length));
|
||||
|
||||
// Padronizar todas as linhas para ter o mesmo número de colunas e arredondar valores
|
||||
const normalizedData = jsonArrayData.map(row => {
|
||||
const normalizedRow = [...row];
|
||||
while (normalizedRow.length < maxColumns) {
|
||||
normalizedRow.push('');
|
||||
}
|
||||
// Arredondar valores decimais na visualização
|
||||
return normalizedRow.map(cell => roundDecimalValue(cell));
|
||||
});
|
||||
|
||||
// Primeira linha como headers
|
||||
const headers = normalizedData[0].map((header, index) =>
|
||||
header || `Coluna ${index + 1}`
|
||||
);
|
||||
|
||||
// Resto como dados para visualização
|
||||
const rows = normalizedData.slice(1);
|
||||
|
||||
const processedResult = {
|
||||
headers,
|
||||
rows,
|
||||
totalRows: rows.length,
|
||||
totalColumns: headers.length,
|
||||
rawData: roundedJsonData
|
||||
};
|
||||
|
||||
setProcessedData(processedResult);
|
||||
setShowPreview(true);
|
||||
|
||||
// Contar quantos valores decimais foram arredondados
|
||||
let decimalCount = 0;
|
||||
jsonDataWithHeaders.forEach(row => {
|
||||
Object.values(row).forEach(value => {
|
||||
if (isDecimalNumber(value)) {
|
||||
decimalCount++;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (decimalCount > 0) {
|
||||
toast.success(`Arquivo processado com sucesso! ${processedResult.totalRows} linhas e ${processedResult.totalColumns} colunas encontradas. ${decimalCount} valores decimais foram arredondados.`);
|
||||
} else {
|
||||
toast.success(`Arquivo processado com sucesso! ${processedResult.totalRows} linhas e ${processedResult.totalColumns} colunas encontradas.`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao processar arquivo:', error);
|
||||
toast.error('Erro ao processar arquivo. Verifique se o arquivo não está corrompido.');
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const generateCSV = async () => {
|
||||
if (!processedData || !selectedPrompt || !file) return;
|
||||
|
||||
setGenerating(true);
|
||||
try {
|
||||
const selectedPromptData = prompts.find(p => p.id === selectedPrompt);
|
||||
console.log('Processando com prompt:', selectedPromptData?.name);
|
||||
console.log('Dados para processar:', processedData.rawData);
|
||||
|
||||
// Processar os dados usando as instruções do prompt
|
||||
const csvContent = processDataWithPrompt(processedData.rawData, selectedPromptData?.content || '');
|
||||
|
||||
// Gerar e baixar CSV
|
||||
const BOM = '\uFEFF';
|
||||
const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const csvFileName = file.name.replace(/\.(xlsx|xls)$/i, '_convertido.csv');
|
||||
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = csvFileName;
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
|
||||
toast.success(`CSV gerado com sucesso usando o prompt: ${selectedPromptData?.name}. Valores decimais foram arredondados para números inteiros.`);
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
console.error('Erro ao gerar CSV:', error);
|
||||
toast.error('Erro ao gerar CSV. Verifique as instruções do prompt e os dados.');
|
||||
} finally {
|
||||
setGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const processDataWithPrompt = (data: any[], promptContent: string): string => {
|
||||
console.log('Aplicando prompt:', promptContent);
|
||||
|
||||
// Por enquanto, vamos fazer uma conversão básica baseada nos dados
|
||||
if (data.length === 0) return '';
|
||||
|
||||
// Obter headers dos dados originais
|
||||
const headers = Object.keys(data[0]);
|
||||
|
||||
// Criar cabeçalho CSV
|
||||
const csvHeaders = headers.join(',');
|
||||
|
||||
// Converter dados para CSV, garantindo que valores numéricos não tenham decimais
|
||||
const csvRows = data.map(row =>
|
||||
headers.map(header => {
|
||||
let value = row[header];
|
||||
if (value === null || value === undefined) return '';
|
||||
|
||||
// Arredondar valores decimais mais uma vez para garantir
|
||||
value = roundDecimalValue(value);
|
||||
|
||||
const stringValue = value.toString();
|
||||
// Escapar valores que contêm vírgula
|
||||
return stringValue.includes(',') ? `"${stringValue}"` : stringValue;
|
||||
}).join(',')
|
||||
);
|
||||
|
||||
return [csvHeaders, ...csvRows].join('\n');
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setFile(null);
|
||||
setProcessedData(null);
|
||||
setSelectedPrompt('');
|
||||
setShowPreview(false);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="bg-slate-800 border-slate-700 max-w-5xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white text-xl">Conversão Genérica</DialogTitle>
|
||||
<DialogDescription className="text-slate-400">
|
||||
Faça upload de uma planilha Excel e selecione um prompt para processar os dados. Valores decimais serão automaticamente arredondados.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Seleção de arquivo */}
|
||||
<Card className="bg-slate-700/50 border-slate-600">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white text-lg flex items-center gap-2">
|
||||
<Upload className="w-5 h-5" />
|
||||
Selecionar Arquivo Excel
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="file-upload" className="text-white mb-2 block">
|
||||
Arquivo da Planilha
|
||||
</Label>
|
||||
<Input
|
||||
id="file-upload"
|
||||
type="file"
|
||||
accept=".xlsx,.xls"
|
||||
onChange={handleFileSelect}
|
||||
className="bg-slate-600 border-slate-500 text-white file:bg-slate-500 file:text-white file:border-0 file:mr-4 file:py-2 file:px-4 file:rounded"
|
||||
/>
|
||||
<p className="text-slate-400 text-xs mt-1">
|
||||
Formatos suportados: .xlsx, .xls
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{file && (
|
||||
<div className="p-3 bg-slate-800/50 rounded border border-slate-600">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="w-4 h-4 text-blue-400" />
|
||||
<span className="text-white text-sm">{file.name}</span>
|
||||
<span className="text-slate-400 text-xs">
|
||||
({(file.size / 1024 / 1024).toFixed(2)} MB)
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
onClick={processFile}
|
||||
disabled={processing}
|
||||
size="sm"
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
{processing ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2" />
|
||||
Processando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
Processar
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Seleção de prompt */}
|
||||
{showPreview && (
|
||||
<Card className="bg-slate-700/50 border-slate-600">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white text-lg">Selecionar Prompt de Conversão</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<Label className="text-white mb-2 block">Prompt</Label>
|
||||
<Select value={selectedPrompt} onValueChange={setSelectedPrompt}>
|
||||
<SelectTrigger className="bg-slate-600 border-slate-500 text-white">
|
||||
<SelectValue placeholder="Escolha um prompt para guiar a conversão" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-700 border-slate-600">
|
||||
{prompts.map((prompt) => (
|
||||
<SelectItem key={prompt.id} value={prompt.id} className="text-white">
|
||||
{prompt.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{prompts.length === 0 && (
|
||||
<p className="text-yellow-400 text-xs mt-1 flex items-center gap-1">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
Nenhum prompt encontrado. Crie um prompt primeiro.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Preview dos dados */}
|
||||
{processedData && (
|
||||
<Card className="bg-slate-700/50 border-slate-600">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white text-lg">Preview dos Dados (Valores Arredondados)</CardTitle>
|
||||
<div className="flex items-center gap-4 p-3 bg-blue-900/30 rounded border border-blue-600">
|
||||
<Eye className="w-5 h-5 text-blue-400" />
|
||||
<div className="text-blue-100 text-sm">
|
||||
<span className="font-medium">Dados encontrados:</span>
|
||||
<span className="ml-2">
|
||||
{processedData.totalRows} linhas × {processedData.totalColumns} colunas
|
||||
</span>
|
||||
<span className="ml-2 text-yellow-200">
|
||||
(Decimais arredondados automaticamente)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="border border-slate-600 rounded overflow-auto max-h-60">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-slate-700">
|
||||
<tr>
|
||||
{processedData.headers.map((header, index) => (
|
||||
<th key={index} className="px-3 py-2 text-left text-white font-medium whitespace-nowrap">
|
||||
{header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{processedData.rows.slice(0, 10).map((row, rowIndex) => (
|
||||
<tr key={rowIndex} className="border-t border-slate-600">
|
||||
{row.map((cell, cellIndex) => (
|
||||
<td key={cellIndex} className="px-3 py-2 text-slate-300 whitespace-nowrap">
|
||||
{cell}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{processedData.rows.length > 10 && (
|
||||
<div className="p-2 text-center text-slate-400 text-xs bg-slate-800/50">
|
||||
... e mais {processedData.rows.length - 10} linhas
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
onClick={generateCSV}
|
||||
disabled={!selectedPrompt || !processedData || generating}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
{generating ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2" />
|
||||
Gerando CSV...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
Gerar CSV
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConversaoGenericaModal;
|
||||
603
src/components/conversores/FileImporter.tsx
Normal file
603
src/components/conversores/FileImporter.tsx
Normal file
@@ -0,0 +1,603 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Upload, FileText, Download, X, Check, Eye, Info } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { usePrompts } from '@/hooks/usePrompts';
|
||||
import * as XLSX from 'xlsx';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
|
||||
interface ProcessedData {
|
||||
headers: string[];
|
||||
rows: any[][];
|
||||
totalRows: number;
|
||||
totalColumns: number;
|
||||
rawData: any[]; // Dados originais para processamento
|
||||
}
|
||||
|
||||
const FileImporter: React.FC = () => {
|
||||
const { prompts, loading: promptsLoading } = usePrompts();
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [processingPP, setProcessingPP] = useState(false);
|
||||
const [processedData, setProcessedData] = useState<ProcessedData | null>(null);
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
const [selectedPrompt, setSelectedPrompt] = useState<string>('');
|
||||
const [generating, setGenerating] = useState(false);
|
||||
|
||||
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const selectedFile = event.target.files?.[0];
|
||||
if (selectedFile) {
|
||||
const validTypes = [
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
|
||||
'application/vnd.ms-excel', // .xls
|
||||
'text/csv', // .csv
|
||||
'application/pdf' // .pdf
|
||||
];
|
||||
|
||||
if (!validTypes.includes(selectedFile.type) &&
|
||||
!selectedFile.name.toLowerCase().endsWith('.csv')) {
|
||||
toast.error('Formato de arquivo inválido. Selecione um arquivo .xlsx, .xls, .csv ou .pdf');
|
||||
return;
|
||||
}
|
||||
|
||||
setFile(selectedFile);
|
||||
setProcessedData(null);
|
||||
}
|
||||
};
|
||||
|
||||
const processFile = async (onlyPP = false) => {
|
||||
if (!file) return;
|
||||
|
||||
if (onlyPP) {
|
||||
setProcessingPP(true);
|
||||
} else {
|
||||
setProcessing(true);
|
||||
}
|
||||
|
||||
try {
|
||||
let processedResult: ProcessedData;
|
||||
|
||||
if (file.type === 'application/pdf') {
|
||||
// Para PDF, ainda simulamos o processamento
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
processedResult = {
|
||||
headers: ['Marca', 'Perfil', 'Quantidade', 'Peso Unit.', 'Peso Total', 'Material'],
|
||||
rows: [
|
||||
['P1', 'VS 200x30x5,0', '2', '15,8', '31,6', 'ASTM A36'],
|
||||
['P2', 'VS 150x25x4,0', '4', '12,3', '49,2', 'ASTM A36'],
|
||||
['P3', 'L 50x50x5,0', '8', '3,77', '30,16', 'ASTM A36'],
|
||||
['P4', 'CH 100x50x17,0', '6', '17,0', '102,0', 'ASTM A36'],
|
||||
['P5', 'FL 200x8,0', '3', '12,56', '37,68', 'ASTM A36']
|
||||
],
|
||||
totalRows: 5,
|
||||
totalColumns: 6,
|
||||
rawData: []
|
||||
};
|
||||
} else {
|
||||
// Processar arquivos Excel/CSV usando xlsx
|
||||
const buffer = await file.arrayBuffer();
|
||||
const workbook = XLSX.read(buffer, { type: 'array' });
|
||||
|
||||
// Pegar a primeira planilha
|
||||
const firstSheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[firstSheetName];
|
||||
|
||||
// Converter para JSON com chaves como nomes das colunas
|
||||
const jsonDataWithHeaders = XLSX.utils.sheet_to_json(worksheet) as any[];
|
||||
|
||||
// Converter para formato de array para visualização
|
||||
const jsonArrayData = XLSX.utils.sheet_to_json(worksheet, {
|
||||
header: 1,
|
||||
defval: '',
|
||||
raw: false
|
||||
}) as any[][];
|
||||
|
||||
if (jsonDataWithHeaders.length === 0) {
|
||||
throw new Error('Nenhum dado encontrado no arquivo');
|
||||
}
|
||||
|
||||
console.log('Dados JSON com headers:', jsonDataWithHeaders);
|
||||
console.log('Dados em array:', jsonArrayData);
|
||||
|
||||
// Se for processamento PP, filtrar apenas linhas onde a coluna "Pos." está vazia
|
||||
let filteredJsonData = jsonDataWithHeaders;
|
||||
let filteredArrayData = jsonArrayData;
|
||||
|
||||
if (onlyPP) {
|
||||
// Filtrar dados JSON com headers
|
||||
filteredJsonData = jsonDataWithHeaders.filter(row => {
|
||||
const posValue = extrairValor(row, ['Pos.', 'Pos', 'pos', 'POS']);
|
||||
return !posValue || posValue.toString().trim() === '';
|
||||
});
|
||||
|
||||
// Filtrar dados em array (manter header + linhas filtradas)
|
||||
const headerRow = jsonArrayData[0];
|
||||
const dataRows = jsonArrayData.slice(1);
|
||||
|
||||
// Encontrar índice da coluna Pos.
|
||||
const posColumnIndex = headerRow.findIndex(header =>
|
||||
['Pos.', 'Pos', 'pos', 'POS'].includes(header?.toString() || '')
|
||||
);
|
||||
|
||||
const filteredDataRows = dataRows.filter(row => {
|
||||
if (posColumnIndex === -1) return true; // Se não encontrar coluna Pos., incluir todas
|
||||
const posValue = row[posColumnIndex];
|
||||
return !posValue || posValue.toString().trim() === '';
|
||||
});
|
||||
|
||||
filteredArrayData = [headerRow, ...filteredDataRows];
|
||||
}
|
||||
|
||||
// Determinar o número máximo de colunas
|
||||
const maxColumns = Math.max(...filteredArrayData.map(row => row.length));
|
||||
|
||||
// Padronizar todas as linhas para ter o mesmo número de colunas
|
||||
const normalizedData = filteredArrayData.map(row => {
|
||||
const normalizedRow = [...row];
|
||||
while (normalizedRow.length < maxColumns) {
|
||||
normalizedRow.push('');
|
||||
}
|
||||
return normalizedRow;
|
||||
});
|
||||
|
||||
// Primeira linha como headers
|
||||
const headers = normalizedData[0].map((header, index) =>
|
||||
header || `Coluna ${index + 1}`
|
||||
);
|
||||
|
||||
// Resto como dados para visualização
|
||||
const rows = normalizedData.slice(1);
|
||||
|
||||
processedResult = {
|
||||
headers,
|
||||
rows,
|
||||
totalRows: rows.length,
|
||||
totalColumns: headers.length,
|
||||
rawData: filteredJsonData // Dados originais filtrados para processamento
|
||||
};
|
||||
|
||||
console.log('Dados processados:', {
|
||||
totalRows: processedResult.totalRows,
|
||||
totalColumns: processedResult.totalColumns,
|
||||
headers: processedResult.headers,
|
||||
rawDataCount: processedResult.rawData.length,
|
||||
onlyPP: onlyPP
|
||||
});
|
||||
}
|
||||
|
||||
setProcessedData(processedResult);
|
||||
setShowPreview(true);
|
||||
|
||||
const processType = onlyPP ? ' (apenas peças principais)' : '';
|
||||
toast.success(`Arquivo processado com sucesso${processType}! ${processedResult.totalRows} linhas e ${processedResult.totalColumns} colunas coletadas.`);
|
||||
} catch (error) {
|
||||
console.error('Erro ao processar arquivo:', error);
|
||||
toast.error('Erro ao processar arquivo. Verifique se o arquivo não está corrompido.');
|
||||
} finally {
|
||||
if (onlyPP) {
|
||||
setProcessingPP(false);
|
||||
} else {
|
||||
setProcessing(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const generateCSV = async () => {
|
||||
if (!processedData || !selectedPrompt || !file) return;
|
||||
|
||||
setGenerating(true);
|
||||
try {
|
||||
const selectedPromptData = prompts.find(p => p.id === selectedPrompt);
|
||||
console.log('Processando com prompt:', selectedPromptData?.name);
|
||||
console.log('Dados originais para processar:', processedData.rawData);
|
||||
|
||||
// Processar os dados usando a lógica do prompt
|
||||
const csvContent = processSpreadsheetToCsv(file.name, processedData.rawData);
|
||||
|
||||
// Gerar e baixar CSV
|
||||
const BOM = '\uFEFF';
|
||||
const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const csvFileName = file.name.replace(/\.(xlsx|xls|pdf)$/i, '_processado.csv');
|
||||
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = csvFileName;
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
|
||||
toast.success(`CSV gerado com sucesso usando o prompt: ${selectedPromptData?.name}`);
|
||||
setShowPreview(false);
|
||||
setFile(null);
|
||||
setProcessedData(null);
|
||||
setSelectedPrompt('');
|
||||
} catch (error) {
|
||||
console.error('Erro ao gerar CSV:', error);
|
||||
toast.error('Erro ao gerar CSV. Verifique as instruções do prompt e os dados.');
|
||||
} finally {
|
||||
setGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const processSpreadsheetToCsv = (fileName: string, data: any[]): string => {
|
||||
console.log('Processando arquivo:', fileName);
|
||||
console.log('Total de linhas:', data.length);
|
||||
|
||||
// Extrair metadados do nome do arquivo
|
||||
const fileNameParts = fileName.replace(/\.(xlsx|xls|pdf)$/i, '').split('-');
|
||||
const ofNumber = fileNameParts[0] || '';
|
||||
const etapaFase = fileNameParts[1] || '';
|
||||
|
||||
console.log('OF Number:', ofNumber);
|
||||
console.log('Etapa/Fase:', etapaFase);
|
||||
|
||||
const csvHeader = '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 csvRows: string[] = [csvHeader];
|
||||
let currentPecaMae: any = null;
|
||||
const componentes: any[] = [];
|
||||
|
||||
// Log das colunas disponíveis
|
||||
if (data.length > 0) {
|
||||
console.log('Colunas disponíveis:', Object.keys(data[0]));
|
||||
}
|
||||
|
||||
// Processar dados linha por linha
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const row = data[i];
|
||||
console.log(`Processando linha ${i + 1}:`, row);
|
||||
|
||||
// Verificar se é uma peça-mãe (tem valor na coluna Marca)
|
||||
const marcaValue = extrairValor(row, ['Marca', 'marca', 'MARCA']);
|
||||
|
||||
if (marcaValue && marcaValue.toString().trim() !== '') {
|
||||
console.log('Encontrou peça-mãe:', marcaValue);
|
||||
|
||||
// Processar peça-mãe anterior se existir
|
||||
if (currentPecaMae) {
|
||||
processarPecaMae(currentPecaMae, componentes, csvRows, ofNumber, etapaFase);
|
||||
}
|
||||
|
||||
// Nova peça-mãe
|
||||
currentPecaMae = row;
|
||||
componentes.length = 0; // Limpar componentes
|
||||
} else {
|
||||
// Verificar se é um componente através da coluna Pos.
|
||||
const posValue = extrairValor(row, ['Pos.', 'Pos', 'pos', 'POS']);
|
||||
|
||||
if (posValue && posValue.toString().trim() !== '') {
|
||||
const posString = posValue.toString();
|
||||
console.log('Verificando Pos.:', posString);
|
||||
|
||||
// Extrair o último número após o último hífen
|
||||
const lastNumber = posString.split('-').pop();
|
||||
|
||||
if (lastNumber && parseInt(lastNumber) >= 1000 && parseInt(lastNumber) <= 9999) {
|
||||
console.log('Componente encontrado:', posString);
|
||||
componentes.push(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Processar última peça-mãe
|
||||
if (currentPecaMae) {
|
||||
processarPecaMae(currentPecaMae, componentes, csvRows, ofNumber, etapaFase);
|
||||
}
|
||||
|
||||
console.log('Total de linhas CSV geradas:', csvRows.length - 1);
|
||||
return csvRows.join('\n');
|
||||
};
|
||||
|
||||
const processarPecaMae = (pecaMae: any, componentes: any[], csvRows: string[], ofNumber: string, etapaFase: string) => {
|
||||
console.log('Processando peça-mãe:', pecaMae);
|
||||
console.log('Com componentes:', componentes);
|
||||
|
||||
// Extrair dados da peça-mãe com diferentes variações de nomes de colunas
|
||||
const marcaCompleta = extrairValor(pecaMae, ['Marca', 'marca', 'MARCA'])?.toString() || '';
|
||||
const marca = marcaCompleta.split('-').pop() || marcaCompleta;
|
||||
const descricao = extrairValor(pecaMae, ['Descrição', 'Descricao', 'descrição', 'descricao', 'DESCRIÇÃO', 'DESCRICAO']) || '';
|
||||
const quantidade = extrairValor(pecaMae, ['Qtde', 'Quantidade', 'qtde', 'quantidade', 'QTDE', 'QUANTIDADE']) || '';
|
||||
const pesoUnitario = formatarNumero(extrairValor(pecaMae, ['P.Un.', 'PesoUnitario', 'Peso Unitário', 'peso_unitario', 'PESO_UNITARIO']));
|
||||
const pesoTotal = formatarNumero(extrairValor(pecaMae, ['P.Tot.', 'PesoTotal', 'Peso Total', 'peso_total', 'PESO_TOTAL']));
|
||||
const material = extrairValor(pecaMae, ['Mat', 'Material', 'mat', 'material', 'MAT', 'MATERIAL']) || '';
|
||||
const perfilPrincipal = material;
|
||||
|
||||
console.log('Dados extraídos - Marca:', marca, 'Descrição:', descricao, 'Qtd:', quantidade);
|
||||
|
||||
if (componentes.length === 0) {
|
||||
// Peça-mãe sem componentes
|
||||
const csvRow = [
|
||||
escapeCSV(ofNumber),
|
||||
escapeCSV(etapaFase),
|
||||
escapeCSV(marca),
|
||||
escapeCSV(descricao),
|
||||
escapeCSV(quantidade),
|
||||
escapeCSV(pesoUnitario),
|
||||
escapeCSV(pesoTotal),
|
||||
escapeCSV('-'), // tratamento_superficial
|
||||
escapeCSV(material),
|
||||
escapeCSV(perfilPrincipal),
|
||||
'false', // tem_componentes
|
||||
'', // marca_componente
|
||||
'', // descricao_componente
|
||||
'', // perfil_componente
|
||||
'', // peso_unitario_componente
|
||||
'' // quantidade_por_peca
|
||||
].join(',');
|
||||
csvRows.push(csvRow);
|
||||
} else {
|
||||
// Peça-mãe com componentes
|
||||
componentes.forEach(componente => {
|
||||
const posValue = extrairValor(componente, ['Pos.', 'Pos', 'pos', 'POS'])?.toString() || '';
|
||||
const marcaComponente = posValue.split('-').pop() || '';
|
||||
const descricaoComponente = extrairValor(componente, ['Descrição', 'Descricao', 'descrição', 'descricao', 'DESCRIÇÃO', 'DESCRICAO']) || '';
|
||||
const perfilComponente = extrairValor(componente, ['Mat', 'Material', 'mat', 'material', 'MAT', 'MATERIAL']) || '';
|
||||
const pesoUnitarioComponente = formatarNumero(extrairValor(componente, ['P.Un.', 'PesoUnitario', 'Peso Unitário', 'peso_unitario', 'PESO_UNITARIO']));
|
||||
const quantidadePorPeca = extrairValor(componente, ['Qtde', 'Quantidade', 'qtde', 'quantidade', 'QTDE', 'QUANTIDADE']) || '';
|
||||
|
||||
const csvRow = [
|
||||
escapeCSV(ofNumber),
|
||||
escapeCSV(etapaFase),
|
||||
escapeCSV(marca),
|
||||
escapeCSV(descricao),
|
||||
escapeCSV(quantidade),
|
||||
escapeCSV(pesoUnitario),
|
||||
escapeCSV(pesoTotal),
|
||||
escapeCSV('-'), // tratamento_superficial
|
||||
escapeCSV(material),
|
||||
escapeCSV(perfilPrincipal),
|
||||
'true', // tem_componentes
|
||||
escapeCSV(marcaComponente),
|
||||
escapeCSV(descricaoComponente),
|
||||
escapeCSV(perfilComponente),
|
||||
escapeCSV(pesoUnitarioComponente),
|
||||
escapeCSV(quantidadePorPeca)
|
||||
].join(',');
|
||||
csvRows.push(csvRow);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const extrairValor = (objeto: any, nomes: string[]): any => {
|
||||
for (const nome of nomes) {
|
||||
if (objeto[nome] !== undefined && objeto[nome] !== null && objeto[nome] !== '') {
|
||||
return objeto[nome];
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const escapeCSV = (valor: any): string => {
|
||||
if (valor === null || valor === undefined) return '';
|
||||
const str = valor.toString();
|
||||
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
||||
return `"${str.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
const formatarNumero = (valor: any): string => {
|
||||
if (!valor) return '';
|
||||
const numeroStr = valor.toString().replace(',', '.');
|
||||
const numero = parseFloat(numeroStr);
|
||||
return isNaN(numero) ? '' : numero.toFixed(1);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setShowPreview(false);
|
||||
setProcessedData(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="bg-slate-700/50 border-slate-600">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white text-lg flex items-center gap-2">
|
||||
<Upload className="w-5 h-5" />
|
||||
Importar Arquivo
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="file-upload" className="text-white mb-2 block">
|
||||
Selecionar Arquivo
|
||||
</Label>
|
||||
<Input
|
||||
id="file-upload"
|
||||
type="file"
|
||||
accept=".xlsx,.xls,.csv,.pdf"
|
||||
onChange={handleFileSelect}
|
||||
className="bg-slate-600 border-slate-500 text-white file:bg-slate-500 file:text-white file:border-0 file:mr-4 file:py-2 file:px-4 file:rounded"
|
||||
/>
|
||||
<p className="text-slate-400 text-xs mt-1">
|
||||
Formatos suportados: .xlsx, .xls, .csv, .pdf
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{file && (
|
||||
<div className="p-3 bg-slate-800/50 rounded border border-slate-600">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="w-4 h-4 text-blue-400" />
|
||||
<span className="text-white text-sm">{file.name}</span>
|
||||
<span className="text-slate-400 text-xs">
|
||||
({(file.size / 1024 / 1024).toFixed(2)} MB)
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={() => processFile(false)}
|
||||
disabled={processing || processingPP}
|
||||
size="sm"
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
{processing ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2" />
|
||||
Processando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
Processar
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => processFile(true)}
|
||||
disabled={processing || processingPP}
|
||||
size="sm"
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
{processingPP ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2" />
|
||||
Processando PP...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
Processar PP
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-slate-300 text-sm">
|
||||
<h4 className="font-medium mb-2">Como funciona:</h4>
|
||||
<ul className="space-y-1 text-xs">
|
||||
<li>1. Selecione um arquivo (.xlsx, .xls, .csv ou .pdf)</li>
|
||||
<li>2. O sistema extrairá todas as tabelas e dados</li>
|
||||
<li>3. Visualize os dados processados</li>
|
||||
<li>4. Escolha um prompt para guiar a conversão</li>
|
||||
<li>5. Gere e baixe o arquivo CSV final</li>
|
||||
</ul>
|
||||
<div className="mt-2 p-2 bg-green-800/20 rounded border border-green-600">
|
||||
<p className="text-green-400 text-xs font-medium">Processar PP:</p>
|
||||
<p className="text-green-300 text-xs">Processa apenas linhas onde a coluna "Pos." está vazia (peças principais).</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog open={showPreview} onOpenChange={setShowPreview}>
|
||||
<DialogContent className="bg-slate-800 border-slate-700 max-w-4xl max-h-[80vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">Dados Processados</DialogTitle>
|
||||
<DialogDescription className="text-slate-400">
|
||||
Visualize os dados extraídos do arquivo e selecione um prompt para gerar o CSV
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label className="text-white mb-2 block">Selecionar Prompt</Label>
|
||||
<Select value={selectedPrompt} onValueChange={setSelectedPrompt}>
|
||||
<SelectTrigger className="bg-slate-700 border-slate-600 text-white">
|
||||
<SelectValue placeholder="Escolha um prompt para guiar a conversão" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-700 border-slate-600">
|
||||
{prompts.map((prompt) => (
|
||||
<SelectItem key={prompt.id} value={prompt.id} className="text-white">
|
||||
{prompt.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{prompts.length === 0 && (
|
||||
<p className="text-yellow-400 text-xs mt-1">
|
||||
Nenhum prompt encontrado. Crie um prompt primeiro.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{processedData && (
|
||||
<>
|
||||
<div className="flex items-center gap-4 p-3 bg-blue-900/30 rounded border border-blue-600">
|
||||
<Info className="w-5 h-5 text-blue-400" />
|
||||
<div className="text-blue-100 text-sm">
|
||||
<span className="font-medium">Dados coletados:</span>
|
||||
<span className="ml-2">
|
||||
{processedData.totalRows} linhas × {processedData.totalColumns} colunas
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-slate-600 rounded overflow-auto max-h-60">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-slate-700">
|
||||
<tr>
|
||||
{processedData.headers.map((header, index) => (
|
||||
<th key={index} className="px-3 py-2 text-left text-white font-medium whitespace-nowrap">
|
||||
{header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{processedData.rows.map((row, rowIndex) => (
|
||||
<tr key={rowIndex} className="border-t border-slate-600">
|
||||
{row.map((cell, cellIndex) => (
|
||||
<td key={cellIndex} className="px-3 py-2 text-slate-300 whitespace-nowrap">
|
||||
{cell}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
onClick={generateCSV}
|
||||
disabled={!selectedPrompt || generating}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
{generating ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2" />
|
||||
Gerando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
Aceitar e Gerar CSV
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileImporter;
|
||||
403
src/components/conversores/PromptsManager.tsx
Normal file
403
src/components/conversores/PromptsManager.tsx
Normal file
@@ -0,0 +1,403 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Plus, Edit, Trash2, Save, X, Download, FileText, RefreshCw } from 'lucide-react';
|
||||
import { usePrompts } from '@/hooks/usePrompts';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
|
||||
const PromptsManager: React.FC = () => {
|
||||
const { prompts, loading, savePrompt, deletePrompt, downloadPromptAsJson, refreshPrompts } = usePrompts();
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const [editingPrompt, setEditingPrompt] = useState<any>(null);
|
||||
const [selectedPrompt, setSelectedPrompt] = useState<any>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
content: ''
|
||||
});
|
||||
|
||||
// Prompts atualizados baseados no processamento avançado
|
||||
const defaultPrompts = [
|
||||
{
|
||||
name: "Processamento Estruturas Metálicas - Peças com Componentes",
|
||||
content: `PROCESSAMENTO AVANÇADO DE PLANILHAS - ESTRUTURAS METÁLICAS
|
||||
|
||||
OBJETIVO: Converter dados de planilhas para CSV estruturado, identificando peças-mãe e seus componentes.
|
||||
|
||||
REGRAS DE IDENTIFICAÇÃO:
|
||||
|
||||
1. PEÇAS-MÃE:
|
||||
- Identificadas pela coluna "Marca" com valor preenchido
|
||||
- Extrair número da marca (último número após hífen)
|
||||
- Se não houver hífen, usar valor completo da marca
|
||||
|
||||
2. COMPONENTES:
|
||||
- Identificados pela coluna "Pos." (Posição)
|
||||
- Devem ter código numérico entre 1000-9999 no final
|
||||
- Formato típico: XXX-XXXX onde XXXX é 1000-9999
|
||||
- Pertencem à peça-mãe anterior na sequência
|
||||
|
||||
3. EXTRAÇÃO DE METADADOS DO NOME DO ARQUIVO:
|
||||
- Formato esperado: "OF_NUMBER-ETAPA_FASE.extensão"
|
||||
- Exemplo: "B117-1.xlsx" → OF: B117, Etapa: 1
|
||||
|
||||
4. MAPEAMENTO DE COLUNAS (buscar variações):
|
||||
- Marca: ['Marca', 'marca', 'MARCA']
|
||||
- Descrição: ['Descrição', 'Descricao', 'descrição', 'descricao', 'DESCRIÇÃO', 'DESCRICAO']
|
||||
- Quantidade: ['Qtde', 'Quantidade', 'qtde', 'quantidade', 'QTDE', 'QUANTIDADE']
|
||||
- Peso Unitário: ['P.Un.', 'PesoUnitario', 'Peso Unitário', 'peso_unitario', 'PESO_UNITARIO']
|
||||
- Peso Total: ['P.Tot.', 'PesoTotal', 'Peso Total', 'peso_total', 'PESO_TOTAL']
|
||||
- Material: ['Mat', 'Material', 'mat', 'material', 'MAT', 'MATERIAL']
|
||||
- Posição: ['Pos.', 'Pos', 'pos', 'POS']
|
||||
|
||||
5. ESTRUTURA CSV DE SAÍDA:
|
||||
Cabeçalho: 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
|
||||
|
||||
6. LÓGICA DE PROCESSAMENTO:
|
||||
- Para peças SEM componentes: uma linha com tem_componentes=false
|
||||
- Para peças COM componentes: uma linha para cada componente com tem_componentes=true
|
||||
- Tratamento superficial sempre "-" (padrão)
|
||||
- Perfil principal = material da peça-mãe
|
||||
- Números formatados com 1 casa decimal
|
||||
|
||||
7. FORMATAÇÃO:
|
||||
- Números: converter vírgula para ponto, 1 casa decimal
|
||||
- Texto com vírgula: envolver em aspas duplas
|
||||
- Valores vazios: deixar em branco
|
||||
- Encoding: UTF-8 com BOM
|
||||
|
||||
EXEMPLO DE PROCESSAMENTO:
|
||||
Entrada: B117-1.xlsx com peça B117-1-1 e componente B117-1-1-1001
|
||||
Saída: B117,1,1-1,GAB,1,210.0,210.0,-,A36,A36,true,1001,CHAPA,A36,15.0,2`
|
||||
},
|
||||
{
|
||||
name: "Conversão Lista de Peças - Formato Detalhado",
|
||||
content: `Converta os dados da tabela para um formato CSV detalhado seguindo estas instruções:
|
||||
|
||||
1. ESTRUTURA DO CSV:
|
||||
- Primeira linha deve conter todos os cabeçalhos das colunas
|
||||
- Cada linha subsequente deve representar uma peça/item
|
||||
- Use vírgula como separador
|
||||
- Se uma célula estiver vazia, deixe em branco mas mantenha as vírgulas
|
||||
|
||||
2. COLUNAS OBRIGATÓRIAS (na ordem):
|
||||
- of_number (número da OF)
|
||||
- etapa_fase (fase da etapa)
|
||||
- marca (marca da peça)
|
||||
- descricao (descrição)
|
||||
- quantidade (quantidade)
|
||||
- peso_unitario (peso unitário)
|
||||
- peso_total (peso total)
|
||||
- tratamento_superficial (tratamento)
|
||||
- material (material)
|
||||
- perfil_principal (perfil principal)
|
||||
- tem_componentes (true/false)
|
||||
- marca_componente (marca do componente)
|
||||
- descricao_componente (descrição do componente)
|
||||
- perfil_componente (perfil do componente)
|
||||
- peso_unitario_componente (peso unitário do componente)
|
||||
- quantidade_por_peca (quantidade por peça)
|
||||
|
||||
3. FORMATAÇÃO:
|
||||
- Mantenha todos os dados originais
|
||||
- Para campos booleanos, use "true" ou "false"
|
||||
- Para campos numéricos, mantenha os valores como estão
|
||||
- Para campos de texto, mantenha exatamente como na tabela original
|
||||
- Se não houver componentes, deixe os campos de componente vazios
|
||||
|
||||
4. EXEMPLO DE SAÍDA:
|
||||
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
|
||||
B117,1,B117-1-1,GAB,1,210,3,,,A36,false,,,,,
|
||||
B117,1,B117-1-2,Ch 3,1,370,3,,,A36,false,,,,,`
|
||||
},
|
||||
{
|
||||
name: "Conversão Lista de Peças - Formato Simplificado",
|
||||
content: `Converta os dados da tabela para formato CSV seguindo estas diretrizes:
|
||||
|
||||
1. MANTER ESTRUTURA ORIGINAL:
|
||||
- Use exatamente os mesmos cabeçalhos da tabela importada
|
||||
- Mantenha a mesma ordem das colunas
|
||||
- Preserve todos os valores como estão na planilha
|
||||
|
||||
2. FORMATAÇÃO CSV:
|
||||
- Primeira linha: cabeçalhos separados por vírgula
|
||||
- Linhas seguintes: dados separados por vírgula
|
||||
- Campos vazios: deixar em branco mas manter vírgulas
|
||||
- Não adicionar aspas desnecessárias
|
||||
|
||||
3. TRATAMENTO DE DADOS:
|
||||
- Números: manter formato original
|
||||
- Texto: sem modificações
|
||||
- Campos vazios: representar como campo vazio (não "null" ou "undefined")
|
||||
|
||||
4. EXEMPLO:
|
||||
#,Marca,Pos.,Descrição,Qtde,Lar.,Esp.,Comp.,Mat.,P.Un.,P.Tot.
|
||||
1,-,-,-,-,-,-,-,-,-,416
|
||||
2,B117-1-1,B117-1-1,GAB,1,210,3,210,A36,1,1`
|
||||
}
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
// Criar prompts padrão se não existirem
|
||||
const createDefaultPrompts = async () => {
|
||||
if (!loading && prompts.length === 0) {
|
||||
for (const defaultPrompt of defaultPrompts) {
|
||||
await savePrompt(defaultPrompt);
|
||||
}
|
||||
await refreshPrompts();
|
||||
}
|
||||
};
|
||||
|
||||
createDefaultPrompts();
|
||||
}, [loading, prompts.length]);
|
||||
|
||||
const handleEdit = (prompt: any) => {
|
||||
setEditingPrompt(prompt);
|
||||
setFormData({
|
||||
name: prompt.name,
|
||||
content: prompt.content
|
||||
});
|
||||
setShowDialog(true);
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
setEditingPrompt(null);
|
||||
setFormData({
|
||||
name: '',
|
||||
content: ''
|
||||
});
|
||||
setShowDialog(true);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!formData.name.trim() || !formData.content.trim()) {
|
||||
toast.error('Nome e conteúdo são obrigatórios');
|
||||
return;
|
||||
}
|
||||
|
||||
await savePrompt({
|
||||
id: editingPrompt?.id,
|
||||
name: formData.name,
|
||||
content: formData.content
|
||||
});
|
||||
|
||||
setShowDialog(false);
|
||||
setFormData({ name: '', content: '' });
|
||||
setEditingPrompt(null);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setShowDialog(false);
|
||||
setFormData({ name: '', content: '' });
|
||||
setEditingPrompt(null);
|
||||
};
|
||||
|
||||
const handleDownload = (prompt: any) => {
|
||||
const filename = window.prompt('Nome do arquivo (sem extensão):', prompt.name.replace(/\s+/g, '_'));
|
||||
if (filename) {
|
||||
downloadPromptAsJson(prompt, `${filename}.json`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateDefaultPrompts = async () => {
|
||||
for (const defaultPrompt of defaultPrompts) {
|
||||
await savePrompt(defaultPrompt);
|
||||
}
|
||||
await refreshPrompts();
|
||||
toast.success('Prompts padrão criados com sucesso!');
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="animate-pulse">Carregando prompts...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="bg-slate-700/50 border-slate-600">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle className="text-white text-lg">Gerenciar Prompts</CardTitle>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleCreateDefaultPrompts}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="bg-blue-600 hover:bg-blue-700 border-blue-500"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4 mr-1" />
|
||||
Criar Padrões
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAdd}
|
||||
size="sm"
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
Novo Prompt
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{prompts.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-slate-400 text-sm mb-4">
|
||||
Nenhum prompt cadastrado
|
||||
</p>
|
||||
<Button
|
||||
onClick={handleCreateDefaultPrompts}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Criar Prompts Padrão
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{prompts.map((prompt) => (
|
||||
<div
|
||||
key={prompt.id}
|
||||
className={`p-4 rounded border cursor-pointer transition-colors ${
|
||||
selectedPrompt?.id === prompt.id
|
||||
? 'bg-blue-800/50 border-blue-500'
|
||||
: 'bg-slate-800/50 border-slate-600 hover:bg-slate-800/70'
|
||||
}`}
|
||||
onClick={() => setSelectedPrompt(prompt)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<h4 className="text-white font-medium">{prompt.name}</h4>
|
||||
<p className="text-slate-400 text-sm mt-1 line-clamp-2">
|
||||
{prompt.content.substring(0, 100)}...
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDownload(prompt);
|
||||
}}
|
||||
className="text-blue-400 hover:text-blue-300"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEdit(prompt);
|
||||
}}
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deletePrompt(prompt.id);
|
||||
}}
|
||||
className="text-red-400 hover:text-red-300"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedPrompt && (
|
||||
<div className="mt-6 p-4 bg-slate-800/30 rounded border border-slate-600">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="text-white font-medium">Prompt Selecionado</h4>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleDownload(selectedPrompt)}
|
||||
className="text-xs"
|
||||
>
|
||||
<Download className="w-3 h-3 mr-1" />
|
||||
Baixar JSON
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-slate-300 text-sm"><strong>Nome:</strong> {selectedPrompt.name}</p>
|
||||
<div>
|
||||
<p className="text-slate-300 text-sm mb-2"><strong>Conteúdo:</strong></p>
|
||||
<div className="bg-slate-900/50 p-3 rounded text-slate-300 text-sm max-h-40 overflow-y-auto whitespace-pre-wrap">
|
||||
{selectedPrompt.content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog open={showDialog} onOpenChange={handleClose}>
|
||||
<DialogContent className="bg-slate-800 border-slate-700 max-w-4xl max-h-[80vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">
|
||||
{editingPrompt ? 'Editar Prompt' : 'Novo Prompt'}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-400">
|
||||
Configure as instruções que serão utilizadas na geração dos arquivos CSV
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="name" className="text-white">Nome do Prompt</Label>
|
||||
<Input
|
||||
id="name"
|
||||
placeholder="Ex: Conversão de Lista de Peças"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="bg-slate-700 border-slate-600 text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="content" className="text-white">Conteúdo do Prompt</Label>
|
||||
<Textarea
|
||||
id="content"
|
||||
placeholder="Insira as instruções detalhadas para a conversão dos dados..."
|
||||
value={formData.content}
|
||||
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
|
||||
className="bg-slate-700 border-slate-600 text-white min-h-[400px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button onClick={handleSave} className="bg-blue-600 hover:bg-blue-700">
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
{editingPrompt ? 'Salvar' : 'Criar'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromptsManager;
|
||||
601
src/components/conversores/TecnometalConverter.tsx
Normal file
601
src/components/conversores/TecnometalConverter.tsx
Normal file
@@ -0,0 +1,601 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Upload, FileText, Download, Trash2, Plus, RefreshCw, Settings, CheckCircle, Clock } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { jsonCodeManager } from '@/utils/jsonCodeManager';
|
||||
import { apiKeyManager } from '@/utils/apiKeyManager';
|
||||
import { useWebhookConfigs } from '@/hooks/useWebhookConfigs';
|
||||
import WebhookConfigManager from './WebhookConfigManager';
|
||||
|
||||
interface ProcessedRow {
|
||||
of_number: string;
|
||||
etapa_fase: string;
|
||||
marca: string;
|
||||
descricao: string;
|
||||
quantidade: string;
|
||||
peso_unitario: string;
|
||||
peso_total: string;
|
||||
tratamento_superficial: string;
|
||||
material: string;
|
||||
perfil_principal: string;
|
||||
tem_componentes: string;
|
||||
marca_componente: string;
|
||||
descricao_componente: string;
|
||||
perfil_componente: string;
|
||||
peso_unitario_componente: string;
|
||||
quantidade_por_peca: string;
|
||||
}
|
||||
|
||||
const TecnometalConverter: React.FC = () => {
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [processedData, setProcessedData] = useState<ProcessedRow[]>([]);
|
||||
const [showTable, setShowTable] = useState(false);
|
||||
const [editingCell, setEditingCell] = useState<{ row: number; col: string } | null>(null);
|
||||
const [debugInfo, setDebugInfo] = useState<string>('');
|
||||
const [tecnometalConfig, setTecnometalConfig] = useState<any>(null);
|
||||
const [showWebhookConfig, setShowWebhookConfig] = useState(false);
|
||||
const [selectedWebhookConfig, setSelectedWebhookConfig] = useState<any>(null);
|
||||
const [currentProcessing, setCurrentProcessing] = useState<any>(null);
|
||||
const [isMonitoring, setIsMonitoring] = useState(false);
|
||||
|
||||
const { createFileProcessing, updateFileProcessing, fileProcessings } = useWebhookConfigs();
|
||||
|
||||
const columnHeaders = [
|
||||
'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 columnLabels = {
|
||||
of_number: 'OF',
|
||||
etapa_fase: 'Fase',
|
||||
marca: 'Marca',
|
||||
descricao: 'Descrição',
|
||||
quantidade: 'Qtd',
|
||||
peso_unitario: 'Peso Un.',
|
||||
peso_total: 'Peso Total',
|
||||
tratamento_superficial: 'Tratamento',
|
||||
material: 'Material',
|
||||
perfil_principal: 'Perfil Principal',
|
||||
tem_componentes: 'Tem Comp.',
|
||||
marca_componente: 'Marca Comp.',
|
||||
descricao_componente: 'Desc. Comp.',
|
||||
perfil_componente: 'Perfil Comp.',
|
||||
peso_unitario_componente: 'Peso Un. Comp.',
|
||||
quantidade_por_peca: 'Qtd por Peça'
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const loadTecnometalConfig = async () => {
|
||||
try {
|
||||
const config = await jsonCodeManager.getJsonCodeByName('Tecnometal');
|
||||
if (config) {
|
||||
setTecnometalConfig(config);
|
||||
setDebugInfo(prev => prev + 'Configuração Tecnometal carregada com sucesso!\n');
|
||||
} else {
|
||||
setDebugInfo(prev => prev + 'AVISO: Configuração "Tecnometal" não encontrada no sistema.\n');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar configuração Tecnometal:', error);
|
||||
setDebugInfo(prev => prev + `ERRO ao carregar configuração: ${error}\n`);
|
||||
}
|
||||
};
|
||||
|
||||
loadTecnometalConfig();
|
||||
}, []);
|
||||
|
||||
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
const allowedTypes = [
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-excel',
|
||||
'application/pdf'
|
||||
];
|
||||
|
||||
if (allowedTypes.includes(file.type)) {
|
||||
setSelectedFile(file);
|
||||
setDebugInfo('');
|
||||
setShowTable(false);
|
||||
setProcessedData([]);
|
||||
setCurrentProcessing(null);
|
||||
toast.success('Arquivo selecionado com sucesso!');
|
||||
} else {
|
||||
toast.error('Tipo de arquivo não suportado. Use .xlsx, .xls ou .pdf');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const sendToWebhook = async (file: File, webhookConfig: any) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await fetch(webhookConfig.link_envio, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Erro no webhook: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Erro ao enviar para webhook:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const checkWebhookStatus = async (webhookConfig: any, processingId: string) => {
|
||||
try {
|
||||
const response = await fetch(`${webhookConfig.link_recebimento}/${processingId}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Erro ao verificar status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Erro ao verificar status:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const startMonitoring = async (processingId: string) => {
|
||||
if (!selectedWebhookConfig) return;
|
||||
|
||||
setIsMonitoring(true);
|
||||
|
||||
const monitorInterval = setInterval(async () => {
|
||||
const status = await checkWebhookStatus(selectedWebhookConfig, processingId);
|
||||
|
||||
if (status) {
|
||||
if (status.completed) {
|
||||
await updateFileProcessing(processingId, {
|
||||
status: 'concluido',
|
||||
completed_at: new Date().toISOString(),
|
||||
download_url: status.download_url
|
||||
});
|
||||
|
||||
setCurrentProcessing({
|
||||
...currentProcessing,
|
||||
status: 'concluido',
|
||||
download_url: status.download_url
|
||||
});
|
||||
|
||||
setIsMonitoring(false);
|
||||
clearInterval(monitorInterval);
|
||||
toast.success('Arquivo processado e disponível para download!');
|
||||
} else if (status.error) {
|
||||
await updateFileProcessing(processingId, {
|
||||
status: 'erro'
|
||||
});
|
||||
|
||||
setIsMonitoring(false);
|
||||
clearInterval(monitorInterval);
|
||||
toast.error('Erro no processamento do arquivo');
|
||||
}
|
||||
}
|
||||
}, 5000); // Verifica a cada 5 segundos
|
||||
|
||||
// Para o monitoramento após 10 minutos
|
||||
setTimeout(() => {
|
||||
clearInterval(monitorInterval);
|
||||
setIsMonitoring(false);
|
||||
}, 600000);
|
||||
};
|
||||
|
||||
const processFile = async () => {
|
||||
if (!selectedFile) {
|
||||
toast.error('Por favor, selecione um arquivo para processar.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedWebhookConfig) {
|
||||
toast.error('Por favor, selecione uma configuração de webhook.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProcessing(true);
|
||||
setDebugInfo('Enviando arquivo para webhook...\n');
|
||||
|
||||
try {
|
||||
// Criar registro de processamento
|
||||
const processing = {
|
||||
webhook_config_id: selectedWebhookConfig.id,
|
||||
file_name: selectedFile.name,
|
||||
file_type: selectedFile.type,
|
||||
status: 'enviando'
|
||||
};
|
||||
|
||||
const success = await createFileProcessing(processing);
|
||||
if (!success) {
|
||||
throw new Error('Erro ao criar registro de processamento');
|
||||
}
|
||||
|
||||
// Enviar arquivo para webhook
|
||||
const webhookResponse = await sendToWebhook(selectedFile, selectedWebhookConfig);
|
||||
|
||||
setDebugInfo(prev => prev + 'Arquivo enviado com sucesso!\n');
|
||||
setDebugInfo(prev => prev + `ID do processamento: ${webhookResponse.processing_id}\n`);
|
||||
|
||||
// Atualizar status para enviado
|
||||
const currentFileProcessing = fileProcessings.find(fp =>
|
||||
fp.file_name === selectedFile.name && fp.status === 'enviando'
|
||||
);
|
||||
|
||||
if (currentFileProcessing) {
|
||||
await updateFileProcessing(currentFileProcessing.id, {
|
||||
status: 'processando'
|
||||
});
|
||||
|
||||
setCurrentProcessing({
|
||||
...currentFileProcessing,
|
||||
status: 'processando'
|
||||
});
|
||||
|
||||
// Iniciar monitoramento
|
||||
startMonitoring(currentFileProcessing.id);
|
||||
}
|
||||
|
||||
toast.success('Arquivo enviado para processamento!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro ao processar arquivo:', error);
|
||||
setDebugInfo(prev => prev + `ERRO: ${error}\n`);
|
||||
toast.error('Erro ao processar o arquivo. Verifique as configurações de webhook.');
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const downloadProcessedFile = async () => {
|
||||
if (!currentProcessing?.download_url) {
|
||||
toast.error('URL de download não disponível.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(currentProcessing.download_url);
|
||||
if (!response.ok) {
|
||||
throw new Error('Erro ao baixar arquivo');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `processed_${currentProcessing.file_name}`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
toast.success('Arquivo baixado com sucesso!');
|
||||
} catch (error) {
|
||||
console.error('Erro ao baixar arquivo:', error);
|
||||
toast.error('Erro ao baixar o arquivo processado.');
|
||||
}
|
||||
};
|
||||
|
||||
const processFileLocally = async (jsonData: any[], ofNumber: string, etapaFase: string) => {
|
||||
const linhasFinais: ProcessedRow[] = [];
|
||||
let dadosPecaMae: any = null;
|
||||
let componentesDoGrupo: any[] = [];
|
||||
|
||||
for (const row of jsonData) {
|
||||
const isEmptyRow = Object.values(row).every(val =>
|
||||
val === null || val === undefined || val === ''
|
||||
);
|
||||
|
||||
if (isEmptyRow) continue;
|
||||
|
||||
if (row['Marca']) {
|
||||
if (dadosPecaMae) {
|
||||
const pecaMaeFormatada = {
|
||||
of_number: ofNumber,
|
||||
etapa_fase: etapaFase,
|
||||
marca: formatarMarca(dadosPecaMae['Marca']),
|
||||
descricao: dadosPecaMae['Descrição'] || '',
|
||||
quantidade: String(dadosPecaMae['Qtde'] || ''),
|
||||
peso_unitario: formatarDecimal(dadosPecaMae['P.Un.'] || ''),
|
||||
peso_total: formatarDecimal(dadosPecaMae['P.Tot.'] || ''),
|
||||
tratamento_superficial: '-',
|
||||
material: componentesDoGrupo.length > 0 ?
|
||||
getMaterial(componentesDoGrupo[0]['Comp. Mat.'] || '') : '',
|
||||
perfil_principal: dadosPecaMae['Descrição'] || '',
|
||||
tem_componentes: componentesDoGrupo.length > 0 ? 'true' : 'false'
|
||||
};
|
||||
|
||||
if (componentesDoGrupo.length > 0) {
|
||||
for (const comp of componentesDoGrupo) {
|
||||
linhasFinais.push({
|
||||
...pecaMaeFormatada,
|
||||
marca_componente: formatarMarca(comp['Pos.'] || ''),
|
||||
descricao_componente: comp['Descrição'] || '',
|
||||
perfil_componente: comp['Descrição'] || '',
|
||||
peso_unitario_componente: formatarDecimal(comp['P.Un.'] || ''),
|
||||
quantidade_por_peca: String(comp['Qtde'] || '')
|
||||
});
|
||||
}
|
||||
} else {
|
||||
linhasFinais.push({
|
||||
...pecaMaeFormatada,
|
||||
marca_componente: '',
|
||||
descricao_componente: '',
|
||||
perfil_componente: '',
|
||||
peso_unitario_componente: '',
|
||||
quantidade_por_peca: ''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
dadosPecaMae = row;
|
||||
componentesDoGrupo = [];
|
||||
} else if (row['Pos.'] && dadosPecaMae) {
|
||||
componentesDoGrupo.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
return linhasFinais;
|
||||
};
|
||||
|
||||
const extractMetadataFromFilename = (filename: string) => {
|
||||
console.log('Extraindo metadados do nome do arquivo:', filename);
|
||||
const ofNumber = filename.split('-')[0];
|
||||
const faseMatch = filename.match(/FASE-(\d+)/i);
|
||||
const etapaFase = faseMatch ? faseMatch[1] : '1';
|
||||
|
||||
console.log('Metadados extraídos:', { ofNumber, etapaFase });
|
||||
return { ofNumber, etapaFase };
|
||||
};
|
||||
|
||||
const formatarMarca = (marca: string): string => {
|
||||
if (typeof marca === 'string' && marca.includes('-')) {
|
||||
return marca.split('-').pop() || marca;
|
||||
}
|
||||
return marca;
|
||||
};
|
||||
|
||||
const formatarDecimal = (numero: any): string => {
|
||||
try {
|
||||
return String(numero).replace(',', '.');
|
||||
} catch {
|
||||
return String(numero);
|
||||
}
|
||||
};
|
||||
|
||||
const getMaterial = (matStr: string): string => {
|
||||
if (typeof matStr === 'string') {
|
||||
const parts = matStr.split(' ');
|
||||
return parts[parts.length - 1] || '';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const handleCellEdit = (rowIndex: number, column: string, value: string) => {
|
||||
const newData = [...processedData];
|
||||
newData[rowIndex] = { ...newData[rowIndex], [column]: value };
|
||||
setProcessedData(newData);
|
||||
};
|
||||
|
||||
const deleteRow = (rowIndex: number) => {
|
||||
const newData = processedData.filter((_, index) => index !== rowIndex);
|
||||
setProcessedData(newData);
|
||||
toast.success('Linha excluída com sucesso!');
|
||||
};
|
||||
|
||||
const addNewRow = () => {
|
||||
const newRow: ProcessedRow = {
|
||||
of_number: '',
|
||||
etapa_fase: '',
|
||||
marca: '',
|
||||
descricao: '',
|
||||
quantidade: '',
|
||||
peso_unitario: '',
|
||||
peso_total: '',
|
||||
tratamento_superficial: '-',
|
||||
material: '',
|
||||
perfil_principal: '',
|
||||
tem_componentes: 'false',
|
||||
marca_componente: '',
|
||||
descricao_componente: '',
|
||||
perfil_componente: '',
|
||||
peso_unitario_componente: '',
|
||||
quantidade_por_peca: ''
|
||||
};
|
||||
|
||||
setProcessedData([...processedData, newRow]);
|
||||
toast.success('Nova linha adicionada!');
|
||||
};
|
||||
|
||||
const generateCSV = () => {
|
||||
if (processedData.length === 0) {
|
||||
toast.error('Nenhum dado para gerar o CSV.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const csvContent = [
|
||||
columnHeaders.join(','),
|
||||
...processedData.map(row =>
|
||||
columnHeaders.map(col => row[col as keyof ProcessedRow]).join(',')
|
||||
)
|
||||
].join('\n');
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
|
||||
const { ofNumber, etapaFase } = extractMetadataFromFilename(selectedFile?.name || 'arquivo');
|
||||
const fileName = `CSV_FINAL_${ofNumber}_FASE-${etapaFase}.csv`;
|
||||
|
||||
link.setAttribute('href', URL.createObjectURL(blob));
|
||||
link.setAttribute('download', fileName);
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
toast.success(`Arquivo ${fileName} gerado com sucesso!`);
|
||||
} catch (error) {
|
||||
console.error('Erro ao gerar CSV:', error);
|
||||
toast.error('Erro ao gerar o arquivo CSV.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card className="bg-slate-800/50 border-slate-700">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<FileText className="w-5 h-5" />
|
||||
Processador Tecnometal com Webhook
|
||||
<Button
|
||||
onClick={() => setShowWebhookConfig(!showWebhookConfig)}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="ml-auto"
|
||||
>
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
Configurações
|
||||
</Button>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{showWebhookConfig && (
|
||||
<WebhookConfigManager
|
||||
converterName="Tecnometal"
|
||||
onConfigSelect={(config) => {
|
||||
setSelectedWebhookConfig(config);
|
||||
setShowWebhookConfig(false);
|
||||
toast.success('Configuração selecionada!');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedWebhookConfig && (
|
||||
<Card className="bg-slate-700/30 border-slate-600">
|
||||
<CardContent className="pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-white text-sm font-medium">Configuração Ativa:</p>
|
||||
<p className="text-slate-400 text-xs">
|
||||
Envio: {selectedWebhookConfig.link_envio}
|
||||
</p>
|
||||
</div>
|
||||
<CheckCircle className="w-5 h-5 text-green-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-white text-sm font-medium">
|
||||
Selecionar Arquivo (.pdf, .xlsx)
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="file"
|
||||
accept=".pdf,.xlsx,.xls"
|
||||
onChange={handleFileSelect}
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full bg-slate-600 border-slate-500 text-white hover:bg-slate-500"
|
||||
>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
{selectedFile ? selectedFile.name : 'Selecionar Arquivo'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={processFile}
|
||||
disabled={!selectedFile || !selectedWebhookConfig || isProcessing}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
|
||||
Enviando para Webhook...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
Enviar e Processar
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{currentProcessing && (
|
||||
<Card className="bg-slate-700/30 border-slate-600">
|
||||
<CardContent className="pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-white text-sm font-medium">Status do Processamento:</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
{currentProcessing.status === 'processando' ? (
|
||||
<>
|
||||
<Clock className="w-4 h-4 text-yellow-500 animate-pulse" />
|
||||
<span className="text-yellow-400 text-sm">Processando...</span>
|
||||
</>
|
||||
) : currentProcessing.status === 'concluido' ? (
|
||||
<>
|
||||
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||
<span className="text-green-400 text-sm">Concluído</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-slate-400 text-sm">{currentProcessing.status}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{currentProcessing.status === 'concluido' && currentProcessing.download_url && (
|
||||
<Button
|
||||
onClick={downloadProcessedFile}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Baixar Arquivo
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{debugInfo && (
|
||||
<div className="bg-slate-900 border border-slate-600 rounded p-3">
|
||||
<h4 className="text-white font-medium mb-2">Log de Processamento:</h4>
|
||||
<pre className="text-green-400 text-xs whitespace-pre-wrap overflow-x-auto">
|
||||
{debugInfo}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{isMonitoring && (
|
||||
<Card className="bg-slate-800/50 border-slate-700">
|
||||
<CardContent className="pt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<RefreshCw className="w-4 h-4 animate-spin text-blue-500" />
|
||||
<span className="text-white">Monitorando processamento...</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TecnometalConverter;
|
||||
199
src/components/conversores/WebhookConfigManager.tsx
Normal file
199
src/components/conversores/WebhookConfigManager.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Plus, Edit, Trash2, Save, X } from 'lucide-react';
|
||||
import { useWebhookConfigs } from '@/hooks/useWebhookConfigs';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
|
||||
interface WebhookConfigManagerProps {
|
||||
converterName: string;
|
||||
onConfigSelect: (config: any) => void;
|
||||
}
|
||||
|
||||
const WebhookConfigManager: React.FC<WebhookConfigManagerProps> = ({
|
||||
converterName,
|
||||
onConfigSelect
|
||||
}) => {
|
||||
const { webhookConfigs, loading, saveWebhookConfig, deleteWebhookConfig } = useWebhookConfigs();
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const [editingConfig, setEditingConfig] = useState<any>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
link_envio: '',
|
||||
link_recebimento: ''
|
||||
});
|
||||
|
||||
const configs = webhookConfigs.filter(config => config.converter_name === converterName);
|
||||
|
||||
const handleEdit = (config: any) => {
|
||||
setEditingConfig(config);
|
||||
setFormData({
|
||||
link_envio: config.link_envio,
|
||||
link_recebimento: config.link_recebimento
|
||||
});
|
||||
setShowDialog(true);
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
setEditingConfig(null);
|
||||
setFormData({
|
||||
link_envio: '',
|
||||
link_recebimento: ''
|
||||
});
|
||||
setShowDialog(true);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!formData.link_envio.trim() || !formData.link_recebimento.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await saveWebhookConfig({
|
||||
id: editingConfig?.id,
|
||||
converter_name: converterName,
|
||||
link_envio: formData.link_envio,
|
||||
link_recebimento: formData.link_recebimento
|
||||
});
|
||||
|
||||
setShowDialog(false);
|
||||
setFormData({ link_envio: '', link_recebimento: '' });
|
||||
setEditingConfig(null);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setShowDialog(false);
|
||||
setFormData({ link_envio: '', link_recebimento: '' });
|
||||
setEditingConfig(null);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="animate-pulse">Carregando configurações...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="bg-slate-700/50 border-slate-600">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle className="text-white text-sm">Configurações de Webhook</CardTitle>
|
||||
<Button
|
||||
onClick={handleAdd}
|
||||
size="sm"
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
Adicionar
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{configs.length === 0 ? (
|
||||
<p className="text-slate-400 text-sm text-center py-4">
|
||||
Nenhuma configuração de webhook cadastrada
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{configs.map((config) => (
|
||||
<div
|
||||
key={config.id}
|
||||
className="flex items-center justify-between p-3 bg-slate-800/50 rounded border border-slate-600"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<p className="text-white text-sm font-medium">
|
||||
Envio: {config.link_envio}
|
||||
</p>
|
||||
<p className="text-slate-400 text-xs">
|
||||
Recebimento: {config.link_recebimento}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onConfigSelect(config)}
|
||||
className="text-xs"
|
||||
>
|
||||
Usar
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => handleEdit(config)}
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => deleteWebhookConfig(config.id)}
|
||||
className="text-red-400 hover:text-red-300"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog open={showDialog} onOpenChange={handleClose}>
|
||||
<DialogContent className="bg-slate-800 border-slate-700">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">
|
||||
{editingConfig ? 'Editar Configuração' : 'Nova Configuração'}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-400">
|
||||
Configure os links de webhook para o conversor {converterName}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="link_envio" className="text-white">Link de Envio</Label>
|
||||
<Input
|
||||
id="link_envio"
|
||||
placeholder="https://api.exemplo.com/upload"
|
||||
value={formData.link_envio}
|
||||
onChange={(e) => setFormData({ ...formData, link_envio: e.target.value })}
|
||||
className="bg-slate-700 border-slate-600 text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="link_recebimento" className="text-white">Link de Recebimento</Label>
|
||||
<Input
|
||||
id="link_recebimento"
|
||||
placeholder="https://api.exemplo.com/download"
|
||||
value={formData.link_recebimento}
|
||||
onChange={(e) => setFormData({ ...formData, link_recebimento: e.target.value })}
|
||||
className="bg-slate-700 border-slate-600 text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button onClick={handleSave} className="bg-blue-600 hover:bg-blue-700">
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
{editingConfig ? 'Salvar' : 'Criar'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebhookConfigManager;
|
||||
Reference in New Issue
Block a user