234 lines
9.1 KiB
JavaScript
234 lines
9.1 KiB
JavaScript
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;
|
|
}
|
|
}
|
|
}
|