1348 lines
63 KiB
HTML
Executable File
1348 lines
63 KiB
HTML
Executable File
{% load case_widgets %}
|
|
|
|
{% partialdef case-series %}
|
|
{% for series in case.get_ordered_series %}
|
|
<span class="series-block" id="series-block-{{ series.pk }}">
|
|
<span>
|
|
<span class="series-block-series-number">Series {{ forloop.counter }}:</span><br>
|
|
<a href="{% url 'atlas:series_detail' pk=series.pk %}">
|
|
{{series.get_block}}
|
|
</a>
|
|
<br>
|
|
<button type="button"
|
|
class="btn btn-outline-primary btn-sm select-series-btn"
|
|
style="display: none;"
|
|
data-series-id="{{series.pk}}">
|
|
Select
|
|
</button>
|
|
<input type="checkbox" class="hide" name="series-ids" value="{{series.pk}}">
|
|
<span class="series-block-popup-link">
|
|
<a href="#"
|
|
onclick="return window.create_popup_window('/atlas/series/{{series.pk}}', 'Series')">Popup</a>
|
|
</span>
|
|
</span>
|
|
</span>
|
|
{% endfor %}
|
|
{% endpartialdef %}
|
|
|
|
<style>
|
|
.case-inline-field {
|
|
position: relative;
|
|
}
|
|
|
|
.case-inline-field .quick-edit-button {
|
|
border: 0;
|
|
background: transparent;
|
|
color: var(--bs-secondary-color, #6c757d);
|
|
font-size: 0.8rem;
|
|
line-height: 1.1;
|
|
padding: 0;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.case-inline-field .quick-edit-button:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.case-inline-field .quick-edit-compact {
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
z-index: 2;
|
|
opacity: 0.35;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.case-inline-field .quick-edit-trigger {
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: opacity 0.15s ease;
|
|
}
|
|
|
|
.case-inline-field:hover .quick-edit-trigger,
|
|
.case-inline-field:focus-within .quick-edit-trigger {
|
|
opacity: 0.65;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.case-inline-field .quick-edit-trigger:hover {
|
|
opacity: 1;
|
|
}
|
|
|
|
.case-inline-field .quick-edit-compact.quick-edit-trigger {
|
|
opacity: 0.35;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.case-inline-field:hover .quick-edit-compact.quick-edit-trigger,
|
|
.case-inline-field:focus-within .quick-edit-compact.quick-edit-trigger {
|
|
opacity: 0.9;
|
|
}
|
|
|
|
/* Generic hover-only quick-edit behaviour for host-wrapped buttons.
|
|
Buttons are positioned absolutely so revealing them does not shift layout. */
|
|
.quick-edit-wrap { position: relative; display: inline-flex; align-items: center; }
|
|
.quick-edit-wrap .quick-edit-button {
|
|
position: absolute;
|
|
right: 0.25rem;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
z-index: 5;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
background: rgba(255,255,255,0.95) !important;
|
|
border-radius: .25rem;
|
|
padding: 0 .25rem;
|
|
color: #0d6efd !important; /* clearer action colour */
|
|
}
|
|
.quick-edit-wrap:hover .quick-edit-button,
|
|
.quick-edit-wrap:focus-within .quick-edit-button {
|
|
opacity: 0.95;
|
|
pointer-events: auto;
|
|
transform: translateY(-50%);
|
|
}
|
|
.quick-edit-wrap .quick-edit-button:hover { opacity: 1; }
|
|
|
|
@media (max-width: 767.98px) {
|
|
.quick-edit-wrap .quick-edit-button,
|
|
.case-meta-item .quick-edit-button,
|
|
.case-inline-field .quick-edit-compact {
|
|
opacity: 0.95;
|
|
pointer-events: auto;
|
|
padding: 0.15rem 0.35rem;
|
|
font-size: 0.9rem;
|
|
}
|
|
}
|
|
|
|
/* Metadata-specific quick-edit: ensure edit button overlays without moving badges */
|
|
.case-meta-item { position: relative; }
|
|
.case-meta-item .quick-edit-button {
|
|
position: absolute;
|
|
right: 0.25rem;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
z-index: 6;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
background: rgba(255,255,255,0.95) !important;
|
|
padding: 0 .25rem;
|
|
border-radius: .25rem;
|
|
color: #0d6efd !important;
|
|
border: 1px solid rgba(13,110,253,0.08);
|
|
}
|
|
.case-meta-item:hover .quick-edit-button,
|
|
.case-meta-item:focus-within .quick-edit-button {
|
|
opacity: 0.95;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
/* 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; }
|
|
|
|
|
|
.section-icon { display:inline-block; width:10px; height:10px; border-radius:50%; margin-right:0.5rem; }
|
|
.section-icon.description { background:#6c757d; }
|
|
.section-icon.presentation { background:#0d6efd; }
|
|
.section-icon.history { background:#198754; }
|
|
.section-icon.discussion { background:#fd7e14; }
|
|
.section-icon.report { background:#dc3545; }
|
|
/* Series block base (Bootstrap-like card appearance) */
|
|
.series-block {
|
|
display: inline-block;
|
|
vertical-align: top;
|
|
min-width: 180px;
|
|
max-width: 320px;
|
|
margin: .25rem;
|
|
padding: .5rem;
|
|
border-radius: .375rem;
|
|
background: #f8f9fa;
|
|
border: 1px solid rgba(0,0,0,0.06);
|
|
text-align: left;
|
|
}
|
|
/* Also support dark theme when site sets a theme attribute/class */
|
|
.atlas[data-bs-theme="dark"] .series-block,
|
|
[data-bs-theme="dark"] .series-block,
|
|
body[data-bs-theme="dark"] .series-block,
|
|
body.bg-dark .series-block,
|
|
.bg-dark .series-block,
|
|
.dark .series-block,
|
|
.theme-dark .series-block {
|
|
background: #071018 !important;
|
|
border-color: rgba(255,255,255,0.03) !important;
|
|
color: #e6eef8 !important;
|
|
}
|
|
.case-metadata { border-left: 1px solid #eee; padding-left: 1rem; text-align:left; }
|
|
.case-meta-item { margin-bottom: .5rem; }
|
|
.case-meta-label { font-weight:600; font-size:0.9rem; display:block; }
|
|
@media (min-width: 768px) {
|
|
/* Keep metadata stacked (one per row), but put label and value on same row */
|
|
.case-metadata { text-align: right; }
|
|
.case-meta-item { display: flex; justify-content: space-between; align-items: center; margin-bottom: .5rem; }
|
|
.case-meta-value { display: flex; align-items: center; gap: .5rem; justify-content: flex-end; }
|
|
.case-meta-item .badge { display: inline-block; margin-left: .25rem; margin-right: 0; }
|
|
.case-meta-item > .case-meta-value > div { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
}
|
|
/* Description & Presentation visual improvements */
|
|
.case-description { background: #eef3f73d; border: 1px solid rgba(0,0,0,0.08); padding: .75rem; border-radius: .375rem; }
|
|
.case-description strong { font-size: 1.05rem; margin-right: .5rem; }
|
|
.case-description .case-inline-field { margin-top: .5rem; }
|
|
|
|
.case-presentation ul { list-style: none; padding-left: 0; margin: .25rem 0 0 0; display: flex; flex-wrap: wrap; gap: .5rem; }
|
|
.case-presentation li { background: rgba(13,110,253,0.12); color: #0b5ed7; padding: .25rem .5rem; border-radius: .25rem; font-size: .9rem; }
|
|
.case-presentation b { margin-right: .5rem; }
|
|
|
|
.displayset-empty-state {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: .6rem;
|
|
margin: .4rem 0;
|
|
padding: .45rem .65rem;
|
|
border-radius: .5rem;
|
|
border: 1px solid rgba(13, 110, 253, 0.2);
|
|
background: rgba(13, 110, 253, 0.06);
|
|
}
|
|
|
|
.displayset-empty-link {
|
|
display: inline-block;
|
|
padding: .25rem .6rem;
|
|
border-radius: 999px;
|
|
background: #0d6efd;
|
|
color: #fff;
|
|
font-size: .82rem;
|
|
font-weight: 600;
|
|
text-decoration: none;
|
|
transition: background-color .15s ease, transform .15s ease;
|
|
}
|
|
|
|
.displayset-empty-link:hover,
|
|
.displayset-empty-link:focus {
|
|
background: #0b5ed7;
|
|
color: #fff;
|
|
transform: translateY(-1px);
|
|
text-decoration: none;
|
|
}
|
|
|
|
/* Dark mode adjustments */
|
|
@media (prefers-color-scheme: dark) {
|
|
.case-description {
|
|
background: #0f1720;
|
|
border: 1px solid rgba(255,255,255,0.06);
|
|
color: #e6eef8;
|
|
}
|
|
.case-description strong { color: #ffffff; }
|
|
.case-description .case-inline-field { color: #e6eef8; }
|
|
.case-presentation li { background: rgba(255,255,255,0.03); color: #9ec5ff; }
|
|
.quick-edit-wrap .quick-edit-button,
|
|
.case-meta-item .quick-edit-button {
|
|
background: rgba(255,255,255,0.04) !important;
|
|
color: #9ec5ff !important;
|
|
border: 1px solid rgba(255,255,255,0.06);
|
|
}
|
|
.displayset-empty-state {
|
|
border-color: rgba(158, 197, 255, 0.3);
|
|
background: rgba(158, 197, 255, 0.1);
|
|
}
|
|
.displayset-empty-link {
|
|
background: #9ec5ff;
|
|
color: #042b61;
|
|
}
|
|
.displayset-empty-link:hover,
|
|
.displayset-empty-link:focus {
|
|
background: #bfdbff;
|
|
color: #042b61;
|
|
}
|
|
/* Series block dark theme adjustments */
|
|
.series-block {
|
|
background: #071018;
|
|
border-color: rgba(255,255,255,0.03);
|
|
color: #e6eef8;
|
|
box-shadow: none;
|
|
}
|
|
.series-block a { color: inherit; }
|
|
.series-block .series-block-series-number { color: #cfe6ff; }
|
|
.series-block .series-block-popup-link a { color: #9ec5ff; }
|
|
#case-series-row { gap: .5rem; }
|
|
}
|
|
</style>
|
|
|
|
<div class="atlas {% if case.scrapped %}atlas-scrapped{% endif %}">
|
|
<div class="btn-group btn-group-sm link-buttons" role="group">
|
|
<button type="button" class="btn btn-primary btn-sm"
|
|
onclick="window.location.href='/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:case_dicom_json' case.pk %}'">View case in OHIF</button>
|
|
<button type="button" class="btn btn-primary btn-sm dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
|
|
<span class="visually-hidden">Toggle dropdown</span>
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li>
|
|
<a class="dropdown-item" target="_blank" href="/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:case_dicom_json' case.pk %}">Open in new tab</a>
|
|
</li>
|
|
{% if can_edit %}
|
|
<li>
|
|
<a class="dropdown-item" href="{% url 'atlas:case_download' case.pk %}">Download DICOMs + metadata (ZIP)</a>
|
|
</li>
|
|
{% endif %}
|
|
</ul>
|
|
</div>
|
|
<div class="normal-block">
|
|
<div class="mt-1">
|
|
{% include "atlas/partials/_normal_toggle.html" %}
|
|
</div>
|
|
</div>
|
|
<h2 class="d-flex align-items-center" style="gap: .5rem; white-space: nowrap;">
|
|
<span>Case:</span>
|
|
<div class="d-flex align-items-center quick-edit-wrap">
|
|
{% url 'atlas:case_inline_field' case.pk 'title' as title_url %}
|
|
{% with endpoint_default=title_url|add:"?show_label=0" endpoint_edit=title_url|add:"?edit=1&show_label=0" %}
|
|
{% include 'atlas/partials/case_inline_field.html' with show_label=0 editing=False field_label="Title" can_edit=can_edit endpoint_url_default=endpoint_default endpoint_url_edit=endpoint_edit container_id='case-inline-title' field_kind='text' value=case.title display_value=case.title field_name='title' error_message='' %}
|
|
{% endwith %}
|
|
{% if can_edit %}
|
|
<button class="btn btn-sm btn-link text-muted p-0 ms-2 quick-edit-button"
|
|
hx-get="{% url 'atlas:case_inline_field' case.pk 'title' %}?edit=1&show_label=0"
|
|
hx-target="#case-inline-title"
|
|
hx-swap="outerHTML">quick edit</button>
|
|
{% endif %}
|
|
</div>
|
|
</h2>
|
|
{% if case.previous_case or case.next_case %}
|
|
<div class="alert alert-info" role="alert">
|
|
This case is part of a <a href='{% url "atlas:linked_cases_overview" case.pk %}'>series.</a><br/>
|
|
<div class="row">
|
|
{% if case.previous_case %}
|
|
<div class="col-sm-6 mb-3 mb-sm-0">
|
|
<h5 class="card-title">Previous Case</h5>
|
|
{{ case.previous_case.get_link }}
|
|
</div>
|
|
{% endif %}
|
|
{% if case.next_case %}
|
|
<div class="col-sm-6">
|
|
<h5 class="card-title">Next Case</h5>
|
|
{{ case.next_case.get_link }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
<div>
|
|
|
|
{% if case.cimar_uuid %}
|
|
|
|
<details><summary>Details</summary>
|
|
Case uuid: {{case.cimar_uuid}}
|
|
<br/>
|
|
|
|
{% if cimar_sid %}
|
|
<a href="{{case.get_cimar_case.viewer_link}}?sid={{cimar_sid}}" target="_blank">External</a>
|
|
{% else %}
|
|
<a href="{{case.get_cimar_case.viewer_link}}" target="_blank">External</a>
|
|
|
|
{% endif %}
|
|
|
|
|
|
<button
|
|
hx-get='{% url "cimar_study_viewer_embed" case.cimar_uuid %}'
|
|
hx-target="#modals-here"
|
|
hx-trigger="click"
|
|
data-bs-toggle="modal"
|
|
data-bs-target="#modals-here"
|
|
class="btn primary">Open Modal</button>
|
|
|
|
<div id="modals-here"
|
|
class="modal modal-blur fade"
|
|
style="display: none"
|
|
aria-hidden="false"
|
|
tabindex="-1">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
|
|
<div class="modal-content"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<a href='{% url "cimar_study_viewer" case.cimar_uuid %}'>Viewer</a>
|
|
|
|
|
|
<details>
|
|
<summary>Study details</summary>
|
|
{{ case.get_cimar_case.get_pretty_study_details }}
|
|
</details>
|
|
<details>
|
|
<summary>Study schema</summary>
|
|
{{ case.get_cimar_case.get_pretty_study_schema }}
|
|
</details>
|
|
<details>
|
|
<summary>Study series</summary>
|
|
<button
|
|
id='load-series'
|
|
hx-get='{% url "cimar_study_series_block" case.cimar_uuid %}'
|
|
hx-trigger='load'
|
|
hx-swap='outerHTML'>
|
|
Load Series
|
|
</button>
|
|
</details>
|
|
<a href='{% url "generic:cimar_case_refresh" case.cimar_uuid %}'>Refresh</a>
|
|
|
|
</details>
|
|
|
|
|
|
|
|
{% else %}
|
|
|
|
{% endif %}
|
|
|
|
<div class="clearfix mb-2">
|
|
<div class="float-start">
|
|
<span class="id">ID: {{ case.id }}</span>
|
|
<span class="mx-1">/</span>
|
|
<span class="date">{{ case.created_date|date:"d/m/Y" }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<details id="dicom-viewer-details">
|
|
<summary>Viewer</summary>
|
|
|
|
<div id="main_viewer" class="dicom-viewer-root"
|
|
style="box-sizing: border-box; background: #222; width: 100%; height: 600px;"
|
|
data-auto-cache-stack="false"
|
|
data-named-stacks='{{case.get_case_named_stacks}}'
|
|
></div>
|
|
</details>
|
|
<div class="pre-whitespace multi-image-block"><b>Series:</b>
|
|
<form>
|
|
<div class="d-flex flex-wrap gap-2 align-items-start" id="case-series-row">
|
|
{% partial case-series %}
|
|
</div>
|
|
</form>
|
|
<div class="d-flex flex-column gap-2 align-items-start">
|
|
<div>
|
|
<button type="button"
|
|
class="btn btn-outline-primary btn-sm"
|
|
onclick="window.location.href='{% url 'atlas:user_uploads_case' case_id=case.pk %}'">
|
|
Import new uploads
|
|
</button>
|
|
</div>
|
|
<details class="series-actions" id="series-actions">
|
|
<summary>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" title="Click to manage series" onclick="this.closest('details').open = !this.closest('details').open;">
|
|
Manage Series
|
|
</button>
|
|
</summary>
|
|
<div class="series-actions-body">
|
|
<a class="btn btn-sm btn-outline-secondary text-start" href="{% url 'atlas:series_id_create' pk=case.pk %}">Create and add new series</a>
|
|
|
|
<div class="series-selection-toolbar d-flex flex-wrap gap-2">
|
|
<button type="button" class="btn btn-sm btn-outline-primary" id="series-select-all-btn">Select all</button>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="series-deselect-all-btn">Deselect all</button>
|
|
</div>
|
|
|
|
<div class="series-actions-grid d-flex flex-column gap-2">
|
|
<button class="btn btn-sm btn-outline-secondary text-start" hx-get="{% url 'atlas:case_order_dicom' pk=case.pk %}"
|
|
title="order dicom by slice location"
|
|
hx-target="#series-action-results"
|
|
hx-swap="innerHTML"
|
|
hx-confirm="This will reorder all case series based upon slice location"
|
|
>
|
|
Order dicoms by slice location
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-primary text-start" hx-post="{% url 'atlas:combine_series' %}"
|
|
title="merge series"
|
|
hx-include="[name='series-ids']"
|
|
hx-target="#series-action-results"
|
|
hx-swap="innerHTML"
|
|
hx-confirm="This will merge all selected series into one"
|
|
>
|
|
Merge selected series
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-secondary text-start" hx-post="{% url 'atlas:use_dates_as_descriptions' case.pk %}"
|
|
title="use dicom dates as series descriptions"
|
|
hx-include="[name='series-ids']"
|
|
hx-target="#series-action-results"
|
|
hx-swap="innerHTML"
|
|
hx-confirm="This will use the dicom date as the series description"
|
|
>
|
|
Use dates as description
|
|
</button>
|
|
<button hx-post="{% url 'atlas:remove_selected_series_from_case' case.pk %}"
|
|
title="remove selected series from this case"
|
|
hx-include="[name='series-ids']:checked"
|
|
hx-target="#series-action-results"
|
|
hx-swap="innerHTML"
|
|
hx-confirm="Are you sure you want to remove the selected series from this case? They will not be deleted from the database."
|
|
class="btn btn-sm btn-warning text-start"
|
|
type="button"
|
|
>
|
|
Remove selected series from case
|
|
</button>
|
|
<button type="button"
|
|
class="btn btn-sm btn-secondary text-start"
|
|
data-bs-toggle="modal"
|
|
data-bs-target="#reorderSeriesModal">
|
|
Reorder series
|
|
</button>
|
|
</div>
|
|
|
|
<details class="series-move-panel border rounded p-2">
|
|
<summary class="small fw-semibold">Move selected series to another case</summary>
|
|
{% if move_series_form %}
|
|
<form id="move-selected-series-form" class="d-flex flex-column gap-2 mt-2">
|
|
{{ move_series_form.destination_case }}
|
|
<div class="small text-muted">Click any case row below to select the destination case.</div>
|
|
<div id="selected-move-case" class="small text-muted">No destination case selected.</div>
|
|
{% case_search_widget collection=None input_id='move-series-case-search-input' target_id='move-series-case-search-results' show_select_button=True %}
|
|
<button type="button"
|
|
id="move-selected-series-submit"
|
|
class="btn btn-sm btn-outline-primary text-start"
|
|
disabled
|
|
hx-post="{% url 'atlas:move_selected_series_to_case' case.pk %}"
|
|
hx-include="[name='series-ids']:checked, #move-selected-series-form [name='destination_case']"
|
|
hx-target="#series-action-results"
|
|
hx-swap="innerHTML"
|
|
hx-confirm="Move selected series to the chosen destination case?">
|
|
Move selected series
|
|
</button>
|
|
</form>
|
|
<script>
|
|
(function () {
|
|
if (window.__moveSeriesCaseSelectionBound) return;
|
|
window.__moveSeriesCaseSelectionBound = true;
|
|
|
|
function applyMoveCaseSelection(casePk, caseTitle) {
|
|
if (!casePk) return;
|
|
|
|
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) 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));
|
|
});
|
|
}
|
|
|
|
syncMoveSeriesSubmitState();
|
|
}
|
|
|
|
function syncMoveSeriesSubmitState() {
|
|
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;
|
|
}
|
|
|
|
document.body.addEventListener('case:selected', function (e) {
|
|
try {
|
|
const detail = e.detail || {};
|
|
const casePk = detail.casePk;
|
|
const caseTitle = detail.caseTitle || ('Case #' + casePk);
|
|
applyMoveCaseSelection(casePk, caseTitle);
|
|
} catch (err) {
|
|
console.error('Move series case:selected handler error', err);
|
|
}
|
|
});
|
|
|
|
syncMoveSeriesSubmitState();
|
|
})();
|
|
</script>
|
|
{% else %}
|
|
<div class="text-muted small mt-2">Case move options are unavailable in this view.</div>
|
|
{% endif %}
|
|
</details>
|
|
</div>
|
|
<div class="alert alert-info mt-2" id="series-action-alert" style="display:none;">
|
|
<span id="series-action-results" ></span>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container">
|
|
|
|
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-12 col-md-8">
|
|
<div class="mb-3 case-description">
|
|
<div class="d-flex align-items-start justify-content-between quick-edit-wrap">
|
|
<div class="d-flex align-items-start flex-grow-1">
|
|
<span class="section-icon description" aria-hidden="true"></span>
|
|
<strong>Description:</strong>
|
|
{% url 'atlas:case_inline_field' case.pk 'description' as desc_url %}
|
|
{% with endpoint_default=desc_url|add:"?show_label=0" endpoint_edit=desc_url|add:"?edit=1&show_label=0" %}
|
|
{% include 'atlas/partials/case_inline_field.html' with show_label=0 editing=False field_label="Description" can_edit=can_edit endpoint_url_default=endpoint_default endpoint_url_edit=endpoint_edit container_id='case-inline-description' field_kind='textarea' value=case.description display_value=case.description field_name='description' error_message='' %}
|
|
{% endwith %}
|
|
</div>
|
|
{% if can_edit %}
|
|
<button class="btn btn-sm btn-link text-muted p-0 ms-2 quick-edit-button"
|
|
hx-get="{% url 'atlas:case_inline_field' case.pk 'description' %}?edit=1&show_label=0"
|
|
hx-target="#case-inline-description"
|
|
hx-swap="outerHTML">quick edit</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{# Presentation moved to metadata column for tighter layout #}
|
|
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-12">
|
|
<div class="card h-100">
|
|
<div class="card-header d-flex justify-content-between align-items-center quick-edit-wrap">
|
|
<span><span class="section-icon history" aria-hidden="true"></span>History</span>
|
|
{% if can_edit %}
|
|
<button class="btn btn-sm btn-link text-muted p-0 quick-edit-button"
|
|
hx-get="{% url 'atlas:case_inline_field' case.pk 'history' %}?edit=1&show_label=0"
|
|
hx-target="#case-inline-history"
|
|
hx-swap="outerHTML">quick edit</button>
|
|
{% endif %}
|
|
</div>
|
|
<div class="card-body">
|
|
{% url 'atlas:case_inline_field' case.pk 'history' as history_url %}
|
|
{% with endpoint_default=history_url|add:"?show_label=0" endpoint_edit=history_url|add:"?edit=1&show_label=0" %}
|
|
{% include 'atlas/partials/case_inline_field.html' with show_label=0 editing=False field_label='History' can_edit=can_edit endpoint_url_default=endpoint_default endpoint_url_edit=endpoint_edit container_id='case-inline-history' field_kind='textarea' value=case.history display_value=case.history field_name='history' error_message='' %}
|
|
{% endwith %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-12">
|
|
<div class="card h-100">
|
|
<div class="card-header d-flex justify-content-between align-items-center quick-edit-wrap">
|
|
<span><span class="section-icon discussion" aria-hidden="true"></span>Discussion</span>
|
|
{% if can_edit %}
|
|
<button class="btn btn-sm btn-link text-muted p-0 quick-edit-button"
|
|
hx-get="{% url 'atlas:case_inline_field' case.pk 'discussion' %}?edit=1&show_label=0"
|
|
hx-target="#case-inline-discussion"
|
|
hx-swap="outerHTML">quick edit</button>
|
|
{% endif %}
|
|
</div>
|
|
<div class="card-body">
|
|
{% url 'atlas:case_inline_field' case.pk 'discussion' as discussion_url %}
|
|
{% with endpoint_default=discussion_url|add:"?show_label=0" endpoint_edit=discussion_url|add:"?edit=1&show_label=0" %}
|
|
{% include 'atlas/partials/case_inline_field.html' with show_label=0 editing=False field_label='Discussion' can_edit=can_edit endpoint_url_default=endpoint_default endpoint_url_edit=endpoint_edit container_id='case-inline-discussion' field_kind='textarea' value=case.discussion display_value=case.discussion field_name='discussion' error_message='' %}
|
|
{% endwith %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-12">
|
|
<div class="card h-100">
|
|
<div class="card-header d-flex justify-content-between align-items-center quick-edit-wrap">
|
|
<span><span class="section-icon report" aria-hidden="true"></span>Report</span>
|
|
{% if can_edit %}
|
|
<button class="btn btn-sm btn-link text-muted p-0 quick-edit-button"
|
|
hx-get="{% url 'atlas:case_inline_field' case.pk 'report' %}?edit=1&show_label=0"
|
|
hx-target="#case-inline-report"
|
|
hx-swap="outerHTML">quick edit</button>
|
|
{% endif %}
|
|
</div>
|
|
<div class="card-body">
|
|
{% url 'atlas:case_inline_field' case.pk 'report' as report_url %}
|
|
{% with endpoint_default=report_url|add:"?show_label=0" endpoint_edit=report_url|add:"?edit=1&show_label=0" %}
|
|
{% include 'atlas/partials/case_inline_field.html' with show_label=0 editing=False field_label='Report' can_edit=can_edit endpoint_url_default=endpoint_default endpoint_url_edit=endpoint_edit container_id='case-inline-report' field_kind='textarea' value=case.report display_value=case.report field_name='report' error_message='' %}
|
|
{% endwith %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<aside class="col-12 col-md-4 case-metadata">
|
|
{# ID/date shown above; removed duplicate here #}
|
|
|
|
<div class="case-meta-item">
|
|
<span class="case-meta-label">Presentation</span>
|
|
<div class="case-meta-value d-flex align-items-center justify-content-end">
|
|
<div id="case-inline-presentation">
|
|
{% if case.presentation.all %}
|
|
{% for presentation in case.presentation.all %}
|
|
<span class="badge bg-secondary ms-1">{{ presentation.get_link|safe }}</span>
|
|
{% endfor %}
|
|
{% else %}
|
|
<span class="text-muted">None</span>
|
|
{% endif %}
|
|
</div>
|
|
{% if can_edit %}
|
|
<button class="btn btn-sm btn-link text-muted p-0 quick-edit-button ms-2"
|
|
hx-get="{% url 'atlas:case_inline_field' case.pk 'presentation' %}?edit=1&show_label=0"
|
|
hx-target="#case-inline-presentation"
|
|
hx-swap="outerHTML">quick edit</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="case-meta-item">
|
|
<span class="case-meta-label">Subspecialty</span>
|
|
<div class="case-meta-value d-flex align-items-center justify-content-end">
|
|
<div id="case-inline-subspecialty">
|
|
{% if case.subspecialty.all %}
|
|
{% for sub in case.subspecialty.all %}
|
|
<span class="badge bg-secondary ms-1">{{ sub.get_link|safe }}</span>
|
|
{% endfor %}
|
|
{% else %}
|
|
<span class="badge bg-secondary ms-1 text-muted">None</span>
|
|
{% endif %}
|
|
</div>
|
|
{% if can_edit %}
|
|
<button class="btn btn-sm btn-link text-muted p-0 quick-edit-button ms-2"
|
|
hx-get="{% url 'atlas:case_inline_field' case.pk 'subspecialty' %}?edit=1&show_label=0"
|
|
hx-target="#case-inline-subspecialty"
|
|
hx-swap="outerHTML">quick edit</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="case-meta-item">
|
|
<span class="case-meta-label">Pathological process</span>
|
|
<div class="case-meta-value d-flex align-items-center justify-content-end">
|
|
<div>
|
|
{% if case.pathological_process.all %}
|
|
{% for p in case.pathological_process.all %}
|
|
<span class="badge bg-secondary ms-1">{{ p.get_link|safe }}</span>
|
|
{% endfor %}
|
|
{% else %}
|
|
<span class="badge bg-secondary ms-1 text-muted">None</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="case-meta-item">
|
|
<span class="case-meta-label">Diagnostic certainty</span>
|
|
<div class="case-meta-value d-flex align-items-center justify-content-end">
|
|
{% url 'atlas:case_inline_field' case.pk 'diagnostic_certainty' as diag_url %}
|
|
{% with endpoint_default=diag_url|add:"?show_label=0" endpoint_edit=diag_url|add:"?edit=1&show_label=0" %}
|
|
{% include 'atlas/partials/case_inline_field.html' with show_label=0 editing=False field_label="Diagnostic certainty" can_edit=can_edit endpoint_url_default=endpoint_default endpoint_url_edit=endpoint_edit container_id='case-inline-diagnostic_certainty' field_kind='select' value=case.get_diagnostic_certainty_display display_value=case.get_diagnostic_certainty_display field_name='diagnostic_certainty' error_message='' %}
|
|
{% endwith %}
|
|
{% if can_edit %}
|
|
<button class="btn btn-sm btn-link text-muted p-0 quick-edit-button ms-2"
|
|
hx-get="{% url 'atlas:case_inline_field' case.pk 'diagnostic_certainty' %}?edit=1&show_label=0"
|
|
hx-target="#case-inline-diagnostic_certainty"
|
|
hx-swap="outerHTML">quick edit</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
|
|
</div>
|
|
<div class="mb-3">
|
|
<h5>Resources</h5>
|
|
|
|
{% if can_edit%}
|
|
<div class="mb-2">
|
|
<div id="resource-search-panel-placeholder">
|
|
<button class="btn btn-sm btn-outline-primary"
|
|
hx-get="{% url 'atlas:resource_search_panel' %}?case_id={{ case.pk }}"
|
|
hx-target="#resource-search-panel-placeholder"
|
|
hx-swap="innerHTML">
|
|
Add Resource
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div>
|
|
{% include 'atlas/partials/case_resources_list.html' with case=case %}
|
|
</div>
|
|
</div>
|
|
|
|
{% comment %} <p><b>Scrapped:</b> {{ case.scrapped }} <a
|
|
href="{% url 'atlas:case_scrap' pk=case.pk %}">(toggle)</a> {% endcomment %}
|
|
|
|
<div class="atlas-answer">
|
|
<div class="pre-whitespace">
|
|
Condition:
|
|
<ul>
|
|
{% for con in case.condition.all %}
|
|
<li>{{con.get_link}}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
<div>
|
|
<strong>Procedure(s):</strong>
|
|
{% if case.procedures.all %}
|
|
<ul>
|
|
{% for proc in case.procedures.all %}
|
|
<li>{{ proc.get_link }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
<span class="text-muted">None</span>
|
|
{% endif %}
|
|
</div>
|
|
<div>
|
|
|
|
{% include 'atlas/partials/_series_findings.html' %}
|
|
|
|
{% include 'atlas/partials/_case_displaysets.html' %}
|
|
</div>
|
|
{% include 'atlas/partials/_case_differentials.html' %}
|
|
</div>
|
|
</div>
|
|
|
|
<b>Collections:</b>
|
|
|
|
{% if can_edit %}
|
|
<button class="btn btn-sm btn-primary"
|
|
hx-get="{% url 'atlas:case_collection_form' case.pk %}"
|
|
hx-target="#collection-form">
|
|
Add to collection</button>
|
|
{% endif %}
|
|
<div id="collection-form"></div>
|
|
<ul id="case-{{ case.pk }}-collections">
|
|
{% for collection in case.casecollection_set.all %}
|
|
<li id="collection-{{ collection.pk }}">
|
|
<a href="{% url 'atlas:collection_detail' pk=collection.pk %}">{{ collection.name }}</a>
|
|
{% if can_edit %}
|
|
<button
|
|
class="btn btn-sm btn-outline-danger remove-button"
|
|
hx-post="{% url 'atlas:remove_case_from_collection' case.pk collection.pk %}"
|
|
hx-target="#collection-{{ collection.pk }}"
|
|
hx-swap="outerHTML"
|
|
hx-confirm="Are you sure you want to remove this case from the collection?">
|
|
Remove
|
|
</button>
|
|
{% endif %}
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</p>
|
|
|
|
|
|
{% if casedetail %}
|
|
{% include 'atlas/partials/collection_question_block.html' with can_edit=can_edit %}
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
<p class="pre-whitespace"><b>Previous case:</b> {{ case.previous_case.get_link }}</p>
|
|
<p class="pre-whitespace"><b>Next case:</b>
|
|
{% if case.next_case %}
|
|
{{ case.next_case.get_link }}
|
|
{% else %}
|
|
<a href="{% url 'atlas:case_clone_next' case.pk %}">Clone and add next case</a>
|
|
{% endif %}
|
|
</p>
|
|
<div>
|
|
{% include 'question_notes.html' %}
|
|
|
|
</div>
|
|
|
|
<p><b>Checked by:</b> {% for verified in case.verified.all %} <a
|
|
href="{% url 'atlas:verified_detail' pk=verified.pk %}">{{verified}}</a>, {% endfor %}</p>
|
|
|
|
<p><b>Author(s):</b> {% for author in case.author.all %} <a
|
|
href="{% url 'atlas:author_detail' pk=author.pk %}">{{author}}</a>, {% endfor %}</p>
|
|
|
|
|
|
<p><b>Case size:</b> {{ case.get_total_series_images_size | filesizeformat }}</p>
|
|
|
|
{% include 'atlas/partials/_finding_modal.html' %}
|
|
<!-- Reorder Series Modal -->
|
|
<div class="modal fade" id="reorderSeriesModal" tabindex="-1" aria-labelledby="reorderSeriesModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<form method="post" action="{% url 'atlas:reorder_series' case.pk %}" id="reorder-series-form">
|
|
{% csrf_token %}
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="reorderSeriesModalLabel">Reorder Series</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<ul id="series-reorder-list" class="list-group">
|
|
{% for series in case.get_ordered_series %}
|
|
<li class="list-group-item d-flex justify-content-between align-items-center" data-series-id="{{ series.pk }}">
|
|
<span>
|
|
<span class="series-block-series-number">Series {{ forloop.counter }}:</span>
|
|
{{ series.get_block|default:series }}
|
|
</span>
|
|
<span>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary move-up">↑</button>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary move-down">↓</button>
|
|
</span>
|
|
<input type="hidden" name="series_order" value="{{ series.pk }}">
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="submit" class="btn btn-primary">Save order</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// From series_viewer.html
|
|
function loadAnnotationAndViewportOnElement(annotation, viewport, dicom_element, current_image_id_index) {
|
|
cornerstoneTools.globalImageIdSpecificToolStateManager.clear(dicom_element);
|
|
cornerstone.getEnabledElement(dicom_element).viewport = viewport
|
|
cornerstoneTools.globalImageIdSpecificToolStateManager.restoreToolState(
|
|
annotation);
|
|
console.log("annotation", annotation)
|
|
console.log("viewport", viewport)
|
|
|
|
//cornerstone.getEnabledElement(dicom_element).toolStateManager.restoreToolState(annotationjson)
|
|
cornerstone.resize(dicom_element);
|
|
console.log("current_image_id_index", current_image_id_index)
|
|
//current_image_id_index = parseInt(current_image_id_index);
|
|
|
|
console.log(current_image_id_index, Number.isInteger(current_image_id_index))
|
|
if (Number.isInteger(current_image_id_index)) {
|
|
console.log("loading stack index")
|
|
dicomViewer.loadStackIndex(current_image_id_index, dicom_element);
|
|
} else {
|
|
try {
|
|
console.log("current_image_id_index", current_image_id_index)
|
|
dicomViewer.loadImageById(current_image_id_index, dicom_element);
|
|
} catch (e) {
|
|
console.log(e)
|
|
console.log("loading image by id failed")
|
|
console.log("loading next annotation image")
|
|
dicomViewer.getNextAnnotationImage(dicom_element);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
|
|
const dicomDetails = document.getElementById("dicom-viewer-details");
|
|
let dicomViewerLoaded = false;
|
|
dicomDetails.addEventListener("toggle", function() {
|
|
if (!dicomViewerLoaded && dicomDetails.open) {
|
|
// Use safe mount helper to avoid arbitrary timeouts
|
|
try { mountDicomViewersSafely(); } catch (e) { if (window.mountDicomViewers) try { window.mountDicomViewers(); } catch (er) { console.warn(er); } }
|
|
dicomViewerLoaded = true;
|
|
}
|
|
});
|
|
|
|
|
|
// Move up/down logic
|
|
document.querySelectorAll("#series-reorder-list .move-up, #series-reorder-list .move-down").forEach(function(btn) {
|
|
btn.addEventListener("click", function(e) {
|
|
e.preventDefault();
|
|
const li = btn.closest("li");
|
|
if (btn.classList.contains("move-up") && li.previousElementSibling) {
|
|
li.parentNode.insertBefore(li, li.previousElementSibling);
|
|
}
|
|
if (btn.classList.contains("move-down") && li.nextElementSibling) {
|
|
li.parentNode.insertBefore(li.nextElementSibling, li);
|
|
}
|
|
});
|
|
});
|
|
|
|
// On submit, update the hidden inputs to match the new order
|
|
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");
|
|
let draggedItem = null;
|
|
|
|
if (reorderList) {
|
|
reorderList.querySelectorAll("li").forEach(function(li) {
|
|
li.setAttribute("draggable", "true");
|
|
|
|
li.addEventListener("dragstart", function(e) {
|
|
draggedItem = li;
|
|
li.classList.add("dragging");
|
|
e.dataTransfer.effectAllowed = "move";
|
|
});
|
|
|
|
li.addEventListener("dragend", function() {
|
|
draggedItem = null;
|
|
li.classList.remove("dragging");
|
|
});
|
|
|
|
li.addEventListener("dragover", function(e) {
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = "move";
|
|
// Visual feedback
|
|
li.classList.add("drag-over");
|
|
});
|
|
|
|
li.addEventListener("dragleave", function() {
|
|
li.classList.remove("drag-over");
|
|
});
|
|
|
|
li.addEventListener("drop", function(e) {
|
|
e.preventDefault();
|
|
li.classList.remove("drag-over");
|
|
if (draggedItem && draggedItem !== li) {
|
|
// Insert before or after depending on mouse position
|
|
const bounding = li.getBoundingClientRect();
|
|
const offset = e.clientY - bounding.top;
|
|
if (offset < bounding.height / 2) {
|
|
li.parentNode.insertBefore(draggedItem, li);
|
|
} else {
|
|
li.parentNode.insertBefore(draggedItem, li.nextSibling);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
const detailsElement = document.getElementById("series-actions");
|
|
const buttons = document.querySelectorAll(".select-series-btn");
|
|
const seriesCheckboxes = document.querySelectorAll("input[type='checkbox'][name='series-ids']");
|
|
const selectAllBtn = document.getElementById("series-select-all-btn");
|
|
const deselectAllBtn = document.getElementById("series-deselect-all-btn");
|
|
|
|
function toggleCheckboxes() {
|
|
const isOpen = detailsElement && detailsElement.hasAttribute("open");
|
|
buttons.forEach((button) => {
|
|
button.style.display = isOpen ? "inline-block" : "none";
|
|
});
|
|
}
|
|
|
|
function syncSeriesSelectionUi() {
|
|
document.querySelectorAll(".select-series-btn").forEach(function(btn) {
|
|
const parent = btn.closest(".series-block");
|
|
if (!parent) {
|
|
return;
|
|
}
|
|
const checkbox = parent.querySelector("input[type='checkbox'][name='series-ids']");
|
|
if (!checkbox) {
|
|
return;
|
|
}
|
|
btn.classList.toggle("btn-primary", checkbox.checked);
|
|
btn.classList.toggle("btn-outline-primary", !checkbox.checked);
|
|
btn.textContent = checkbox.checked ? "Selected" : "Select";
|
|
if (checkbox.checked) {
|
|
parent.classList.add("highlight-series");
|
|
} else {
|
|
parent.classList.remove("highlight-series");
|
|
}
|
|
});
|
|
|
|
const selectedCount = Array.from(seriesCheckboxes).filter((checkbox) => checkbox.checked).length;
|
|
if (selectAllBtn) {
|
|
selectAllBtn.disabled = selectedCount === seriesCheckboxes.length && seriesCheckboxes.length > 0;
|
|
}
|
|
if (deselectAllBtn) {
|
|
deselectAllBtn.disabled = selectedCount === 0;
|
|
}
|
|
}
|
|
|
|
document.querySelectorAll(".select-series-btn").forEach(function(btn) {
|
|
btn.addEventListener("click", function() {
|
|
const parent = btn.closest(".series-block");
|
|
const checkbox = parent.querySelector("input[type='checkbox'][name='series-ids']");
|
|
checkbox.checked = !checkbox.checked;
|
|
syncSeriesSelectionUi();
|
|
});
|
|
});
|
|
|
|
seriesCheckboxes.forEach((checkbox) => {
|
|
checkbox.addEventListener("change", syncSeriesSelectionUi);
|
|
});
|
|
|
|
if (selectAllBtn) {
|
|
selectAllBtn.addEventListener("click", function() {
|
|
seriesCheckboxes.forEach((checkbox) => {
|
|
checkbox.checked = true;
|
|
});
|
|
syncSeriesSelectionUi();
|
|
});
|
|
}
|
|
|
|
if (deselectAllBtn) {
|
|
deselectAllBtn.addEventListener("click", function() {
|
|
seriesCheckboxes.forEach((checkbox) => {
|
|
checkbox.checked = false;
|
|
});
|
|
syncSeriesSelectionUi();
|
|
});
|
|
}
|
|
|
|
// Initial state
|
|
toggleCheckboxes();
|
|
syncSeriesSelectionUi();
|
|
|
|
|
|
// Show the alert when a result is received
|
|
const resultsSpan = document.getElementById("series-action-results");
|
|
const alertDiv = document.getElementById("series-action-alert");
|
|
const observer = new MutationObserver(function(mutations) {
|
|
if (resultsSpan.textContent.trim() !== "") {
|
|
alertDiv.style.display = "block";
|
|
} else {
|
|
alertDiv.style.display = "none";
|
|
}
|
|
});
|
|
observer.observe(resultsSpan, { childList: true, subtree: true });
|
|
|
|
// Listen for toggle events on the <details> element
|
|
if (detailsElement) {
|
|
detailsElement.addEventListener("toggle", toggleCheckboxes);
|
|
}
|
|
|
|
// Display Set modal handler
|
|
const dsModal = new bootstrap.Modal(document.getElementById("displaysetModal"));
|
|
const dsModalBody = document.getElementById("displaysetModalBody");
|
|
const dsFullscreenModal = new bootstrap.Modal(document.getElementById("displaysetFullscreenModal"));
|
|
|
|
// Loader functions: can be called directly on reload
|
|
// Helper: wait for mount function and mount the viewers safely
|
|
function waitForMountFn(timeout = 5000) {
|
|
if (window.mountDicomViewers) return Promise.resolve(window.mountDicomViewers);
|
|
return new Promise((resolve, reject) => {
|
|
const start = Date.now();
|
|
function poll() {
|
|
if (window.mountDicomViewers) return resolve(window.mountDicomViewers);
|
|
if (Date.now() - start > timeout) return reject(new Error('mountDicomViewers not available'));
|
|
requestAnimationFrame(poll);
|
|
}
|
|
poll();
|
|
});
|
|
}
|
|
|
|
async function mountDicomViewersSafely(timeout = 5000) {
|
|
try {
|
|
// wait for layout to stabilise
|
|
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
|
|
const mountFn = await waitForMountFn(timeout);
|
|
try { mountFn(); } catch (e) { console.warn('mountDicomViewers failed', e); }
|
|
// allow any async render to settle then force a resize event
|
|
requestAnimationFrame(() => {
|
|
try { window.dispatchEvent(new Event('resize')); } catch (e) {}
|
|
if (typeof window.resizeDicomViewers === 'function') try { window.resizeDicomViewers(); } catch (e) {}
|
|
});
|
|
} catch (err) {
|
|
console.warn('Viewer initialization deferred/fallback:', err);
|
|
}
|
|
}
|
|
|
|
async function loadDisplaysetModal(url, dsId) {
|
|
if (!url && dsId) url = `/atlas/${dsId}/display_sets/modal`;
|
|
if (!url) return;
|
|
dsModalBody.innerHTML = "<p>Loading...</p>";
|
|
try {
|
|
const resp = await fetch(url);
|
|
const html = await resp.text();
|
|
dsModalBody.innerHTML = html;
|
|
try {
|
|
const fsBtn = document.querySelector('.displayset-fullscreen-btn');
|
|
if (fsBtn) {
|
|
fsBtn.dataset.url = url;
|
|
if (dsId) fsBtn.dataset.dsId = dsId;
|
|
}
|
|
} catch (e) { console.warn(e); }
|
|
try { setUrlParam('displayset', dsId); } catch (e) {}
|
|
// Initialize viewer deterministically after injecting HTML
|
|
mountDicomViewersSafely();
|
|
dsModal.show();
|
|
} catch (error) {
|
|
console.error('Error loading display set details:', error);
|
|
dsModalBody.innerHTML = "<p>Failed to load display set.</p>";
|
|
}
|
|
}
|
|
|
|
async function loadDisplaysetFullscreen(url, dsId) {
|
|
if (!url && dsId) url = `/atlas/${dsId}/display_sets/modal`;
|
|
if (!url) return;
|
|
document.getElementById('displaysetFullscreenLeft').innerHTML = '<p style="color:#fff; padding:1rem;">Loading viewer...</p>';
|
|
document.getElementById('displaysetFullscreenRight').innerHTML = '<p>Loading details...</p>';
|
|
try {
|
|
const resp = await fetch(url);
|
|
const html = await resp.text();
|
|
const tmp = document.createElement('div');
|
|
tmp.innerHTML = html;
|
|
const viewer = tmp.querySelector('.dicom-viewer-root');
|
|
if (viewer) {
|
|
const left = document.getElementById('displaysetFullscreenLeft');
|
|
left.innerHTML = '';
|
|
left.appendChild(viewer);
|
|
try { viewer.style.height='100%'; viewer.style.minHeight='100%'; viewer.style.width='100%'; viewer.style.display='block'; viewer.style.flex='1 1 auto'; } catch (e){}
|
|
} else {
|
|
document.getElementById('displaysetFullscreenLeft').innerHTML = '<p style="color:#fff; padding:1rem;">No viewer available</p>';
|
|
}
|
|
const viewerInTmp = tmp.querySelector('.dicom-viewer-root');
|
|
if (viewerInTmp) viewerInTmp.remove();
|
|
document.getElementById('displaysetFullscreenRight').innerHTML = tmp.innerHTML;
|
|
try { if (dsId) { setUrlParam('displayset', dsId); setUrlParam('displayset_full', '1'); } } catch (e) {}
|
|
dsFullscreenModal.show();
|
|
// Ensure viewer is mounted and resized after moving into fullscreen
|
|
mountDicomViewersSafely();
|
|
} catch (error) {
|
|
console.error('Error loading fullscreen display set:', error);
|
|
document.getElementById('displaysetFullscreenLeft').innerHTML = '<p style="color:#fff; padding:1rem;">Failed to load viewer</p>';
|
|
document.getElementById('displaysetFullscreenRight').innerHTML = '<p>Failed to load details.</p>';
|
|
}
|
|
}
|
|
|
|
async function loadFindingModal(findingId, url) {
|
|
if (!url && findingId) url = `/atlas/series_finding/${findingId}/details/`;
|
|
if (!url) return;
|
|
const modalBody = document.getElementById('findingModalBody');
|
|
modalBody.innerHTML = '<p>Loading...</p>';
|
|
try {
|
|
const resp = await fetch(url);
|
|
const html = await resp.text();
|
|
modalBody.innerHTML = html;
|
|
// Ensure viewers mount after content injection
|
|
mountDicomViewersSafely();
|
|
try { setUrlParam('finding', findingId); } catch (e) {}
|
|
const modal = new bootstrap.Modal(document.getElementById('findingModal'));
|
|
modal.show();
|
|
document.getElementById('reload-finding')?.addEventListener('click', function () { mountDicomViewersSafely(); });
|
|
} catch (e) {
|
|
console.error('Error loading finding details:', e);
|
|
modalBody.innerHTML = '<p>Failed to load finding details.</p>';
|
|
}
|
|
}
|
|
|
|
// remove displayset param when modal closed
|
|
document.getElementById('displaysetModal').addEventListener('hidden.bs.modal', function () {
|
|
try { removeUrlParam('displayset'); removeUrlParam('displayset_full'); } catch (e) {}
|
|
});
|
|
|
|
// remove fullscreen param when fullscreen modal closed
|
|
document.getElementById('displaysetFullscreenModal').addEventListener('hidden.bs.modal', function () {
|
|
try { removeUrlParam('displayset_full'); } catch (e) {}
|
|
});
|
|
|
|
// FINDING modal: push/remove param so it can be reopened after reload
|
|
document.querySelectorAll(".view-finding-modal").forEach(button => {
|
|
button.addEventListener("click", function () {
|
|
const findingId = this.getAttribute("data-finding-id");
|
|
const url = this.getAttribute('data-url');
|
|
loadFindingModal(findingId, url);
|
|
});
|
|
});
|
|
document.getElementById('findingModal').addEventListener('hidden.bs.modal', function () {
|
|
try { removeUrlParam('finding'); } catch (e) {}
|
|
});
|
|
|
|
// Helper: set/remove URL search params without reloading
|
|
function setUrlParam(key, value) {
|
|
if (!key) return;
|
|
const url = new URL(window.location.href);
|
|
if (value === null || value === undefined) {
|
|
url.searchParams.delete(key);
|
|
} else {
|
|
url.searchParams.set(key, value);
|
|
}
|
|
window.history.replaceState({}, '', url.toString());
|
|
}
|
|
|
|
function removeUrlParam(key) {
|
|
const url = new URL(window.location.href);
|
|
url.searchParams.delete(key);
|
|
window.history.replaceState({}, '', url.toString());
|
|
}
|
|
|
|
// On load: reopen modals if URL params present — call loader functions directly
|
|
(function reopenModalsFromUrl() {
|
|
const params = new URL(window.location.href).searchParams;
|
|
const dsId = params.get('displayset');
|
|
const dsFull = params.get('displayset_full');
|
|
const findingId = params.get('finding');
|
|
if (dsId) {
|
|
// prefer to use existing button url if present, otherwise construct endpoint
|
|
const btn = document.querySelector(`.view-displayset-modal[data-ds-id="${dsId}"]`);
|
|
const url = btn ? btn.dataset.url : null;
|
|
if (dsFull) {
|
|
loadDisplaysetFullscreen(url, dsId);
|
|
} else {
|
|
loadDisplaysetModal(url, dsId);
|
|
}
|
|
} else if (findingId) {
|
|
// open finding modal
|
|
const fbtn = document.querySelector(`.view-finding-modal[data-finding-id="${findingId}"]`);
|
|
const url = fbtn ? fbtn.getAttribute('data-url') : null;
|
|
loadFindingModal(findingId, url);
|
|
}
|
|
})();
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
<style>
|
|
/* Highlight style for the series-block (subtle Bootstrap-like accent) */
|
|
.highlight-series {
|
|
background-color: rgba(13,110,253,0.04);
|
|
border: 1px solid rgba(13,110,253,0.12);
|
|
transition: box-shadow 0.18s ease, transform 0.12s ease;
|
|
box-shadow: 0 4px 12px rgba(13,110,253,0.04);
|
|
}
|
|
#series-reorder-list .dragging {
|
|
opacity: 0.5;
|
|
}
|
|
#series-reorder-list .drag-over {
|
|
border-top: 2px solid #0d6efd;
|
|
}
|
|
.series-actions[open] {
|
|
border: 2px solid #0d6efd;
|
|
border-radius: 6px;
|
|
padding: 8px;
|
|
margin-top: 8px;
|
|
}
|
|
.series-actions {
|
|
flex: 1 1 auto;
|
|
min-width: 0; /* allow flex children to shrink without overflow */
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: .5rem;
|
|
border: 1px solid rgba(0,0,0,0.06);
|
|
border-radius: .375rem;
|
|
padding: .5rem;
|
|
background: rgba(255,255,255,0.02);
|
|
margin-top: .5rem;
|
|
}
|
|
.series-actions-body {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: .5rem;
|
|
}
|
|
.series-selection-toolbar {
|
|
border-top: 1px solid rgba(0,0,0,0.08);
|
|
border-bottom: 1px solid rgba(0,0,0,0.08);
|
|
padding: .5rem 0;
|
|
}
|
|
.series-actions-grid .btn,
|
|
.series-actions-body > a.btn,
|
|
.series-move-panel .btn {
|
|
width: 100%;
|
|
}
|
|
.series-move-panel {
|
|
background: rgba(255,255,255,0.03);
|
|
}
|
|
.series-move-panel .case-details {
|
|
margin-bottom: .5rem;
|
|
}
|
|
.series-actions summary { list-style: none; }
|
|
.series-actions summary::-webkit-details-marker { display:none; }
|
|
.series-actions .btn {
|
|
vertical-align: middle;
|
|
text-align: left;
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
.series-actions { background: rgba(255,255,255,0.02); border-color: rgba(255,255,255,0.04); }
|
|
}
|
|
.link-buttons {
|
|
float: right;
|
|
}
|
|
.normal-block {
|
|
float: right;
|
|
}
|
|
</style> |