922 lines
37 KiB
Plaintext
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 = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
};
|
|
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>
|