Add case selection handling and import functionality in user uploads
This commit is contained in:
@@ -91,6 +91,11 @@
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
.quick-edit-wrap .quick-edit-button:hover { opacity: 1; }
|
.quick-edit-wrap .quick-edit-button:hover { opacity: 1; }
|
||||||
|
|
||||||
|
/* Normal group visual style */
|
||||||
|
.normal-group { border: 1px solid rgba(13,110,253,0.15); background: #f8fbff; padding: .75rem; border-radius: .375rem; margin-bottom: .75rem; }
|
||||||
|
.normal-group .normal-heading { font-weight: 600; color: #0d6efd; margin-bottom: .5rem; }
|
||||||
|
.normal-group .badge { margin-right: .25rem; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="atlas {% if case.scrapped %}atlas-scrapped{% endif %}">
|
<div class="atlas {% if case.scrapped %}atlas-scrapped{% endif %}">
|
||||||
|
|||||||
@@ -185,4 +185,38 @@
|
|||||||
} catch (e) { console.warn('collection warning persistence error', e); }
|
} catch (e) { console.warn('collection warning persistence error', e); }
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<script>
|
||||||
|
// Listen for case selection events from a case-search-widget included in this page/modal
|
||||||
|
document.body.addEventListener('case:selected', async function(e) {
|
||||||
|
try {
|
||||||
|
const detail = e.detail || {};
|
||||||
|
const casePk = detail.casePk;
|
||||||
|
const caseTitle = detail.caseTitle || ('#' + casePk);
|
||||||
|
if (!casePk) return;
|
||||||
|
|
||||||
|
if (!window.confirm(`Add case ${caseTitle} to collection {{ collection.name }}?`)) return;
|
||||||
|
|
||||||
|
const url = "{% url 'atlas:add_case_to_collection' collection.pk %}";
|
||||||
|
const form = new URLSearchParams();
|
||||||
|
form.append('case', casePk);
|
||||||
|
const csrfToken = '{{ csrf_token }}';
|
||||||
|
const resp = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRFToken': csrfToken,
|
||||||
|
},
|
||||||
|
body: form.toString(),
|
||||||
|
credentials: 'same-origin'
|
||||||
|
});
|
||||||
|
const html = await resp.text();
|
||||||
|
// Append the returned case item into the list
|
||||||
|
const list = document.getElementById('full-question-list');
|
||||||
|
if (list) list.insertAdjacentHTML('beforeend', html);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Add case to collection failed', err);
|
||||||
|
alert('Failed to add case to collection — check console.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -46,3 +46,37 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
try {
|
||||||
|
var targetId = '{{ target_id|default:"case-search-results" }}';
|
||||||
|
var inputId = '{{ input_id|default:"case-search-input" }}';
|
||||||
|
var inputEl = document.getElementById(inputId);
|
||||||
|
var container = inputEl ? inputEl.closest('.case-search-widget') : document.querySelector('.case-search-widget');
|
||||||
|
var targetEl = document.getElementById(targetId);
|
||||||
|
if (!container || !targetEl) return;
|
||||||
|
|
||||||
|
// Delegate clicks on result items and dispatch a scoped custom event
|
||||||
|
targetEl.addEventListener('click', function(e){
|
||||||
|
var item = e.target.closest('.list-group-item');
|
||||||
|
if (!item || !targetEl.contains(item)) return;
|
||||||
|
// Ignore clicks on interactive elements inside the item (links, buttons, forms, inputs)
|
||||||
|
if (e.target.closest('a,button,form,input')) return;
|
||||||
|
|
||||||
|
var casePk = item.getAttribute('data-case-pk');
|
||||||
|
if (!casePk) {
|
||||||
|
var href = item.getAttribute('href') || (item.querySelector('a') ? item.querySelector('a').getAttribute('href') : '');
|
||||||
|
var m = href.match(/case\/(\d+)\/?$/);
|
||||||
|
if (m) casePk = m[1];
|
||||||
|
}
|
||||||
|
var caseTitleEl = item.querySelector('h6');
|
||||||
|
var caseTitle = caseTitleEl ? caseTitleEl.textContent.trim() : (casePk ? ('#' + casePk) : null);
|
||||||
|
if (!casePk) return;
|
||||||
|
|
||||||
|
var ev = new CustomEvent('case:selected', { detail: { casePk: casePk, caseTitle: caseTitle }, bubbles: true });
|
||||||
|
container.dispatchEvent(ev);
|
||||||
|
}, false);
|
||||||
|
} catch (e) { console.error('case search widget selection init error', e); }
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -322,84 +322,64 @@
|
|||||||
const toImport = checkboxes.length
|
const toImport = checkboxes.length
|
||||||
? checkboxes.filter(cb => !cb.disabled)
|
? checkboxes.filter(cb => !cb.disabled)
|
||||||
: selectableCheckboxes;
|
: selectableCheckboxes;
|
||||||
if (toImport.length === 0) {
|
<script>
|
||||||
alert("Please select at least one series to import (not already imported).");
|
// Listen for `case:selected` events dispatched by the case-search widget and perform the import
|
||||||
return;
|
document.body.addEventListener('case:selected', async function (e) {
|
||||||
}
|
try {
|
||||||
const statusDiv = document.getElementById('import-status');
|
const detail = e.detail || {};
|
||||||
statusDiv.innerHTML = '';
|
const casePk = detail.casePk;
|
||||||
const orderSeries = document.querySelector('input[name="order-series"]:checked').value;
|
const caseTitle = detail.caseTitle || `#${casePk}`;
|
||||||
const csrfToken = '{{ csrf_token }}';
|
if (!casePk) {
|
||||||
{% if case %}
|
console.error('case:selected without casePk');
|
||||||
const importUrl = "{% url 'api-1:import_dicoms_case' case.id %}";
|
return;
|
||||||
{% else %}
|
|
||||||
const importUrl = "{% url 'api-1:import_dicoms' %}";
|
|
||||||
{% endif %}
|
|
||||||
for (let i = 0; i < toImport.length; i++) {
|
|
||||||
const seriesId = toImport[i].value;
|
|
||||||
statusDiv.innerHTML += `<div id="import-status-${seriesId}">Importing series ${seriesId}...</div>`;
|
|
||||||
try {
|
|
||||||
const response = await fetch(importUrl, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
"X-CSRFToken": csrfToken,
|
|
||||||
},
|
|
||||||
body: `selection=${encodeURIComponent(seriesId)}&order-series=${encodeURIComponent(orderSeries)}`
|
|
||||||
});
|
|
||||||
const text = await response.text();
|
|
||||||
document.getElementById(`import-status-${seriesId}`).innerHTML = `Series ${seriesId}: ${text}`;
|
|
||||||
// Mark as imported in the UI and make unselectable
|
|
||||||
const li = document.querySelector(`#series-list input[value="${seriesId}"]`)?.closest('li');
|
|
||||||
if (li && !li.classList.contains('imported')) {
|
|
||||||
li.classList.add('imported');
|
|
||||||
// Add badge if not already present
|
|
||||||
if (!li.querySelector('.badge.bg-success')) {
|
|
||||||
const badge = document.createElement('span');
|
|
||||||
badge.className = 'badge bg-success ms-2';
|
|
||||||
badge.textContent = 'Imported';
|
|
||||||
li.appendChild(badge);
|
|
||||||
}
|
}
|
||||||
// Disable checkbox and prevent further selection
|
|
||||||
const checkbox = li.querySelector('input[name="selection"]');
|
|
||||||
if (checkbox) {
|
|
||||||
checkbox.disabled = true;
|
|
||||||
checkbox.checked = false;
|
|
||||||
}
|
|
||||||
// Remove click handler for selection
|
|
||||||
li.querySelector('span').onclick = null;
|
|
||||||
li.style.pointerEvents = "none";
|
|
||||||
li.style.opacity = "0.7";
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
document.getElementById(`import-status-${seriesId}`).innerHTML = `Series ${seriesId}: <span style="color:red;">Failed</span>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
statusDiv.innerHTML += "<div>All done.</div>";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
// Gather selected series
|
||||||
// Select / Deselect all series within a study block (JS-driven)
|
const checkboxes = Array.from(document.querySelectorAll('input[name="selection"]:checked'));
|
||||||
document.addEventListener('click', function (e) {
|
const allCheckboxes = Array.from(document.querySelectorAll('input[name="selection"]'));
|
||||||
const target = e.target;
|
const selectable = allCheckboxes.filter(cb => !cb.disabled).map(cb => cb.value);
|
||||||
if (target.matches('.select-all-btn')) {
|
const selectionList = checkboxes.length ? checkboxes.map(cb => cb.value) : selectable;
|
||||||
const detail = target.closest('.study-block');
|
if (!selectionList.length) {
|
||||||
if (!detail) return;
|
alert('Please select at least one series to import into the case.');
|
||||||
const inputs = detail.querySelectorAll('input[name="selection"]');
|
return;
|
||||||
inputs.forEach(cb => {
|
}
|
||||||
if (!cb.disabled) {
|
|
||||||
cb.checked = true;
|
const confirmMsg = `Import ${selectionList.length} series into case "${caseTitle}" (ID ${casePk})?`;
|
||||||
const li = cb.closest('li');
|
if (!window.confirm(confirmMsg)) return;
|
||||||
if (li) li.classList.add('selected');
|
|
||||||
cb.dispatchEvent(new Event('change', { bubbles: true }));
|
// Build import URL and POST
|
||||||
}
|
const importTemplate = "{% url 'api-1:import_dicoms_case' 0 %}";
|
||||||
});
|
const importUrl = importTemplate.replace(/0\/?$/, casePk + '/');
|
||||||
if (typeof updateSelectionCount === 'function') updateSelectionCount();
|
const form = new URLSearchParams();
|
||||||
}
|
for (const s of selectionList) form.append('selection', s);
|
||||||
if (target.matches('.deselect-all-btn')) {
|
const orderSeriesInput = document.querySelector('input[name="order-series"]:checked');
|
||||||
const detail = target.closest('.study-block');
|
if (orderSeriesInput) form.append('order-series', orderSeriesInput.value);
|
||||||
|
const csrfToken = '{{ csrf_token }}';
|
||||||
|
|
||||||
|
const resp = await fetch(importUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRFToken': csrfToken,
|
||||||
|
},
|
||||||
|
body: form.toString(),
|
||||||
|
credentials: 'same-origin'
|
||||||
|
});
|
||||||
|
const text = await resp.text();
|
||||||
|
// Close modal and show result in import-status
|
||||||
|
const modalEl = document.getElementById('caseSelectModal');
|
||||||
|
if (modalEl && window.bootstrap && bootstrap.Modal) {
|
||||||
|
bootstrap.Modal.getInstance(modalEl)?.hide();
|
||||||
|
}
|
||||||
|
const statusDiv = document.getElementById('import-status');
|
||||||
|
if (statusDiv) statusDiv.innerHTML = `<div>Imported into case ${casePk}: ${text}</div>`;
|
||||||
|
setTimeout(function(){ location.reload(); }, 1200);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Import handler error', err);
|
||||||
|
alert('Import failed — check console for details.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
if (!detail) return;
|
if (!detail) return;
|
||||||
const inputs = detail.querySelectorAll('input[name="selection"]');
|
const inputs = detail.querySelectorAll('input[name="selection"]');
|
||||||
inputs.forEach(cb => {
|
inputs.forEach(cb => {
|
||||||
|
|||||||
Reference in New Issue
Block a user