Marwan Alwali 0a037d3d9d update
2025-09-01 11:26:11 +03:00

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 %}