coming together
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user