Mango.Nop.Plugins/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Extras/ImageTextExtraction.cshtml

922 lines
37 KiB
Plaintext

@{
Layout = "../_FruitBankEmptyAdminLayout.cshtml";
}
@await Component.InvokeAsync("StoreScopeConfiguration")
<form asp-action="ExtractTextFromImage" asp-controller="FileManager" method="post">
@Html.AntiForgeryToken()
</form>
<div class="card card-default">
<div class="card-body">
<h4>Image & PDF Text Extraction (OCR)</h4>
<p>Upload an image or PDF to extract text using OpenAI Vision API.</p>
<div class="form-group row">
<div class="col-md-9">
<div class="custom-file">
<input type="file" class="custom-file-input" id="imageFile" accept="image/*,.pdf,application/pdf">
<label class="custom-file-label" for="imageFile">Choose image or PDF file...</label>
</div>
<small class="form-text text-muted">Supported formats: JPG, PNG, GIF, WebP, PDF</small>
</div>
</div>
<div class="form-group row">
<div class="col-md-9">
<label for="customPrompt">Custom Prompt (Optional)</label>
<input type="text" class="form-control" id="customPrompt" placeholder="Leave empty for default: 'Olvasd ki a szöveget és add vissza szépen strukturálva.'">
<small class="form-text text-muted">Customize how the AI should extract and format the text</small>
</div>
</div>
<div class="form-group row">
<div class="col-md-9">
<button type="button" id="uploadButton" class="btn btn-primary" disabled>
<i class="fas fa-upload"></i> Extract Text
</button>
</div>
</div>
<div class="form-group row">
<div class="col-md-9">
<div id="responseMessage" class="alert" style="display:none;"></div>
</div>
</div>
<div class="form-group row" id="filePreviewSection" style="display:none;">
<div class="col-md-9">
<h5>File Preview:</h5>
<div id="imagePreviewContainer" style="display:none;">
<img id="imagePreview" src="" alt="Preview" style="max-width: 100%; max-height: 400px; border: 1px solid #ddd; padding: 5px;">
</div>
<div id="pdfPreviewContainer" style="display:none;">
<div class="alert alert-info">
<i class="fas fa-file-pdf"></i> PDF file selected. The first page will be converted to image for text extraction.
</div>
<embed id="pdfPreview" type="application/pdf" style="width: 100%; height: 500px; border: 1px solid #ddd;">
</div>
</div>
</div>
<!-- Shipping Document Section - Now Editable -->
<div class="form-group row" id="shippingDocumentSection" style="display:none;">
<div class="col-md-12">
<h4 class="mt-4"><i class="fas fa-shipping-fast"></i> Shipping Document Details</h4>
<!-- Document Info Card - Now Editable -->
<div class="card card-primary">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-file-alt"></i> Document Information</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<div class="form-group">
<label for="editDocumentIdNumber">Document ID:</label>
<input type="text" class="form-control" id="editDocumentIdNumber">
</div>
</div>
<div class="col-md-8">
<div class="form-group">
<label for="editPartnerName">Partner:</label>
<input type="text" class="form-control partner-search-input" id="editPartnerName" placeholder="Type to search partners...">
<small class="form-text text-muted">
<span id="partnerIdDisplay"></span>
<span id="partnerTaxIdDisplay"></span>
</small>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-group">
<label for="editTotalPallets">Total Pallets:</label>
<input type="number" class="form-control" id="editTotalPallets" readonly>
<small class="form-text text-muted">Auto-calculated from items</small>
</div>
</div>
<div class="col-md-8">
<div class="form-group">
<label for="editPdfFileName">PDF Filename:</label>
<input type="text" class="form-control" id="editPdfFileName" readonly>
</div>
</div>
</div>
</div>
</div>
<!-- Shipping Items Table - Now Editable -->
<div class="card card-info mt-3">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-boxes"></i> Shipping Items (<span id="itemCount">0</span>)
<button type="button" id="addItemButton" class="btn btn-sm btn-success float-right">
<i class="fas fa-plus"></i> Add Item
</button>
</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-hover" id="shippingItemsTable">
<thead class="thead-dark">
<tr>
<th style="width: 50px;">#</th>
<th>Name</th>
<th>Hungarian Name</th>
<th>Name on Document</th>
<th style="width: 100px;">Product ID</th>
<th style="width: 80px;">Pallets</th>
<th style="width: 80px;">Quantity</th>
<th style="width: 100px;">Net Weight (kg)</th>
<th style="width: 100px;">Gross Weight (kg)</th>
<th style="width: 100px;">Unit Cost</th>
<th style="width: 80px;">Actions</th>
</tr>
</thead>
<tbody id="shippingItemsBody">
<!-- Items will be populated here -->
</tbody>
</table>
</div>
</div>
</div>
<!-- Save Button -->
<div class="card card-success mt-3">
<div class="card-body">
<button type="button" id="saveShippingDocumentButton" class="btn btn-success btn-lg btn-block">
<i class="fas fa-save"></i> Save Shipping Document
</button>
</div>
</div>
<!-- Extracted Text Card (Collapsible) -->
<div class="card card-secondary mt-3">
<div class="card-header" data-toggle="collapse" data-target="#extractedTextCollapse" style="cursor: pointer;">
<h5 class="mb-0">
<i class="fas fa-file-alt"></i> Raw Extracted Text
<i class="fas fa-chevron-down float-right"></i>
</h5>
</div>
<div id="extractedTextCollapse" class="collapse">
<div class="card-body">
<pre id="extractedText" style="white-space: pre-wrap; word-wrap: break-word; max-height: 400px; overflow-y: auto;"></pre>
<button type="button" id="copyButton" class="btn btn-secondary mt-2">
<i class="fas fa-copy"></i> Copy to Clipboard
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.autocomplete-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #ddd;
border-top: none;
max-height: 300px;
overflow-y: auto;
z-index: 1000;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.autocomplete-item {
padding: 10px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
}
.autocomplete-item:hover {
background-color: #f8f9fa;
}
.autocomplete-item:last-child {
border-bottom: none;
}
.product-search-input {
background-color: #fff3cd !important; /* Yellow for unmatched */
}
.product-search-input.matched-product {
background-color: #d1ecf1 !important; /* Light blue for matched (changeable) */
}
.partner-search-input-unmatched {
background-color: #fff3cd !important; /* Yellow for unmatched */
}
.partner-search-input-matched {
background-color: #d1ecf1 !important; /* Light blue for matched */
}
/* Add to existing styles */
.border-warning {
border: 2px solid #ffc107 !important;
}
.duplicate-warning-banner {
background-color: #fff3cd;
border: 1px solid #ffc107;
padding: 15px;
margin-bottom: 15px;
border-radius: 4px;
}
.duplicate-warning-banner i {
color: #856404;
margin-right: 10px;
}
</style>
<script>
const imageFileInput = document.getElementById('imageFile');
const customPromptInput = document.getElementById('customPrompt');
const uploadButton = document.getElementById('uploadButton');
const responseMessage = document.getElementById('responseMessage');
const filePreviewSection = document.getElementById('filePreviewSection');
const imagePreviewContainer = document.getElementById('imagePreviewContainer');
const pdfPreviewContainer = document.getElementById('pdfPreviewContainer');
const imagePreview = document.getElementById('imagePreview');
const pdfPreview = document.getElementById('pdfPreview');
const shippingDocumentSection = document.getElementById('shippingDocumentSection');
const extractedText = document.getElementById('extractedText');
const copyButton = document.getElementById('copyButton');
const fileLabel = document.querySelector('.custom-file-label');
const addItemButton = document.getElementById('addItemButton');
const saveShippingDocumentButton = document.getElementById('saveShippingDocumentButton');
let selectedFile = null;
let shippingItems = [];
let currentPartner = { id: null, name: '', taxId: '' };
let originalUploadedFile = null; // Store the original file for later save
let extractedFullText = ''; // Store extracted text for file metadata
let isKnownDuplicate = false; // Track if current document is a duplicate
// Update file label and enable upload button when file is selected
imageFileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
selectedFile = file;
fileLabel.textContent = file.name;
uploadButton.disabled = false;
// Check file type and show appropriate preview
const isPdf = file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf');
if (isPdf) {
// Show PDF preview
imagePreviewContainer.style.display = 'none';
pdfPreviewContainer.style.display = 'block';
const fileUrl = URL.createObjectURL(file);
pdfPreview.src = fileUrl;
filePreviewSection.style.display = 'block';
} else {
// Show image preview
pdfPreviewContainer.style.display = 'none';
imagePreviewContainer.style.display = 'block';
const reader = new FileReader();
reader.onload = (e) => {
imagePreview.src = e.target.result;
filePreviewSection.style.display = 'block';
};
reader.readAsDataURL(file);
}
} else {
selectedFile = null;
fileLabel.textContent = 'Choose image or PDF file...';
uploadButton.disabled = true;
filePreviewSection.style.display = 'none';
}
shippingDocumentSection.style.display = 'none';
});
uploadButton.addEventListener('click', async () => {
if (!selectedFile) {
showMessage('Please select a file first!', 'warning');
return;
}
const formData = new FormData();
formData.append('imageFile', selectedFile);
const customPrompt = customPromptInput.value.trim();
if (customPrompt) {
formData.append('customPrompt', customPrompt);
}
try {
uploadButton.disabled = true;
const isPdf = selectedFile.type === 'application/pdf' || selectedFile.name.toLowerCase().endsWith('.pdf');
uploadButton.innerHTML = isPdf
? '<i class="fas fa-spinner fa-spin"></i> Converting PDF & Extracting...'
: '<i class="fas fa-spinner fa-spin"></i> Extracting...';
// Get the antiforgery token
const token = document.querySelector('input[name="__RequestVerificationToken"]').value;
const response = await fetch('@Url.Action("ExtractTextFromImage", "FileManager")', {
method: 'POST',
headers: {
'RequestVerificationToken': token
},
body: formData
});
const result = await response.json();
if (response.ok && result.success) {
// ✅ Check if this is a duplicate document
if (result.isDuplicate) {
isKnownDuplicate = true; // ✅ Set duplicate flag
// Show warning message with different style
showMessage(
`⚠️ DUPLICATE DETECTED: ${result.message} You can review/edit the data or upload a different document.`,
'warning'
);
console.log('📋 Loaded existing document ID:', result.existingDocumentId);
} else {
isKnownDuplicate = false; // ✅ Clear duplicate flag
// Normal success message for new documents
const message = result.wasConverted
? 'PDF converted and text extracted successfully!'
: 'Text extracted successfully!';
showMessage(message, 'success');
}
// ✅ Store the file reference (for new documents or if user wants to create duplicate)
originalUploadedFile = selectedFile;
extractedFullText = result.shippingDocument.extractedText || '';
// Display shipping document data (works for both new and duplicate)
if (result.shippingDocument) {
displayShippingDocument(result.shippingDocument, result.isDuplicate);
document.getElementById('shippingDocumentSection').style.display = 'block';
}
} else {
showMessage('Error: ' + (result.message || 'Failed to extract text'), 'danger');
}
} catch (error) {
console.error('Error extracting text:', error);
showMessage('Error: Failed to communicate with server', 'danger');
} finally {
uploadButton.disabled = false;
uploadButton.innerHTML = '<i class="fas fa-upload"></i> Extract Text';
}
});
copyButton.addEventListener('click', () => {
const textToCopy = extractedText.textContent;
navigator.clipboard.writeText(textToCopy).then(() => {
const originalText = copyButton.innerHTML;
copyButton.innerHTML = '<i class="fas fa-check"></i> Copied!';
copyButton.classList.remove('btn-secondary');
copyButton.classList.add('btn-success');
setTimeout(() => {
copyButton.innerHTML = originalText;
copyButton.classList.remove('btn-success');
copyButton.classList.add('btn-secondary');
}, 2000);
}).catch(err => {
console.error('Failed to copy text:', err);
showMessage('Failed to copy to clipboard', 'warning');
});
});
// Add new item button handler
addItemButton.addEventListener('click', () => {
addNewShippingItem();
});
// Save button handler
saveShippingDocumentButton.addEventListener('click', async () => {
await saveShippingDocument();
});
function displayShippingDocument(shippingDoc, isDuplicate = false) {
// Populate document information fields
document.getElementById('editDocumentIdNumber').value = shippingDoc.documentIdNumber || '';
document.getElementById('editTotalPallets').value = shippingDoc.totalPallets || '0';
document.getElementById('editPdfFileName').value = shippingDoc.pdfFileName || '';
// ✅ OPTIONAL: Add visual indicator if duplicate
if (isDuplicate) {
const documentCard = document.querySelector('.card-primary');
documentCard.classList.add('border-warning');
// Add duplicate badge to header
const cardHeader = documentCard.querySelector('.card-header h5');
if (!cardHeader.querySelector('.badge-warning')) {
cardHeader.innerHTML += ' <span class="badge badge-warning ml-2">DUPLICATE</span>';
}
}
// Populate partner information
currentPartner = {
id: shippingDoc.partnerId || null,
name: shippingDoc.partnerName || '',
taxId: shippingDoc.partnerTaxId || ''
};
updatePartnerDisplay();
// Populate extracted text (collapsible section)
extractedText.textContent = shippingDoc.extractedText || 'No text extracted';
// Store items in global array
shippingItems = shippingDoc.shippingItems || [];
// Render items table
renderShippingItemsTable();
// Initialize partner autocomplete
initializePartnerAutocomplete();
}
function updatePartnerDisplay() {
const partnerNameInput = document.getElementById('editPartnerName');
const partnerIdDisplay = document.getElementById('partnerIdDisplay');
const partnerTaxIdDisplay = document.getElementById('partnerTaxIdDisplay');
if (currentPartner.id) {
partnerNameInput.value = currentPartner.name;
partnerNameInput.classList.remove('partner-search-input-unmatched');
partnerNameInput.classList.add('partner-search-input-matched');
partnerIdDisplay.textContent = `Partner ID: ${currentPartner.id}`;
partnerTaxIdDisplay.textContent = currentPartner.taxId ? ` | Tax ID: ${currentPartner.taxId}` : '';
} else {
partnerNameInput.value = '';
partnerNameInput.classList.add('partner-search-input-unmatched');
partnerNameInput.classList.remove('partner-search-input-matched');
partnerIdDisplay.textContent = 'Partner ID: Not Matched';
partnerTaxIdDisplay.textContent = '';
}
}
function renderShippingItemsTable() {
const tbody = document.getElementById('shippingItemsBody');
tbody.innerHTML = ''; // Clear existing rows
document.getElementById('itemCount').textContent = shippingItems.length;
if (shippingItems.length === 0) {
tbody.innerHTML = '<tr><td colspan="11" class="text-center text-muted">No shipping items found. Click "Add Item" to add one.</td></tr>';
} else {
shippingItems.forEach((item, index) => {
const row = createEditableRow(item, index);
tbody.appendChild(row);
});
}
// Update total pallets
updateTotalPallets();
}
function createEditableRow(item, index) {
const row = document.createElement('tr');
// Add status class based on whether product is matched
if (!item.productId) {
row.classList.add('table-warning');
}
// Determine if product is matched or not
const isMatched = item.productId && item.productId > 0;
// Create name field - always searchable, but pre-filled if matched
const nameFieldValue = isMatched ? item.name : (item.nameOnDocument || '');
const nameFieldClass = isMatched ? 'product-search-input matched-product' : 'product-search-input';
row.innerHTML = `
<td>${index + 1}</td>
<td>
<input type="text" class="form-control form-control-sm ${nameFieldClass}"
value="${escapeHtml(nameFieldValue)}"
data-index="${index}"
placeholder="Type to search products...">
</td>
<td><input type="text" class="form-control form-control-sm" value="${escapeHtml(item.hungarianName || '')}" data-field="hungarianName" data-index="${index}" readonly></td>
<td><input type="text" class="form-control form-control-sm" value="${escapeHtml(item.nameOnDocument || '')}" data-field="nameOnDocument" data-index="${index}"></td>
<td>
${isMatched
? `<span class="badge badge-success">${item.productId}</span>`
: `<span class="badge badge-warning">Not Matched</span>`}
</td>
<td><input type="number" class="form-control form-control-sm" value="${item.palletsOnDocument || 0}" data-field="palletsOnDocument" data-index="${index}"></td>
<td><input type="number" class="form-control form-control-sm" value="${item.quantityOnDocument || 0}" data-field="quantityOnDocument" data-index="${index}"></td>
<td><input type="number" step="0.01" class="form-control form-control-sm" value="${item.netWeightOnDocument ? item.netWeightOnDocument.toFixed(2) : '0.00'}" data-field="netWeightOnDocument" data-index="${index}"></td>
<td><input type="number" step="0.01" class="form-control form-control-sm" value="${item.grossWeightOnDocument ? item.grossWeightOnDocument.toFixed(2) : '0.00'}" data-field="grossWeightOnDocument" data-index="${index}"></td>
<td><input type="number" step="0.01" class="form-control form-control-sm" value="${item.unitPriceOnDocument ? item.unitPriceOnDocument.toFixed(2) : '0.00'}" data-field="unitPriceOnDocument" data-index="${index}"></td>
<td>
<button type="button" class="btn btn-sm btn-danger" onclick="deleteShippingItem(${index})">
<i class="fas fa-trash"></i>
</button>
</td>
`;
// Add change listeners to update the data array
const inputs = row.querySelectorAll('input:not(.product-search-input), select');
inputs.forEach(input => {
input.addEventListener('change', (e) => {
const field = e.target.dataset.field;
const idx = parseInt(e.target.dataset.index);
let value = e.target.value;
// Parse numeric values
if (field === 'productId' || field === 'palletsOnDocument' || field === 'quantityOnDocument') {
value = value ? parseInt(value) : null;
} else if (field === 'netWeightOnDocument' || field === 'grossWeightOnDocument' || field === 'unitPriceOnDocument') {
value = value ? parseFloat(value) : 0;
}
shippingItems[idx][field] = value;
// Update total pallets if pallets changed
if (field === 'palletsOnDocument') {
updateTotalPallets();
}
});
});
// Initialize autocomplete for all product search inputs
const searchInput = row.querySelector('.product-search-input');
initializeProductAutocomplete(searchInput, index);
return row;
}
function addNewShippingItem() {
const newItem = {
name: '',
hungarianName: '',
nameOnDocument: '',
productId: null,
palletsOnDocument: 0,
quantityOnDocument: 0,
netWeightOnDocument: 0,
grossWeightOnDocument: 0,
unitPriceOnDocument: 0
};
shippingItems.push(newItem);
renderShippingItemsTable();
}
window.deleteShippingItem = function(index) {
if (confirm('Are you sure you want to delete this item?')) {
shippingItems.splice(index, 1);
renderShippingItemsTable();
}
};
function updateTotalPallets() {
const total = shippingItems.reduce((sum, item) => sum + (item.palletsOnDocument || 0), 0);
document.getElementById('editTotalPallets').value = total;
}
// Product autocomplete functionality
let autocompleteTimeout = null;
let currentSearchIndex = null;
let partnerAutocompleteTimeout = null;
// Partner autocomplete initialization
function initializePartnerAutocomplete() {
const partnerInput = document.getElementById('editPartnerName');
// Create autocomplete container
const wrapper = document.createElement('div');
wrapper.style.position = 'relative';
wrapper.style.width = '100%';
// Check if wrapper already exists
if (partnerInput.parentNode.querySelector('.autocomplete-results')) {
return; // Already initialized
}
partnerInput.parentNode.insertBefore(wrapper, partnerInput);
wrapper.appendChild(partnerInput);
const resultsContainer = document.createElement('div');
resultsContainer.className = 'autocomplete-results';
resultsContainer.style.display = 'none';
wrapper.appendChild(resultsContainer);
// Input event for searching
partnerInput.addEventListener('input', (e) => {
const searchTerm = e.target.value.trim();
// Clear existing timeout
if (partnerAutocompleteTimeout) {
clearTimeout(partnerAutocompleteTimeout);
}
if (searchTerm.length < 2) {
resultsContainer.style.display = 'none';
return;
}
// Debounce search
partnerAutocompleteTimeout = setTimeout(async () => {
await searchPartners(searchTerm, resultsContainer);
}, 300);
});
// Hide results when clicking outside
document.addEventListener('click', (e) => {
if (!wrapper.contains(e.target)) {
resultsContainer.style.display = 'none';
}
});
}
async function searchPartners(term, resultsContainer) {
try {
const response = await fetch(`@Url.Action("PartnerSearchAutoComplete", "FileManager")?term=${encodeURIComponent(term)}`);
const partners = await response.json();
displayPartnerResults(partners, resultsContainer);
} catch (error) {
console.error('Error searching partners:', error);
}
}
function displayPartnerResults(partners, resultsContainer) {
resultsContainer.innerHTML = '';
if (!partners || partners.length === 0) {
resultsContainer.innerHTML = '<div class="autocomplete-item">No partners found</div>';
resultsContainer.style.display = 'block';
return;
}
partners.forEach(partner => {
const item = document.createElement('div');
item.className = 'autocomplete-item';
item.innerHTML = `
<div style="font-weight: 500;">${escapeHtml(partner.name)}</div>
<div style="font-size: 0.85em; color: #666;">
Tax ID: ${escapeHtml(partner.taxId || 'N/A')} |
${escapeHtml(partner.city || '')}${partner.country ? ', ' + escapeHtml(partner.country) : ''}
</div>
`;
item.addEventListener('click', () => {
selectPartner(partner);
resultsContainer.style.display = 'none';
});
resultsContainer.appendChild(item);
});
resultsContainer.style.display = 'block';
}
function selectPartner(partner) {
const previousPartnerId = currentPartner.id;
const wasMatched = previousPartnerId && previousPartnerId > 0;
currentPartner = {
id: partner.value,
name: partner.name,
taxId: partner.taxId || ''
};
updatePartnerDisplay();
if (wasMatched) {
showMessage(`Partner changed from ID ${previousPartnerId} to: ${partner.name}`, 'info');
} else {
showMessage(`Partner matched: ${partner.name}`, 'success');
}
}
function initializeProductAutocomplete(inputElement, itemIndex) {
// Create autocomplete container
const wrapper = document.createElement('div');
wrapper.style.position = 'relative';
wrapper.style.width = '100%';
inputElement.parentNode.insertBefore(wrapper, inputElement);
wrapper.appendChild(inputElement);
const resultsContainer = document.createElement('div');
resultsContainer.className = 'autocomplete-results';
resultsContainer.style.display = 'none';
wrapper.appendChild(resultsContainer);
// Input event for searching
inputElement.addEventListener('input', (e) => {
const searchTerm = e.target.value.trim();
// Clear existing timeout
if (autocompleteTimeout) {
clearTimeout(autocompleteTimeout);
}
if (searchTerm.length < 2) {
resultsContainer.style.display = 'none';
return;
}
// Debounce search
autocompleteTimeout = setTimeout(async () => {
await searchProducts(searchTerm, resultsContainer, itemIndex);
}, 300);
});
// Hide results when clicking outside
document.addEventListener('click', (e) => {
if (!wrapper.contains(e.target)) {
resultsContainer.style.display = 'none';
}
});
}
async function searchProducts(term, resultsContainer, itemIndex) {
try {
const response = await fetch(`@Url.Action("ProductSearchUnfilteredAutoComplete", "CustomOrder")?term=${encodeURIComponent(term)}`);
const products = await response.json();
displayProductResults(products, resultsContainer, itemIndex);
} catch (error) {
console.error('Error searching products:', error);
}
}
function displayProductResults(products, resultsContainer, itemIndex) {
resultsContainer.innerHTML = '';
if (!products || products.length === 0) {
resultsContainer.innerHTML = '<div class="autocomplete-item">No products found</div>';
resultsContainer.style.display = 'block';
return;
}
products.forEach(product => {
const item = document.createElement('div');
item.className = 'autocomplete-item';
item.innerHTML = `
<div style="font-weight: 500;">${escapeHtml(product.label)}</div>
<div style="font-size: 0.85em; color: #666;">SKU: ${escapeHtml(product.sku || 'N/A')}</div>
`;
item.addEventListener('click', () => {
selectProduct(product, itemIndex);
resultsContainer.style.display = 'none';
});
resultsContainer.appendChild(item);
});
resultsContainer.style.display = 'block';
}
function selectProduct(product, itemIndex) {
// Extract product name from label (remove the stock and price info)
const productName = product.label.split('[')[0].trim();
const previousProductId = shippingItems[itemIndex].productId;
const wasMatched = previousProductId && previousProductId > 0;
// Update the shipping item with selected product
shippingItems[itemIndex] = {
...shippingItems[itemIndex],
productId: product.value,
name: productName,
hungarianName: productName,
unitPriceOnDocument: product.price || 0
};
// Re-render the table to show the matched product
renderShippingItemsTable();
if (wasMatched) {
showMessage(`Product changed from ID ${previousProductId} to: ${productName}`, 'info');
} else {
showMessage(`Product matched: ${productName}`, 'success');
}
}
async function saveShippingDocument() {
if (isKnownDuplicate) {
const confirmSave = confirm(
'This document has already been processed before. ' +
'Do you want to create a new entry with the same data? ' +
'(This will NOT create duplicate file storage, but will create a new shipping document record.)'
);
if (!confirmSave) {
showMessage('Save cancelled.', 'info');
return;
}
}
// Validate partner is selected
if (!currentPartner.id) {
showMessage('Please select a partner before saving', 'warning');
return;
}
// Collect data from form
const shippingDocument = {
documentIdNumber: document.getElementById('editDocumentIdNumber').value,
partnerId: currentPartner.id,
totalPallets: parseInt(document.getElementById('editTotalPallets').value) || 0,
pdfFileName: document.getElementById('editPdfFileName').value,
shippingItems: shippingItems
};
try {
saveShippingDocumentButton.disabled = true;
saveShippingDocumentButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Saving...';
// Get the antiforgery token
const token = document.querySelector('input[name="__RequestVerificationToken"]').value;
// Create FormData to send both JSON and file
const formData = new FormData();
// Add document data as JSON string
formData.append('documentData', JSON.stringify(shippingDocument));
// Add extracted text
formData.append('extractedText', extractedFullText);
// Add original file if available
if (originalUploadedFile) {
formData.append('originalFile', originalUploadedFile);
console.log('✓ Including original file:', originalUploadedFile.name);
} else {
console.warn('⚠ No original file to save');
}
const response = await fetch('@Url.Action("SaveShippingDocument", "FileManager")', {
method: 'POST',
headers: {
'RequestVerificationToken': token
// Don't set Content-Type - browser sets it automatically for FormData
},
body: formData
});
const result = await response.json();
if (response.ok && result.success) {
showMessage('Shipping document and file saved successfully!', 'success');
console.log('✓ Saved:', result);
// Optionally reload or redirect after save
// window.location.reload();
} else {
showMessage('Error: ' + (result.message || 'Failed to save shipping document'), 'danger');
}
} catch (error) {
console.error('Error saving shipping document:', error);
showMessage('Error: Failed to communicate with server', 'danger');
} finally {
saveShippingDocumentButton.disabled = false;
saveShippingDocumentButton.innerHTML = '<i class="fas fa-save"></i> Save Shipping Document';
}
}
function escapeHtml(text) {
if (!text) return '';
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.toString().replace(/[&<>"']/g, m => map[m]);
}
function showMessage(message, type) {
responseMessage.textContent = message;
responseMessage.className = 'alert alert-' + type;
responseMessage.style.display = 'block';
setTimeout(() => {
responseMessage.style.display = 'none';
}, 5000);
}
</script>