mirror of
https://gitlab.com/CIEFWorldwideSdnBhd/exchange-2.0.git
synced 2026-08-19 04:23:55 +00:00
540 lines
21 KiB
Vue
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>
|