811 lines
29 KiB
HTML
811 lines
29 KiB
HTML
{% extends 'base.html' %}
|
||
{% load static %}
|
||
|
||
{% block title %}QC Trend Analysis{% endblock %}
|
||
|
||
{% block extra_css %}
|
||
<style>
|
||
.trend-card {
|
||
background: #fff;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 0.375rem;
|
||
padding: 1rem;
|
||
margin-bottom: 1rem;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.trend-card:hover {
|
||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||
}
|
||
|
||
.chart-container {
|
||
position: relative;
|
||
height: 400px;
|
||
margin: 1rem 0;
|
||
}
|
||
|
||
.mini-chart-container {
|
||
position: relative;
|
||
height: 150px;
|
||
margin: 0.5rem 0;
|
||
}
|
||
|
||
.filter-section {
|
||
background: #f8f9fa;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 0.375rem;
|
||
padding: 1.5rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.stat-card {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
border-radius: 0.375rem;
|
||
padding: 1.5rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.stat-card.success {
|
||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||
}
|
||
|
||
.stat-card.warning {
|
||
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
||
}
|
||
|
||
.stat-card.danger {
|
||
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);
|
||
}
|
||
|
||
.westgard-rule {
|
||
background: #fff3cd;
|
||
border: 1px solid #ffeaa7;
|
||
border-radius: 0.25rem;
|
||
padding: 0.75rem;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.westgard-rule.violated {
|
||
background: #f8d7da;
|
||
border-color: #f5c6cb;
|
||
}
|
||
|
||
.trend-indicator {
|
||
display: inline-block;
|
||
padding: 0.25rem 0.5rem;
|
||
border-radius: 0.25rem;
|
||
font-size: 0.875rem;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.trend-up {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
}
|
||
|
||
.trend-down {
|
||
background: #f8d7da;
|
||
color: #721c24;
|
||
}
|
||
|
||
.trend-stable {
|
||
background: #d1ecf1;
|
||
color: #0c5460;
|
||
}
|
||
|
||
.control-limits {
|
||
position: absolute;
|
||
right: 10px;
|
||
top: 10px;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
padding: 0.5rem;
|
||
border-radius: 0.25rem;
|
||
font-size: 0.75rem;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.chart-container {
|
||
height: 300px;
|
||
}
|
||
|
||
.mini-chart-container {
|
||
height: 120px;
|
||
}
|
||
|
||
.filter-section {
|
||
padding: 1rem;
|
||
}
|
||
}
|
||
</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 'laboratory:dashboard' %}">Laboratory</a></li>
|
||
<li class="breadcrumb-item active">QC Trend Analysis</li>
|
||
</ol>
|
||
<h1 class="page-header mb-0">
|
||
<i class="fas fa-chart-line me-2"></i>QC Trend Analysis
|
||
</h1>
|
||
</div>
|
||
<div class="ms-auto">
|
||
<div class="btn-group">
|
||
<button type="button" class="btn btn-outline-primary" onclick="exportTrendData()">
|
||
<i class="fas fa-download me-1"></i>Export
|
||
</button>
|
||
<button type="button" class="btn btn-outline-info" onclick="printTrendReport()">
|
||
<i class="fas fa-print me-1"></i>Print
|
||
</button>
|
||
<button type="button" class="btn btn-primary" onclick="refreshTrends()">
|
||
<i class="fas fa-sync me-1"></i>Refresh
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Filter Section -->
|
||
<div class="filter-section">
|
||
<div class="row">
|
||
<div class="col-md-3">
|
||
<div class="form-group">
|
||
<label class="form-label">Test Type</label>
|
||
<select class="form-select" id="test-type-filter">
|
||
<option value="">All Test Types</option>
|
||
{% for test_type in test_types %}
|
||
<option value="{{ test_type.id }}">{{ test_type.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<div class="form-group">
|
||
<label class="form-label">QC Level</label>
|
||
<select class="form-select" id="qc-level-filter">
|
||
<option value="">All Levels</option>
|
||
<option value="level1">Level 1 (Normal)</option>
|
||
<option value="level2">Level 2 (Abnormal Low)</option>
|
||
<option value="level3">Level 3 (Abnormal High)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<div class="form-group">
|
||
<label class="form-label">Date Range</label>
|
||
<select class="form-select" id="date-range-filter">
|
||
<option value="7">Last 7 days</option>
|
||
<option value="30" selected>Last 30 days</option>
|
||
<option value="90">Last 90 days</option>
|
||
<option value="365">Last year</option>
|
||
<option value="custom">Custom range</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<div class="form-group">
|
||
<label class="form-label">Instrument</label>
|
||
<select class="form-select" id="instrument-filter">
|
||
<option value="">All Instruments</option>
|
||
{% for instrument in instruments %}
|
||
<option value="{{ instrument.id }}">{{ instrument.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="row mt-3" id="custom-date-range" style="display: none;">
|
||
<div class="col-md-6">
|
||
<div class="form-group">
|
||
<label class="form-label">Start Date</label>
|
||
<input type="date" class="form-control" id="start-date">
|
||
</div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="form-group">
|
||
<label class="form-label">End Date</label>
|
||
<input type="date" class="form-control" id="end-date">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="row mt-3">
|
||
<div class="col-12">
|
||
<button type="button" class="btn btn-primary" onclick="applyFilters()">
|
||
<i class="fas fa-filter me-1"></i>Apply Filters
|
||
</button>
|
||
<button type="button" class="btn btn-outline-secondary ms-2" onclick="clearFilters()">
|
||
<i class="fas fa-times me-1"></i>Clear
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Statistics Cards -->
|
||
<div class="row mb-4">
|
||
<div class="col-md-3">
|
||
<div class="stat-card success">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<h3 class="mb-1" id="total-samples">{{ stats.total_samples }}</h3>
|
||
<p class="mb-0">Total Samples</p>
|
||
</div>
|
||
<i class="fas fa-vial fa-2x opacity-75"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<div class="stat-card">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<h3 class="mb-1" id="pass-rate">{{ stats.pass_rate }}%</h3>
|
||
<p class="mb-0">Pass Rate</p>
|
||
</div>
|
||
<i class="fas fa-check-circle fa-2x opacity-75"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<div class="stat-card warning">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<h3 class="mb-1" id="out-of-range">{{ stats.out_of_range }}</h3>
|
||
<p class="mb-0">Out of Range</p>
|
||
</div>
|
||
<i class="fas fa-exclamation-triangle fa-2x opacity-75"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<div class="stat-card danger">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<h3 class="mb-1" id="westgard-violations">{{ stats.westgard_violations }}</h3>
|
||
<p class="mb-0">Westgard Violations</p>
|
||
</div>
|
||
<i class="fas fa-ban fa-2x opacity-75"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<!-- Main Trend Chart -->
|
||
<div class="col-lg-8">
|
||
<div class="card mb-4">
|
||
<div class="card-header">
|
||
<h5 class="card-title mb-0">
|
||
<i class="fas fa-chart-line me-2"></i>QC Trend Chart
|
||
</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="chart-container">
|
||
<canvas id="mainTrendChart"></canvas>
|
||
<div class="control-limits">
|
||
<div><strong>Control Limits:</strong></div>
|
||
<div>+3σ: <span id="upper-3s">--</span></div>
|
||
<div>+2σ: <span id="upper-2s">--</span></div>
|
||
<div>Mean: <span id="mean-value">--</span></div>
|
||
<div>-2σ: <span id="lower-2s">--</span></div>
|
||
<div>-3σ: <span id="lower-3s">--</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Westgard Rules Analysis -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h5 class="card-title mb-0">
|
||
<i class="fas fa-rules me-2"></i>Westgard Rules Analysis
|
||
</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div id="westgard-rules">
|
||
<div class="westgard-rule">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<strong>1₃s Rule:</strong> 1 control observation exceeds ±3s
|
||
</div>
|
||
<span class="badge bg-success" id="rule-1-3s">Pass</span>
|
||
</div>
|
||
</div>
|
||
<div class="westgard-rule">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<strong>2₂s Rule:</strong> 2 consecutive controls exceed ±2s
|
||
</div>
|
||
<span class="badge bg-success" id="rule-2-2s">Pass</span>
|
||
</div>
|
||
</div>
|
||
<div class="westgard-rule">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<strong>R₄s Rule:</strong> Range of 4 consecutive controls exceeds 4s
|
||
</div>
|
||
<span class="badge bg-success" id="rule-r-4s">Pass</span>
|
||
</div>
|
||
</div>
|
||
<div class="westgard-rule">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<strong>4₁s Rule:</strong> 4 consecutive controls exceed ±1s
|
||
</div>
|
||
<span class="badge bg-success" id="rule-4-1s">Pass</span>
|
||
</div>
|
||
</div>
|
||
<div class="westgard-rule">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<strong>10x̄ Rule:</strong> 10 consecutive controls on same side of mean
|
||
</div>
|
||
<span class="badge bg-success" id="rule-10-mean">Pass</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sidebar -->
|
||
<div class="col-lg-4">
|
||
<!-- Test Type Trends -->
|
||
<div class="card mb-4">
|
||
<div class="card-header">
|
||
<h5 class="card-title mb-0">
|
||
<i class="fas fa-flask me-2"></i>Test Type Trends
|
||
</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
{% for test_trend in test_trends %}
|
||
<div class="trend-card">
|
||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||
<h6 class="mb-0">{{ test_trend.test_type.name }}</h6>
|
||
<span class="trend-indicator trend-{{ test_trend.trend_direction }}">
|
||
{% if test_trend.trend_direction == 'up' %}
|
||
<i class="fas fa-arrow-up me-1"></i>{{ test_trend.trend_percentage }}%
|
||
{% elif test_trend.trend_direction == 'down' %}
|
||
<i class="fas fa-arrow-down me-1"></i>{{ test_trend.trend_percentage }}%
|
||
{% else %}
|
||
<i class="fas fa-minus me-1"></i>Stable
|
||
{% endif %}
|
||
</span>
|
||
</div>
|
||
<div class="mini-chart-container">
|
||
<canvas id="trend-chart-{{ test_trend.test_type.id }}"></canvas>
|
||
</div>
|
||
<div class="row text-center mt-2">
|
||
<div class="col-4">
|
||
<small class="text-muted">Samples</small>
|
||
<div class="fw-bold">{{ test_trend.sample_count }}</div>
|
||
</div>
|
||
<div class="col-4">
|
||
<small class="text-muted">Pass Rate</small>
|
||
<div class="fw-bold">{{ test_trend.pass_rate }}%</div>
|
||
</div>
|
||
<div class="col-4">
|
||
<small class="text-muted">CV</small>
|
||
<div class="fw-bold">{{ test_trend.cv }}%</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% empty %}
|
||
<div class="text-muted text-center py-3">
|
||
<i class="fas fa-chart-line fa-2x mb-2"></i>
|
||
<p>No trend data available</p>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Recent Violations -->
|
||
<div class="card mb-4">
|
||
<div class="card-header">
|
||
<h5 class="card-title mb-0">
|
||
<i class="fas fa-exclamation-triangle me-2"></i>Recent Violations
|
||
</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
{% for violation in recent_violations %}
|
||
<div class="d-flex align-items-center mb-3">
|
||
<div class="flex-shrink-0">
|
||
<i class="fas fa-exclamation-circle text-danger"></i>
|
||
</div>
|
||
<div class="flex-grow-1 ms-3">
|
||
<div class="fw-bold">{{ violation.rule_name }}</div>
|
||
<small class="text-muted">
|
||
{{ violation.test_type.name }} - {{ violation.sample_id }}
|
||
</small>
|
||
<div class="text-muted">{{ violation.created_at|date:"M d, Y H:i" }}</div>
|
||
</div>
|
||
<div class="flex-shrink-0">
|
||
<span class="badge bg-danger">{{ violation.severity }}</span>
|
||
</div>
|
||
</div>
|
||
{% empty %}
|
||
<div class="text-muted text-center py-3">
|
||
<i class="fas fa-check-circle fa-2x mb-2 text-success"></i>
|
||
<p>No recent violations</p>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Control Statistics -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h5 class="card-title mb-0">
|
||
<i class="fas fa-calculator me-2"></i>Control Statistics
|
||
</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row text-center mb-3">
|
||
<div class="col-6">
|
||
<h4 class="text-primary" id="mean-stat">{{ control_stats.mean|floatformat:2 }}</h4>
|
||
<small class="text-muted">Mean</small>
|
||
</div>
|
||
<div class="col-6">
|
||
<h4 class="text-info" id="sd-stat">{{ control_stats.sd|floatformat:2 }}</h4>
|
||
<small class="text-muted">Standard Deviation</small>
|
||
</div>
|
||
</div>
|
||
<div class="row text-center mb-3">
|
||
<div class="col-6">
|
||
<h5 class="text-success" id="cv-stat">{{ control_stats.cv|floatformat:1 }}%</h5>
|
||
<small class="text-muted">CV</small>
|
||
</div>
|
||
<div class="col-6">
|
||
<h5 class="text-warning" id="range-stat">{{ control_stats.range|floatformat:2 }}</h5>
|
||
<small class="text-muted">Range</small>
|
||
</div>
|
||
</div>
|
||
<hr>
|
||
<div class="mb-2">
|
||
<div class="d-flex justify-content-between">
|
||
<span>+3σ Limit:</span>
|
||
<span class="fw-bold" id="plus-3s">{{ control_stats.plus_3s|floatformat:2 }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="mb-2">
|
||
<div class="d-flex justify-content-between">
|
||
<span>+2σ Limit:</span>
|
||
<span class="fw-bold" id="plus-2s">{{ control_stats.plus_2s|floatformat:2 }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="mb-2">
|
||
<div class="d-flex justify-content-between">
|
||
<span>-2σ Limit:</span>
|
||
<span class="fw-bold" id="minus-2s">{{ control_stats.minus_2s|floatformat:2 }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="mb-2">
|
||
<div class="d-flex justify-content-between">
|
||
<span>-3σ Limit:</span>
|
||
<span class="fw-bold" id="minus-3s">{{ control_stats.minus_3s|floatformat:2 }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script src="{% static 'assets/plugins/chart.js/dist/chart.min.js' %}"></script>
|
||
|
||
<script>
|
||
let mainChart = null;
|
||
let miniCharts = {};
|
||
|
||
$(document).ready(function() {
|
||
// Initialize main trend chart
|
||
initializeMainTrendChart();
|
||
|
||
// Initialize mini charts for each test type
|
||
initializeMiniCharts();
|
||
|
||
// Filter event handlers
|
||
$('#date-range-filter').on('change', function() {
|
||
if ($(this).val() === 'custom') {
|
||
$('#custom-date-range').show();
|
||
} else {
|
||
$('#custom-date-range').hide();
|
||
}
|
||
});
|
||
|
||
// Auto-apply filters when changed
|
||
$('#test-type-filter, #qc-level-filter, #instrument-filter').on('change', function() {
|
||
applyFilters();
|
||
});
|
||
});
|
||
|
||
function initializeMainTrendChart() {
|
||
const ctx = document.getElementById('mainTrendChart').getContext('2d');
|
||
|
||
const chartData = {
|
||
labels: {{ trend_labels|safe|default:"[]" }},
|
||
datasets: [{
|
||
label: 'QC Results',
|
||
data: {{ trend_data|safe|default:"[]" }},
|
||
borderColor: 'rgb(75, 192, 192)',
|
||
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
||
tension: 0.1,
|
||
pointRadius: 4,
|
||
pointHoverRadius: 6
|
||
}, {
|
||
label: '+3σ',
|
||
data: {{ upper_3s_line|safe|default:"[]" }},
|
||
borderColor: 'rgb(255, 99, 132)',
|
||
borderDash: [5, 5],
|
||
fill: false,
|
||
pointRadius: 0
|
||
}, {
|
||
label: '+2σ',
|
||
data: {{ upper_2s_line|safe|default:"[]" }},
|
||
borderColor: 'rgb(255, 159, 64)',
|
||
borderDash: [3, 3],
|
||
fill: false,
|
||
pointRadius: 0
|
||
}, {
|
||
label: 'Mean',
|
||
data: {{ mean_line|safe|default:"[]" }},
|
||
borderColor: 'rgb(54, 162, 235)',
|
||
borderDash: [1, 1],
|
||
fill: false,
|
||
pointRadius: 0
|
||
}, {
|
||
label: '-2σ',
|
||
data: {{ lower_2s_line|safe|default:"[]" }},
|
||
borderColor: 'rgb(255, 159, 64)',
|
||
borderDash: [3, 3],
|
||
fill: false,
|
||
pointRadius: 0
|
||
}, {
|
||
label: '-3σ',
|
||
data: {{ lower_3s_line|safe|default:"[]" }},
|
||
borderColor: 'rgb(255, 99, 132)',
|
||
borderDash: [5, 5],
|
||
fill: false,
|
||
pointRadius: 0
|
||
}]
|
||
};
|
||
|
||
mainChart = new Chart(ctx, {
|
||
type: 'line',
|
||
data: chartData,
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
title: {
|
||
display: true,
|
||
text: 'Quality Control Trend Analysis'
|
||
},
|
||
legend: {
|
||
position: 'bottom'
|
||
}
|
||
},
|
||
scales: {
|
||
y: {
|
||
beginAtZero: false,
|
||
title: {
|
||
display: true,
|
||
text: 'Value'
|
||
}
|
||
},
|
||
x: {
|
||
title: {
|
||
display: true,
|
||
text: 'Date'
|
||
}
|
||
}
|
||
},
|
||
interaction: {
|
||
intersect: false,
|
||
mode: 'index'
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function initializeMiniCharts() {
|
||
{% for test_trend in test_trends %}
|
||
const ctx{{ test_trend.test_type.id }} = document.getElementById('trend-chart-{{ test_trend.test_type.id }}').getContext('2d');
|
||
|
||
miniCharts[{{ test_trend.test_type.id }}] = new Chart(ctx{{ test_trend.test_type.id }}, {
|
||
type: 'line',
|
||
data: {
|
||
labels: {{ test_trend.labels|safe|default:"[]" }},
|
||
datasets: [{
|
||
data: {{ test_trend.data|safe|default:"[]" }},
|
||
borderColor: 'rgb(75, 192, 192)',
|
||
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
||
tension: 0.1,
|
||
pointRadius: 2,
|
||
pointHoverRadius: 4
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: {
|
||
display: false
|
||
}
|
||
},
|
||
scales: {
|
||
y: {
|
||
display: false,
|
||
beginAtZero: false
|
||
},
|
||
x: {
|
||
display: false
|
||
}
|
||
},
|
||
elements: {
|
||
point: {
|
||
radius: 2
|
||
}
|
||
}
|
||
}
|
||
});
|
||
{% endfor %}
|
||
}
|
||
|
||
function applyFilters() {
|
||
const filters = {
|
||
test_type: $('#test-type-filter').val(),
|
||
qc_level: $('#qc-level-filter').val(),
|
||
date_range: $('#date-range-filter').val(),
|
||
instrument: $('#instrument-filter').val(),
|
||
start_date: $('#start-date').val(),
|
||
end_date: $('#end-date').val()
|
||
};
|
||
|
||
// Show loading state
|
||
showLoadingState();
|
||
|
||
// Make AJAX request to update trends
|
||
$.ajax({
|
||
url: '{% url "laboratory:qc_trend_data" %}',
|
||
method: 'GET',
|
||
data: filters,
|
||
success: function(response) {
|
||
updateTrendData(response);
|
||
updateStatistics(response.stats);
|
||
updateWestgardRules(response.westgard_rules);
|
||
hideLoadingState();
|
||
},
|
||
error: function() {
|
||
alert('Error loading trend data');
|
||
hideLoadingState();
|
||
}
|
||
});
|
||
}
|
||
|
||
function clearFilters() {
|
||
$('#test-type-filter').val('');
|
||
$('#qc-level-filter').val('');
|
||
$('#date-range-filter').val('30');
|
||
$('#instrument-filter').val('');
|
||
$('#custom-date-range').hide();
|
||
applyFilters();
|
||
}
|
||
|
||
function updateTrendData(data) {
|
||
if (mainChart) {
|
||
mainChart.data.labels = data.trend_labels;
|
||
mainChart.data.datasets[0].data = data.trend_data;
|
||
mainChart.data.datasets[1].data = data.upper_3s_line;
|
||
mainChart.data.datasets[2].data = data.upper_2s_line;
|
||
mainChart.data.datasets[3].data = data.mean_line;
|
||
mainChart.data.datasets[4].data = data.lower_2s_line;
|
||
mainChart.data.datasets[5].data = data.lower_3s_line;
|
||
mainChart.update();
|
||
}
|
||
|
||
// Update control limits display
|
||
$('#upper-3s').text(data.control_limits.upper_3s);
|
||
$('#upper-2s').text(data.control_limits.upper_2s);
|
||
$('#mean-value').text(data.control_limits.mean);
|
||
$('#lower-2s').text(data.control_limits.lower_2s);
|
||
$('#lower-3s').text(data.control_limits.lower_3s);
|
||
}
|
||
|
||
function updateStatistics(stats) {
|
||
$('#total-samples').text(stats.total_samples);
|
||
$('#pass-rate').text(stats.pass_rate + '%');
|
||
$('#out-of-range').text(stats.out_of_range);
|
||
$('#westgard-violations').text(stats.westgard_violations);
|
||
|
||
$('#mean-stat').text(stats.mean);
|
||
$('#sd-stat').text(stats.sd);
|
||
$('#cv-stat').text(stats.cv + '%');
|
||
$('#range-stat').text(stats.range);
|
||
$('#plus-3s').text(stats.plus_3s);
|
||
$('#plus-2s').text(stats.plus_2s);
|
||
$('#minus-2s').text(stats.minus_2s);
|
||
$('#minus-3s').text(stats.minus_3s);
|
||
}
|
||
|
||
function updateWestgardRules(rules) {
|
||
Object.keys(rules).forEach(rule => {
|
||
const element = $('#rule-' + rule.replace('_', '-'));
|
||
if (rules[rule].violated) {
|
||
element.removeClass('bg-success').addClass('bg-danger').text('Violated');
|
||
element.closest('.westgard-rule').addClass('violated');
|
||
} else {
|
||
element.removeClass('bg-danger').addClass('bg-success').text('Pass');
|
||
element.closest('.westgard-rule').removeClass('violated');
|
||
}
|
||
});
|
||
}
|
||
|
||
function showLoadingState() {
|
||
// Add loading overlay or spinner
|
||
$('.chart-container').append('<div class="loading-overlay"><i class="fas fa-spinner fa-spin fa-2x"></i></div>');
|
||
}
|
||
|
||
function hideLoadingState() {
|
||
$('.loading-overlay').remove();
|
||
}
|
||
|
||
function refreshTrends() {
|
||
applyFilters();
|
||
}
|
||
|
||
function exportTrendData() {
|
||
const filters = {
|
||
test_type: $('#test-type-filter').val(),
|
||
qc_level: $('#qc-level-filter').val(),
|
||
date_range: $('#date-range-filter').val(),
|
||
instrument: $('#instrument-filter').val(),
|
||
start_date: $('#start-date').val(),
|
||
end_date: $('#end-date').val(),
|
||
format: 'excel'
|
||
};
|
||
|
||
const params = new URLSearchParams(filters);
|
||
window.location.href = '{% url "laboratory:export_qc_trends" %}?' + params.toString();
|
||
}
|
||
|
||
function printTrendReport() {
|
||
window.print();
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
@media print {
|
||
.btn-group, .filter-section {
|
||
display: none !important;
|
||
}
|
||
|
||
.chart-container {
|
||
height: 300px !important;
|
||
}
|
||
|
||
.card {
|
||
break-inside: avoid;
|
||
}
|
||
}
|
||
|
||
.loading-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(255, 255, 255, 0.8);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|