Add patient grouping and selection functionality to user uploads
This commit is contained in:
@@ -17,7 +17,97 @@
|
||||
|
||||
{% if series_list %}
|
||||
<div id="series-list">
|
||||
{% if grouped_series %}
|
||||
{% if grouped_patients %}
|
||||
{% for patient in grouped_patients %}
|
||||
<div class="patient-block mb-4 p-3 border rounded" style="background: rgba(255,255,255,0.015); border-color: rgba(255,255,255,0.08) !important;">
|
||||
<div class="patient-header d-flex flex-wrap align-items-center justify-content-between pb-2 mb-3 border-bottom border-secondary">
|
||||
<span>
|
||||
<i class="bi bi-person-badge me-2 text-info"></i>
|
||||
<strong>Patient ID:</strong> <span class="badge bg-secondary font-monospace">{{ patient.patient_id }}</span>
|
||||
</span>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary select-all-patient-btn" title="Select all series for this patient">Select Patient Series</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary deselect-all-patient-btn" title="Deselect all series for this patient">Deselect all</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% for study_uid, study in patient.studies %}
|
||||
<details class="study-block {% if study.partial_imported_uids %}study-block--partial-import{% endif %}" open>
|
||||
<summary>
|
||||
<strong>{{ study.description|default:"(no description)" }}</strong>
|
||||
<small class="text-muted"> ({{ study_uid }}) — {{ study.series|length }} series</small>
|
||||
<span class="ms-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary select-all-btn" title="Select all in study">Select all</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary deselect-all-btn" title="Deselect all in study">Deselect all</button>
|
||||
</span>
|
||||
</summary>
|
||||
{% if study.partial_imported_uids %}
|
||||
<div class="alert alert-warning py-2 mt-2 mb-2 d-flex flex-wrap align-items-center justify-content-between gap-2">
|
||||
<div>
|
||||
<strong>Partial import detected:</strong>
|
||||
{{ study.partial_imported_count }} series in this study already exist.
|
||||
The highlighted rows can finish importing the remaining images.
|
||||
{% if study.matched_existing_series %}
|
||||
<div class="small mt-1">
|
||||
Existing series in Atlas:
|
||||
{% for existing_series in study.matched_existing_series %}
|
||||
<a href="{% url 'atlas:series_detail' existing_series.pk %}">{{ existing_series }}</a>{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-warning import-partial-series-btn"
|
||||
data-series-uids="{{ study.partial_imported_uids|join:',' }}"
|
||||
{% if study.preferred_case_id %}data-case-id="{{ study.preferred_case_id }}"{% endif %}
|
||||
>Finish these imports</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if study.import_suggestions %}
|
||||
<div class="alert alert-info py-2 mt-2 mb-2">
|
||||
<strong>Study already imported:</strong>
|
||||
{% for suggestion in study.import_suggestions %}
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 mt-1">
|
||||
<span>
|
||||
Case:
|
||||
<a href="{% url 'atlas:case_detail' suggestion.case.pk %}">{{ suggestion.case }}</a>
|
||||
({{ suggestion.already_imported_count }} series already imported)
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-primary import-missing-series-btn"
|
||||
data-case-id="{{ suggestion.case.pk }}"
|
||||
data-series-uids="{{ suggestion.missing_uids|join:',' }}"
|
||||
>
|
||||
Import remaining {{ suggestion.missing_uids|length }} series
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<ul class="study-series-list">
|
||||
{% for series, n, tags, date in study.series %}
|
||||
{% if series in study.partial_imported_uids %}
|
||||
{% if case %}
|
||||
{% include 'atlas/partials/_series_item.html' with show_tags_link=True series_is_partial=True series_item_classes='series-item--partial-import' import_case_id=case.pk %}
|
||||
{% else %}
|
||||
{% include 'atlas/partials/_series_item.html' with show_tags_link=True series_is_partial=True series_item_classes='series-item--partial-import' import_case_id=study.preferred_case_id %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if case %}
|
||||
{% include 'atlas/partials/_series_item.html' with show_tags_link=True series_is_partial=False series_item_classes='' import_case_id=case.pk %}
|
||||
{% else %}
|
||||
{% include 'atlas/partials/_series_item.html' with show_tags_link=True series_is_partial=False series_item_classes='' %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% elif grouped_series %}
|
||||
{% for study_uid, study in grouped_series %}
|
||||
<details class="study-block {% if study.partial_imported_uids %}study-block--partial-import{% endif %}" open>
|
||||
<summary>
|
||||
@@ -233,7 +323,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="caseSeriesModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal fade" id="caseSeriesModal" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@@ -531,7 +621,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Select / Deselect all series within a study block
|
||||
// Select / Deselect all series within a study block or patient block
|
||||
document.addEventListener('click', function (e) {
|
||||
const target = e.target;
|
||||
if (target.matches('.select-all-btn')) {
|
||||
@@ -562,6 +652,34 @@
|
||||
});
|
||||
if (typeof updateSelectionCount === 'function') updateSelectionCount();
|
||||
}
|
||||
if (target.matches('.select-all-patient-btn')) {
|
||||
const patientBlock = target.closest('.patient-block');
|
||||
if (!patientBlock) return;
|
||||
const inputs = patientBlock.querySelectorAll('input[name="selection"]');
|
||||
inputs.forEach(cb => {
|
||||
if (!cb.disabled) {
|
||||
cb.checked = true;
|
||||
const li = cb.closest('.series-item');
|
||||
if (li) li.classList.add('selected');
|
||||
cb.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
});
|
||||
if (typeof updateSelectionCount === 'function') updateSelectionCount();
|
||||
}
|
||||
if (target.matches('.deselect-all-patient-btn')) {
|
||||
const patientBlock = target.closest('.patient-block');
|
||||
if (!patientBlock) return;
|
||||
const inputs = patientBlock.querySelectorAll('input[name="selection"]');
|
||||
inputs.forEach(cb => {
|
||||
if (!cb.disabled) {
|
||||
cb.checked = false;
|
||||
const li = cb.closest('.series-item');
|
||||
if (li) li.classList.remove('selected');
|
||||
cb.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
});
|
||||
if (typeof updateSelectionCount === 'function') updateSelectionCount();
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for `case:selected` events dispatched by the case-search widget and perform the import
|
||||
@@ -884,6 +1002,10 @@
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.select2-container {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
#series-list li {
|
||||
}
|
||||
#series-list .selected {
|
||||
|
||||
Reference in New Issue
Block a user