feat: Enhance linked cases overview with detailed case information and navigation, and add linked case card partial
This commit is contained in:
@@ -493,6 +493,60 @@
|
||||
Move selected series
|
||||
</button>
|
||||
</form>
|
||||
<script>
|
||||
(function () {
|
||||
if (window.__moveSeriesCasePickerInit) return;
|
||||
window.__moveSeriesCasePickerInit = true;
|
||||
|
||||
function syncMoveSubmit() {
|
||||
const form = document.getElementById('move-selected-series-form');
|
||||
const hiddenInput = form ? form.querySelector("[name='destination_case']") : null;
|
||||
const submitBtn = document.getElementById('move-selected-series-submit');
|
||||
if (!hiddenInput || !submitBtn) return;
|
||||
submitBtn.disabled = !hiddenInput.value;
|
||||
}
|
||||
|
||||
function selectMoveDestination(casePk, caseTitle) {
|
||||
const form = document.getElementById('move-selected-series-form');
|
||||
const hiddenInput = form ? form.querySelector("[name='destination_case']") : null;
|
||||
const label = document.getElementById('selected-move-case');
|
||||
const results = document.getElementById('move-series-case-search-results');
|
||||
if (!hiddenInput || !casePk) return;
|
||||
|
||||
hiddenInput.value = casePk;
|
||||
if (label) {
|
||||
label.innerHTML = 'Selected destination: <strong>' + caseTitle + ' (' + casePk + ')</strong>';
|
||||
label.classList.remove('text-muted');
|
||||
}
|
||||
if (results) {
|
||||
results.querySelectorAll('.list-group-item[data-case-pk]').forEach(function (item) {
|
||||
item.classList.toggle('active', String(item.getAttribute('data-case-pk')) === String(casePk));
|
||||
});
|
||||
}
|
||||
syncMoveSubmit();
|
||||
}
|
||||
|
||||
document.body.addEventListener('click', function (e) {
|
||||
const item = e.target.closest('#move-series-case-search-results .list-group-item[data-case-pk]');
|
||||
if (!item) return;
|
||||
if (e.target.closest('a,button,form,input')) return;
|
||||
const casePk = item.getAttribute('data-case-pk');
|
||||
const titleEl = item.querySelector('h6');
|
||||
const caseTitle = titleEl ? titleEl.textContent.trim() : ('Case #' + casePk);
|
||||
selectMoveDestination(casePk, caseTitle);
|
||||
});
|
||||
|
||||
document.body.addEventListener('case:selected', function (e) {
|
||||
const detail = e.detail || {};
|
||||
if (!detail.casePk) return;
|
||||
const caseTitle = detail.caseTitle || ('Case #' + detail.casePk);
|
||||
selectMoveDestination(detail.casePk, caseTitle);
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', syncMoveSubmit);
|
||||
syncMoveSubmit();
|
||||
})();
|
||||
</script>
|
||||
{% else %}
|
||||
<div class="text-muted small mt-2">Case move options are unavailable in this view.</div>
|
||||
{% endif %}
|
||||
@@ -881,12 +935,15 @@
|
||||
});
|
||||
|
||||
// On submit, update the hidden inputs to match the new order
|
||||
document.getElementById("reorder-series-form").addEventListener("submit", function(e) {
|
||||
const lis = document.querynelectorAll("#series-reorder-list li");
|
||||
lis.forEach((li, idx) => {
|
||||
li.querySelector("input[name='series_order']").setAttribute("name", "series_order_" + idx);
|
||||
const reorderSeriesForm = document.getElementById("reorder-series-form");
|
||||
if (reorderSeriesForm) {
|
||||
reorderSeriesForm.addEventListener("submit", function(e) {
|
||||
const lis = document.querySelectorAll("#series-reorder-list li");
|
||||
lis.forEach((li, idx) => {
|
||||
li.querySelector("input[name='series_order']").setAttribute("name", "series_order_" + idx);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Drag and drop reordering for series list
|
||||
const reorderList = document.getElementById("series-reorder-list");
|
||||
@@ -1035,6 +1092,8 @@
|
||||
if (moveSeriesForm) {
|
||||
const hiddenInput = moveSeriesForm.querySelector("[name='destination_case']");
|
||||
const moveSubmitButton = document.getElementById("move-selected-series-submit");
|
||||
const moveSearchResults = document.getElementById("move-series-case-search-results");
|
||||
const selectionLabel = document.getElementById("selected-move-case");
|
||||
|
||||
function syncMoveSeriesSubmitState() {
|
||||
if (!moveSubmitButton || !hiddenInput) {
|
||||
@@ -1043,22 +1102,51 @@
|
||||
moveSubmitButton.disabled = !hiddenInput.value;
|
||||
}
|
||||
|
||||
function setSelectedDestination(casePk, caseTitle) {
|
||||
if (!hiddenInput || !casePk) {
|
||||
return;
|
||||
}
|
||||
|
||||
hiddenInput.value = casePk;
|
||||
if (selectionLabel) {
|
||||
selectionLabel.innerHTML = "Selected destination: <strong>" + caseTitle + " (" + casePk + ")</strong>";
|
||||
selectionLabel.classList.remove("text-muted");
|
||||
}
|
||||
|
||||
if (moveSearchResults) {
|
||||
moveSearchResults.querySelectorAll(".list-group-item").forEach((item) => {
|
||||
item.classList.toggle("active", String(item.getAttribute("data-case-pk")) === String(casePk));
|
||||
});
|
||||
}
|
||||
|
||||
syncMoveSeriesSubmitState();
|
||||
}
|
||||
|
||||
if (moveSearchResults) {
|
||||
moveSearchResults.addEventListener("click", function(event) {
|
||||
const item = event.target.closest(".list-group-item[data-case-pk]");
|
||||
if (!item || !moveSearchResults.contains(item)) {
|
||||
return;
|
||||
}
|
||||
if (event.target.closest("a,button,form,input")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const casePk = item.getAttribute("data-case-pk");
|
||||
const titleEl = item.querySelector("h6");
|
||||
const caseTitle = titleEl ? titleEl.textContent.trim() : ("Case #" + casePk);
|
||||
setSelectedDestination(casePk, caseTitle);
|
||||
});
|
||||
}
|
||||
|
||||
moveSeriesForm.addEventListener("case:selected", function(event) {
|
||||
const selectionLabel = document.getElementById("selected-move-case");
|
||||
if (!hiddenInput || !event.detail) {
|
||||
return;
|
||||
}
|
||||
|
||||
const casePk = event.detail.casePk;
|
||||
const caseTitle = event.detail.caseTitle || ("Case #" + casePk);
|
||||
hiddenInput.value = casePk;
|
||||
|
||||
if (selectionLabel) {
|
||||
selectionLabel.innerHTML = "Selected destination: <strong>" + caseTitle + " (" + casePk + ")</strong>";
|
||||
selectionLabel.classList.remove("text-muted");
|
||||
}
|
||||
|
||||
syncMoveSeriesSubmitState();
|
||||
setSelectedDestination(casePk, caseTitle);
|
||||
});
|
||||
|
||||
syncMoveSeriesSubmitState();
|
||||
|
||||
@@ -1,38 +1,119 @@
|
||||
<!-- filepath: /home/ross/rad/rad/atlas/templates/atlas/linked_cases_overview.html -->
|
||||
{% extends "base.html" %}
|
||||
|
||||
|
||||
{% extends "atlas/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Linked Cases Overview</h1>
|
||||
<h2>Current Case: {{ current_case.title }}</h2>
|
||||
<div class="container py-4">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-4">
|
||||
<div>
|
||||
<h1 class="h2 mb-1">Linked Cases Overview</h1>
|
||||
<p class="text-muted mb-0">Review the full case chain around this study, with quick access to related cases and their linked series.</p>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a class="btn btn-outline-secondary" href="{{ current_case.get_absolute_url }}">Open current case</a>
|
||||
{% if direct_previous %}
|
||||
<a class="btn btn-outline-secondary" href="{% url 'atlas:linked_cases_overview' direct_previous.pk %}">Previous in chain</a>
|
||||
{% endif %}
|
||||
{% if direct_next %}
|
||||
<a class="btn btn-outline-secondary" href="{% url 'atlas:linked_cases_overview' direct_next.pk %}">Next in chain</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Previous Cases</h3>
|
||||
{% if previous_cases %}
|
||||
<ul>
|
||||
{% for case in previous_cases %}
|
||||
<li>
|
||||
<a href="{{ prev_case.get_absolute_url }}">{{ case.title }}</a>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-lg-8">
|
||||
<div class="card border-secondary-subtle shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
|
||||
<span class="badge bg-primary">Current case</span>
|
||||
<span class="badge bg-dark-subtle text-dark-emphasis">{{ current_index }} of {{ chain_total }}</span>
|
||||
{% if previous_cases %}
|
||||
<span class="badge text-bg-light border">{{ previous_cases|length }} previous</span>
|
||||
{% endif %}
|
||||
{% if next_cases %}
|
||||
<span class="badge text-bg-light border">{{ next_cases|length }} follow-up</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h2 class="h4 mb-1">{{ current_case.title|default:"Untitled case" }}</h2>
|
||||
<div class="small text-muted mb-3">Case {{ current_case.pk }}</div>
|
||||
{% if current_case.description %}
|
||||
<p class="mb-3">{{ current_case.description|striptags|truncatechars:280 }}</p>
|
||||
{% elif current_case.history %}
|
||||
<p class="mb-3">{{ current_case.history|striptags|truncatechars:280 }}</p>
|
||||
{% endif %}
|
||||
|
||||
</li>
|
||||
{% include "atlas/case_display_block.html#case-series" %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>No previous cases.</p>
|
||||
{% endif %}
|
||||
<div class="d-flex flex-wrap gap-2 small">
|
||||
{% if current_case.open_access %}
|
||||
<span class="badge bg-success">Open access</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning text-dark">Restricted</span>
|
||||
{% endif %}
|
||||
{% if current_case.verified %}
|
||||
<span class="badge bg-info text-dark">Verified</span>
|
||||
{% endif %}
|
||||
{% if current_case.archive %}
|
||||
<span class="badge bg-dark">Archived</span>
|
||||
{% endif %}
|
||||
{% if current_case.diagnostic_certainty %}
|
||||
<span class="badge text-bg-light border">{{ current_case.get_diagnostic_certainty_display }}</span>
|
||||
{% endif %}
|
||||
<span class="badge text-bg-light border">{{ current_case.get_ordered_series|length }} series</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-secondary-subtle shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 mb-3">Chain Summary</h2>
|
||||
<div class="list-group list-group-flush small">
|
||||
{% for linked_case in chain_cases %}
|
||||
<a href="{% url 'atlas:linked_cases_overview' linked_case.pk %}"
|
||||
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center {% if linked_case.pk == current_case.pk %}active{% endif %}">
|
||||
<span class="text-truncate pe-3">{{ forloop.counter }}. {{ linked_case.title|default:"Untitled case" }}</span>
|
||||
<span class="badge {% if linked_case.pk == current_case.pk %}text-bg-light{% else %}text-bg-secondary{% endif %}">{{ linked_case.pk }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Next Cases</h3>
|
||||
{% if next_cases %}
|
||||
<ul>
|
||||
{% for case in next_cases %}
|
||||
<li>
|
||||
<a href="{{ next_case.get_absolute_url }}">{{ case.title }}</a>
|
||||
</li>
|
||||
{% include "atlas/case_display_block.html#case-series" %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>No next cases.</p>
|
||||
{% endif %}
|
||||
<div class="row g-4">
|
||||
<div class="col-xl-6">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2 class="h4 mb-0">Previous Cases</h2>
|
||||
<span class="badge text-bg-light border">{{ previous_cases|length }}</span>
|
||||
</div>
|
||||
{% if previous_cases %}
|
||||
<div class="d-flex flex-column gap-3">
|
||||
{% for linked_case in previous_cases %}
|
||||
{% include "atlas/partials/_linked_case_card.html" with relation_label="Earlier in the linked chain" is_current=False %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card border-secondary-subtle shadow-sm">
|
||||
<div class="card-body text-muted">No previous linked cases.</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-xl-6">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2 class="h4 mb-0">Next Cases</h2>
|
||||
<span class="badge text-bg-light border">{{ next_cases|length }}</span>
|
||||
</div>
|
||||
{% if next_cases %}
|
||||
<div class="d-flex flex-column gap-3">
|
||||
{% for linked_case in next_cases %}
|
||||
{% include "atlas/partials/_linked_case_card.html" with relation_label="Later in the linked chain" is_current=False %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card border-secondary-subtle shadow-sm">
|
||||
<div class="card-body text-muted">No follow-up linked cases.</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,116 @@
|
||||
<article class="card h-100 border-secondary-subtle shadow-sm">
|
||||
<div class="card-body d-flex flex-column gap-3">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-start gap-2">
|
||||
<div>
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 mb-1">
|
||||
<span class="badge bg-secondary">Case {{ linked_case.pk }}</span>
|
||||
{% if is_current %}
|
||||
<span class="badge bg-primary">Current</span>
|
||||
{% endif %}
|
||||
{% if linked_case.open_access %}
|
||||
<span class="badge bg-success">Open access</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning text-dark">Restricted</span>
|
||||
{% endif %}
|
||||
{% if linked_case.verified %}
|
||||
<span class="badge bg-info text-dark">Verified</span>
|
||||
{% endif %}
|
||||
{% if linked_case.archive %}
|
||||
<span class="badge bg-dark">Archived</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h3 class="h5 mb-1">
|
||||
<a class="link-body-emphasis text-decoration-none" href="{{ linked_case.get_absolute_url }}">
|
||||
{{ linked_case.title|default:"Untitled case" }}
|
||||
</a>
|
||||
</h3>
|
||||
{% if relation_label %}
|
||||
<div class="small text-muted">{{ relation_label }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-sm-end small text-muted">
|
||||
{% if linked_case.published_date %}
|
||||
<div>Published {{ linked_case.published_date|date:"Y-m-d" }}</div>
|
||||
{% else %}
|
||||
<div>Created {{ linked_case.created_date|date:"Y-m-d" }}</div>
|
||||
{% endif %}
|
||||
{% if linked_case.diagnostic_certainty %}
|
||||
<div>Certainty: {{ linked_case.get_diagnostic_certainty_display }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if linked_case.description %}
|
||||
<p class="mb-0 text-body-secondary">{{ linked_case.description|striptags|truncatechars:220 }}</p>
|
||||
{% elif linked_case.history %}
|
||||
<p class="mb-0 text-body-secondary">{{ linked_case.history|striptags|truncatechars:220 }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-flex flex-column gap-2 small">
|
||||
{% if linked_case.subspecialty.all %}
|
||||
<div>
|
||||
<span class="text-muted me-2">Subspecialty</span>
|
||||
{% for item in linked_case.subspecialty.all %}
|
||||
<span class="badge text-bg-light border">{{ item }}</span>
|
||||
{% empty %}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if linked_case.condition.all %}
|
||||
<div>
|
||||
<span class="text-muted me-2">Condition</span>
|
||||
{% for item in linked_case.condition.all|slice:":4" %}
|
||||
<span class="badge text-bg-light border">{{ item }}</span>
|
||||
{% endfor %}
|
||||
{% if linked_case.condition.count > 4 %}
|
||||
<span class="text-muted">+{{ linked_case.condition.count|add:-4 }} more</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if linked_case.presentation.all %}
|
||||
<div>
|
||||
<span class="text-muted me-2">Presentation</span>
|
||||
{% for item in linked_case.presentation.all|slice:":4" %}
|
||||
<span class="badge text-bg-light border">{{ item }}</span>
|
||||
{% endfor %}
|
||||
{% if linked_case.presentation.count > 4 %}
|
||||
<span class="text-muted">+{{ linked_case.presentation.count|add:-4 }} more</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="border rounded p-3 bg-body-tertiary">
|
||||
<div class="d-flex justify-content-between align-items-center gap-2 mb-2">
|
||||
<strong class="small">Series</strong>
|
||||
<span class="badge bg-dark-subtle text-dark-emphasis">{{ linked_case.get_ordered_series|length }} linked</span>
|
||||
</div>
|
||||
{% with ordered_series=linked_case.get_ordered_series %}
|
||||
{% if ordered_series %}
|
||||
<div class="d-flex flex-column gap-2 small">
|
||||
{% for series in ordered_series|slice:":4" %}
|
||||
<div class="d-flex justify-content-between align-items-start gap-2 border rounded px-2 py-1 bg-body">
|
||||
<div>
|
||||
<a class="text-decoration-none" href="{{ series.get_absolute_url }}">
|
||||
{{ series.description|default:series.examination }}
|
||||
</a>
|
||||
<div class="text-muted">{{ series.modality }}{% if series.examination %} · {{ series.examination }}{% endif %}{% if series.plane %} · {{ series.plane }}{% endif %}</div>
|
||||
</div>
|
||||
<span class="badge text-bg-light border">#{{ forloop.counter }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if ordered_series|length > 4 %}
|
||||
<div class="small text-muted mt-2">Showing 4 of {{ ordered_series|length }} linked series.</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="small text-muted">No series linked to this case.</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mt-auto">
|
||||
<a class="btn btn-sm btn-primary" href="{{ linked_case.get_absolute_url }}">Open case</a>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="{% url 'atlas:linked_cases_overview' linked_case.pk %}">Re-centre overview</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
+10
-1
@@ -7289,7 +7289,7 @@ def reorder_series(request, case_pk):
|
||||
|
||||
def linked_cases_overview(request, case_id):
|
||||
"""
|
||||
View to display an overview of linked cases via the previous_case field.
|
||||
Display the linked-case chain around a case, including prior and follow-up cases.
|
||||
"""
|
||||
# Get the current case
|
||||
case = get_object_or_404(Case, pk=case_id)
|
||||
@@ -7314,6 +7314,10 @@ def linked_cases_overview(request, case_id):
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
chain_cases = previous_cases + [case] + next_cases
|
||||
direct_previous = previous_cases[-1] if previous_cases else None
|
||||
direct_next = next_cases[0] if next_cases else None
|
||||
|
||||
# Render the template with the linked cases
|
||||
return render(
|
||||
request,
|
||||
@@ -7322,6 +7326,11 @@ def linked_cases_overview(request, case_id):
|
||||
"current_case": case,
|
||||
"previous_cases": previous_cases,
|
||||
"next_cases": next_cases,
|
||||
"chain_cases": chain_cases,
|
||||
"chain_total": len(chain_cases),
|
||||
"current_index": len(previous_cases) + 1,
|
||||
"direct_previous": direct_previous,
|
||||
"direct_next": direct_next,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user