Files
exchange-2.0/resources/assets/vue/components/bookings/forms/PdfTableExtractorComponent.vue
T
2025-12-27 17:18:39 +08:00

540 lines
21 KiB
Vue

<template>
<div class="p-4">
<div class="dropbox p-5 mb-4 bg-light text-secondary rounded text-center position-relative"
@dragenter.prevent="onDragEnter"
@dragover.prevent="onDragOver"
@drop.prevent="onDrop"
:class="{ 'bg-secondary text-white': isDragging }">
<div id="drop">
<span>
<img class="logo mb-3" src="logo.png" alt="Your Logo" v-if="false"><br>
Drag and drop your PDF file here
</span>
</div>
<div v-if="filesProcessed" class="alert alert-success mt-3 mb-0" role="alert">
<strong>{{ fileName }}</strong> content extracted. <br/>
<div class="row m-b-10">
<div class="col">
<span class="text-complete pointer requestModal text-underline" data-type="previewExtractedTable">Preview</span>
</div>
</div>
</div>
<div class="d-flex justify-content-center mt-3">
<label for="pdf-file-input"
class="btn btn-sm btn-outline-complete rounded-0 pointer flex-fill mx-1 text-center mb-0">
Browse files
</label>
<button type="button"
class="btn btn-sm btn-outline-complete rounded-0 pointer flex-fill mx-1"
v-if="isPurchaseOrderWithTablesPDF"
@click="exportToCSV"
:disabled="isExporting">
{{ isExporting ? 'Exporting...' : 'Export Extracted Content (csv)' }}
</button>
<button type="button"
class="btn btn-sm btn-outline-complete rounded-0 pointer flex-fill mx-1"
v-if="isPurchaseOrderWithTablesPDF"
@click="importToSystem"
:disabled="isImporting">
{{ isImporting ? 'Uploading...' : 'Continue Upload' }}
</button>
</div>
<input type="file" id="pdf-file-input" @change="onFileChange" accept="application/pdf" style="display: none;" />
</div>
<modal-component class="animate__animated animate__fast animate__fadeIn" styleType="fill-in" type="previewExtractedTable" size="large">
<div class="row p-t-25 text-left">
<div class="col bg-white padding-40 b-rad-lg">
<div class="modal-header">
<h2>Preview Extracted Content</h2>
</div>
<div id="html-result" v-if="tables.length > 0">
<div v-for="(table, tIndex) in tables" :key="tIndex" class="table-wrapper">
<table border="1">
<tbody>
<tr v-for="(row, rIndex) in table" :key="rIndex">
<td v-for="(cell, cIndex) in row"
:key="cIndex"
:colspan="cell.colspan"
:rowspan="cell.rowspan"
v-if="!cell.hidden">
{{ cell.text }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</modal-component>
</div>
</template>
<script>
import { integer } from 'vuelidate/lib/validators';
export default {
name: 'PdfTableExtractorComponent',
props: {
section:{
type: String,
required: true
},
bookingId: {
type: Number,
required: true
}
},
data() {
return {
isDragging: false,
tables: [],
nonTableData: [],
allTablesForExportImport: [],
isExporting: false,
isImporting: false,
filesProcessed: false,
showPreviewModal: false,
fileName: '',
translationCache: {},
mergeTableRemoveFirstLine: false,
parameters: {
products: [],
productsMetadata: []
}
};
},
computed: {
isPurchaseOrderWithTablesPDF() {
return this.allTablesForExportImport.length > 0;
}
},
methods: {
onDragEnter(e) {
this.isDragging = true;
},
onDragOver(e) {
this.isDragging = true;
e.dataTransfer.dropEffect = 'copy';
},
onDrop(e) {
this.isDragging = false;
const files = e.dataTransfer.files;
if (files.length) {
this.processFile(files[0]);
}
},
onFileChange(e) {
const files = e.target.files;
if (files.length) {
this.processFile(files[0]);
}
},
async processFile(file) {
this.tables = [];
this.allTablesForExportImport = [];
this.filesProcessed = false;
this.fileName = file.name;
this.parameters.products = [];
this.parameters.productsMetadata = [];
try {
await this.loadDependencies();
} catch (error) {
console.error("Failed to load PDF dependencies:", error);
alert("Critical Error: Could not load PDF libraries.");
return;
}
const reader = new FileReader();
reader.onload = (e) => {
const data = e.target.result;
this.parseContent(data);
};
reader.readAsArrayBuffer(file);
},
async loadDependencies() {
if (typeof pdfjsLib !== 'undefined' && typeof pdf_table_extractor !== 'undefined') {
return;
}
if (typeof pdfjsLib === 'undefined') {
await this.loadScript(window.pdfJsUrl);
pdfjsLib.GlobalWorkerOptions.workerSrc = window.pdfWorkerJsUrl;
}
if (typeof pdf_table_extractor === 'undefined') {
await this.loadScript(window.pdfTableExtractorJsUrl);
}
},
loadScript(src) {
return new Promise((resolve, reject) => {
if (document.querySelector(`script[src="${src}"]`)) {
resolve();
return;
}
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
},
parseContent(content) {
if (typeof pdfjsLib === 'undefined' || typeof pdf_table_extractor === 'undefined') {
console.error('PDF.js or pdf-table-extractor not loaded.');
alert('Required PDF libraries are missing.');
return;
}
if (!pdfjsLib.GlobalWorkerOptions.workerSrc) {
pdfjsLib.GlobalWorkerOptions.workerSrc = '/js/pdf.worker.js';
}
const loadingTask = pdfjsLib.getDocument(content);
loadingTask.promise
.then(async (doc) => {
const result = await pdf_table_extractor(doc);
this.processExtractedTables(result);
await this.extractNonTableData4(doc, result);
this.filesProcessed = true;
})
.catch(error => {
console.error("Error extracting tables:", error);
alert("Error parsing PDF.");
});
},
processExtractedTables(result) {
let extractedTables = [];
let exportData = [];
const pages = [...result.pageTables];
pages.forEach(page_tables => {
const tables = page_tables.tables;
const merge_alias = page_tables.merge_alias;
const merges = page_tables.merges;
if (tables.length > 0) {
let currentTable = [];
for (let r = 0; r < tables.length; r++) {
if (this.mergeTableRemoveFirstLine && page_tables.page != 1 && r == 0) {
continue;
}
let rowCells = [];
let rowForExport = [];
for (let c = 0; c < tables[r].length; c++) {
let r_c = [r, c].join('-');
if (merge_alias[r_c]) {
rowCells.push({ hidden: true });
continue;
}
if (merges[r_c] && merges[r_c].width > 2) {
rowCells.push({ hidden: true });
continue;
}
let cell = {
text: tables[r][c],
hidden: false,
rowspan: 1,
colspan: 1
};
if (merges[r_c]) {
if (merges[r_c].width > 1) cell.colspan = merges[r_c].width;
if (merges[r_c].height > 1) cell.rowspan = merges[r_c].height;
}
rowCells.push(cell);
let value = tables[r][c];
let cleanValue = value.replace(/[\r\n]+/g, ' ').trim();
rowForExport.push(cleanValue);
}
currentTable.push(rowCells);
if (rowForExport.length > 1) {
const limitedRowForExport = rowForExport.slice(0, 7);
const headerRow = exportData[0];
const isSameAsHeader = headerRow && limitedRowForExport.every((val, idx) => val === headerRow[idx]);
if (!isSameAsHeader) {
exportData.push(limitedRowForExport);
}
}
}
extractedTables.push(currentTable);
}
});
this.tables = extractedTables;
this.allTablesForExportImport = exportData;
},
async extractNonTableData4(doc, result) {
this.nonTableData = [];
for (let i = 1; i <= doc.numPages; i++) {
const page = await doc.getPage(i);
const content = await page.getTextContent();
const pageStrItems = content.items;
const pageTableData = result.pageTables.find(pt => pt.page === i);
const tableBoxes = [];
if (pageTableData?.tables?.length) {
pageTableData.tables.forEach((table, tIdx) => {
let dataRowCount = 0;
table.forEach(row => {
const hasData = Array.isArray(row)
? row.some(cell => cell.trim().length > 0)
: String(row).trim().length > 0;
if (hasData) dataRowCount++;
});
if (dataRowCount > 1) {
let xMin = Infinity, xMax = -Infinity, yMin = Infinity, yMax = -Infinity;
table.forEach((row, rIdx) => {
if (dataRowCount === 1 && rIdx !== 0) return;
if (Array.isArray(row)) {
row.forEach(cell => {
const lines = String(cell).split('\n').map(l => l.trim()).filter(Boolean);
lines.forEach(line => {
const matched = pageStrItems.find(item => item.str.includes(line));
if (matched) {
const x = matched.transform[4];
const y = matched.transform[5];
const w = matched.width || 0;
xMin = Math.min(xMin, x);
xMax = Math.max(xMax, x + w);
yMin = Math.min(yMin, y);
yMax = Math.max(yMax, y);
}
});
});
} else if (typeof row === 'string') {
const lines = row.split('\n').map(l => l.trim()).filter(Boolean);
lines.forEach(line => {
const matched = pageStrItems.find(item => item.str.includes(line));
if (matched) {
const x = matched.transform[4];
const y = matched.transform[5];
const w = matched.width || 0;
xMin = Math.min(xMin, x);
xMax = Math.max(xMax, x + w);
yMin = Math.min(yMin, y);
yMax = Math.max(yMax, y);
}
});
}
});
if (xMin < Infinity) {
tableBoxes.push({ xMin, xMax, yMin, yMax });
}
}
});
}
const filteredItems = pageStrItems.filter(item => {
const x = item.transform[4];
const y = item.transform[5];
const str = item.str.trim();
if (!str) return false;
const isInTable = tableBoxes.some(box =>
x >= box.xMin && x <= box.xMax &&
y >= box.yMin && y <= box.yMax
);
return !isInTable;
});
const lines = {};
const tolerance = 5;
filteredItems.forEach(item => {
const y = item.transform[5];
const x = item.transform[4];
let added = false;
for (const lineY in lines) {
if (Math.abs(parseFloat(lineY) - y) < tolerance) {
lines[lineY].push({ str: item.str, x });
added = true;
break;
}
}
if (!added) {
lines[y] = [{ str: item.str, x }];
}
});
const sortedLineKeys = Object.keys(lines)
.sort((a, b) => parseFloat(b) - parseFloat(a));
const readableContent = sortedLineKeys.map(key => {
const lineItems = lines[key].sort((a, b) => a.x - b.x);
return lineItems.map(i => i.str).join(' ');
});
if (readableContent.length) {
this.nonTableData.push({
page: i,
content: readableContent
});
}
}
},
exportToCSV() {
if (this.isExporting) return;
this.isExporting = true;
this.parameters.products = [];
this.parameters.productsMetadata = [];
for (let index = 0; index < this.allTablesForExportImport.length; index++) {
if (index === 0) continue;
const rowArray = this.allTablesForExportImport[index];
const partNumber = rowArray[1] || '';
let productName = rowArray[2] || '';
const specification = rowArray[3] || '';
const quantity = rowArray[4] || '';
const rawUnitPrice = rowArray[5] || '';
const unitPrice = rawUnitPrice.match(/\d+\.?\d*/)?.[0] || '';
productName = productName.replace(/^["'](.*)["']$/, '$1');
this.parameters.products.push({
stockCode: partNumber,
description: productName + ' | ' + specification,
quantity: quantity,
unit_price: unitPrice
});
this.parameters.productsMetadata = this.nonTableData;
}
this.submit(route('api.transaction.po.etl', this.bookingId), 'post', this.section + 'PDFExport', true, true);
},
importToSystem() {
if (this.isImporting) return;
this.isImporting = true;
if(this.parameters.products && this.parameters.products.length === 0){
for (let index = 0; index < this.allTablesForExportImport.length; index++) {
if (index === 0) continue;
const rowArray = this.allTablesForExportImport[index];
const partNumber = rowArray[1] || '';
let productName = rowArray[2] || '';
const specification = rowArray[3] || '';
const quantity = rowArray[4] || '';
const rawUnitPrice = rowArray[5] || '';
const unitPrice = rawUnitPrice.match(/\d+\.?\d*/)?.[0] || '';
productName = productName.replace(/^["'](.*)["']$/, '$1');
this.parameters.products.push({
stockCode: partNumber,
description: productName + ' | ' + specification,
quantity: quantity,
unit_price: unitPrice
});
this.parameters.productsMetadata = this.nonTableData;
}
}
this.submit(route('api.transaction.po.import', this.bookingId), 'post', this.section + 'PDFImport', true, true);
},
escapeCSV(value) {
if (value === null || value === undefined) return '';
const str = value.toString();
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
return `"${str.replace(/"/g, '""')}"`;
}
return str;
},
containsChinese(text) {
return /[\u4e00-\u9fff]/.test(text);
},
successHandler(response, section){
if(this.section + 'PDFImport' === section){
this.updateList()
this.isImporting = false;
}
else if(this.section + 'PDFExport' === section){
try {
const BOM = '\uFEFF';
let csvContent = "stock code,description,quantity,unit price\r\n";
response.payload.forEach(item => {
const formattedRow = [
item.stockCode,
this.escapeCSV(item.description),
item.quantity,
item.unit_price
].join(',');
csvContent += formattedRow + "\r\n";
});
const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.setAttribute("download", "order_details.csv");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
this.parameters.products = response.payload;
this.parameters.productsMetadata = [];
} catch (e) {
console.error("Export failed:", e);
alert("Export failed. Please try again.");
} finally {
this.isExporting = false;
}
}
},
errorHandler(response, statusCode, section){
this.isExporting = false;
this.isImporting = false;
}
}
}
</script>
<style scoped>
.dropbox {
border: 2px dashed #bbb;
border-radius: 5px;
padding: 25px;
text-align: center;
color: #bbb;
margin-bottom: 20px;
transition: background-color 0.2s;
}
.table-wrapper {
margin-bottom: 20px;
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
td, th {
border: 1px solid #ddd;
padding: 8px;
}
</style>