inou/portal/static/upload.js

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(/&amp;/g, '&').replace(/&quot;/g, '"').trim();
const series = (s.series || '').replace(/&amp;/g, '&').replace(/&quot;/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;
}
}
}