feat: Add anonymisation issue summary rendering for flagged series in upload process
This commit is contained in:
@@ -71,6 +71,7 @@
|
|||||||
|
|
||||||
<div class="alert alert-warning d-none anon-alert" role="alert">
|
<div class="alert alert-warning d-none anon-alert" role="alert">
|
||||||
<p class="mb-2">Potential anonymisation issues were detected. Flagged series are highlighted below.</p>
|
<p class="mb-2">Potential anonymisation issues were detected. Flagged series are highlighted below.</p>
|
||||||
|
<div id="anon-issue-summary" class="small"></div>
|
||||||
<details>
|
<details>
|
||||||
<summary>Override anonymisation check</summary>
|
<summary>Override anonymisation check</summary>
|
||||||
<p class="mt-2 mb-2">Only override if you are certain the warnings are false positives.</p>
|
<p class="mt-2 mb-2">Only override if you are certain the warnings are false positives.</p>
|
||||||
@@ -210,6 +211,12 @@
|
|||||||
return String(value).trim();
|
return String(value).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.textContent = value === undefined || value === null ? "" : String(value);
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
async function parseDicomMetadata(file) {
|
async function parseDicomMetadata(file) {
|
||||||
try {
|
try {
|
||||||
const buffer = await file.arrayBuffer();
|
const buffer = await file.arrayBuffer();
|
||||||
@@ -319,6 +326,108 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildAnonymisationIssueSummary() {
|
||||||
|
const selectedRecords = window.uploadPreview.records.filter((record) => record.include);
|
||||||
|
if (!selectedRecords.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const seriesMap = {};
|
||||||
|
let tagWarningFiles = 0;
|
||||||
|
let ocrWarningFiles = 0;
|
||||||
|
|
||||||
|
for (const record of selectedRecords) {
|
||||||
|
const hasTagWarnings = record.tagWarnings.length > 0;
|
||||||
|
const hasOcrWarning = !!record.ocrWarning;
|
||||||
|
if (!hasTagWarnings && !hasOcrWarning) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!seriesMap[record.seriesUid]) {
|
||||||
|
seriesMap[record.seriesUid] = {
|
||||||
|
uid: record.seriesUid,
|
||||||
|
description: record.seriesDescription || "(no series description)",
|
||||||
|
tagFiles: 0,
|
||||||
|
ocrFiles: 0,
|
||||||
|
details: new Set(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = seriesMap[record.seriesUid];
|
||||||
|
if (hasTagWarnings) {
|
||||||
|
tagWarningFiles += 1;
|
||||||
|
entry.tagFiles += 1;
|
||||||
|
record.tagWarnings.forEach((warning) => entry.details.add(warning));
|
||||||
|
}
|
||||||
|
if (hasOcrWarning) {
|
||||||
|
ocrWarningFiles += 1;
|
||||||
|
entry.ocrFiles += 1;
|
||||||
|
entry.details.add("Possible burned-in patient text detected by OCR");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const seriesIssues = Object.values(seriesMap);
|
||||||
|
if (!seriesIssues.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
seriesIssues.sort((a, b) => {
|
||||||
|
const aScore = a.tagFiles + a.ocrFiles;
|
||||||
|
const bScore = b.tagFiles + b.ocrFiles;
|
||||||
|
return bScore - aScore;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedFiles: selectedRecords.length,
|
||||||
|
flaggedSeries: seriesIssues.length,
|
||||||
|
tagWarningFiles,
|
||||||
|
ocrWarningFiles,
|
||||||
|
issues: seriesIssues,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAnonymisationIssueSummary() {
|
||||||
|
const summary = document.getElementById("anon-issue-summary");
|
||||||
|
if (!summary) return;
|
||||||
|
|
||||||
|
const data = buildAnonymisationIssueSummary();
|
||||||
|
if (!data) {
|
||||||
|
summary.innerHTML = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const issueItems = data.issues.slice(0, 8).map((issue) => {
|
||||||
|
const issueBits = [];
|
||||||
|
if (issue.tagFiles) issueBits.push(`Tag warnings in ${issue.tagFiles} file${issue.tagFiles === 1 ? "" : "s"}`);
|
||||||
|
if (issue.ocrFiles) issueBits.push(`OCR warnings in ${issue.ocrFiles} file${issue.ocrFiles === 1 ? "" : "s"}`);
|
||||||
|
const detailPreview = Array.from(issue.details).slice(0, 2).join("; ");
|
||||||
|
|
||||||
|
return `
|
||||||
|
<li>
|
||||||
|
<strong>${escapeHtml(issue.description)}</strong>
|
||||||
|
<span class="text-muted">(${escapeHtml(issue.uid)})</span><br>
|
||||||
|
${escapeHtml(issueBits.join(" \u00b7 "))}
|
||||||
|
${detailPreview ? `<br><span class="text-muted">${escapeHtml(detailPreview)}</span>` : ""}
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
}).join("");
|
||||||
|
|
||||||
|
const extraCount = Math.max(0, data.issues.length - 8);
|
||||||
|
const extraLine = extraCount ? `<p class="mb-0 mt-2 text-muted">+ ${extraCount} more flagged series</p>` : "";
|
||||||
|
|
||||||
|
summary.innerHTML = `
|
||||||
|
<p class="mb-2">
|
||||||
|
<strong>Summary:</strong>
|
||||||
|
${data.flaggedSeries} flagged series across ${data.selectedFiles} selected files.
|
||||||
|
Tag warnings: ${data.tagWarningFiles}. OCR warnings: ${data.ocrWarningFiles}.
|
||||||
|
</p>
|
||||||
|
<ul class="mb-0 ps-3">
|
||||||
|
${issueItems}
|
||||||
|
</ul>
|
||||||
|
${extraLine}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function updateUploadButtonState() {
|
function updateUploadButtonState() {
|
||||||
const uploadButton = document.getElementById("uploadButton");
|
const uploadButton = document.getElementById("uploadButton");
|
||||||
const anonAlert = document.querySelector(".anon-alert");
|
const anonAlert = document.querySelector(".anon-alert");
|
||||||
@@ -329,6 +438,8 @@
|
|||||||
const hasPendingDuplicateChecks = duplicatesEnabled && hasSelected &&
|
const hasPendingDuplicateChecks = duplicatesEnabled && hasSelected &&
|
||||||
window.uploadPreview.records.some((r) => r.include && !r.duplicateChecked);
|
window.uploadPreview.records.some((r) => r.include && !r.duplicateChecked);
|
||||||
|
|
||||||
|
renderAnonymisationIssueSummary();
|
||||||
|
|
||||||
if (!hasSelected) {
|
if (!hasSelected) {
|
||||||
uploadButton.disabled = true;
|
uploadButton.disabled = true;
|
||||||
anonAlert.classList.add("d-none");
|
anonAlert.classList.add("d-none");
|
||||||
|
|||||||
Reference in New Issue
Block a user