feat: motor de conversao PDF Tecnometal e exportacao Excel padronizada
This commit is contained in:
@@ -0,0 +1,692 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Conversor Tecnometal (PDF para Excel)</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--primary: #1e3a8a;
|
||||
--accent: #0284c7;
|
||||
--bg: #0f172a;
|
||||
--card-bg: #1e293b;
|
||||
--text: #f8fafc;
|
||||
--text-muted: #94a3b8;
|
||||
--border: #334155;
|
||||
--success: #16a34a;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
padding: 24px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
background: var(--card-bg);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 25px -5px rgb(0 0 0 / 0.3);
|
||||
border: 1px solid var(--border);
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
header {
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 18px;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #38bdf8;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 12px;
|
||||
background: rgba(14, 165, 233, 0.15);
|
||||
color: #38bdf8;
|
||||
border: 1px solid rgba(14, 165, 233, 0.3);
|
||||
padding: 4px 12px;
|
||||
border-radius: 9999px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.upload-zone {
|
||||
border: 2px dashed #475569;
|
||||
border-radius: 10px;
|
||||
padding: 36px 20px;
|
||||
text-align: center;
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.upload-zone:hover, .upload-zone.dragover {
|
||||
border-color: #38bdf8;
|
||||
background: rgba(56, 189, 248, 0.08);
|
||||
}
|
||||
|
||||
.upload-zone p {
|
||||
color: var(--text-muted);
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.upload-zone .btn-select {
|
||||
display: inline-block;
|
||||
margin-top: 14px;
|
||||
padding: 8px 18px;
|
||||
background: #0284c7;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border-radius: 6px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.actions-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
background: rgba(15, 23, 42, 0.4);
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.btn-export {
|
||||
background: var(--success);
|
||||
color: white;
|
||||
}
|
||||
.btn-export:hover {
|
||||
background: #15803d;
|
||||
}
|
||||
.btn-export:disabled {
|
||||
background: #475569;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
font-size: 13px;
|
||||
color: #38bdf8;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #090d16;
|
||||
color: #94a3b8;
|
||||
padding: 10px 12px;
|
||||
font-weight: 600;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
white-space: nowrap;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #334155;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: rgba(51, 65, 85, 0.4);
|
||||
}
|
||||
|
||||
.badge-sim {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: #34d399;
|
||||
border: 1px solid rgba(16, 185, 129, 0.4);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.badge-nao {
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
color: #fbbf24;
|
||||
border: 1px solid rgba(245, 158, 11, 0.4);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.log-panel {
|
||||
margin-top: 18px;
|
||||
padding: 12px;
|
||||
background: #090d16;
|
||||
color: #38bdf8;
|
||||
font-family: monospace;
|
||||
font-size: 11px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
max-height: 140px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<header>
|
||||
<div>
|
||||
<h1>Conversor Tecnometal (PDF para Excel)</h1>
|
||||
<p style="color: var(--text-muted); font-size: 13px; margin-top: 4px;">Leitura e conversão 100% no navegador de Listas de Conjuntos Tecnometal</p>
|
||||
</div>
|
||||
<div class="badge">Autônomo / Client-Side</div>
|
||||
</header>
|
||||
|
||||
<div class="upload-zone" id="dropZone">
|
||||
<p>Arraste e solte o arquivo <strong>PDF da Lista de Conjuntos Tecnometal</strong> aqui</p>
|
||||
<div class="btn-select">Selecionar PDF do Computador</div>
|
||||
<input type="file" id="fileInput" accept="application/pdf" style="display:none;">
|
||||
</div>
|
||||
|
||||
<div class="actions-bar">
|
||||
<div class="stats-card" id="statsCard">
|
||||
Aguardando arquivo PDF...
|
||||
</div>
|
||||
<button class="btn btn-export" id="btnExport" disabled>
|
||||
Baixar Planilha Excel (.xlsx)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>OF</th>
|
||||
<th>Fase</th>
|
||||
<th>Marca</th>
|
||||
<th>Descrição</th>
|
||||
<th>Composto?</th>
|
||||
<th style="text-align:center;">Qtd</th>
|
||||
<th>Material</th>
|
||||
<th>Perfil Principal</th>
|
||||
<th style="text-align:right;">Comp. Max (mm)</th>
|
||||
<th style="text-align:right;">Peso Unit. (kg)</th>
|
||||
<th style="text-align:right;">Peso Total (kg)</th>
|
||||
<th>Tratamento</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tableBody">
|
||||
<tr>
|
||||
<td colspan="12" style="text-align:center; padding: 24px; color: var(--text-muted);">
|
||||
Nenhum dado extraído ainda. Selecione um arquivo PDF acima.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="log-panel" id="logPanel">
|
||||
<div>> Sistema pronto. Aguardando arquivo...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
||||
|
||||
const dropZone = document.getElementById('dropZone');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const btnExport = document.getElementById('btnExport');
|
||||
const tableBody = document.getElementById('tableBody');
|
||||
const statsCard = document.getElementById('statsCard');
|
||||
const logPanel = document.getElementById('logPanel');
|
||||
|
||||
let extractedData = [];
|
||||
let currentFileName = 'Lista_Tecnometal';
|
||||
|
||||
function addLog(msg) {
|
||||
const line = document.createElement('div');
|
||||
line.textContent = `> ${msg}`;
|
||||
logPanel.appendChild(line);
|
||||
logPanel.scrollTop = logPanel.scrollHeight;
|
||||
}
|
||||
|
||||
dropZone.addEventListener('click', () => fileInput.click());
|
||||
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('dragover'); });
|
||||
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('dragover');
|
||||
if (e.dataTransfer.files.length > 0) handleFile(e.dataTransfer.files[0]);
|
||||
});
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
if (e.target.files.length > 0) handleFile(e.target.files[0]);
|
||||
});
|
||||
|
||||
const normalizeMaterial = (rawMat) => {
|
||||
if (!rawMat) return 'A36';
|
||||
const clean = rawMat.trim().toUpperCase();
|
||||
if (clean.includes('A572') || clean.includes('A-572') || clean.includes('A572GR5') || clean.includes('GR5')) {
|
||||
return 'A572-GR 50';
|
||||
}
|
||||
if (clean.includes('A36') || clean.includes('A-36')) {
|
||||
return 'A36';
|
||||
}
|
||||
if (clean.includes('1020') || clean.includes('SAE1020') || clean.includes('SAE 1020')) {
|
||||
return 'SAE 1020';
|
||||
}
|
||||
return clean;
|
||||
};
|
||||
|
||||
const parseNumeric = (valStr) => {
|
||||
if (!valStr && valStr !== 0) return 0;
|
||||
let clean = valStr.toString().trim();
|
||||
if (clean.includes(',') && clean.includes('.')) {
|
||||
if (clean.indexOf('.') < clean.indexOf(',')) {
|
||||
clean = clean.replace(/\./g, '').replace(',', '.');
|
||||
} else {
|
||||
clean = clean.replace(/,/g, '');
|
||||
}
|
||||
} else if (clean.includes(',')) {
|
||||
clean = clean.replace(',', '.');
|
||||
} else if (clean.includes('.')) {
|
||||
const parts = clean.split('.');
|
||||
if (parts.length === 2 && parts[1].length === 3) {
|
||||
clean = parts[0] + parts[1];
|
||||
}
|
||||
}
|
||||
const num = parseFloat(clean);
|
||||
return isNaN(num) ? 0 : num;
|
||||
};
|
||||
|
||||
async function handleFile(file) {
|
||||
if (file.type !== 'application/pdf' && !file.name.endsWith('.pdf')) {
|
||||
alert('Por favor, selecione um arquivo no formato PDF válido.');
|
||||
return;
|
||||
}
|
||||
|
||||
currentFileName = file.name.replace(/\.[^/.]+$/, '');
|
||||
statsCard.innerHTML = `Processando: <strong>${file.name}</strong>...`;
|
||||
addLog(`Carregando arquivo: ${file.name}`);
|
||||
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
||||
addLog(`PDF aberto! Total de páginas: ${pdf.numPages}`);
|
||||
|
||||
const allItems = [];
|
||||
for (let p = 1; p <= pdf.numPages; p++) {
|
||||
const page = await pdf.getPage(p);
|
||||
const textContent = await page.getTextContent();
|
||||
textContent.items.forEach(item => {
|
||||
if (item.str && item.str.trim() !== '') {
|
||||
allItems.push({
|
||||
text: item.str.trim(),
|
||||
x: item.transform[4],
|
||||
y: item.transform[5],
|
||||
page: p
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addLog(`Total de fragmentos de texto: ${allItems.length}`);
|
||||
processPdfLines(allItems);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Erro ao processar PDF: ' + err.message);
|
||||
addLog(`ERRO: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function processPdfLines(items) {
|
||||
items.sort((a, b) => {
|
||||
if (a.page !== b.page) return a.page - b.page;
|
||||
if (Math.abs(b.y - a.y) > 3) return b.y - a.y;
|
||||
return a.x - b.x;
|
||||
});
|
||||
|
||||
const lines = [];
|
||||
let currentLine = [];
|
||||
let lastY = null;
|
||||
let lastPage = null;
|
||||
|
||||
items.forEach(item => {
|
||||
if (lastPage === null || item.page !== lastPage || Math.abs(item.y - (lastY ?? item.y)) > 3.5) {
|
||||
if (currentLine.length > 0) lines.push(currentLine);
|
||||
currentLine = [item];
|
||||
lastY = item.y;
|
||||
lastPage = item.page;
|
||||
} else {
|
||||
currentLine.push(item);
|
||||
}
|
||||
});
|
||||
if (currentLine.length > 0) lines.push(currentLine);
|
||||
|
||||
let defaultOf = '';
|
||||
let defaultFase = '1';
|
||||
|
||||
for (const line of lines) {
|
||||
const lineText = line.map(i => i.text).join(' ');
|
||||
const matchTitulo =
|
||||
lineText.match(/^([A-Za-z0-9]+)-FASE\s*(\d+)/i) ||
|
||||
lineText.match(/([A-Za-z0-9]+)-FASE\s*(\d+)/i) ||
|
||||
lineText.match(/([A-Za-z0-9]+)-F(\d+)/i);
|
||||
if (matchTitulo) {
|
||||
defaultOf = matchTitulo[1];
|
||||
defaultFase = matchTitulo[2];
|
||||
addLog(`OF detectada: ${defaultOf} | Fase: ${defaultFase}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const colX = {
|
||||
marca: 35,
|
||||
pos: 95,
|
||||
descricao: 175,
|
||||
qtde: 255,
|
||||
largura: 300,
|
||||
espessura: 340,
|
||||
comprimento: 395,
|
||||
material: 450,
|
||||
pesoUnit: 510,
|
||||
pesoTotal: 570,
|
||||
superficieTotal: 640
|
||||
};
|
||||
|
||||
for (const line of lines) {
|
||||
const lineText = line.map(i => i.text).join(' ').toLowerCase();
|
||||
if (lineText.includes('marca') && lineText.includes('pos')) {
|
||||
for (const item of line) {
|
||||
const t = item.text.toLowerCase().trim();
|
||||
if (t === 'marca') colX.marca = item.x;
|
||||
else if (t.startsWith('pos')) colX.pos = item.x;
|
||||
else if (t.startsWith('descr')) colX.descricao = item.x;
|
||||
else if (t.startsWith('qtd')) colX.qtde = item.x;
|
||||
else if (t.startsWith('lar')) colX.largura = item.x;
|
||||
else if (t.startsWith('esp')) colX.espessura = item.x;
|
||||
else if (t.startsWith('comp')) colX.comprimento = item.x;
|
||||
else if (t.startsWith('mat')) colX.material = item.x;
|
||||
else if (t.startsWith('p.un') || t === 'p.un.') colX.pesoUnit = item.x;
|
||||
else if (t.startsWith('p.tot') || t === 'p.tot.') colX.pesoTotal = item.x;
|
||||
else if (t.startsWith('s.tot')) colX.superficieTotal = item.x;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getNearestCol = (x) => {
|
||||
let bestCol = '';
|
||||
let minDiff = Infinity;
|
||||
for (const [colName, targetX] of Object.entries(colX)) {
|
||||
const diff = Math.abs(x - targetX);
|
||||
if (diff < minDiff) {
|
||||
minDiff = diff;
|
||||
bestCol = colName;
|
||||
}
|
||||
}
|
||||
return bestCol;
|
||||
};
|
||||
|
||||
const assemblies = [];
|
||||
let currentAssembly = null;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const lineText = line.map(it => it.text).join(' ');
|
||||
|
||||
if (
|
||||
lineText.includes('LISTA DE CONJUNTOS') ||
|
||||
lineText.includes('Marca Pos. Descrição') ||
|
||||
lineText.includes('P.Un. P.Tot. S.Tot.') ||
|
||||
lineText.includes('B121-FASE')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rowByCol = {};
|
||||
for (const it of line) {
|
||||
const col = getNearestCol(it.x);
|
||||
if (col === 'superficieTotal') continue; // Ignora S.Tot.
|
||||
|
||||
if (!rowByCol[col]) {
|
||||
rowByCol[col] = it.text.trim();
|
||||
} else {
|
||||
rowByCol[col] += ' ' + it.text.trim();
|
||||
}
|
||||
}
|
||||
|
||||
const rawMarca = (rowByCol.marca || '').trim();
|
||||
const rawPos = (rowByCol.pos || '').trim();
|
||||
|
||||
const markMatch = rawMarca.match(/^([A-Za-z0-9]+)-(\d+)-(\d+)$/) || rawMarca.match(/^([A-Za-z0-9]+)-(\d+)$/);
|
||||
|
||||
if (markMatch) {
|
||||
const parts = rawMarca.split('-');
|
||||
const ofCode = parts.length === 3 ? parts[0] : defaultOf;
|
||||
const phaseCode = parts.length === 3 ? parts[1] : defaultFase;
|
||||
const pieceNumber = parts.length === 3 ? parts[2] : parts[1];
|
||||
|
||||
const qVal = parseInt(rowByCol.qtde || '1', 10);
|
||||
const mainQuant = isNaN(qVal) || qVal <= 0 ? 1 : qVal;
|
||||
|
||||
const pUnVal = parseNumeric(rowByCol.pesoUnit || 0);
|
||||
const pTotVal = parseNumeric(rowByCol.pesoTotal || 0);
|
||||
const compVal = parseNumeric(rowByCol.comprimento || 0);
|
||||
const matVal = rowByCol.material ? normalizeMaterial(rowByCol.material) : '';
|
||||
|
||||
const isSimple = rawPos === rawMarca || (compVal > 0 && !!matVal);
|
||||
|
||||
currentAssembly = {
|
||||
rawMark: rawMarca,
|
||||
of: ofCode || defaultOf,
|
||||
fase: phaseCode || defaultFase,
|
||||
marca: pieceNumber,
|
||||
descricao: rowByCol.descricao || 'ESTRUTURA',
|
||||
quantidade: mainQuant,
|
||||
material: matVal,
|
||||
perfilPrincipal: rowByCol.descricao || 'ESTRUTURA',
|
||||
comprimento: compVal,
|
||||
pesoUnitario: pUnVal,
|
||||
pesoTotal: pTotVal,
|
||||
isComposed: isSimple ? 'NÃO' : 'NÃO',
|
||||
components: []
|
||||
};
|
||||
|
||||
assemblies.push(currentAssembly);
|
||||
} else if (rawPos && currentAssembly) {
|
||||
currentAssembly.isComposed = 'SIM';
|
||||
|
||||
const compQuant = parseInt(rowByCol.qtde || '1', 10) || 1;
|
||||
const compLength = parseNumeric(rowByCol.comprimento || 0);
|
||||
const compMaterial = rowByCol.material ? normalizeMaterial(rowByCol.material) : 'A36';
|
||||
const compPUn = parseNumeric(rowByCol.pesoUnit || 0);
|
||||
const compPTot = parseNumeric(rowByCol.pesoTotal || 0);
|
||||
|
||||
currentAssembly.components.push({
|
||||
pos: rawPos,
|
||||
descricao: rowByCol.descricao || '',
|
||||
quantidade: compQuant,
|
||||
comprimento: compLength,
|
||||
material: compMaterial,
|
||||
pesoUnitario: compPUn,
|
||||
pesoTotal: compPTot
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
addLog(`Total de Peças extraídas: ${assemblies.length}`);
|
||||
|
||||
extractedData = assemblies.map(asm => {
|
||||
const isComposed = asm.isComposed === 'SIM' && asm.components.length > 0;
|
||||
let perfilPrincipal = asm.descricao;
|
||||
let materialPrincipal = asm.material || 'A36';
|
||||
let maxComp = asm.comprimento || 0;
|
||||
let pesoUnit = asm.pesoUnitario;
|
||||
let pesoTot = asm.pesoTotal;
|
||||
|
||||
if (isComposed) {
|
||||
let bestComp = asm.components[0];
|
||||
maxComp = bestComp.comprimento;
|
||||
|
||||
for (const comp of asm.components) {
|
||||
if (comp.comprimento > maxComp) {
|
||||
maxComp = comp.comprimento;
|
||||
bestComp = comp;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestComp) {
|
||||
materialPrincipal = bestComp.material || 'A36';
|
||||
perfilPrincipal = asm.descricao;
|
||||
}
|
||||
|
||||
if (pesoTot === 0 && asm.components.length > 0) {
|
||||
pesoTot = asm.components.reduce((acc, c) => acc + c.pesoTotal, 0);
|
||||
}
|
||||
if (pesoUnit === 0 && asm.quantidade > 0) {
|
||||
pesoUnit = pesoTot / asm.quantidade;
|
||||
}
|
||||
} else {
|
||||
if (pesoTot === 0 && pesoUnit > 0) {
|
||||
pesoTot = pesoUnit * asm.quantidade;
|
||||
}
|
||||
if (pesoUnit === 0 && pesoTot > 0 && asm.quantidade > 0) {
|
||||
pesoUnit = pesoTot / asm.quantidade;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
of: asm.of,
|
||||
fase: asm.fase,
|
||||
marca: asm.marca,
|
||||
descricao: asm.descricao,
|
||||
isComposed: isComposed ? 'SIM' : 'NÃO',
|
||||
quantidade: asm.quantidade,
|
||||
material: materialPrincipal,
|
||||
perfilPrincipal: perfilPrincipal,
|
||||
comprimentoMax: maxComp > 0 ? maxComp : '-',
|
||||
pesoUnit: pesoUnit > 0 ? pesoUnit.toFixed(1) : '-',
|
||||
pesoTotal: pesoTot > 0 ? pesoTot.toFixed(0) : '-',
|
||||
tratamentoSuperficial: 'pintura'
|
||||
};
|
||||
});
|
||||
|
||||
renderTable();
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
tableBody.innerHTML = '';
|
||||
if (extractedData.length === 0) {
|
||||
tableBody.innerHTML = `<tr><td colspan="12" style="text-align:center; padding:24px; color:#ef4444;">Nenhuma peça encontrada no PDF Tecnometal.</td></tr>`;
|
||||
btnExport.disabled = true;
|
||||
statsCard.innerText = 'Nenhuma peça identificada.';
|
||||
return;
|
||||
}
|
||||
|
||||
extractedData.forEach(row => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td><strong style="color:#38bdf8;">${row.of}</strong></td>
|
||||
<td>${row.fase}</td>
|
||||
<td><strong>${row.marca}</strong></td>
|
||||
<td>${row.descricao}</td>
|
||||
<td><span class="${row.isComposed === 'SIM' ? 'badge-sim' : 'badge-nao'}">${row.isComposed}</span></td>
|
||||
<td style="text-align:center;"><strong>${row.quantidade}</strong></td>
|
||||
<td>${row.material}</td>
|
||||
<td><strong>${row.perfilPrincipal}</strong></td>
|
||||
<td style="text-align:right;">${row.comprimentoMax}</td>
|
||||
<td style="text-align:right;">${row.pesoUnit}</td>
|
||||
<td style="text-align:right; font-weight:600; color:#34d399;">${row.pesoTotal}</td>
|
||||
<td>${row.tratamentoSuperficial}</td>
|
||||
`;
|
||||
tableBody.appendChild(tr);
|
||||
});
|
||||
|
||||
statsCard.innerHTML = `<strong>${extractedData.length}</strong> peças extraídas com sucesso!`;
|
||||
btnExport.disabled = false;
|
||||
addLog(`Tabela renderizada com ${extractedData.length} linhas.`);
|
||||
}
|
||||
|
||||
btnExport.addEventListener('click', () => {
|
||||
if (extractedData.length === 0) return;
|
||||
|
||||
const excelRows = extractedData.map(item => ({
|
||||
"OF": item.of,
|
||||
"Fase": isNaN(Number(item.fase)) ? item.fase : Number(item.fase),
|
||||
"Marca": item.marca,
|
||||
"Descrição": item.descricao,
|
||||
"Composto por Componentes?": item.isComposed,
|
||||
"Quantidade": Number(item.quantidade),
|
||||
"Peso Unitário (kg)": item.pesoUnit === "-" ? "" : Number(item.pesoUnit),
|
||||
"Peso Total (kg)": item.pesoTotal === "-" ? "" : Number(item.pesoTotal),
|
||||
"Tratamento Superficial": item.tratamentoSuperficial,
|
||||
"Material": item.material,
|
||||
"Perfil Principal": item.perfilPrincipal,
|
||||
"Comprimento Ref. (mm)": item.comprimentoMax === "-" ? "" : Number(item.comprimentoMax)
|
||||
}));
|
||||
|
||||
const wb = XLSX.utils.book_new();
|
||||
const ws = XLSX.utils.json_to_sheet(excelRows);
|
||||
|
||||
ws['!cols'] = [
|
||||
{ wch: 12 }, { wch: 8 }, { wch: 12 }, { wch: 20 },
|
||||
{ wch: 15 }, { wch: 12 }, { wch: 18 }, { wch: 16 },
|
||||
{ wch: 22 }, { wch: 16 }, { wch: 22 }, { wch: 20 }
|
||||
];
|
||||
|
||||
XLSX.utils.book_append_sheet(wb, ws, "Lista_Pecas");
|
||||
const outFileName = `${currentFileName}_Corrigido.xlsx`;
|
||||
XLSX.writeFile(wb, outFileName);
|
||||
addLog(`Planilha Excel baixada: ${outFileName}`);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user