@ -1,96 +1,103 @@
{{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 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 >
<!-- 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 >
<!-- 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-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 id = "csv-loading-indicator" class = "htmx-indicator" style = "display: none;" >
< span > Processing CSV...< / span >
< div class = "loading-indicator" > < / 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 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 >
<!-- 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 >
<!-- Step 2: Document Upload -->
< div id = "step2" class = "content" style = "display: none;" >
< h3 class = "submenu-header" > Step 2: Select Documents & Types< / h3 >
< 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 >
<!-- 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 >
<!-- 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 >
<!-- 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 >
< div id = "upload-loading-indicator" class = "htmx-indicator" >
< span > Uploading...< / span >
< div class = "loading-indicator" > < / div >
< / 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">
< div id = "upload-results" class = "upload-results" > < / div >
< 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 >
<!-- 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 >
<!-- 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 >
< / form >
<!-- 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;" >
@ -200,39 +207,40 @@
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 >
< 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 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
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';
}
}
}
}
document.getElementById('editFileModal').style.display = 'flex';
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() {
@ -241,137 +249,150 @@
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;
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 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());
// 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 and hide the job IDs container
const jobIdsContainer = document.getElementById('job-ids-container');
jobIdsContainer.innerHTML = '';
jobIdsContainer.style.display = 'none';
// Clear results
document.getElementById('upload-results').innerHTML = '';
// Clear and hide the CSV preview
// Reset CSV preview if it exists
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 > ';
if (csvPreview) {
csvPreview.style.display = 'none';
}
// Clear upload results
const uploadResults = document.getElementById('upload-results');
if (uploadResults) uploadResults.innerHTML = '';
const csvPreviewContent = document.getElementById('csv-preview-content');
if (csvPreviewContent) {
csvPreviewContent.innerHTML = '< p > No jobs loaded yet< / p > ';
}
// 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';
// Reset job IDs container
document.getElementById('job-ids-container').innerHTML = '';
// Show step 1
document.getElementById('step1').style.display = 'block';
// Disable the continue-to-step3 button
if (continueToStep3Button) continueToStep3Button.disabled = true;
}
// Reset any file inputs
const fileInput = document.getElementById('csv-file');
if (fileInput) {
fileInput.value = '';
}
const documentFilesInput = document.getElementById('document-files');
if (documentFilesInput) {
documentFilesInput.value = '';
}
// 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';
// 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 > ';
}
});
// Prepare metadata for backend on form submissi on
document.getElementById('upload-form').addEventListener('htmx:configRequest', function (evt ) {
const displayNamesArr = [] ;
const documentTypesArr = [];
const isActiveArr = [];
// Reset continue butt on
if (continueToStep3Button ) {
continueToStep3Button.disabled = true ;
}
}
const documentFilesInput = document.getElementById('document-files');
const filesFromInput = documentFilesInput.files; // These are the files the browser will send by default
// 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';
}
});
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);
form.addEventListener('htmx:afterRequest', function (evt) {
if (evt.detail.pathInfo.requestPath.includes('/upload-documents')) {
document.querySelector('.upload-overlay').style.display = 'none';
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
// 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);
// 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" >
// 'jobNumbers' will be sent from the hidden input populated by the CSV step.
// 'documentFiles' will be sent by the browser from the < input type = "file" name = "documentFiles" >
});
});
< / script >
{{end}}