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.
372 lines
17 KiB
372 lines
17 KiB
{{define "document_upload"}}
|
|
<h2>Document Uploads</h2>
|
|
|
|
<form id="upload-form" hx-post="/upload-documents" hx-encoding="multipart/form-data" hx-target="#upload-results"
|
|
hx-indicator=".upload-overlay">
|
|
|
|
<div class="upload-container">
|
|
<!-- Upload overlay -->
|
|
<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 -->
|
|
<div id="step3" class="content" style="display: none;">
|
|
<h3 class="submenu-header">Step 3: Submit Uploads</h3>
|
|
<div>
|
|
<button type="submit" class="success-button" id="final-submit-button">Upload Documents to Jobs</button>
|
|
|
|
<div id="upload-loading-indicator" class="htmx-indicator">
|
|
<span>Uploading...</span>
|
|
<div class="loading-indicator"></div>
|
|
</div>
|
|
|
|
<div id="upload-results" class="upload-results"></div>
|
|
</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>
|
|
</form>
|
|
|
|
<!-- 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>
|
|
let selectedFilesData = []; // To store {originalFile, displayName, documentType, isActive, originalIndex}
|
|
const MAX_FILENAME_LENGTH = 30; // Max length for displayed filename on chip
|
|
|
|
document.getElementById('document-files').addEventListener('change', handleFileSelectionChange);
|
|
const 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);
|
|
|
|
// Get document type text
|
|
const docTypeSelect = document.getElementById('editDocumentType'); // Use the modal's select for options
|
|
let docTypeText = '';
|
|
if (docTypeSelect) {
|
|
const selectedOption = Array.from(docTypeSelect.options).find(opt => opt.value === fileMetadata.documentType);
|
|
if (selectedOption) {
|
|
docTypeText = selectedOption.text;
|
|
}
|
|
}
|
|
|
|
chip.innerHTML = `
|
|
<span class="file-chip-icon">${icon}</span>
|
|
<span class="file-chip-name" title="${fileMetadata.displayName}" onclick="openEditModal(${index})">${truncatedName}</span>
|
|
<span class="file-chip-doctype">${docTypeText}</span>
|
|
<button type="button" class="file-chip-edit" onclick="openEditModal(${index})">✏️</button>
|
|
<button type="button" class="file-chip-remove" onclick="removeFileChip(${index})">×</button>
|
|
`;
|
|
filesArea.appendChild(chip);
|
|
}
|
|
|
|
function openEditModal(index) {
|
|
const fileMetadata = selectedFilesData[index];
|
|
if (!fileMetadata || !fileMetadata.isActive) return; // Don't edit removed or non-existent files
|
|
|
|
document.getElementById('editFileOriginalIndex').value = index;
|
|
document.getElementById('editDisplayName').value = fileMetadata.displayName;
|
|
document.getElementById('editDocumentType').value = fileMetadata.documentType;
|
|
|
|
// Later: Add preview logic here if possible for fileMetadata.originalFile
|
|
const previewArea = document.getElementById('modal-preview-area');
|
|
previewArea.innerHTML = '<p>Document preview will be shown here in a future update.</p>'; // Placeholder
|
|
|
|
document.getElementById('editFileModal').style.display = 'flex';
|
|
}
|
|
|
|
function closeEditModal() {
|
|
document.getElementById('editFileModal').style.display = 'none';
|
|
}
|
|
|
|
function saveFileChanges() {
|
|
const index = parseInt(document.getElementById('editFileOriginalIndex').value);
|
|
const newDisplayName = document.getElementById('editDisplayName').value;
|
|
const newDocumentType = document.getElementById('editDocumentType').value;
|
|
|
|
if (selectedFilesData[index]) {
|
|
selectedFilesData[index].displayName = newDisplayName;
|
|
selectedFilesData[index].documentType = newDocumentType;
|
|
|
|
// Re-render the chip's display name, icon, and doc type
|
|
const chipElement = document.querySelector(`.file-chip[data-index="${index}"]`);
|
|
if (chipElement) {
|
|
const truncatedName = truncateFilename(newDisplayName);
|
|
chipElement.querySelector('.file-chip-name').textContent = truncatedName;
|
|
chipElement.querySelector('.file-chip-name').title = newDisplayName;
|
|
chipElement.querySelector('.file-chip-icon').textContent = getFileIcon(newDisplayName);
|
|
|
|
// Update doc type text on chip
|
|
const docTypeSelect = document.getElementById('editDocumentType');
|
|
let docTypeText = '';
|
|
if (docTypeSelect) {
|
|
const selectedOption = Array.from(docTypeSelect.options).find(opt => opt.value === newDocumentType);
|
|
if (selectedOption) {
|
|
docTypeText = selectedOption.text;
|
|
}
|
|
}
|
|
chipElement.querySelector('.file-chip-doctype').textContent = docTypeText;
|
|
}
|
|
}
|
|
closeEditModal();
|
|
}
|
|
|
|
function removeFileChip(index) {
|
|
if (selectedFilesData[index]) {
|
|
selectedFilesData[index].isActive = false;
|
|
const chipElement = document.querySelector(`.file-chip[data-index="${index}"]`);
|
|
if (chipElement) {
|
|
chipElement.classList.add('removed');
|
|
// Optionally disable edit/remove buttons on a "removed" chip
|
|
chipElement.querySelector('.file-chip-edit').disabled = true;
|
|
}
|
|
// Check if all files are removed to disable "Continue" button
|
|
const allRemoved = selectedFilesData.every(f => !f.isActive);
|
|
continueToStep3Button.disabled = allRemoved || selectedFilesData.length === 0;
|
|
}
|
|
}
|
|
|
|
// Function to restart the upload process
|
|
function restartUpload() {
|
|
// Reset the form and file input
|
|
const form = document.getElementById('upload-form');
|
|
form.reset();
|
|
const fileInput = document.getElementById('document-files');
|
|
if (fileInput) fileInput.value = '';
|
|
|
|
// Remove any hidden jobNumbers inputs
|
|
form.querySelectorAll('input[name="jobNumbers"]').forEach(el => el.remove());
|
|
|
|
// Clear and hide the job IDs container
|
|
const jobIdsContainer = document.getElementById('job-ids-container');
|
|
jobIdsContainer.innerHTML = '';
|
|
jobIdsContainer.style.display = 'none';
|
|
|
|
// Clear and hide the CSV preview
|
|
const csvPreview = document.getElementById('csv-preview');
|
|
if (csvPreview) csvPreview.style.display = 'none';
|
|
const csvContent = document.getElementById('csv-preview-content');
|
|
if (csvContent) csvContent.innerHTML = '<p>No jobs loaded yet</p>';
|
|
|
|
// Clear selected files data and UI
|
|
selectedFilesData = [];
|
|
const filesArea = document.getElementById('selected-files-area');
|
|
filesArea.innerHTML = '<p id="no-files-selected-placeholder">No files selected yet</p>';
|
|
|
|
// Clear upload results
|
|
const uploadResults = document.getElementById('upload-results');
|
|
if (uploadResults) uploadResults.innerHTML = '';
|
|
|
|
// Hide steps 2, 3, and restart section
|
|
document.getElementById('step2').style.display = 'none';
|
|
document.getElementById('step3').style.display = 'none';
|
|
document.getElementById('restart-section').style.display = 'none';
|
|
|
|
// Show step 1
|
|
document.getElementById('step1').style.display = 'block';
|
|
|
|
// Disable the continue-to-step3 button
|
|
if (continueToStep3Button) continueToStep3Button.disabled = true;
|
|
}
|
|
|
|
// Show restart section after successful upload
|
|
document.getElementById('upload-form').addEventListener('htmx:afterRequest', function (evt) {
|
|
if (evt.detail.pathInfo.requestPath === '/upload-documents' && evt.detail.successful) {
|
|
document.getElementById('restart-section').style.display = 'block';
|
|
}
|
|
});
|
|
|
|
// 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);
|
|
|
|
// 'documentFiles' will be sent by the browser from the <input type="file" name="documentFiles">
|
|
// 'jobNumbers' will be sent from the hidden input populated by the CSV step.
|
|
});
|
|
</script>
|
|
{{end}}
|