const area = document.getElementById('upload-area'); const inputFolder = document.getElementById('file-input'); const inputFiles = document.getElementById('file-input-files'); const overlay = document.getElementById('progress-overlay'); const uploadBar = document.getElementById('upload-bar'); const uploadText = document.getElementById('upload-text'); const uploadDetail = document.getElementById('upload-detail'); const processText = document.getElementById('process-text'); const processDetail = document.getElementById('process-detail'); const processLabel = document.getElementById('process-label'); const processBar = document.getElementById('process-bar'); const processTrack = document.getElementById('process-track'); const T = JSON.parse(document.getElementById('upload-i18n').textContent); const dossierGUID = T.dossier; let totalFileCount = 0; area.onclick = e => { if (e.shiftKey) { inputFolder.click(); } else { inputFiles.click(); } }; area.ondragover = e => { e.preventDefault(); area.classList.add('dragover'); }; area.ondragleave = () => area.classList.remove('dragover'); area.ondrop = async e => { e.preventDefault(); area.classList.remove('dragover'); const items = e.dataTransfer.items; if (items && items.length > 0) { overlay.style.display = 'flex'; uploadBar.style.width = '0%'; uploadText.textContent = T.reading_files; uploadDetail.textContent = ''; processText.textContent = ''; processDetail.textContent = ''; processLabel.style.display = 'none'; const files = await getAllFiles(items); if (files.length > 0) handleFiles(files); else overlay.style.display = 'none'; } }; inputFolder.onchange = e => handleFiles(Array.from(e.target.files)); inputFiles.onchange = e => handleFiles(Array.from(e.target.files)); async function getAllFiles(items) { const files = []; const entries = []; for (let i = 0; i < items.length; i++) { const entry = items[i].webkitGetAsEntry ? items[i].webkitGetAsEntry() : null; if (entry) entries.push(entry); } async function readEntry(entry, path) { if (entry.isFile) { return new Promise(resolve => { entry.file(f => { Object.defineProperty(f, 'relativePath', { value: path + f.name }); files.push(f); resolve(); }); }); } else if (entry.isDirectory) { const reader = entry.createReader(); const readAll = () => new Promise(resolve => { reader.readEntries(async entries => { if (entries.length === 0) { resolve(); return; } for (const e of entries) await readEntry(e, path + entry.name + '/'); await readAll(); resolve(); }); }); await readAll(); } } for (const entry of entries) await readEntry(entry, ''); return files; } function submitPaste() { const text = document.getElementById('paste-input').value.trim(); if (!text) return; const blob = new Blob([text], { type: 'text/plain' }); const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); const file = new File([blob], 'paste-' + ts + '.txt', { type: 'text/plain' }); handleFiles([file]); } async function handleFiles(files) { if (!files.length) return; const category = ''; overlay.style.display = 'flex'; uploadBar.style.width = '0%'; uploadText.textContent = ''; processText.textContent = ''; processDetail.textContent = ''; processLabel.style.display = 'none'; let geneticsFileId = null; let pdfFileIds = []; totalFileCount = files.length; let completed = 0; const concurrency = 10; async function uploadOne(file) { const name = file.relativePath || file.webkitRelativePath || file.name; const form = new FormData(); form.append('file', file); form.append('path', name); form.append('category', category); try { const resp = await fetch('/dossier/' + dossierGUID + '/upload', { method: 'POST', body: form }); const data = await resp.json(); if (category === 'genetics' && data.id) geneticsFileId = data.id; if (category === 'pdf' && data.id) pdfFileIds.push(data.id); } catch (e) { console.error('Upload failed:', e); } completed++; uploadText.textContent = completed + ' / ' + files.length; uploadBar.style.width = (completed / files.length * 100) + '%'; } const queue = [...files]; const workers = []; for (let w = 0; w < Math.min(concurrency, queue.length); w++) { workers.push((async () => { while (queue.length > 0) { await uploadOne(queue.shift()); } })()); } await Promise.all(workers); if (geneticsFileId) { processText.textContent = T.processing_genetics; processDetail.textContent = T.analyzing_variants; await pollStatus(geneticsFileId); overlay.style.display = 'none'; location.reload(); return; } if (pdfFileIds.length > 0) { processText.textContent = T.processing_documents; processDetail.textContent = T.extracting_data; processLabel.style.display = ''; await Promise.all(pdfFileIds.map(id => pollStatus(id))); overlay.style.display = 'none'; location.reload(); return; } if (category !== 'genetics') { fetch('/dossier/' + dossierGUID + '/process-imaging', { method: 'POST' }); await pollProcessing(); } overlay.style.display = 'none'; location.reload(); } async function pollStatus(fileId) { const maxAttempts = 60; for (let i = 0; i < maxAttempts; i++) { try { const resp = await fetch('/dossier/' + dossierGUID + '/files/' + fileId + '/status'); const data = await resp.json(); if (data.status === 'completed' || data.status === 'processed') { showToast(T.processing_complete, 'success'); return; } else if (data.status === 'failed') { showToast(T.processing_failed + ' ' + data.details, 'error'); return; } } catch (e) { console.error('Status check failed:', e); } await new Promise(r => setTimeout(r, 1000)); } showToast(T.processing_timeout, 'error'); } function showToast(message, type) { const toast = document.createElement('div'); toast.textContent = message; toast.style.cssText = 'position:fixed;bottom:20px;right:20px;padding:16px 24px;border-radius:8px;color:white;font-weight:500;z-index:9999'; toast.style.background = type === 'success' ? '#10b981' : '#ef4444'; document.body.appendChild(toast); setTimeout(() => toast.remove(), 5000); } function deleteFile(fileId) { if (!confirm(T.confirm_delete)) return; fetch('/dossier/' + dossierGUID + '/files/' + fileId + '/delete', { method: 'POST' }).then(() => location.reload()); } function undoImport(fileId) { if (!confirm(T.confirm_undo)) return; fetch('/dossier/' + dossierGUID + '/files/' + fileId + '/undo', { method: 'POST' }) .then(resp => resp.json()) .then(data => { showToast(data.status === 'ok' ? T.import_undone : T.undo_failed, data.status === 'ok' ? 'success' : 'error'); location.reload(); }) .catch(() => { showToast(T.undo_failed, 'error'); }); } function reprocessFile(fileId) { fetch('/dossier/' + dossierGUID + '/files/' + fileId + '/reprocess', { method: 'POST' }) .then(resp => resp.json()) .then(data => { if (data.status === 'ok') location.reload(); }) .catch(() => { showToast(T.undo_failed, 'error'); }); } async function pollProcessing() { while (true) { await new Promise(r => setTimeout(r, 300)); let s; try { const resp = await fetch('/dossier/' + dossierGUID + '/process-status'); s = await resp.json(); } catch (e) { continue; } if (s.stage === 'idle' || s.stage === 'starting') continue; processLabel.style.display = ''; processTrack.style.display = ''; const total = totalFileCount || s.total || 1; if (s.stage === 'decrypting') { processText.textContent = T.preparing + ' ' + (s.processed || 0) + ' / ' + total; processDetail.textContent = ''; processBar.style.width = ((s.processed || 0) / total * 100) + '%'; } else if (s.stage === 'importing') { processText.textContent = T.importing + ' ' + (s.processed || 0) + ' / ' + total; const study = (s.study || '').replace(/&/g, '&').replace(/"/g, '"').trim(); const series = (s.series || '').replace(/&/g, '&').replace(/"/g, '"').trim(); processDetail.textContent = study + ' \u2014 ' + series; processBar.style.width = ((s.processed || 0) / total * 100) + '%'; } else if (s.stage === 'importing_json') { processText.textContent = T.analyzing + ' ' + (s.processed || 0) + ' / ' + (s.total || total); processDetail.textContent = T.extracting_labs; processBar.style.width = ((s.processed || 0) / (s.total || total) * 100) + '%'; } else if (s.stage === 'normalizing') { const nt = s.total || 1; processText.textContent = T.normalizing + ' ' + (s.processed || 0) + ' / ' + nt; processDetail.textContent = T.mapping_loinc; processBar.style.width = ((s.processed || 0) / nt * 100) + '%'; } else if (s.stage === 'done') { if (s.error) showToast(T.import_error + ' ' + s.error, 'error'); else if (s.slices > 0) showToast(T.imported + ' ' + s.studies + ' studies, ' + s.series_count + ' series, ' + s.slices + ' slices', 'success'); else if (s.json_imported > 0) showToast(T.imported + ' ' + s.json_imported + ' files', 'success'); return; } } }