1714 lines
55 KiB
HTML
1714 lines
55 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}DICOM Workflow Management{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
|
<style>
|
|
.workflow-header {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border-radius: 0.5rem;
|
|
padding: 2rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.workflow-layout {
|
|
display: grid;
|
|
grid-template-columns: 300px 1fr;
|
|
gap: 2rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.workflow-sidebar {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1.5rem;
|
|
}
|
|
|
|
.workflow-main {
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.5rem;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.sidebar-section {
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.5rem;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.section-header {
|
|
background: #f8f9fa;
|
|
border-bottom: 1px solid #dee2e6;
|
|
padding: 1rem 1.5rem;
|
|
font-weight: 600;
|
|
color: #495057;
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: center;
|
|
}
|
|
|
|
.section-content {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.workflow-canvas {
|
|
position: relative;
|
|
min-height: 600px;
|
|
background: #f8f9fa;
|
|
background-image:
|
|
linear-gradient(rgba(0,0,0,.1) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(0,0,0,.1) 1px, transparent 1px);
|
|
background-size: 20px 20px;
|
|
overflow: auto;
|
|
}
|
|
|
|
.workflow-node {
|
|
position: absolute;
|
|
background: white;
|
|
border: 2px solid #dee2e6;
|
|
border-radius: 0.5rem;
|
|
padding: 1rem;
|
|
min-width: 150px;
|
|
cursor: move;
|
|
transition: all 0.2s;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.workflow-node:hover {
|
|
border-color: #007bff;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
}
|
|
|
|
.workflow-node.selected {
|
|
border-color: #007bff;
|
|
box-shadow: 0 0 0 3px rgba(0,123,255,0.25);
|
|
}
|
|
|
|
.workflow-node.processing {
|
|
border-color: #ffc107;
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
.workflow-node.completed {
|
|
border-color: #28a745;
|
|
}
|
|
|
|
.workflow-node.error {
|
|
border-color: #dc3545;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0% { box-shadow: 0 0 0 0 rgba(255,193,7,0.7); }
|
|
70% { box-shadow: 0 0 0 10px rgba(255,193,7,0); }
|
|
100% { box-shadow: 0 0 0 0 rgba(255,193,7,0); }
|
|
}
|
|
|
|
.node-header {
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: center;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.node-title {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.node-status {
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 0.25rem;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.status-pending { background: #e9ecef; color: #6c757d; }
|
|
.status-processing { background: #fff3cd; color: #856404; }
|
|
.status-completed { background: #d4edda; color: #155724; }
|
|
.status-error { background: #f8d7da; color: #721c24; }
|
|
|
|
.node-content {
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.node-progress {
|
|
background: #e9ecef;
|
|
border-radius: 0.25rem;
|
|
height: 4px;
|
|
overflow: hidden;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.progress-bar {
|
|
background: #007bff;
|
|
height: 100%;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.node-actions {
|
|
display: flex;
|
|
gap: 0.25rem;
|
|
justify-content: end;
|
|
}
|
|
|
|
.node-btn {
|
|
padding: 0.25rem 0.5rem;
|
|
border: none;
|
|
border-radius: 0.25rem;
|
|
cursor: pointer;
|
|
font-size: 0.75rem;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.btn-play { background: #d4edda; color: #155724; }
|
|
.btn-pause { background: #fff3cd; color: #856404; }
|
|
.btn-stop { background: #f8d7da; color: #721c24; }
|
|
.btn-config { background: #e2e3e5; color: #383d41; }
|
|
|
|
.workflow-connection {
|
|
position: absolute;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.connection-line {
|
|
stroke: #6c757d;
|
|
stroke-width: 2;
|
|
fill: none;
|
|
marker-end: url(#arrowhead);
|
|
}
|
|
|
|
.connection-line.active {
|
|
stroke: #007bff;
|
|
stroke-width: 3;
|
|
}
|
|
|
|
.node-palette {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.palette-node {
|
|
padding: 0.75rem;
|
|
border: 1px solid #dee2e6;
|
|
background: white;
|
|
border-radius: 0.25rem;
|
|
cursor: grab;
|
|
transition: all 0.2s;
|
|
text-align: center;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.palette-node:hover {
|
|
background: #f8f9fa;
|
|
border-color: #007bff;
|
|
}
|
|
|
|
.palette-node:active {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
.palette-icon {
|
|
font-size: 1.25rem;
|
|
margin-bottom: 0.5rem;
|
|
display: block;
|
|
color: #007bff;
|
|
}
|
|
|
|
.workflow-controls {
|
|
display: flex;
|
|
gap: 1rem;
|
|
align-items: center;
|
|
padding: 1rem 1.5rem;
|
|
background: #f8f9fa;
|
|
border-bottom: 1px solid #dee2e6;
|
|
}
|
|
|
|
.control-group {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.workflow-stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.stat-card {
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.375rem;
|
|
padding: 1rem;
|
|
text-align: center;
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.stat-card:hover {
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.stat-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin: 0 auto 0.5rem;
|
|
color: white;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.stat-number {
|
|
font-size: 1.5rem;
|
|
font-weight: bold;
|
|
color: #495057;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.stat-label {
|
|
color: #6c757d;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.workflow-templates {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.template-item {
|
|
padding: 0.75rem;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.25rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
background: white;
|
|
}
|
|
|
|
.template-item:hover {
|
|
background: #f8f9fa;
|
|
border-color: #007bff;
|
|
}
|
|
|
|
.template-name {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.template-description {
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.execution-log {
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
background: #f8f9fa;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.25rem;
|
|
padding: 0.75rem;
|
|
font-family: monospace;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.log-entry {
|
|
margin-bottom: 0.25rem;
|
|
padding: 0.25rem 0;
|
|
border-bottom: 1px solid #e9ecef;
|
|
}
|
|
|
|
.log-entry:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.log-timestamp {
|
|
color: #6c757d;
|
|
margin-right: 0.5rem;
|
|
}
|
|
|
|
.log-level {
|
|
font-weight: 600;
|
|
margin-right: 0.5rem;
|
|
}
|
|
|
|
.log-level.info { color: #007bff; }
|
|
.log-level.warning { color: #ffc107; }
|
|
.log-level.error { color: #dc3545; }
|
|
.log-level.success { color: #28a745; }
|
|
|
|
.log-message {
|
|
color: #495057;
|
|
}
|
|
|
|
.properties-panel {
|
|
background: #f8f9fa;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.25rem;
|
|
padding: 1rem;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.property-row {
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: center;
|
|
padding: 0.5rem 0;
|
|
border-bottom: 1px solid #dee2e6;
|
|
}
|
|
|
|
.property-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.property-label {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.property-value {
|
|
flex: 1;
|
|
margin-left: 1rem;
|
|
}
|
|
|
|
.property-value input,
|
|
.property-value select {
|
|
width: 100%;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.25rem;
|
|
padding: 0.25rem 0.5rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.workflow-toolbar {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.toolbar-btn {
|
|
padding: 0.5rem 1rem;
|
|
border: 1px solid #dee2e6;
|
|
background: white;
|
|
border-radius: 0.25rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
font-size: 0.875rem;
|
|
color: #495057;
|
|
}
|
|
|
|
.toolbar-btn:hover, .toolbar-btn.active {
|
|
background: #007bff;
|
|
color: white;
|
|
border-color: #007bff;
|
|
}
|
|
|
|
@media (max-width: 1200px) {
|
|
.workflow-layout {
|
|
grid-template-columns: 1fr;
|
|
gap: 1.5rem;
|
|
}
|
|
|
|
.workflow-sidebar {
|
|
order: 2;
|
|
}
|
|
|
|
.workflow-main {
|
|
order: 1;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.workflow-header {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.workflow-stats {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
|
|
.workflow-controls {
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.control-group {
|
|
justify-content: center;
|
|
}
|
|
}
|
|
|
|
@media print {
|
|
.workflow-sidebar, .workflow-controls, .btn {
|
|
display: none !important;
|
|
}
|
|
|
|
.workflow-layout {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.section-header {
|
|
background: none;
|
|
border-bottom: 2px solid #000;
|
|
color: #000;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div id="content" class="app-content">
|
|
<!-- Page Header -->
|
|
<div class="d-flex align-items-center mb-3">
|
|
<div>
|
|
<ol class="breadcrumb">
|
|
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
|
<li class="breadcrumb-item"><a href="{% url 'radiology:dashboard' %}">Radiology</a></li>
|
|
<li class="breadcrumb-item active">DICOM Workflow</li>
|
|
</ol>
|
|
<h1 class="page-header mb-0">
|
|
<i class="fas fa-project-diagram me-2"></i>DICOM Workflow Management
|
|
</h1>
|
|
</div>
|
|
<div class="ms-auto">
|
|
<button type="button" class="btn btn-outline-secondary me-2" onclick="importWorkflow()">
|
|
<i class="fas fa-upload me-1"></i>Import
|
|
</button>
|
|
<button type="button" class="btn btn-outline-info me-2" onclick="exportWorkflow()">
|
|
<i class="fas fa-download me-1"></i>Export
|
|
</button>
|
|
<button type="button" class="btn btn-outline-success me-2" onclick="saveWorkflow()">
|
|
<i class="fas fa-save me-1"></i>Save Workflow
|
|
</button>
|
|
<button type="button" class="btn btn-primary" onclick="createNewWorkflow()">
|
|
<i class="fas fa-plus me-1"></i>New Workflow
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Workflow Header -->
|
|
<div class="workflow-header">
|
|
<div class="row align-items-center">
|
|
<div class="col-md-8">
|
|
<h3 class="mb-2" id="workflow-title">DICOM Processing Workflow</h3>
|
|
<p class="mb-2" id="workflow-description">Automated DICOM file processing and analysis pipeline</p>
|
|
<div class="d-flex align-items-center gap-3">
|
|
<span class="badge bg-primary" id="workflow-status">Active</span>
|
|
<span class="badge bg-info" id="workflow-nodes">5 Nodes</span>
|
|
<span class="badge bg-success" id="workflow-files">12 Files Processed</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4 text-md-end">
|
|
<div class="text-white-50 mb-1">Last Execution</div>
|
|
<div class="h6 mb-2" id="last-execution">{{ workflow.last_execution|date:"M d, Y g:i A"|default:"Never" }}</div>
|
|
<div class="text-white-50">Next Run: <span id="next-run">{{ workflow.next_run|date:"M d, Y g:i A"|default:"Manual" }}</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Workflow Statistics -->
|
|
<div class="workflow-stats">
|
|
<div class="stat-card">
|
|
<div class="stat-icon" style="background: #007bff;">
|
|
<i class="fas fa-play"></i>
|
|
</div>
|
|
<div class="stat-number" id="stat-running">{{ stats.running|default:0 }}</div>
|
|
<div class="stat-label">Running</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon" style="background: #28a745;">
|
|
<i class="fas fa-check"></i>
|
|
</div>
|
|
<div class="stat-number" id="stat-completed">{{ stats.completed|default:0 }}</div>
|
|
<div class="stat-label">Completed</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon" style="background: #ffc107;">
|
|
<i class="fas fa-clock"></i>
|
|
</div>
|
|
<div class="stat-number" id="stat-queued">{{ stats.queued|default:0 }}</div>
|
|
<div class="stat-label">Queued</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon" style="background: #dc3545;">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
</div>
|
|
<div class="stat-number" id="stat-failed">{{ stats.failed|default:0 }}</div>
|
|
<div class="stat-label">Failed</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="workflow-layout">
|
|
<!-- Workflow Sidebar -->
|
|
<div class="workflow-sidebar">
|
|
<!-- Node Palette -->
|
|
<div class="sidebar-section">
|
|
<div class="section-header">
|
|
<i class="fas fa-puzzle-piece me-2"></i>Node Palette
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="node-palette">
|
|
<div class="palette-node" draggable="true" data-node-type="input">
|
|
<i class="fas fa-upload palette-icon"></i>
|
|
Input
|
|
</div>
|
|
<div class="palette-node" draggable="true" data-node-type="validate">
|
|
<i class="fas fa-check-circle palette-icon"></i>
|
|
Validate
|
|
</div>
|
|
<div class="palette-node" draggable="true" data-node-type="convert">
|
|
<i class="fas fa-exchange-alt palette-icon"></i>
|
|
Convert
|
|
</div>
|
|
<div class="palette-node" draggable="true" data-node-type="analyze">
|
|
<i class="fas fa-search palette-icon"></i>
|
|
Analyze
|
|
</div>
|
|
<div class="palette-node" draggable="true" data-node-type="anonymize">
|
|
<i class="fas fa-user-secret palette-icon"></i>
|
|
Anonymize
|
|
</div>
|
|
<div class="palette-node" draggable="true" data-node-type="archive">
|
|
<i class="fas fa-archive palette-icon"></i>
|
|
Archive
|
|
</div>
|
|
<div class="palette-node" draggable="true" data-node-type="notify">
|
|
<i class="fas fa-bell palette-icon"></i>
|
|
Notify
|
|
</div>
|
|
<div class="palette-node" draggable="true" data-node-type="output">
|
|
<i class="fas fa-download palette-icon"></i>
|
|
Output
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Workflow Templates -->
|
|
<div class="sidebar-section">
|
|
<div class="section-header">
|
|
<i class="fas fa-layer-group me-2"></i>Templates
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="workflow-templates">
|
|
<div class="template-item" onclick="loadTemplate('basic_processing')">
|
|
<div class="template-name">Basic Processing</div>
|
|
<div class="template-description">Input → Validate → Archive</div>
|
|
</div>
|
|
<div class="template-item" onclick="loadTemplate('full_analysis')">
|
|
<div class="template-name">Full Analysis</div>
|
|
<div class="template-description">Complete processing with analysis</div>
|
|
</div>
|
|
<div class="template-item" onclick="loadTemplate('anonymization')">
|
|
<div class="template-name">Anonymization</div>
|
|
<div class="template-description">Anonymize and archive workflow</div>
|
|
</div>
|
|
<div class="template-item" onclick="loadTemplate('quality_control')">
|
|
<div class="template-name">Quality Control</div>
|
|
<div class="template-description">QC validation and reporting</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Properties Panel -->
|
|
<div class="sidebar-section" id="properties-panel" style="display: none;">
|
|
<div class="section-header">
|
|
<i class="fas fa-cog me-2"></i>Node Properties
|
|
</div>
|
|
<div class="section-content">
|
|
<div id="node-properties">
|
|
<!-- Properties will be populated when a node is selected -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Execution Log -->
|
|
<div class="sidebar-section">
|
|
<div class="section-header">
|
|
<div>
|
|
<i class="fas fa-list me-2"></i>Execution Log
|
|
</div>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="clearLog()">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="execution-log" id="execution-log">
|
|
<div class="log-entry">
|
|
<span class="log-timestamp">10:30:15</span>
|
|
<span class="log-level info">INFO</span>
|
|
<span class="log-message">Workflow started</span>
|
|
</div>
|
|
<div class="log-entry">
|
|
<span class="log-timestamp">10:30:16</span>
|
|
<span class="log-level success">SUCCESS</span>
|
|
<span class="log-message">Input node completed</span>
|
|
</div>
|
|
<div class="log-entry">
|
|
<span class="log-timestamp">10:30:18</span>
|
|
<span class="log-level info">INFO</span>
|
|
<span class="log-message">Validation in progress</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Workflow Main Area -->
|
|
<div class="workflow-main">
|
|
<!-- Workflow Controls -->
|
|
<div class="workflow-controls">
|
|
<div class="control-group">
|
|
<button type="button" class="btn btn-success btn-sm" onclick="startWorkflow()" id="start-btn">
|
|
<i class="fas fa-play me-1"></i>Start
|
|
</button>
|
|
<button type="button" class="btn btn-warning btn-sm" onclick="pauseWorkflow()" id="pause-btn" disabled>
|
|
<i class="fas fa-pause me-1"></i>Pause
|
|
</button>
|
|
<button type="button" class="btn btn-danger btn-sm" onclick="stopWorkflow()" id="stop-btn" disabled>
|
|
<i class="fas fa-stop me-1"></i>Stop
|
|
</button>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label class="form-label mb-0 me-2">Auto-run:</label>
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" id="auto-run" onchange="toggleAutoRun()">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label class="form-label mb-0 me-2">Interval:</label>
|
|
<select class="form-select form-select-sm" id="run-interval" style="width: auto;">
|
|
<option value="manual">Manual</option>
|
|
<option value="5min">Every 5 minutes</option>
|
|
<option value="15min">Every 15 minutes</option>
|
|
<option value="1hour">Every hour</option>
|
|
<option value="daily">Daily</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="control-group ms-auto">
|
|
<div class="workflow-toolbar">
|
|
<button type="button" class="toolbar-btn active" onclick="setMode('select')" data-mode="select">
|
|
<i class="fas fa-mouse-pointer me-1"></i>Select
|
|
</button>
|
|
<button type="button" class="toolbar-btn" onclick="setMode('connect')" data-mode="connect">
|
|
<i class="fas fa-link me-1"></i>Connect
|
|
</button>
|
|
<button type="button" class="toolbar-btn" onclick="setMode('delete')" data-mode="delete">
|
|
<i class="fas fa-trash me-1"></i>Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Workflow Canvas -->
|
|
<div class="workflow-canvas" id="workflow-canvas">
|
|
<!-- SVG for connections -->
|
|
<svg style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;">
|
|
<defs>
|
|
<marker id="arrowhead" markerWidth="10" markerHeight="7"
|
|
refX="9" refY="3.5" orient="auto">
|
|
<polygon points="0 0, 10 3.5, 0 7" fill="#6c757d" />
|
|
</marker>
|
|
</defs>
|
|
<g id="connections"></g>
|
|
</svg>
|
|
|
|
<!-- Sample workflow nodes -->
|
|
<div class="workflow-node completed" style="top: 50px; left: 50px;" data-node-id="1">
|
|
<div class="node-header">
|
|
<div class="node-title">DICOM Input</div>
|
|
<div class="node-status status-completed">Completed</div>
|
|
</div>
|
|
<div class="node-content">
|
|
Receives DICOM files from PACS or upload
|
|
</div>
|
|
<div class="node-progress">
|
|
<div class="progress-bar" style="width: 100%;"></div>
|
|
</div>
|
|
<div class="node-actions">
|
|
<button type="button" class="node-btn btn-config" onclick="configureNode(1)">
|
|
<i class="fas fa-cog"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="workflow-node processing" style="top: 50px; left: 250px;" data-node-id="2">
|
|
<div class="node-header">
|
|
<div class="node-title">Validation</div>
|
|
<div class="node-status status-processing">Processing</div>
|
|
</div>
|
|
<div class="node-content">
|
|
Validates DICOM file integrity and metadata
|
|
</div>
|
|
<div class="node-progress">
|
|
<div class="progress-bar" style="width: 65%;"></div>
|
|
</div>
|
|
<div class="node-actions">
|
|
<button type="button" class="node-btn btn-pause" onclick="pauseNode(2)">
|
|
<i class="fas fa-pause"></i>
|
|
</button>
|
|
<button type="button" class="node-btn btn-config" onclick="configureNode(2)">
|
|
<i class="fas fa-cog"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="workflow-node" style="top: 200px; left: 150px;" data-node-id="3">
|
|
<div class="node-header">
|
|
<div class="node-title">Analysis</div>
|
|
<div class="node-status status-pending">Pending</div>
|
|
</div>
|
|
<div class="node-content">
|
|
Performs automated image analysis
|
|
</div>
|
|
<div class="node-progress">
|
|
<div class="progress-bar" style="width: 0%;"></div>
|
|
</div>
|
|
<div class="node-actions">
|
|
<button type="button" class="node-btn btn-play" onclick="startNode(3)">
|
|
<i class="fas fa-play"></i>
|
|
</button>
|
|
<button type="button" class="node-btn btn-config" onclick="configureNode(3)">
|
|
<i class="fas fa-cog"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="workflow-node" style="top: 350px; left: 50px;" data-node-id="4">
|
|
<div class="node-header">
|
|
<div class="node-title">Anonymization</div>
|
|
<div class="node-status status-pending">Pending</div>
|
|
</div>
|
|
<div class="node-content">
|
|
Removes patient identifying information
|
|
</div>
|
|
<div class="node-progress">
|
|
<div class="progress-bar" style="width: 0%;"></div>
|
|
</div>
|
|
<div class="node-actions">
|
|
<button type="button" class="node-btn btn-play" onclick="startNode(4)">
|
|
<i class="fas fa-play"></i>
|
|
</button>
|
|
<button type="button" class="node-btn btn-config" onclick="configureNode(4)">
|
|
<i class="fas fa-cog"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="workflow-node" style="top: 350px; left: 250px;" data-node-id="5">
|
|
<div class="node-header">
|
|
<div class="node-title">Archive</div>
|
|
<div class="node-status status-pending">Pending</div>
|
|
</div>
|
|
<div class="node-content">
|
|
Archives processed files to storage
|
|
</div>
|
|
<div class="node-progress">
|
|
<div class="progress-bar" style="width: 0%;"></div>
|
|
</div>
|
|
<div class="node-actions">
|
|
<button type="button" class="node-btn btn-play" onclick="startNode(5)">
|
|
<i class="fas fa-play"></i>
|
|
</button>
|
|
<button type="button" class="node-btn btn-config" onclick="configureNode(5)">
|
|
<i class="fas fa-cog"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Node Configuration Modal -->
|
|
<div class="modal fade" id="nodeConfigModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-cog me-2"></i>Node Configuration
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div id="node-config-content">
|
|
<!-- Configuration content will be loaded here -->
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
|
<i class="fas fa-times me-1"></i>Cancel
|
|
</button>
|
|
<button type="button" class="btn btn-primary" onclick="saveNodeConfig()">
|
|
<i class="fas fa-save me-1"></i>Save Configuration
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Workflow Template Modal -->
|
|
<div class="modal fade" id="templateModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-layer-group me-2"></i>Load Workflow Template
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="alert alert-info">
|
|
<i class="fas fa-info-circle me-2"></i>
|
|
Loading this template will replace the current workflow. Any unsaved changes will be lost.
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Template Name</label>
|
|
<input type="text" class="form-control" id="template-name" readonly>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Description</label>
|
|
<textarea class="form-control" id="template-description" rows="3" readonly></textarea>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Nodes</label>
|
|
<div id="template-nodes" class="border rounded p-2 bg-light">
|
|
<!-- Template nodes will be listed here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
|
<i class="fas fa-times me-1"></i>Cancel
|
|
</button>
|
|
<button type="button" class="btn btn-primary" onclick="confirmLoadTemplate()">
|
|
<i class="fas fa-download me-1"></i>Load Template
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
|
|
|
|
<script>
|
|
let currentMode = 'select';
|
|
let selectedNode = null;
|
|
let workflowNodes = [];
|
|
let workflowConnections = [];
|
|
let isWorkflowRunning = false;
|
|
let draggedNode = null;
|
|
|
|
$(document).ready(function() {
|
|
initializeWorkflow();
|
|
setupEventListeners();
|
|
loadWorkflowData();
|
|
|
|
// Auto-refresh workflow status
|
|
setInterval(updateWorkflowStatus, 5000);
|
|
});
|
|
|
|
function initializeWorkflow() {
|
|
// Initialize drag and drop for palette nodes
|
|
$('.palette-node').on('dragstart', function(e) {
|
|
draggedNode = $(this).data('node-type');
|
|
e.originalEvent.dataTransfer.effectAllowed = 'copy';
|
|
});
|
|
|
|
// Setup canvas drop zone
|
|
$('#workflow-canvas').on('dragover', function(e) {
|
|
e.preventDefault();
|
|
e.originalEvent.dataTransfer.dropEffect = 'copy';
|
|
});
|
|
|
|
$('#workflow-canvas').on('drop', function(e) {
|
|
e.preventDefault();
|
|
if (draggedNode) {
|
|
const rect = this.getBoundingClientRect();
|
|
const x = e.originalEvent.clientX - rect.left;
|
|
const y = e.originalEvent.clientY - rect.top;
|
|
|
|
createNode(draggedNode, x, y);
|
|
draggedNode = null;
|
|
}
|
|
});
|
|
|
|
// Make existing nodes draggable
|
|
makeNodesDraggable();
|
|
|
|
// Draw initial connections
|
|
drawConnections();
|
|
}
|
|
|
|
function setupEventListeners() {
|
|
// Node selection
|
|
$(document).on('click', '.workflow-node', function(e) {
|
|
if (currentMode === 'select') {
|
|
selectNode($(this));
|
|
} else if (currentMode === 'delete') {
|
|
deleteNode($(this));
|
|
}
|
|
e.stopPropagation();
|
|
});
|
|
|
|
// Canvas click (deselect)
|
|
$('#workflow-canvas').on('click', function(e) {
|
|
if (e.target === this) {
|
|
deselectAllNodes();
|
|
}
|
|
});
|
|
}
|
|
|
|
function loadWorkflowData() {
|
|
// Load existing workflow data
|
|
fetch('/radiology/dicom/workflow/data/', {
|
|
method: 'GET',
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
updateWorkflowInfo(data.workflow);
|
|
updateStatistics(data.stats);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading workflow data:', error);
|
|
});
|
|
}
|
|
|
|
function updateWorkflowStatus() {
|
|
fetch('/radiology/dicom/workflow/status/', {
|
|
method: 'GET',
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
updateNodeStatuses(data.nodes);
|
|
updateExecutionLog(data.log_entries);
|
|
updateStatistics(data.stats);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error updating workflow status:', error);
|
|
});
|
|
}
|
|
|
|
function setMode(mode) {
|
|
currentMode = mode;
|
|
|
|
// Update toolbar buttons
|
|
$('.toolbar-btn').removeClass('active');
|
|
$(`[data-mode="${mode}"]`).addClass('active');
|
|
|
|
// Update cursor
|
|
const canvas = $('#workflow-canvas');
|
|
switch (mode) {
|
|
case 'connect':
|
|
canvas.css('cursor', 'crosshair');
|
|
break;
|
|
case 'delete':
|
|
canvas.css('cursor', 'not-allowed');
|
|
break;
|
|
default:
|
|
canvas.css('cursor', 'default');
|
|
}
|
|
}
|
|
|
|
function createNode(type, x, y) {
|
|
const nodeId = Date.now();
|
|
const nodeConfig = getNodeConfig(type);
|
|
|
|
const nodeHtml = `
|
|
<div class="workflow-node" style="top: ${y}px; left: ${x}px;" data-node-id="${nodeId}">
|
|
<div class="node-header">
|
|
<div class="node-title">${nodeConfig.title}</div>
|
|
<div class="node-status status-pending">Pending</div>
|
|
</div>
|
|
<div class="node-content">
|
|
${nodeConfig.description}
|
|
</div>
|
|
<div class="node-progress">
|
|
<div class="progress-bar" style="width: 0%;"></div>
|
|
</div>
|
|
<div class="node-actions">
|
|
<button type="button" class="node-btn btn-play" onclick="startNode(${nodeId})">
|
|
<i class="fas fa-play"></i>
|
|
</button>
|
|
<button type="button" class="node-btn btn-config" onclick="configureNode(${nodeId})">
|
|
<i class="fas fa-cog"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
$('#workflow-canvas').append(nodeHtml);
|
|
|
|
// Make the new node draggable
|
|
makeNodeDraggable($(`[data-node-id="${nodeId}"]`));
|
|
|
|
// Add to nodes array
|
|
workflowNodes.push({
|
|
id: nodeId,
|
|
type: type,
|
|
x: x,
|
|
y: y,
|
|
config: nodeConfig
|
|
});
|
|
|
|
showAlert(`${nodeConfig.title} node added`, 'success');
|
|
}
|
|
|
|
function getNodeConfig(type) {
|
|
const configs = {
|
|
input: {
|
|
title: 'DICOM Input',
|
|
description: 'Receives DICOM files from PACS or upload',
|
|
icon: 'fas fa-upload'
|
|
},
|
|
validate: {
|
|
title: 'Validation',
|
|
description: 'Validates DICOM file integrity and metadata',
|
|
icon: 'fas fa-check-circle'
|
|
},
|
|
convert: {
|
|
title: 'Format Conversion',
|
|
description: 'Converts DICOM to other formats',
|
|
icon: 'fas fa-exchange-alt'
|
|
},
|
|
analyze: {
|
|
title: 'Analysis',
|
|
description: 'Performs automated image analysis',
|
|
icon: 'fas fa-search'
|
|
},
|
|
anonymize: {
|
|
title: 'Anonymization',
|
|
description: 'Removes patient identifying information',
|
|
icon: 'fas fa-user-secret'
|
|
},
|
|
archive: {
|
|
title: 'Archive',
|
|
description: 'Archives processed files to storage',
|
|
icon: 'fas fa-archive'
|
|
},
|
|
notify: {
|
|
title: 'Notification',
|
|
description: 'Sends notifications about processing status',
|
|
icon: 'fas fa-bell'
|
|
},
|
|
output: {
|
|
title: 'Output',
|
|
description: 'Outputs processed files to destination',
|
|
icon: 'fas fa-download'
|
|
}
|
|
};
|
|
|
|
return configs[type] || configs.input;
|
|
}
|
|
|
|
function makeNodesDraggable() {
|
|
$('.workflow-node').each(function() {
|
|
makeNodeDraggable($(this));
|
|
});
|
|
}
|
|
|
|
function makeNodeDraggable(node) {
|
|
node.draggable({
|
|
containment: '#workflow-canvas',
|
|
grid: [10, 10],
|
|
start: function() {
|
|
$(this).css('z-index', 1000);
|
|
},
|
|
stop: function() {
|
|
$(this).css('z-index', 'auto');
|
|
updateNodePosition($(this));
|
|
drawConnections();
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateNodePosition(node) {
|
|
const nodeId = node.data('node-id');
|
|
const position = node.position();
|
|
|
|
// Update node position in array
|
|
const nodeData = workflowNodes.find(n => n.id == nodeId);
|
|
if (nodeData) {
|
|
nodeData.x = position.left;
|
|
nodeData.y = position.top;
|
|
}
|
|
}
|
|
|
|
function selectNode(node) {
|
|
deselectAllNodes();
|
|
node.addClass('selected');
|
|
selectedNode = node;
|
|
|
|
// Show properties panel
|
|
showNodeProperties(node);
|
|
}
|
|
|
|
function deselectAllNodes() {
|
|
$('.workflow-node').removeClass('selected');
|
|
selectedNode = null;
|
|
$('#properties-panel').hide();
|
|
}
|
|
|
|
function deleteNode(node) {
|
|
if (confirm('Are you sure you want to delete this node?')) {
|
|
const nodeId = node.data('node-id');
|
|
|
|
// Remove from DOM
|
|
node.remove();
|
|
|
|
// Remove from arrays
|
|
workflowNodes = workflowNodes.filter(n => n.id != nodeId);
|
|
workflowConnections = workflowConnections.filter(c =>
|
|
c.from != nodeId && c.to != nodeId
|
|
);
|
|
|
|
// Redraw connections
|
|
drawConnections();
|
|
|
|
showAlert('Node deleted', 'success');
|
|
}
|
|
}
|
|
|
|
function showNodeProperties(node) {
|
|
const nodeId = node.data('node-id');
|
|
const nodeData = workflowNodes.find(n => n.id == nodeId);
|
|
|
|
if (!nodeData) return;
|
|
|
|
const propertiesHtml = `
|
|
<div class="property-row">
|
|
<div class="property-label">Name:</div>
|
|
<div class="property-value">
|
|
<input type="text" value="${nodeData.config.title}" onchange="updateNodeProperty(${nodeId}, 'title', this.value)">
|
|
</div>
|
|
</div>
|
|
<div class="property-row">
|
|
<div class="property-label">Description:</div>
|
|
<div class="property-value">
|
|
<input type="text" value="${nodeData.config.description}" onchange="updateNodeProperty(${nodeId}, 'description', this.value)">
|
|
</div>
|
|
</div>
|
|
<div class="property-row">
|
|
<div class="property-label">Enabled:</div>
|
|
<div class="property-value">
|
|
<select onchange="updateNodeProperty(${nodeId}, 'enabled', this.value)">
|
|
<option value="true" ${nodeData.config.enabled !== false ? 'selected' : ''}>Yes</option>
|
|
<option value="false" ${nodeData.config.enabled === false ? 'selected' : ''}>No</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
$('#node-properties').html(propertiesHtml);
|
|
$('#properties-panel').show();
|
|
}
|
|
|
|
function updateNodeProperty(nodeId, property, value) {
|
|
const nodeData = workflowNodes.find(n => n.id == nodeId);
|
|
if (nodeData) {
|
|
nodeData.config[property] = value;
|
|
|
|
// Update node display if needed
|
|
if (property === 'title') {
|
|
$(`[data-node-id="${nodeId}"] .node-title`).text(value);
|
|
} else if (property === 'description') {
|
|
$(`[data-node-id="${nodeId}"] .node-content`).text(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
function drawConnections() {
|
|
const connectionsGroup = $('#connections');
|
|
connectionsGroup.empty();
|
|
|
|
// Sample connections (in a real implementation, these would be stored)
|
|
const connections = [
|
|
{ from: 1, to: 2 },
|
|
{ from: 2, to: 3 },
|
|
{ from: 3, to: 4 },
|
|
{ from: 3, to: 5 }
|
|
];
|
|
|
|
connections.forEach(connection => {
|
|
const fromNode = $(`[data-node-id="${connection.from}"]`);
|
|
const toNode = $(`[data-node-id="${connection.to}"]`);
|
|
|
|
if (fromNode.length && toNode.length) {
|
|
const fromPos = fromNode.position();
|
|
const toPos = toNode.position();
|
|
|
|
const fromX = fromPos.left + fromNode.outerWidth();
|
|
const fromY = fromPos.top + fromNode.outerHeight() / 2;
|
|
const toX = toPos.left;
|
|
const toY = toPos.top + toNode.outerHeight() / 2;
|
|
|
|
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
line.setAttribute('x1', fromX);
|
|
line.setAttribute('y1', fromY);
|
|
line.setAttribute('x2', toX);
|
|
line.setAttribute('y2', toY);
|
|
line.setAttribute('class', 'connection-line');
|
|
|
|
connectionsGroup.append(line);
|
|
}
|
|
});
|
|
}
|
|
|
|
function configureNode(nodeId) {
|
|
const nodeData = workflowNodes.find(n => n.id == nodeId);
|
|
if (!nodeData) return;
|
|
|
|
// Load node-specific configuration
|
|
const configHtml = generateNodeConfig(nodeData);
|
|
$('#node-config-content').html(configHtml);
|
|
|
|
// Store current node ID for saving
|
|
window.currentConfigNodeId = nodeId;
|
|
|
|
new bootstrap.Modal(document.getElementById('nodeConfigModal')).show();
|
|
}
|
|
|
|
function generateNodeConfig(nodeData) {
|
|
// Generate configuration form based on node type
|
|
switch (nodeData.type) {
|
|
case 'input':
|
|
return `
|
|
<div class="mb-3">
|
|
<label class="form-label">Input Source</label>
|
|
<select class="form-control">
|
|
<option>PACS Server</option>
|
|
<option>File Upload</option>
|
|
<option>Directory Watch</option>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">File Filter</label>
|
|
<input type="text" class="form-control" placeholder="*.dcm">
|
|
</div>
|
|
`;
|
|
case 'validate':
|
|
return `
|
|
<div class="mb-3">
|
|
<label class="form-label">Validation Rules</label>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" checked>
|
|
<label class="form-check-label">Check DICOM compliance</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" checked>
|
|
<label class="form-check-label">Validate metadata</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox">
|
|
<label class="form-check-label">Check image integrity</label>
|
|
</div>
|
|
</div>
|
|
`;
|
|
case 'analyze':
|
|
return `
|
|
<div class="mb-3">
|
|
<label class="form-label">Analysis Type</label>
|
|
<select class="form-control">
|
|
<option>Basic Statistics</option>
|
|
<option>Image Quality Assessment</option>
|
|
<option>Automated Measurements</option>
|
|
<option>AI Analysis</option>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Output Format</label>
|
|
<select class="form-control">
|
|
<option>JSON Report</option>
|
|
<option>DICOM SR</option>
|
|
<option>PDF Report</option>
|
|
</select>
|
|
</div>
|
|
`;
|
|
default:
|
|
return `
|
|
<div class="mb-3">
|
|
<label class="form-label">Configuration</label>
|
|
<textarea class="form-control" rows="5" placeholder="Enter configuration parameters..."></textarea>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
function saveNodeConfig() {
|
|
const nodeId = window.currentConfigNodeId;
|
|
// Save configuration logic here
|
|
|
|
bootstrap.Modal.getInstance(document.getElementById('nodeConfigModal')).hide();
|
|
showAlert('Node configuration saved', 'success');
|
|
}
|
|
|
|
function startWorkflow() {
|
|
if (isWorkflowRunning) {
|
|
showAlert('Workflow is already running', 'warning');
|
|
return;
|
|
}
|
|
|
|
fetch('/radiology/dicom/workflow/start/', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
isWorkflowRunning = true;
|
|
updateWorkflowControls();
|
|
showAlert('Workflow started', 'success');
|
|
addLogEntry('info', 'Workflow execution started');
|
|
} else {
|
|
showAlert('Error starting workflow', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error starting workflow', 'danger');
|
|
});
|
|
}
|
|
|
|
function pauseWorkflow() {
|
|
fetch('/radiology/dicom/workflow/pause/', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Workflow paused', 'warning');
|
|
addLogEntry('warning', 'Workflow execution paused');
|
|
}
|
|
});
|
|
}
|
|
|
|
function stopWorkflow() {
|
|
if (!isWorkflowRunning) {
|
|
showAlert('Workflow is not running', 'info');
|
|
return;
|
|
}
|
|
|
|
if (confirm('Are you sure you want to stop the workflow?')) {
|
|
fetch('/radiology/dicom/workflow/stop/', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
isWorkflowRunning = false;
|
|
updateWorkflowControls();
|
|
showAlert('Workflow stopped', 'danger');
|
|
addLogEntry('error', 'Workflow execution stopped');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function updateWorkflowControls() {
|
|
if (isWorkflowRunning) {
|
|
$('#start-btn').prop('disabled', true);
|
|
$('#pause-btn').prop('disabled', false);
|
|
$('#stop-btn').prop('disabled', false);
|
|
} else {
|
|
$('#start-btn').prop('disabled', false);
|
|
$('#pause-btn').prop('disabled', true);
|
|
$('#stop-btn').prop('disabled', true);
|
|
}
|
|
}
|
|
|
|
function startNode(nodeId) {
|
|
$(`[data-node-id="${nodeId}"]`).removeClass('completed error').addClass('processing');
|
|
$(`[data-node-id="${nodeId}"] .node-status`).removeClass().addClass('node-status status-processing').text('Processing');
|
|
|
|
addLogEntry('info', `Node ${nodeId} started`);
|
|
}
|
|
|
|
function pauseNode(nodeId) {
|
|
$(`[data-node-id="${nodeId}"]`).removeClass('processing');
|
|
$(`[data-node-id="${nodeId}"] .node-status`).removeClass().addClass('node-status status-pending').text('Paused');
|
|
|
|
addLogEntry('warning', `Node ${nodeId} paused`);
|
|
}
|
|
|
|
function updateNodeStatuses(nodes) {
|
|
nodes.forEach(node => {
|
|
const nodeElement = $(`[data-node-id="${node.id}"]`);
|
|
nodeElement.removeClass('processing completed error').addClass(node.status);
|
|
nodeElement.find('.node-status').removeClass().addClass(`node-status status-${node.status}`).text(node.status_display);
|
|
nodeElement.find('.progress-bar').css('width', `${node.progress}%`);
|
|
});
|
|
}
|
|
|
|
function updateExecutionLog(entries) {
|
|
const log = $('#execution-log');
|
|
|
|
entries.forEach(entry => {
|
|
addLogEntry(entry.level, entry.message, entry.timestamp);
|
|
});
|
|
}
|
|
|
|
function addLogEntry(level, message, timestamp = null) {
|
|
const log = $('#execution-log');
|
|
const time = timestamp || new Date().toLocaleTimeString();
|
|
|
|
const entryHtml = `
|
|
<div class="log-entry">
|
|
<span class="log-timestamp">${time}</span>
|
|
<span class="log-level ${level}">${level.toUpperCase()}</span>
|
|
<span class="log-message">${message}</span>
|
|
</div>
|
|
`;
|
|
|
|
log.append(entryHtml);
|
|
log.scrollTop(log[0].scrollHeight);
|
|
}
|
|
|
|
function clearLog() {
|
|
$('#execution-log').empty();
|
|
}
|
|
|
|
function updateStatistics(stats) {
|
|
$('#stat-running').text(stats.running || 0);
|
|
$('#stat-completed').text(stats.completed || 0);
|
|
$('#stat-queued').text(stats.queued || 0);
|
|
$('#stat-failed').text(stats.failed || 0);
|
|
}
|
|
|
|
function updateWorkflowInfo(workflow) {
|
|
$('#workflow-title').text(workflow.name || 'DICOM Processing Workflow');
|
|
$('#workflow-description').text(workflow.description || 'Automated DICOM file processing and analysis pipeline');
|
|
$('#workflow-status').text(workflow.status || 'Active');
|
|
$('#workflow-nodes').text(`${workflow.node_count || 5} Nodes`);
|
|
$('#workflow-files').text(`${workflow.files_processed || 0} Files Processed`);
|
|
}
|
|
|
|
function toggleAutoRun() {
|
|
const autoRun = $('#auto-run').is(':checked');
|
|
const interval = $('#run-interval').val();
|
|
|
|
if (autoRun && interval === 'manual') {
|
|
$('#run-interval').val('1hour');
|
|
}
|
|
|
|
// Save auto-run settings
|
|
fetch('/radiology/dicom/workflow/auto-run/', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
enabled: autoRun,
|
|
interval: $('#run-interval').val()
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert(`Auto-run ${autoRun ? 'enabled' : 'disabled'}`, 'success');
|
|
}
|
|
});
|
|
}
|
|
|
|
function loadTemplate(templateName) {
|
|
// Load template information
|
|
const templates = {
|
|
basic_processing: {
|
|
name: 'Basic Processing',
|
|
description: 'Simple workflow for basic DICOM processing: Input → Validate → Archive',
|
|
nodes: ['Input', 'Validation', 'Archive']
|
|
},
|
|
full_analysis: {
|
|
name: 'Full Analysis',
|
|
description: 'Complete processing workflow with analysis and reporting',
|
|
nodes: ['Input', 'Validation', 'Analysis', 'Report Generation', 'Archive']
|
|
},
|
|
anonymization: {
|
|
name: 'Anonymization',
|
|
description: 'Workflow for anonymizing DICOM files before archiving',
|
|
nodes: ['Input', 'Validation', 'Anonymization', 'Archive']
|
|
},
|
|
quality_control: {
|
|
name: 'Quality Control',
|
|
description: 'QC workflow with validation and quality assessment',
|
|
nodes: ['Input', 'Validation', 'Quality Check', 'Report', 'Archive']
|
|
}
|
|
};
|
|
|
|
const template = templates[templateName];
|
|
if (template) {
|
|
$('#template-name').val(template.name);
|
|
$('#template-description').val(template.description);
|
|
$('#template-nodes').html(template.nodes.map(node => `<span class="badge bg-primary me-1">${node}</span>`).join(''));
|
|
|
|
// Store template for loading
|
|
window.currentTemplate = templateName;
|
|
|
|
new bootstrap.Modal(document.getElementById('templateModal')).show();
|
|
}
|
|
}
|
|
|
|
function confirmLoadTemplate() {
|
|
const templateName = window.currentTemplate;
|
|
|
|
// Clear current workflow
|
|
$('.workflow-node').remove();
|
|
workflowNodes = [];
|
|
workflowConnections = [];
|
|
|
|
// Load template nodes (simplified implementation)
|
|
// In a real implementation, this would load the actual template configuration
|
|
|
|
bootstrap.Modal.getInstance(document.getElementById('templateModal')).hide();
|
|
showAlert('Template loaded successfully', 'success');
|
|
}
|
|
|
|
function saveWorkflow() {
|
|
const workflowData = {
|
|
nodes: workflowNodes,
|
|
connections: workflowConnections,
|
|
name: $('#workflow-title').text(),
|
|
description: $('#workflow-description').text()
|
|
};
|
|
|
|
fetch('/radiology/dicom/workflow/save/', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(workflowData)
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Workflow saved successfully', 'success');
|
|
} else {
|
|
showAlert('Error saving workflow', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error saving workflow', 'danger');
|
|
});
|
|
}
|
|
|
|
function createNewWorkflow() {
|
|
if (confirm('Create a new workflow? This will clear the current workflow.')) {
|
|
$('.workflow-node').remove();
|
|
workflowNodes = [];
|
|
workflowConnections = [];
|
|
drawConnections();
|
|
|
|
$('#workflow-title').text('New Workflow');
|
|
$('#workflow-description').text('New DICOM processing workflow');
|
|
|
|
showAlert('New workflow created', 'success');
|
|
}
|
|
}
|
|
|
|
function importWorkflow() {
|
|
// Create file input for importing workflow
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.accept = '.json';
|
|
input.onchange = function(e) {
|
|
const file = e.target.files[0];
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
try {
|
|
const workflowData = JSON.parse(e.target.result);
|
|
// Load workflow data
|
|
showAlert('Workflow imported successfully', 'success');
|
|
} catch (error) {
|
|
showAlert('Error importing workflow', 'danger');
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
};
|
|
input.click();
|
|
}
|
|
|
|
function exportWorkflow() {
|
|
const workflowData = {
|
|
nodes: workflowNodes,
|
|
connections: workflowConnections,
|
|
name: $('#workflow-title').text(),
|
|
description: $('#workflow-description').text(),
|
|
exported_at: new Date().toISOString()
|
|
};
|
|
|
|
const blob = new Blob([JSON.stringify(workflowData, null, 2)], { type: 'application/json' });
|
|
const link = document.createElement('a');
|
|
link.download = 'dicom_workflow.json';
|
|
link.href = URL.createObjectURL(blob);
|
|
link.click();
|
|
}
|
|
|
|
function showAlert(message, type) {
|
|
const alertDiv = document.createElement('div');
|
|
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
|
|
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 1060; min-width: 300px;';
|
|
alertDiv.innerHTML = `
|
|
${message}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
|
|
document.body.appendChild(alertDiv);
|
|
|
|
setTimeout(() => {
|
|
if (alertDiv.parentNode) {
|
|
alertDiv.remove();
|
|
}
|
|
}, 5000);
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
|