an updated and hopefully faster version of the ST Toolbox
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.
 
 
 
 

472 lines
21 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()">&times;</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>
console.log('Document upload script loaded');
// 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');
console.log('Event listeners attached');
function handleFileSelectionChange(event) {
console.log('File selection changed');
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;
console.log('Files selected:', files.length);
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 getDocumentTypeName(typeValue) {
const typeMap = {
"1": "Job Paperwork",
"2": "Job Vendor Bill",
"7": "Generic Attachment",
"10": "Blank Paperwork",
"14": "Job Invoice"
};
return typeMap[typeValue] || `Type: ${typeValue}`;
}
function renderFileChip(fileMetadata, index) {
console.log('renderFileChip called for index:', index, 'file:', fileMetadata.displayName);
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);
const documentTypeName = getDocumentTypeName(fileMetadata.documentType);
chip.innerHTML = `
<span class="file-chip-icon">${icon}</span>
<span class="file-chip-name" title="${fileMetadata.displayName}" onclick="openEditModal(${index})" style="cursor: pointer;">${truncatedName}</span>
<span class="file-chip-doctype">${documentTypeName}</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);
console.log('File chip rendered for:', fileMetadata.displayName);
}
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) {
console.log('openEditModal called with index:', index);
console.log('selectedFilesData length:', selectedFilesData.length);
if (index >= 0 && index < selectedFilesData.length) {
const fileData = selectedFilesData[index];
console.log('File data:', fileData);
const originalIndexInput = document.getElementById('editFileOriginalIndex');
const displayNameInput = document.getElementById('editDisplayName');
const documentTypeInput = document.getElementById('editDocumentType');
const modal = document.getElementById('editFileModal');
console.log('Modal element:', modal);
console.log('Input elements:', { originalIndexInput, displayNameInput, documentTypeInput });
if (originalIndexInput && displayNameInput && documentTypeInput && modal) {
originalIndexInput.value = index;
displayNameInput.value = fileData.displayName;
documentTypeInput.value = fileData.documentType;
// Ensure modal is displayed properly
modal.style.display = 'flex'; // Use flex instead of block for better centering
modal.style.zIndex = '1002'; // Ensure it's above the overlay
console.log('Modal opened for file:', fileData.displayName);
console.log('Modal display style:', modal.style.display);
console.log('Modal z-index:', modal.style.zIndex);
} else {
console.error('Missing modal elements:', { originalIndexInput, displayNameInput, documentTypeInput, modal });
}
} else {
console.error('Invalid index or selectedFilesData:', { index, selectedFilesDataLength: selectedFilesData.length });
}
}
function closeEditModal() {
console.log('closeEditModal called');
const modal = document.getElementById('editFileModal');
if (modal) {
modal.style.display = 'none';
console.log('Modal closed');
} else {
console.error('Modal element not found');
}
}
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);
const documentTypeName = getDocumentTypeName(selectedFilesData[index].documentType);
chip.innerHTML = `
<span class="file-chip-icon">${icon}</span>
<span class="file-chip-name" title="${selectedFilesData[index].displayName}" onclick="openEditModal(${index})" style="cursor: pointer;">${truncatedName}</span>
<span class="file-chip-doctype">${documentTypeName}</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 () {
// Add click outside modal to close functionality
const modal = document.getElementById('editFileModal');
if (modal) {
modal.addEventListener('click', function (event) {
if (event.target === modal) {
closeEditModal();
}
});
}
// Add escape key to close modal
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
const modal = document.getElementById('editFileModal');
if (modal && modal.style.display !== 'none') {
closeEditModal();
}
}
});
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}}