coming together

This commit is contained in:
HotSwapp
2025-08-13 18:53:35 -05:00
parent acc5155bf7
commit 5111079149
51 changed files with 14457 additions and 588 deletions

View File

@@ -133,8 +133,41 @@
<div id="generated" class="tabcontent p-6 hidden">
<div class="mt-3 bg-white dark:bg-neutral-800 rounded-xl shadow-soft border border-neutral-200 dark:border-neutral-700">
<div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700"><h5 class="mb-0 font-semibold"><i class="fa-regular fa-file-pdf"></i> Generated Documents</h5></div>
<div class="p-6"><div id="generatedDocuments"><p class="text-neutral-500">Generated documents will appear here...</p></div></div>
<div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700"><h5 class="mb-0 font-semibold"><i class="fa-regular fa-file-pdf"></i> Generated & Uploaded Documents</h5></div>
<div class="p-6">
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 items-end mb-4">
<div>
<label for="uploadFileNo" class="block text-sm font-medium mb-1">File Number</label>
<div class="flex gap-2">
<input type="text" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100" id="uploadFileNo" placeholder="Enter file #">
<button type="button" id="clearUploadFileNoBtn" class="px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-700" title="Clear">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div id="uploadFileNoError" class="text-xs text-red-600 mt-1 hidden">Please enter a file number</div>
</div>
<div>
<label for="uploadInput" class="block text-sm font-medium mb-1">Choose File</label>
<input type="file" id="uploadInput" class="block w-full text-sm text-neutral-900 dark:text-neutral-100 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-neutral-100 dark:file:bg-neutral-700 file:text-neutral-700 dark:file:text-neutral-200 hover:file:bg-neutral-200 dark:hover:file:bg-neutral-600" />
<div id="uploadInputError" class="text-xs text-red-600 mt-1 hidden">Please choose a file to upload</div>
</div>
<div class="flex gap-2">
<button type="button" id="uploadBtn" class="px-4 py-2 bg-primary-600 text-white hover:bg-primary-700 rounded-lg transition-colors"><i class="fa-solid fa-upload mr-2"></i>Upload</button>
<button type="button" id="refreshUploadsBtn" class="px-4 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-700 transition-colors"><i class="fa-solid fa-rotate-right mr-2"></i>Refresh</button>
</div>
</div>
<div id="uploadDropZone" class="mt-3 p-6 border-2 border-dashed border-neutral-300 dark:border-neutral-600 rounded-lg text-center text-neutral-500">
<i class="fa-solid fa-cloud-arrow-up text-2xl mb-2"></i>
<div>Drag & drop files here to upload</div>
<div class="text-xs mt-1">or use the chooser above and click Upload</div>
</div>
<div id="uploadingIndicator" class="mt-2 text-sm text-neutral-500 hidden"><i class="fa-solid fa-spinner animate-spin mr-2"></i>Uploading…</div>
<div id="uploadProgressList" class="space-y-2 mt-3"></div>
<div id="uploadedDocuments" class="mb-6">
<p class="text-neutral-500">Uploaded documents will appear here.</p>
</div>
<div id="generatedDocuments"><p class="text-neutral-500">Generated documents will appear here...</p></div>
</div>
</div>
</div>
</div>
@@ -432,8 +465,36 @@
</div>
</div>
<!-- Edit Upload Description Modal -->
<div class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" id="editUploadModal">
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-md w-full">
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="text-lg font-semibold">Edit Description</h5>
<button type="button" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300" onclick="closeModal('editUploadModal')"><i class="fa-solid fa-xmark"></i></button>
</div>
<div class="px-6 py-4">
<input type="hidden" id="editUploadId">
<label for="editUploadDescription" class="block text-sm font-medium mb-1">Description</label>
<textarea id="editUploadDescription" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" rows="4" placeholder="Enter description..."></textarea>
</div>
<div class="flex items-center justify-end gap-2 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700">
<button type="button" class="px-4 py-2 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600 rounded-lg" onclick="closeModal('editUploadModal')">Cancel</button>
<button type="button" class="px-4 py-2 bg-primary-600 text-white hover:bg-primary-700 rounded-lg" id="saveEditUploadBtn"><i class="fa-regular fa-circle-check"></i> Save</button>
</div>
</div>
</div>
<script>
// Document Management JavaScript
// Example: Upload with correlation-aware alerts
// -------------------------------------------------
// const input = document.querySelector('#uploadInput');
// const fileNo = 'ABC-123';
// const form = new FormData();
// form.append('file', input.files[0]);
// uploadWithAlerts(`/api/documents/upload/${fileNo}`, form)
// .then(() => alerts.success('Upload completed', { duration: 3000 }))
// .catch(() => {/* failure already alerted with Ref: <cid> */});
document.addEventListener('DOMContentLoaded', function() {
// Check authentication first
const token = localStorage.getItem('auth_token');
@@ -590,6 +651,153 @@ function setupEventHandlers() {
// Refresh buttons
document.getElementById('refreshTemplatesBtn').addEventListener('click', loadTemplates);
document.getElementById('refreshQdrosBtn').addEventListener('click', loadQdros);
const refreshUploadsBtn = document.getElementById('refreshUploadsBtn');
if (refreshUploadsBtn) refreshUploadsBtn.addEventListener('click', loadUploadedDocuments);
const uploadBtn = document.getElementById('uploadBtn');
if (uploadBtn) uploadBtn.addEventListener('click', handleUploadClick);
const saveEditUploadBtn = document.getElementById('saveEditUploadBtn');
if (saveEditUploadBtn) saveEditUploadBtn.addEventListener('click', saveEditUpload);
const dropZone = document.getElementById('uploadDropZone');
if (dropZone) initUploadDropZone(dropZone);
const clearUploadBtn = document.getElementById('clearUploadFileNoBtn');
if (clearUploadBtn) clearUploadBtn.addEventListener('click', clearUploadFileNo);
// Upload controls enable/disable
const fileNoInput = document.getElementById('uploadFileNo');
const uploadInput = document.getElementById('uploadInput');
if (fileNoInput) {
fileNoInput.addEventListener('input', updateUploadControlsState);
fileNoInput.addEventListener('change', updateUploadControlsState);
fileNoInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
const fileNo = (fileNoInput.value || '').trim();
const hasFile = !!(uploadInput && uploadInput.files && uploadInput.files.length > 0);
if (fileNo && hasFile) {
const btn = document.getElementById('uploadBtn');
if (btn && btn.disabled) return;
handleUploadClick();
} else if (!hasFile && uploadInput) {
const inputErr = document.getElementById('uploadInputError');
if (inputErr) inputErr.classList.remove('hidden');
uploadInput.focus();
}
}
});
}
if (uploadInput) {
uploadInput.addEventListener('change', () => {
updateUploadControlsState();
try {
const fileNoInputEl = document.getElementById('uploadFileNo');
const fileNoVal = (fileNoInputEl?.value || '').trim();
const hasFile = !!(uploadInput && uploadInput.files && uploadInput.files.length > 0);
if (hasFile && !fileNoVal && fileNoInputEl) {
fileNoInputEl.focus();
const fileNoErr = document.getElementById('uploadFileNoError');
if (fileNoErr) fileNoErr.classList.remove('hidden');
shakeElement(fileNoInputEl);
}
} catch (_) {}
});
uploadInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
const fileNo = (document.getElementById('uploadFileNo')?.value || '').trim();
const hasFile = !!(uploadInput && uploadInput.files && uploadInput.files.length > 0);
if (fileNo && hasFile) {
const btn = document.getElementById('uploadBtn');
if (btn && btn.disabled) return;
handleUploadClick();
} else if (!fileNo) {
const err = document.getElementById('uploadFileNoError');
if (err) err.classList.remove('hidden');
const fileNoInputEl = document.getElementById('uploadFileNo');
if (fileNoInputEl) {
fileNoInputEl.focus();
shakeElement(fileNoInputEl);
}
}
} else if (e.key === 'Escape') {
e.preventDefault();
try {
uploadInput.value = '';
updateUploadControlsState();
uploadInput.focus();
const inputErr = document.getElementById('uploadInputError');
if (inputErr) inputErr.classList.add('hidden');
} catch (_) {}
}
});
}
updateUploadControlsState();
// Persist and restore last used upload file number
const fileNoInput = document.getElementById('uploadFileNo');
if (fileNoInput) {
try {
const saved = localStorage.getItem('docs_last_upload_file_no');
if (saved) {
fileNoInput.value = saved;
// Auto-load uploads for restored file number and show one-time hint
try {
if ((saved || '').trim()) {
loadUploadedDocuments().then(() => {
try {
if (!sessionStorage.getItem('docs_auto_loaded_hint_shown')) {
showAlert(`Loaded uploads for file ${saved}`, 'info');
sessionStorage.setItem('docs_auto_loaded_hint_shown', '1');
}
} catch (_) {}
});
}
} catch (_) {}
}
const persist = () => {
try { localStorage.setItem('docs_last_upload_file_no', (fileNoInput.value || '').trim()); } catch (_) {}
};
fileNoInput.addEventListener('input', persist);
fileNoInput.addEventListener('change', persist);
} catch (_) {}
}
}
function updateUploadControlsState() {
try {
const btn = document.getElementById('uploadBtn');
const fileNo = (document.getElementById('uploadFileNo')?.value || '').trim();
const input = document.getElementById('uploadInput');
const hasFile = !!(input && input.files && input.files.length > 0);
const enabled = !!fileNo && hasFile;
if (btn) {
btn.disabled = !enabled;
btn.classList.toggle('opacity-50', !enabled);
btn.classList.toggle('cursor-not-allowed', !enabled);
btn.setAttribute('aria-disabled', String(!enabled));
}
// Inline error messages
const fileNoErr = document.getElementById('uploadFileNoError');
if (fileNoErr) fileNoErr.classList.toggle('hidden', !!fileNo);
const inputErr = document.getElementById('uploadInputError');
if (inputErr) inputErr.classList.toggle('hidden', hasFile);
} catch (_) {}
}
function shakeElement(el) {
try {
el.classList.add('animate-shake');
setTimeout(() => el.classList.remove('animate-shake'), 400);
} catch (_) {}
}
function clearUploadFileNo() {
try {
const input = document.getElementById('uploadFileNo');
if (input) input.value = '';
try { localStorage.removeItem('docs_last_upload_file_no'); } catch (_) {}
const container = document.getElementById('uploadedDocuments');
if (container) container.innerHTML = '<p class="text-neutral-500">No uploads found for this file.</p>';
} catch (_) {}
}
// Authorization and JSON headers are injected by window.http.wrappedFetch
@@ -633,6 +841,209 @@ async function loadTemplates() {
}
}
function initUploadDropZone(zoneEl) {
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter','dragover','dragleave','drop'].forEach(eventName => {
zoneEl.addEventListener(eventName, preventDefaults, false);
});
zoneEl.addEventListener('dragover', () => zoneEl.classList.add('bg-neutral-50', 'dark:bg-neutral-900/20'));
zoneEl.addEventListener('dragleave', () => zoneEl.classList.remove('bg-neutral-50', 'dark:bg-neutral-900/20', 'border-red-400'));
zoneEl.addEventListener('drop', async (e) => {
zoneEl.classList.remove('bg-neutral-50', 'dark:bg-neutral-900/20', 'border-red-400');
const dt = e.dataTransfer;
const files = dt && dt.files ? Array.from(dt.files) : [];
if (!files.length) return;
const fileNo = (document.getElementById('uploadFileNo')?.value || '').trim();
if (!fileNo) {
zoneEl.classList.add('border-red-400');
const fileNoErr = document.getElementById('uploadFileNoError');
if (fileNoErr) fileNoErr.classList.remove('hidden');
showAlert('Please enter a file number', 'warning');
return;
}
try { localStorage.setItem('docs_last_upload_file_no', fileNo); } catch (_) {}
setUploadingState(true);
await concurrentUploads(files, fileNo, 3);
setUploadingState(false);
loadUploadedDocuments();
});
}
function createUploadItem(file) {
const list = document.getElementById('uploadProgressList');
const item = document.createElement('div');
item.className = 'flex items-center justify-between p-3 border rounded-lg';
const name = document.createElement('div');
name.className = 'text-sm font-medium truncate max-w-[60%]';
name.textContent = file.name;
const right = document.createElement('div');
right.className = 'flex items-center gap-3';
const status = document.createElement('span');
status.className = 'text-xs text-neutral-500';
status.textContent = 'Queued';
const barWrap = document.createElement('div');
barWrap.className = 'w-40 h-2 bg-neutral-200 dark:bg-neutral-700 rounded overflow-hidden';
const bar = document.createElement('div');
bar.className = 'h-2 bg-primary-500 w-0 transition-all';
bar.style.width = '0%';
barWrap.appendChild(bar);
const cancelBtn = document.createElement('button');
cancelBtn.type = 'button';
cancelBtn.title = 'Cancel upload';
cancelBtn.className = 'px-2 py-1 border border-neutral-400 text-neutral-600 rounded hover:bg-neutral-100 text-xs';
cancelBtn.innerHTML = '<i class="fa-solid fa-xmark"></i>';
cancelBtn.disabled = true;
right.appendChild(status);
right.appendChild(barWrap);
right.appendChild(cancelBtn);
item.appendChild(name);
item.appendChild(right);
list.appendChild(item);
let abortFn = null;
cancelBtn.addEventListener('click', () => {
if (typeof abortFn === 'function') {
status.textContent = 'Cancelling…';
cancelBtn.disabled = true;
abortFn();
}
});
return { item, status, bar, cancelBtn, setAbort: (fn) => { abortFn = fn; cancelBtn.disabled = !fn; } };
}
function setUploadingState(isUploading) {
try {
const uploadBtn = document.getElementById('uploadBtn');
const dropZone = document.getElementById('uploadDropZone');
const indicator = document.getElementById('uploadingIndicator');
if (uploadBtn) uploadBtn.disabled = !!isUploading;
if (dropZone) dropZone.classList.toggle('opacity-50', !!isUploading);
if (dropZone) dropZone.classList.toggle('pointer-events-none', !!isUploading);
if (indicator) indicator.classList.toggle('hidden', !isUploading);
} catch (_) {}
}
async function concurrentUploads(files, fileNo, concurrency = 3) {
const queue = Array.from(files);
const active = new Set();
let completed = 0;
function startNext() {
if (queue.length === 0 || active.size >= concurrency) return null;
const file = queue.shift();
const ui = createUploadItem(file);
const controller = new AbortController();
ui.setAbort(() => controller.abort());
const task = (async () => {
try {
ui.status.textContent = 'Uploading…';
ui.bar.style.width = '25%';
const form = new FormData();
form.append('file', file);
await uploadWithAlerts(`/api/documents/upload/${encodeURIComponent(fileNo)}`, form, { alertTitle: 'Upload failed', extraOptions: { signal: controller.signal } });
ui.bar.style.width = '100%';
ui.status.textContent = 'Done';
ui.status.className = 'text-xs text-green-600';
} catch (err) {
if (err && (err.name === 'AbortError' || /aborted/i.test(String(err && err.message)))) {
ui.status.textContent = 'Canceled';
ui.status.className = 'text-xs text-yellow-600';
ui.bar.style.width = '100%';
ui.bar.classList.remove('bg-primary-500');
ui.bar.classList.add('bg-yellow-500');
} else {
ui.status.textContent = 'Failed';
ui.status.className = 'text-xs text-red-600';
ui.bar.style.width = '100%';
ui.bar.classList.remove('bg-primary-500');
ui.bar.classList.add('bg-red-500');
// Keep failed item visible and allow dismiss
try {
ui.item.classList.add('bg-red-50');
ui.item.classList.add('border');
ui.item.classList.add('border-red-300');
ui.cancelBtn.disabled = false;
ui.cancelBtn.title = 'Dismiss';
ui.cancelBtn.addEventListener('click', () => {
try {
ui.item.style.transition = 'opacity 250ms ease-in-out';
ui.item.style.opacity = '0';
setTimeout(() => ui.item.remove(), 260);
} catch (_) { ui.item.remove(); }
});
} catch (_) {}
}
} finally {
completed += 1;
active.delete(task);
ui.setAbort(null);
startNext();
}
})();
active.add(task);
return task;
}
// Kick off initial batch
const starters = [];
for (let i = 0; i < concurrency; i++) {
const t = startNext();
if (t) starters.push(t);
}
await Promise.allSettled(starters);
// Drain the rest
while (active.size > 0 || queue.length > 0) {
if (queue.length > 0) startNext();
await Promise.race(Array.from(active));
}
const anyFailed = Array.from(document.querySelectorAll('#uploadProgressList .text-red-600')).length > 0;
if (!anyFailed) {
showAlert('All uploads completed', 'success');
}
cleanupUploadProgress(true);
}
function cleanupUploadProgress(preserveFailures = true) {
try {
const list = document.getElementById('uploadProgressList');
if (!list) return;
const items = Array.from(list.children);
items.forEach((item) => {
try {
const statusEl = item.querySelector('span.text-xs');
const isFailed = preserveFailures && statusEl && statusEl.classList.contains('text-red-600');
if (isFailed) {
// Ensure a dismiss button exists
const hasDismiss = item.querySelector('button');
if (!hasDismiss) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'px-2 py-1 border border-neutral-400 text-neutral-600 rounded hover:bg-neutral-100 text-xs';
btn.title = 'Dismiss';
btn.innerHTML = '<i class="fa-solid fa-xmark"></i>';
btn.addEventListener('click', () => {
try {
item.style.transition = 'opacity 250ms ease-in-out';
item.style.opacity = '0';
setTimeout(() => item.remove(), 260);
} catch (_) { item.remove(); }
});
const right = item.querySelector('.flex.items-center.gap-3');
if (right) right.appendChild(btn);
}
return;
}
item.style.transition = 'opacity 250ms ease-in-out';
item.style.opacity = '0';
setTimeout(() => item.remove(), 260);
} catch (_) {}
});
} catch (_) {}
}
function displayTemplates(templates) {
const tbody = document.getElementById('templatesTableBody');
tbody.innerHTML = '';
@@ -954,6 +1365,168 @@ function showAlert(message, type = 'info') {
}
}
// Uploads UI
async function loadUploadedDocuments() {
try {
const fileNo = (document.getElementById('uploadFileNo')?.value || '').trim();
if (!fileNo) {
showAlert('Enter a file number to view uploads', 'warning');
return;
}
const resp = await window.http.wrappedFetch(`/api/documents/${encodeURIComponent(fileNo)}/uploaded`);
if (!resp.ok) {
const err = await window.http.toError(resp, 'Failed to load uploads');
const msg = window.http.formatAlert(err, 'Failed to load uploads');
showAlert(msg, 'danger');
return;
}
const docs = await resp.json();
displayUploadedDocuments(docs);
} catch (error) {
console.error('Error loading uploaded documents', error);
showAlert('Failed to load uploads', 'danger');
}
}
function displayUploadedDocuments(docs) {
const container = document.getElementById('uploadedDocuments');
if (!Array.isArray(docs) || docs.length === 0) {
container.innerHTML = '<p class="text-neutral-500">No uploads found for this file.</p>';
return;
}
const rows = docs.map((d) => `
<tr>
<td class="px-4 py-2">${d.id || ''}</td>
<td class="px-4 py-2">${d.filename || ''}</td>
<td class="px-4 py-2">${(d.type || '').split('/').pop()}</td>
<td class="px-4 py-2">${Number(d.size || 0).toLocaleString()} bytes</td>
<td class="px-4 py-2"><a href="/${d.path || ''}" target="_blank" class="text-primary-600 hover:underline">View</a></td>
<td class="px-4 py-2">
<button class="px-2 py-1 border border-cyan-600 text-cyan-600 rounded hover:bg-blue-100 mr-2" title="Edit description" onclick="openEditUploadModal(${JSON.stringify(String(d.id || ''))}, ${JSON.stringify(String(d.description || ''))})"><i class="fa-solid fa-pencil"></i></button>
<button class="px-2 py-1 border border-danger-600 text-danger-600 rounded hover:bg-red-100" title="Delete" onclick="deleteUploadedDocument(${JSON.stringify(String(d.id || ''))})"><i class="fa-solid fa-trash"></i></button>
</td>
</tr>
`).join('');
const html = `
<table class="w-full text-sm text-left border border-neutral-200 dark:border-neutral-700 rounded-lg overflow-hidden">
<thead>
<tr>
<th class="px-4 py-2">ID</th>
<th class="px-4 py-2">Name</th>
<th class="px-4 py-2">Type</th>
<th class="px-4 py-2">Size</th>
<th class="px-4 py-2">Link</th>
<th class="px-4 py-2">Actions</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
`;
if (window.setSafeHTML) { window.setSafeHTML(container, html); } else { container.innerHTML = html; }
}
async function handleUploadClick() {
const fileNo = (document.getElementById('uploadFileNo')?.value || '').trim();
const input = document.getElementById('uploadInput');
if (!fileNo) {
showAlert('Please enter a file number', 'warning');
const fileNoErr = document.getElementById('uploadFileNoError');
if (fileNoErr) fileNoErr.classList.remove('hidden');
return;
}
try { localStorage.setItem('docs_last_upload_file_no', fileNo); } catch (_) {}
if (!input || !input.files || input.files.length === 0) {
showAlert('Please choose a file to upload', 'warning');
const inputErr = document.getElementById('uploadInputError');
if (inputErr) inputErr.classList.remove('hidden');
return;
}
const form = new FormData();
form.append('file', input.files[0]);
setUploadingState(true);
try {
await uploadWithAlerts(`/api/documents/upload/${encodeURIComponent(fileNo)}`, form);
if (window.alerts && window.alerts.success) {
window.alerts.success('Upload completed', { duration: 3000 });
} else {
showAlert('Upload completed', 'success');
}
// refresh list
loadUploadedDocuments();
// clear chooser
input.value = '';
} catch (_) {
// Error already alerted by helper
} finally {
setUploadingState(false);
}
}
async function deleteUploadedDocument(docId) {
try {
if (!docId) {
showAlert('Invalid document id', 'warning');
return;
}
if (!confirm('Are you sure you want to delete this uploaded document?')) return;
const resp = await window.http.wrappedFetch(`/api/documents/uploaded/${encodeURIComponent(String(docId))}`, { method: 'DELETE' });
if (!resp.ok) {
const err = await window.http.toError(resp, 'Failed to delete upload');
const msg = window.http.formatAlert(err, 'Failed to delete upload');
showAlert(msg, 'danger');
return;
}
showAlert('Upload deleted successfully', 'success');
loadUploadedDocuments();
} catch (error) {
const msg = window.http && window.http.formatAlert && error instanceof Error
? window.http.formatAlert(error, 'Failed to delete upload')
: 'Failed to delete upload';
showAlert(msg, 'danger');
}
}
function openEditUploadModal(docId, currentDescription) {
try {
document.getElementById('editUploadId').value = String(docId || '');
document.getElementById('editUploadDescription').value = String(currentDescription || '');
openModal('editUploadModal');
} catch (error) {
showAlert('Unable to open editor', 'danger');
}
}
async function saveEditUpload() {
const docId = document.getElementById('editUploadId').value;
const description = document.getElementById('editUploadDescription').value;
if (!docId) {
showAlert('Invalid document id', 'warning');
return;
}
try {
const form = new FormData();
form.append('description', description || '');
const resp = await window.http.wrappedFetch(`/api/documents/uploaded/${encodeURIComponent(String(docId))}`, {
method: 'PUT',
body: form
});
if (!resp.ok) {
const err = await window.http.toError(resp, 'Failed to update description');
const msg = window.http.formatAlert(err, 'Failed to update description');
showAlert(msg, 'danger');
return;
}
showAlert('Description updated', 'success');
closeModal('editUploadModal');
loadUploadedDocuments();
} catch (error) {
const msg = window.http && window.http.formatAlert && error instanceof Error
? window.http.formatAlert(error, 'Failed to update description')
: 'Failed to update description';
showAlert(msg, 'danger');
}
}
// Lightweight client error logger specific to Documents page
async function logClientError({ message, action = null, error = null, extra = null }) {
try {
@@ -1185,6 +1758,12 @@ function openTab(evt, tabName) {
loadTemplates();
} else if (tabName === 'qdros') {
loadQdros();
} else if (tabName === 'generated') {
const fileNoInput = document.getElementById('uploadFileNo');
const uploadInput = document.getElementById('uploadInput');
if (fileNoInput && (fileNoInput.value || '').trim() && uploadInput) {
uploadInput.focus();
}
}
}
</script>