889 lines
29 KiB
HTML
889 lines
29 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="pt-BR">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Conversor de Lista de Peças 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;
|
|
--primary-hover: #1d4ed8;
|
|
--accent: #0284c7;
|
|
--bg: #f8fafc;
|
|
--card-bg: #ffffff;
|
|
--text: #0f172a;
|
|
--text-muted: #64748b;
|
|
--border: #e2e8f0;
|
|
--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 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
|
border: 1px solid var(--border);
|
|
padding: 28px;
|
|
}
|
|
|
|
header {
|
|
border-bottom: 2px 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: var(--primary);
|
|
}
|
|
|
|
.badge {
|
|
font-size: 12px;
|
|
background: #e0f2fe;
|
|
color: #0369a1;
|
|
padding: 4px 10px;
|
|
border-radius: 9999px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.upload-zone {
|
|
border: 2px dashed #cbd5e1;
|
|
border-radius: 10px;
|
|
padding: 36px 20px;
|
|
text-align: center;
|
|
background: #f1f5f9;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.upload-zone:hover, .upload-zone.dragover {
|
|
border-color: var(--accent);
|
|
background: #e0f2fe;
|
|
}
|
|
|
|
.upload-zone p {
|
|
color: var(--text-muted);
|
|
margin-top: 8px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.upload-zone .btn-select {
|
|
display: inline-block;
|
|
margin-top: 12px;
|
|
padding: 8px 18px;
|
|
background: var(--primary);
|
|
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;
|
|
}
|
|
|
|
.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: #94a3b8;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.stats-card {
|
|
font-size: 14px;
|
|
color: var(--text-muted);
|
|
background: #f8fafc;
|
|
padding: 8px 16px;
|
|
border-radius: 6px;
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.table-container {
|
|
overflow-x: auto;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
max-height: 550px;
|
|
position: relative;
|
|
}
|
|
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 13px;
|
|
text-align: left;
|
|
}
|
|
|
|
th {
|
|
background: #0f172a;
|
|
color: #ffffff;
|
|
padding: 10px 12px;
|
|
font-weight: 600;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 1;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
td {
|
|
padding: 8px 12px;
|
|
border-bottom: 1px solid var(--border);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
tr:nth-child(even) {
|
|
background-color: #f8fafc;
|
|
}
|
|
|
|
tr:hover {
|
|
background-color: #f1f5f9;
|
|
}
|
|
|
|
.status-badge {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
}
|
|
.status-yes {
|
|
background: #dcfce7;
|
|
color: #166534;
|
|
}
|
|
.status-no {
|
|
background: #fef3c7;
|
|
color: #92400e;
|
|
}
|
|
|
|
.log-panel {
|
|
margin-top: 18px;
|
|
padding: 12px;
|
|
background: #0f172a;
|
|
color: #38bdf8;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
border-radius: 6px;
|
|
max-height: 120px;
|
|
overflow-y: auto;
|
|
display: none;
|
|
}
|
|
|
|
#fileInput {
|
|
display: none;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="container">
|
|
<header>
|
|
<div>
|
|
<h1>Extrator & Conversor de Lista de Peças (Corrigido)</h1>
|
|
<p style="color: var(--text-muted); font-size: 13px; margin-top: 4px;">Conversão 100% no navegador (Client-Side) com regras ajustadas</p>
|
|
</div>
|
|
<div class="badge">Autônomo / Sem Servidor</div>
|
|
</header>
|
|
|
|
<div class="upload-zone" id="dropZone">
|
|
<svg width="40" height="40" fill="none" stroke="#64748b" stroke-width="2" viewBox="0 0 24 24" style="margin: 0 auto;">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
|
|
</svg>
|
|
<p>Arraste e solte o arquivo <strong>PDF da Lista de Peças Estruturada</strong> aqui</p>
|
|
<div class="btn-select">Selecionar PDF do Computador</div>
|
|
<input type="file" id="fileInput" accept="application/pdf">
|
|
</div>
|
|
|
|
<div class="actions-bar">
|
|
<div class="stats-card" id="statsCard">
|
|
Aguardando arquivo PDF...
|
|
</div>
|
|
<button class="btn btn-export" id="btnExport" disabled>
|
|
<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
|
|
</svg>
|
|
Baixar Planilha Excel (.xlsx)
|
|
</button>
|
|
</div>
|
|
|
|
<div class="table-container">
|
|
<table id="resultTable">
|
|
<thead>
|
|
<tr>
|
|
<th>OF</th>
|
|
<th>Fase</th>
|
|
<th>Marca (Nº Peça)</th>
|
|
<th>Descrição (Nome)</th>
|
|
<th>Composto?</th>
|
|
<th>Quantidade</th>
|
|
<th>Material Principal</th>
|
|
<th>Perfil Principal (Maior Comp.)</th>
|
|
<th>Comp. Max (mm)</th>
|
|
<th>Peso Unit. (kg)</th>
|
|
<th>Peso Total (kg)</th>
|
|
<th>Tratamento Superficial</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. Carregue um PDF acima.
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="log-panel" id="logPanel"></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_Pecas';
|
|
|
|
function log(msg) {
|
|
logPanel.style.display = 'block';
|
|
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]);
|
|
}
|
|
});
|
|
|
|
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>...`;
|
|
logPanel.innerHTML = '';
|
|
log(`Carregando PDF: ${file.name}`);
|
|
|
|
try {
|
|
const arrayBuffer = await file.arrayBuffer();
|
|
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
|
log(`PDF carregado com sucesso! 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
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
log(`Total de elementos de texto extraídos: ${allItems.length}`);
|
|
processPdfLines(allItems);
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('Erro ao processar o arquivo PDF: ' + err.message);
|
|
log(`ERRO: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
function parseNumeric(valStr) {
|
|
if (!valStr) 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;
|
|
}
|
|
|
|
function processPdfLines(items) {
|
|
// 1. Agrupamento robusto de linhas por Clustering de Coordenada Y em cada página
|
|
const pagesMap = new Map();
|
|
items.forEach((it) => {
|
|
if (!pagesMap.has(it.page)) pagesMap.set(it.page, []);
|
|
pagesMap.get(it.page).push(it);
|
|
});
|
|
|
|
const lines = [];
|
|
const Y_TOLERANCE = 6.0;
|
|
|
|
const pageNumbers = Array.from(pagesMap.keys()).sort((a, b) => a - b);
|
|
for (const pNum of pageNumbers) {
|
|
const pageItems = pagesMap.get(pNum);
|
|
const clusters = [];
|
|
|
|
pageItems.sort((a, b) => b.y - a.y);
|
|
|
|
for (const item of pageItems) {
|
|
let bestCluster = null;
|
|
let minDiff = Infinity;
|
|
|
|
for (const cluster of clusters) {
|
|
const diff = Math.abs(item.y - cluster.avgY);
|
|
if (diff <= Y_TOLERANCE && diff < minDiff) {
|
|
minDiff = diff;
|
|
bestCluster = cluster;
|
|
}
|
|
}
|
|
|
|
if (bestCluster) {
|
|
bestCluster.items.push(item);
|
|
bestCluster.ySum += item.y;
|
|
bestCluster.count += 1;
|
|
bestCluster.avgY = bestCluster.ySum / bestCluster.count;
|
|
} else {
|
|
clusters.push({
|
|
ySum: item.y,
|
|
count: 1,
|
|
avgY: item.y,
|
|
items: [item]
|
|
});
|
|
}
|
|
}
|
|
|
|
clusters.sort((a, b) => b.avgY - a.avgY);
|
|
|
|
for (const cluster of clusters) {
|
|
cluster.items.sort((a, b) => a.x - b.x);
|
|
lines.push(cluster.items);
|
|
}
|
|
}
|
|
|
|
log(`Total de linhas identificadas: ${lines.length}`);
|
|
|
|
let defaultOf = "";
|
|
for (const line of lines) {
|
|
const lineText = line.map(i => i.text).join(' ');
|
|
const matchTrabalho = lineText.match(/Trabalho:\s*([A-Za-z0-9\-]+)/i) || lineText.match(/Projeto:\s*([A-Za-z0-9\-]+)/i);
|
|
if (matchTrabalho) {
|
|
defaultOf = matchTrabalho[1].replace(/-/g, '');
|
|
log(`OF / Trabalho identificado no cabeçalho: ${defaultOf}`);
|
|
break;
|
|
}
|
|
}
|
|
|
|
const KNOWN_MATERIALS = [
|
|
'A572-GR 50', 'A572-GR50', 'A572-GR.50', 'A572 GR50', 'A572GR50', 'A572GR5', 'A572',
|
|
'A36', 'ASTM A36', 'ASTM A572', '10.9', '8.8', 'SAE 1020', 'SAE 1045', 'INOX', '304', '316'
|
|
];
|
|
|
|
function normalizeLineText(lineStr) {
|
|
let s = lineStr.trim();
|
|
s = s.replace(/([A-Za-z0-9]+)\s*-\s*(\d+)\s*-\s*(\d+)/g, '$1-$2-$3');
|
|
s = s.replace(/([A-Za-z0-9]+)\s*-\s*(\d+)/g, '$1-$2');
|
|
s = s.replace(/A572\s*-\s*GR\s*50/gi, 'A572-GR 50');
|
|
s = s.replace(/A572\s*-\s*GR/gi, 'A572-GR');
|
|
return s;
|
|
}
|
|
|
|
const assemblies = [];
|
|
let currentAssembly = null;
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
const rawLineText = line.map(it => it.text).join(' ').trim();
|
|
const lineText = normalizeLineText(rawLineText);
|
|
|
|
if (
|
|
lineText.includes('LISTA DE PEÇAS') ||
|
|
lineText.includes('LISTA ESTRUTURADA') ||
|
|
lineText.includes('Lista de Peças') ||
|
|
lineText.includes('DetailDwgExtract') ||
|
|
lineText.includes('Cliente:') ||
|
|
lineText.includes('Desenhado') ||
|
|
lineText.includes('Superfície') ||
|
|
lineText.includes('Trabalho:') ||
|
|
lineText.includes('Kg/piece') ||
|
|
lineText.includes('( mm)') ||
|
|
lineText.toLowerCase().includes('peso total:') ||
|
|
(lineText.toLowerCase().includes('marca') && lineText.toLowerCase().includes('nome'))
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
// 1. Linha Mestre de Peça Principal (ex: "B134-13-1 90 W 150x13.0" ou "B134-13-2 72 W 150x13.0")
|
|
const masterMatch = lineText.match(/^([A-Za-z0-9]+(?:-\d+)+)(?:\s+(\d+))?\s+([A-Za-z].*)$/);
|
|
if (masterMatch) {
|
|
const fullMark = masterMatch[1];
|
|
const quantFromLine = masterMatch[2] ? parseInt(masterMatch[2], 10) : 1;
|
|
const descCandidate = masterMatch[3].trim();
|
|
const parts = fullMark.split('-');
|
|
const pieceNumber = parts[parts.length - 1];
|
|
const pieceNumInt = parseInt(pieceNumber, 10);
|
|
|
|
if (pieceNumInt < 1000) {
|
|
const hasMaterial = KNOWN_MATERIALS.some(m => descCandidate.includes(m));
|
|
|
|
if (!hasMaterial) {
|
|
const ofCode = parts.length >= 2 ? parts[0] : defaultOf;
|
|
const phaseCode = parts.length >= 3 ? parts[1] : (parts.length === 2 ? parts[0] : '1');
|
|
|
|
currentAssembly = {
|
|
rawMark: fullMark,
|
|
of: ofCode || defaultOf,
|
|
fase: phaseCode || '1',
|
|
marca: pieceNumber,
|
|
numeroPeca: pieceNumber,
|
|
descricao: descCandidate || 'ESTRUTURA',
|
|
quantidade: quantFromLine > 0 ? quantFromLine : 1,
|
|
material: '',
|
|
comprimento: 0,
|
|
pesoUnitario: 0,
|
|
pesoTotal: 0,
|
|
subtotalPeso: 0,
|
|
isComposed: 'NÃO',
|
|
components: []
|
|
};
|
|
assemblies.push(currentAssembly);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Linha com Subtotal da Marca
|
|
const tokens = lineText.split(/\s+/);
|
|
const allTokensNumeric = tokens.length > 0 && tokens.every(tok => /^[0-9.,]+$/.test(tok));
|
|
if (allTokensNumeric && tokens.length <= 3) {
|
|
if (currentAssembly && (!currentAssembly.subtotalPeso || currentAssembly.subtotalPeso === 0)) {
|
|
const subP = parseNumeric(tokens[0]);
|
|
if (subP > 0) {
|
|
currentAssembly.subtotalPeso = subP;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// 3. Linha de Detalhe ou Componente
|
|
let itemMark = '';
|
|
let itemQuant = 1;
|
|
let remaining = lineText;
|
|
|
|
const mMark = remaining.match(/^([A-Za-z0-9]+(?:-\d+)+|-)\s+(\d+)\s+/);
|
|
if (mMark) {
|
|
itemMark = mMark[1];
|
|
itemQuant = parseInt(mMark[2], 10);
|
|
remaining = remaining.substring(mMark[0].length).trim();
|
|
} else if (remaining.startsWith('-')) {
|
|
itemMark = '-';
|
|
remaining = remaining.substring(1).trim();
|
|
const mQ = remaining.match(/^(\d+)\s+/);
|
|
if (mQ) {
|
|
itemQuant = parseInt(mQ[1], 10);
|
|
remaining = remaining.substring(mQ[0].length).trim();
|
|
}
|
|
}
|
|
|
|
let detectedMaterial = '';
|
|
let matIdx = -1;
|
|
let matLen = 0;
|
|
|
|
for (const mat of KNOWN_MATERIALS) {
|
|
const idx = remaining.indexOf(mat);
|
|
if (idx !== -1 && idx > matIdx) {
|
|
detectedMaterial = mat;
|
|
matIdx = idx;
|
|
matLen = mat.length;
|
|
}
|
|
}
|
|
|
|
let itemDesc = '';
|
|
let afterMat = '';
|
|
|
|
if (matIdx !== -1) {
|
|
itemDesc = remaining.substring(0, matIdx).trim();
|
|
afterMat = remaining.substring(matIdx + matLen).trim();
|
|
} else {
|
|
const parts = remaining.split(/\s+/);
|
|
const firstNumIdx = parts.findIndex(p => /^[0-9.,]+$/.test(p));
|
|
if (firstNumIdx > 0) {
|
|
itemDesc = parts.slice(0, firstNumIdx).join(' ');
|
|
afterMat = parts.slice(firstNumIdx).join(' ');
|
|
} else {
|
|
itemDesc = remaining;
|
|
}
|
|
}
|
|
|
|
const numTokens = afterMat.split(/\s+/).filter(tok => /^[0-9.,]+$/.test(tok));
|
|
const nums = numTokens.map(parseNumeric);
|
|
|
|
let itemComp = 0;
|
|
let itemPUn = 0;
|
|
let itemPesoTot = 0;
|
|
|
|
if (nums.length >= 1) {
|
|
itemComp = nums[0];
|
|
}
|
|
|
|
if (nums.length === 3) {
|
|
itemPUn = nums[1];
|
|
itemPesoTot = nums[2];
|
|
} else if (nums.length === 4) {
|
|
itemPUn = nums[1];
|
|
itemPesoTot = nums[2];
|
|
} else if (nums.length === 5) {
|
|
itemPUn = nums[1];
|
|
itemPesoTot = nums[2];
|
|
} else if (nums.length >= 6) {
|
|
itemPUn = nums[2];
|
|
itemPesoTot = nums[3];
|
|
}
|
|
|
|
if (currentAssembly) {
|
|
const lastNumberMatch = itemMark.match(/(?:^|-)(\d+)$/);
|
|
const itemNum = lastNumberMatch ? parseInt(lastNumberMatch[1], 10) : (parseInt(itemMark, 10) || 0);
|
|
const isSubComp = itemMark === '-' || itemNum >= 1000;
|
|
|
|
if (isSubComp) {
|
|
currentAssembly.isComposed = 'SIM';
|
|
|
|
let cleanCompMark = itemMark;
|
|
const subMatch = itemMark.match(/-?(\d{4,})$/) || itemMark.match(/(\d{4,})/);
|
|
if (subMatch) {
|
|
cleanCompMark = subMatch[1];
|
|
} else if (itemMark === '-' || !/^\d{4}$/.test(itemMark)) {
|
|
cleanCompMark = String(1000 + currentAssembly.components.length);
|
|
}
|
|
|
|
currentAssembly.components.push({
|
|
marca: cleanCompMark,
|
|
quantidade: itemQuant,
|
|
perfil: itemDesc || currentAssembly.descricao,
|
|
material: detectedMaterial || 'A36',
|
|
comprimento: itemComp,
|
|
pesoUnitario: itemPUn,
|
|
pesoTotal: itemPesoTot
|
|
});
|
|
} else {
|
|
// Monopeça
|
|
if (!currentAssembly.material && detectedMaterial) {
|
|
currentAssembly.material = detectedMaterial;
|
|
}
|
|
if (itemComp > 0 && (!currentAssembly.comprimento || currentAssembly.comprimento === 0)) {
|
|
currentAssembly.comprimento = itemComp;
|
|
}
|
|
if (itemPUn > 0 && (!currentAssembly.pesoUnitario || currentAssembly.pesoUnitario === 0)) {
|
|
currentAssembly.pesoUnitario = itemPUn;
|
|
}
|
|
if (itemPesoTot > 0 && (!currentAssembly.pesoTotal || currentAssembly.pesoTotal === 0)) {
|
|
currentAssembly.pesoTotal = itemPesoTot;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
log(`Total de Peças Principais processadas: ${assemblies.length}`);
|
|
|
|
extractedData = assemblies.map(asm => {
|
|
const isComposed = asm.isComposed === 'SIM' && asm.components.length > 0;
|
|
let perfilPrincipal = asm.descricao;
|
|
let materialPrincipal = 'A36';
|
|
let maxComp = asm.comprimento || 0;
|
|
let totalPeso = asm.subtotalPeso > 0 ? asm.subtotalPeso : asm.pesoTotal;
|
|
|
|
if (isComposed) {
|
|
let sumComponentsWeight = 0;
|
|
let sumUnitComponentsWeight = 0;
|
|
let bestComp = asm.components[0];
|
|
maxComp = 0;
|
|
|
|
asm.components.forEach(c => {
|
|
if (c.comprimento > maxComp) {
|
|
maxComp = c.comprimento;
|
|
bestComp = c;
|
|
}
|
|
sumComponentsWeight += c.pesoTotal;
|
|
if (c.pesoUnitario && c.pesoUnitario > 0) {
|
|
sumUnitComponentsWeight += c.pesoUnitario * c.quantidade;
|
|
}
|
|
});
|
|
|
|
if (bestComp) {
|
|
perfilPrincipal = bestComp.perfil || asm.descricao;
|
|
materialPrincipal = bestComp.material || 'A36';
|
|
maxComp = bestComp.comprimento;
|
|
}
|
|
|
|
if (!totalPeso || totalPeso === 0) {
|
|
totalPeso = sumComponentsWeight;
|
|
}
|
|
|
|
if (asm.quantidade === 1 && sumUnitComponentsWeight > 0 && totalPeso > 0) {
|
|
const ratio = Math.round(totalPeso / sumUnitComponentsWeight);
|
|
if (ratio > 1) {
|
|
asm.quantidade = ratio;
|
|
}
|
|
}
|
|
} else {
|
|
// Monopeça simples
|
|
if (!totalPeso || totalPeso === 0) {
|
|
totalPeso = (asm.pesoUnitario || 0) * asm.quantidade;
|
|
}
|
|
|
|
if (asm.quantidade === 1 && asm.pesoUnitario > 0 && totalPeso > asm.pesoUnitario) {
|
|
const ratio = Math.round(totalPeso / asm.pesoUnitario);
|
|
if (ratio > 1) {
|
|
asm.quantidade = ratio;
|
|
}
|
|
}
|
|
}
|
|
|
|
const pesoUnit = asm.quantidade > 0 ? (totalPeso / asm.quantidade) : (asm.pesoUnitario || totalPeso);
|
|
|
|
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(2) : "-",
|
|
pesoTotal: totalPeso > 0 ? totalPeso.toFixed(2) : "-",
|
|
tratamentoSuperficial: "pintura",
|
|
components: isComposed ? asm.components : []
|
|
};
|
|
});
|
|
|
|
renderTable();
|
|
}
|
|
|
|
function renderTable() {
|
|
tableBody.innerHTML = '';
|
|
|
|
if (extractedData.length === 0) {
|
|
tableBody.innerHTML = `<tr><td colspan="12" style="text-align:center; padding: 20px; color: #ef4444;">Nenhuma peça identificada no PDF. Verifique o documento.</td></tr>`;
|
|
btnExport.disabled = true;
|
|
statsCard.innerHTML = `Nenhum registro encontrado.`;
|
|
return;
|
|
}
|
|
|
|
extractedData.forEach(row => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td><strong>${row.of}</strong></td>
|
|
<td>${row.fase}</td>
|
|
<td><strong>${row.marca}</strong></td>
|
|
<td>${row.descricao}</td>
|
|
<td><span class="status-badge ${row.isComposed === 'SIM' ? 'status-yes' : 'status-no'}">${row.isComposed}</span></td>
|
|
<td style="text-align:center;">${row.quantidade}</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;">${row.pesoTotal}</td>
|
|
<td>${row.tratamentoSuperficial}</td>
|
|
`;
|
|
tableBody.appendChild(tr);
|
|
});
|
|
|
|
statsCard.innerHTML = `<strong>${extractedData.length}</strong> peças principais extraídas com sucesso!`;
|
|
btnExport.disabled = false;
|
|
log(`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 excelRowsComponentes = [];
|
|
extractedData.forEach(item => {
|
|
if (item.components && item.components.length > 0) {
|
|
item.components.forEach(comp => {
|
|
const qtdPorPeca = Number(comp.quantidade) || 1;
|
|
const qtdTotal = qtdPorPeca * Number(item.quantidade);
|
|
excelRowsComponentes.push({
|
|
"OF": item.of,
|
|
"Fase": isNaN(Number(item.fase)) ? item.fase : Number(item.fase),
|
|
"Peça Principal": item.marca,
|
|
"Marca Componente": comp.marca || "-",
|
|
"Descrição Componente": comp.perfil || item.descricao,
|
|
"Material": comp.material || item.material,
|
|
"Comprimento (mm)": comp.comprimento || "",
|
|
"Qtd / Peça": qtdPorPeca,
|
|
"Qtd Total no Lote": qtdTotal,
|
|
"Peso Unitário (kg)": comp.pesoUnitario || "",
|
|
"Peso Total (kg)": comp.pesoTotal || ""
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
const excelRowsImportacao = [];
|
|
extractedData.forEach(item => {
|
|
if (item.components && item.components.length > 0) {
|
|
item.components.forEach(comp => {
|
|
excelRowsImportacao.push({
|
|
of_number: item.of,
|
|
etapa_fase: item.fase,
|
|
marca: item.marca,
|
|
descricao: item.descricao,
|
|
quantidade: Number(item.quantidade),
|
|
peso_unitario: item.pesoUnit === "-" ? 0 : Number(item.pesoUnit),
|
|
peso_total: item.pesoTotal === "-" ? 0 : Number(item.pesoTotal),
|
|
tratamento_superficial: item.tratamentoSuperficial,
|
|
material: item.material,
|
|
perfil_principal: item.perfilPrincipal,
|
|
tem_componentes: true,
|
|
marca_componente: comp.marca || "",
|
|
descricao_componente: comp.perfil || item.descricao,
|
|
perfil_componente: comp.perfil || item.descricao,
|
|
peso_unitario_componente: comp.pesoUnitario || 0,
|
|
quantidade_por_peca: Number(comp.quantidade) || 1
|
|
});
|
|
});
|
|
} else {
|
|
excelRowsImportacao.push({
|
|
of_number: item.of,
|
|
etapa_fase: item.fase,
|
|
marca: item.marca,
|
|
descricao: item.descricao,
|
|
quantidade: Number(item.quantidade),
|
|
peso_unitario: item.pesoUnit === "-" ? 0 : Number(item.pesoUnit),
|
|
peso_total: item.pesoTotal === "-" ? 0 : Number(item.pesoTotal),
|
|
tratamento_superficial: item.tratamentoSuperficial,
|
|
material: item.material,
|
|
perfil_principal: item.perfilPrincipal,
|
|
tem_componentes: false,
|
|
marca_componente: "",
|
|
descricao_componente: "",
|
|
perfil_componente: "",
|
|
peso_unitario_componente: 0,
|
|
quantidade_por_peca: 0
|
|
});
|
|
}
|
|
});
|
|
|
|
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");
|
|
|
|
if (excelRowsComponentes.length > 0) {
|
|
const wsComp = XLSX.utils.json_to_sheet(excelRowsComponentes);
|
|
wsComp['!cols'] = [
|
|
{ wch: 12 }, { wch: 8 }, { wch: 16 }, { wch: 18 }, { wch: 22 },
|
|
{ wch: 16 }, { wch: 18 }, { wch: 12 }, { wch: 16 }, { wch: 18 }, { wch: 16 }
|
|
];
|
|
XLSX.utils.book_append_sheet(wb, wsComp, "Lista_Componentes");
|
|
}
|
|
|
|
const wsImport = XLSX.utils.json_to_sheet(excelRowsImportacao);
|
|
XLSX.utils.book_append_sheet(wb, wsImport, "Importacao_TrackSteel");
|
|
|
|
const outFileName = `${currentFileName}_Corrigido.xlsx`;
|
|
XLSX.writeFile(wb, outFileName);
|
|
log(`Arquivo Excel exportado com ${excelRows.length} peças e ${excelRowsComponentes.length} componentes: ${outFileName}`);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|