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.
 
 
 
 

377 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()">&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>
// 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);
// 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})">&times;</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}}