You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
398 lines
18 KiB
398 lines
18 KiB
{{define "document_upload"}}
|
|
<h2>Document Uploads</h2>
|
|
|
|
<div class="upload-container">
|
|
<!-- Upload overlay - moved outside the form -->
|
|
<div class="upload-overlay htmx-indicator">
|
|
<div class="upload-overlay-content">
|
|
<div class="overlay-spinner"></div>
|
|
<h3>Uploading Documents</h3>
|
|
<p>Please wait while your documents are being uploaded...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Job numbers will be added here by the CSV process -->
|
|
<div id="job-ids-container" style="display: none;">
|
|
<!-- Hidden input placeholder for job IDs -->
|
|
</div>
|
|
|
|
<!-- Step 1: CSV Upload -->
|
|
<div id="step1" class="content">
|
|
<h3 class="submenu-header">Step 1: Upload CSV file with Job IDs</h3>
|
|
<div>
|
|
<label>Select CSV file with job IDs:</label>
|
|
<input class="card-input" type="file" id="csv-file" name="csvFile" accept=".csv" required>
|
|
|
|
<button type="button" class="btn-primary" hx-post="/process-csv" hx-target="#job-ids-container"
|
|
hx-encoding="multipart/form-data" hx-include="#csv-file" hx-indicator="#csv-loading-indicator"
|
|
hx-on::after-request="if(event.detail.successful) { document.getElementById('step2').style.display = 'block'; }">
|
|
Upload CSV
|
|
</button>
|
|
|
|
<div id="csv-loading-indicator" class="htmx-indicator" style="display: none;">
|
|
<span>Processing CSV...</span>
|
|
<div class="loading-indicator"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="csv-preview" class="fade-me-out" style="display: none; margin-top: 1rem;">
|
|
<h4>Detected Jobs</h4>
|
|
<div id="csv-preview-content" class="job-list">
|
|
<!-- Job numbers will be displayed here -->
|
|
<p>No jobs loaded yet</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 2: Document Upload -->
|
|
<div id="step2" class="content" style="display: none;">
|
|
<h3 class="submenu-header">Step 2: Select Documents & Types</h3>
|
|
|
|
<!-- Single file input for multiple documents -->
|
|
<div class="document-field">
|
|
<label for="document-files">Select Document(s):</label>
|
|
<input class="card-input" type="file" id="document-files" name="documentFiles" multiple>
|
|
</div>
|
|
|
|
<!-- Area to display selected file chips -->
|
|
<div id="selected-files-area" class="selected-files-grid" style="margin-top: 1rem; margin-bottom: 1rem;">
|
|
<!-- File chips will be dynamically inserted here by JavaScript -->
|
|
<p id="no-files-selected-placeholder">No files selected yet.</p>
|
|
</div>
|
|
|
|
<button type="button" class="btn-primary" id="continue-to-step3-button" disabled
|
|
onclick="if (!this.disabled) document.getElementById('step3').style.display = 'block';">
|
|
Continue to Step 3
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Step 3: Submit - form moved here -->
|
|
<div id="step3" class="content" style="display: none;">
|
|
<h3 class="submenu-header">Step 3: Submit Uploads</h3>
|
|
<div>
|
|
<form id="upload-form" hx-post="/upload-documents" hx-encoding="multipart/form-data"
|
|
hx-include="[name='documentFiles']" hx-target="#upload-results" hx-indicator=".upload-overlay">
|
|
|
|
<input type="hidden" name="job-ids" id="job-ids-field">
|
|
<button type="submit" class="success-button" id="final-submit-button">Upload Documents to Jobs</button>
|
|
</form>
|
|
|
|
<div id="upload-loading-indicator" class="htmx-indicator">
|
|
<span>Uploading...</span>
|
|
<div class="loading-indicator"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 4: Results - moved outside the form -->
|
|
<div id="step4" class="content" style="display: none;">
|
|
<h3 class="submenu-header">Step 4: Upload Results</h3>
|
|
<div id="upload-results" class="upload-results">
|
|
<!-- Results will appear here after uploading documents -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Restart Button (initially hidden) -->
|
|
<div id="restart-section" class="content" style="display: none;">
|
|
<h3 class="submenu-header">Upload Complete</h3>
|
|
<button type="button" class="btn-primary" hx-on:click="restartUpload()">Start New Upload</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit File Modal (Initially Hidden) -->
|
|
<div id="editFileModal" class="modal" style="display:none;">
|
|
<div class="modal-content">
|
|
<span class="close-button" onclick="closeEditModal()">×</span>
|
|
<h4>Edit File Details</h4>
|
|
<input type="hidden" id="editFileOriginalIndex">
|
|
|
|
<div class="form-group">
|
|
<label for="editDisplayName">Display Name:</label>
|
|
<input type="text" id="editDisplayName" class="card-input">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="editDocumentType">Document Type:</label>
|
|
<select id="editDocumentType" class="card-input">
|
|
<option value="1">Job Paperwork</option>
|
|
<option value="2">Job Vendor Bill</option>
|
|
<option value="7">Generic Attachment</option>
|
|
<option value="10">Blank Paperwork</option>
|
|
<option value="14">Job Invoice</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div id="modal-preview-area"
|
|
style="margin-top: 1rem; margin-bottom: 1rem; max-height: 300px; overflow-y: auto;">
|
|
<!-- Document preview will be attempted here later -->
|
|
<p>Document preview will be shown here in a future update.</p>
|
|
</div>
|
|
|
|
<button type="button" class="btn-primary" onclick="saveFileChanges()">Save Changes</button>
|
|
<button type="button" class="btn-secondary" onclick="closeEditModal()">Cancel</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Check if variable already exists to avoid redeclaration error with HTMX
|
|
if (typeof selectedFilesData === 'undefined') {
|
|
var selectedFilesData = []; // To store {originalFile, displayName, documentType, isActive, originalIndex}
|
|
}
|
|
if (typeof MAX_FILENAME_LENGTH === 'undefined') {
|
|
var MAX_FILENAME_LENGTH = 30; // Max length for displayed filename on chip
|
|
}
|
|
|
|
document.getElementById('document-files').addEventListener('change', handleFileSelectionChange);
|
|
var continueToStep3Button = document.getElementById('continue-to-step3-button');
|
|
|
|
function handleFileSelectionChange(event) {
|
|
selectedFilesData = []; // Reset
|
|
const filesArea = document.getElementById('selected-files-area');
|
|
filesArea.innerHTML = ''; // Clear previous chips
|
|
const noFilesPlaceholder = document.getElementById('no-files-selected-placeholder');
|
|
|
|
const files = event.target.files;
|
|
if (files.length === 0) {
|
|
if (noFilesPlaceholder) noFilesPlaceholder.style.display = 'block';
|
|
filesArea.innerHTML = '<p id="no-files-selected-placeholder">No files selected yet.</p>'; // Re-add if cleared
|
|
continueToStep3Button.disabled = true;
|
|
return;
|
|
}
|
|
|
|
if (noFilesPlaceholder) noFilesPlaceholder.style.display = 'none';
|
|
else { // if it was removed entirely, ensure it's not there
|
|
const existingPlaceholder = document.getElementById('no-files-selected-placeholder');
|
|
if (existingPlaceholder) existingPlaceholder.remove();
|
|
}
|
|
|
|
|
|
for (let i = 0; i < files.length; i++) {
|
|
const file = files[i];
|
|
const fileMetadata = {
|
|
originalFile: file,
|
|
displayName: file.name,
|
|
documentType: "1", // Default to "Job Paperwork"
|
|
isActive: true,
|
|
originalIndex: i
|
|
};
|
|
selectedFilesData.push(fileMetadata);
|
|
renderFileChip(fileMetadata, i);
|
|
}
|
|
continueToStep3Button.disabled = files.length === 0;
|
|
}
|
|
|
|
function getFileIcon(filename) {
|
|
const extension = filename.split('.').pop().toLowerCase();
|
|
// Simple icon logic, can be expanded
|
|
if (['jpg', 'jpeg', 'png', 'gif'].includes(extension)) return '🖼️'; // Image icon
|
|
if (extension === 'pdf') return '📄'; // PDF icon
|
|
if (['doc', 'docx'].includes(extension)) return '📝'; // Word doc
|
|
return '📁'; // Generic file icon
|
|
}
|
|
|
|
function truncateFilename(filename, maxLength = MAX_FILENAME_LENGTH) {
|
|
if (filename.length <= maxLength) return filename;
|
|
const start = filename.substring(0, maxLength - 3 - Math.floor((maxLength - 3) / 3));
|
|
const end = filename.substring(filename.length - Math.floor((maxLength - 3) / 3));
|
|
return `${start}...${end}`;
|
|
}
|
|
|
|
|
|
function renderFileChip(fileMetadata, index) {
|
|
const filesArea = document.getElementById('selected-files-area');
|
|
const chip = document.createElement('div');
|
|
chip.className = `file-chip ${fileMetadata.isActive ? '' : 'removed'}`;
|
|
chip.dataset.index = index;
|
|
|
|
const icon = getFileIcon(fileMetadata.displayName);
|
|
const truncatedName = truncateFilename(fileMetadata.displayName);
|
|
|
|
chip.innerHTML = `
|
|
<span class="file-chip-icon">${icon}</span>
|
|
<span class="file-chip-name" title="${fileMetadata.displayName}">${truncatedName}</span>
|
|
<span class="file-chip-doctype">Type: ${fileMetadata.documentType}</span>
|
|
<button type="button" class="file-chip-edit" onclick="openEditModal(${index})" title="Edit file details">✏️</button>
|
|
<button type="button" class="file-chip-remove" onclick="toggleFileActive(${index})" title="${fileMetadata.isActive ? 'Remove from upload' : 'Add back to upload'}">${fileMetadata.isActive ? '❌' : '➕'}</button>
|
|
`;
|
|
|
|
filesArea.appendChild(chip);
|
|
}
|
|
|
|
function toggleFileActive(index) {
|
|
if (index >= 0 && index < selectedFilesData.length) {
|
|
selectedFilesData[index].isActive = !selectedFilesData[index].isActive;
|
|
const chip = document.querySelector(`[data-index="${index}"]`);
|
|
if (chip) {
|
|
chip.className = `file-chip ${selectedFilesData[index].isActive ? '' : 'removed'}`;
|
|
const removeBtn = chip.querySelector('.file-chip-remove');
|
|
if (removeBtn) {
|
|
removeBtn.innerHTML = selectedFilesData[index].isActive ? '❌' : '➕';
|
|
removeBtn.title = selectedFilesData[index].isActive ? 'Remove from upload' : 'Add back to upload';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function openEditModal(index) {
|
|
if (index >= 0 && index < selectedFilesData.length) {
|
|
const fileData = selectedFilesData[index];
|
|
document.getElementById('editFileOriginalIndex').value = index;
|
|
document.getElementById('editDisplayName').value = fileData.displayName;
|
|
document.getElementById('editDocumentType').value = fileData.documentType;
|
|
document.getElementById('editFileModal').style.display = 'block';
|
|
}
|
|
}
|
|
|
|
function closeEditModal() {
|
|
document.getElementById('editFileModal').style.display = 'none';
|
|
}
|
|
|
|
function saveFileChanges() {
|
|
const index = parseInt(document.getElementById('editFileOriginalIndex').value);
|
|
if (index >= 0 && index < selectedFilesData.length) {
|
|
selectedFilesData[index].displayName = document.getElementById('editDisplayName').value;
|
|
selectedFilesData[index].documentType = document.getElementById('editDocumentType').value;
|
|
|
|
// Re-render the chip
|
|
const chip = document.querySelector(`[data-index="${index}"]`);
|
|
if (chip) {
|
|
const icon = getFileIcon(selectedFilesData[index].displayName);
|
|
const truncatedName = truncateFilename(selectedFilesData[index].displayName);
|
|
|
|
chip.innerHTML = `
|
|
<span class="file-chip-icon">${icon}</span>
|
|
<span class="file-chip-name" title="${selectedFilesData[index].displayName}">${truncatedName}</span>
|
|
<span class="file-chip-doctype">Type: ${selectedFilesData[index].documentType}</span>
|
|
<button type="button" class="file-chip-edit" onclick="openEditModal(${index})" title="Edit file details">✏️</button>
|
|
<button type="button" class="file-chip-remove" onclick="toggleFileActive(${index})" title="${selectedFilesData[index].isActive ? 'Remove from upload' : 'Add back to upload'}">${selectedFilesData[index].isActive ? '❌' : '➕'}</button>
|
|
`;
|
|
}
|
|
}
|
|
closeEditModal();
|
|
}
|
|
|
|
// Function to restart the upload process
|
|
function restartUpload() {
|
|
// Hide all sections except step 1
|
|
document.getElementById('step2').style.display = 'none';
|
|
document.getElementById('step3').style.display = 'none';
|
|
document.getElementById('step4').style.display = 'none';
|
|
document.getElementById('restart-section').style.display = 'none';
|
|
|
|
// Clear results
|
|
document.getElementById('upload-results').innerHTML = '';
|
|
|
|
// Reset CSV preview if it exists
|
|
const csvPreview = document.getElementById('csv-preview');
|
|
if (csvPreview) {
|
|
csvPreview.style.display = 'none';
|
|
}
|
|
|
|
const csvPreviewContent = document.getElementById('csv-preview-content');
|
|
if (csvPreviewContent) {
|
|
csvPreviewContent.innerHTML = '<p>No jobs loaded yet</p>';
|
|
}
|
|
|
|
// Reset job IDs container
|
|
document.getElementById('job-ids-container').innerHTML = '';
|
|
|
|
// Show step 1
|
|
document.getElementById('step1').style.display = 'block';
|
|
|
|
// Reset any file inputs
|
|
const fileInput = document.getElementById('csv-file');
|
|
if (fileInput) {
|
|
fileInput.value = '';
|
|
}
|
|
|
|
const documentFilesInput = document.getElementById('document-files');
|
|
if (documentFilesInput) {
|
|
documentFilesInput.value = '';
|
|
}
|
|
|
|
// Reset selected files
|
|
selectedFilesData = [];
|
|
const filesArea = document.getElementById('selected-files-area');
|
|
if (filesArea) {
|
|
filesArea.innerHTML = '<p id="no-files-selected-placeholder">No files selected yet.</p>';
|
|
}
|
|
|
|
// Reset continue button
|
|
if (continueToStep3Button) {
|
|
continueToStep3Button.disabled = true;
|
|
}
|
|
}
|
|
|
|
// Add event listeners for the upload overlay
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
document.querySelectorAll('form').forEach(form => {
|
|
form.addEventListener('htmx:beforeRequest', function (evt) {
|
|
if (evt.detail.pathInfo.requestPath.includes('/upload-documents')) {
|
|
document.querySelector('.upload-overlay').style.display = 'flex';
|
|
}
|
|
});
|
|
|
|
form.addEventListener('htmx:afterRequest', function (evt) {
|
|
if (evt.detail.pathInfo.requestPath.includes('/upload-documents')) {
|
|
document.querySelector('.upload-overlay').style.display = 'none';
|
|
|
|
// If it's an upload action and successful, show step 4 and restart section
|
|
if (evt.detail.successful) {
|
|
document.getElementById('step4').style.display = 'block';
|
|
document.getElementById('restart-section').style.display = 'block';
|
|
}
|
|
}
|
|
});
|
|
|
|
form.addEventListener('htmx:error', function (evt) {
|
|
document.querySelector('.upload-overlay').style.display = 'none';
|
|
});
|
|
});
|
|
|
|
// Prepare metadata for backend on form submission
|
|
document.getElementById('upload-form').addEventListener('htmx:configRequest', function (evt) {
|
|
const displayNamesArr = [];
|
|
const documentTypesArr = [];
|
|
const isActiveArr = [];
|
|
|
|
const documentFilesInput = document.getElementById('document-files');
|
|
const filesFromInput = documentFilesInput.files; // These are the files the browser will send by default
|
|
|
|
for (let i = 0; i < filesFromInput.length; i++) {
|
|
// Find the corresponding metadata in selectedFilesData.
|
|
// selectedFilesData is built with originalIndex matching the input files' index.
|
|
const fileMetadata = selectedFilesData.find(meta => meta.originalIndex === i);
|
|
|
|
if (fileMetadata) {
|
|
displayNamesArr.push(fileMetadata.displayName);
|
|
documentTypesArr.push(fileMetadata.documentType);
|
|
isActiveArr.push(fileMetadata.isActive);
|
|
} else {
|
|
// This case should ideally not happen if selectedFilesData is managed correctly.
|
|
// Log an error and send fallback data to prevent backend length mismatch errors.
|
|
console.error(`Client-side warning: No metadata in selectedFilesData for file at originalIndex ${i}. Filename: ${filesFromInput[i].name}. Sending defaults.`);
|
|
displayNamesArr.push(filesFromInput[i].name); // Fallback to original name
|
|
documentTypesArr.push("1"); // Default type "Job Paperwork"
|
|
isActiveArr.push(false); // Default to not active to be safe, it won't be processed
|
|
}
|
|
}
|
|
|
|
// Add these arrays as JSON strings to the request parameters.
|
|
// HTMX will include these in the multipart/form-data POST request.
|
|
evt.detail.parameters['file_display_names'] = JSON.stringify(displayNamesArr);
|
|
evt.detail.parameters['file_document_types'] = JSON.stringify(documentTypesArr);
|
|
evt.detail.parameters['file_is_active_flags'] = JSON.stringify(isActiveArr);
|
|
|
|
// Set the job numbers from the hidden input created by CSV processing
|
|
const jobIdsInput = document.querySelector('input[name="jobNumbers"]');
|
|
if (jobIdsInput) {
|
|
evt.detail.parameters['jobNumbers'] = jobIdsInput.value;
|
|
} else {
|
|
console.error('No jobNumbers input found. Make sure CSV was processed first.');
|
|
}
|
|
|
|
// 'documentFiles' will be sent by the browser from the <input type="file" name="documentFiles">
|
|
});
|
|
});
|
|
</script>
|
|
{{end}}
|