613 lines
23 KiB
HTML
613 lines
23 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}Quality Control - {{ qc_record.test_name }}{% endblock %}
|
|
|
|
{% block css %}
|
|
<style>
|
|
.info-section {
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
border-left: 4px solid #007bff;
|
|
}
|
|
|
|
.info-section h5 {
|
|
color: #007bff;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.test-info {
|
|
background: #d1ecf1;
|
|
border-left: 4px solid #17a2b8;
|
|
}
|
|
|
|
.results-section {
|
|
background: #d4edda;
|
|
border-left: 4px solid #28a745;
|
|
}
|
|
|
|
.compliance-section {
|
|
background: #fff3cd;
|
|
border-left: 4px solid #ffc107;
|
|
}
|
|
|
|
.failed-section {
|
|
background: #f8d7da;
|
|
border-left: 4px solid #dc3545;
|
|
}
|
|
|
|
.result-indicator {
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 50%;
|
|
display: inline-block;
|
|
margin-right: 10px;
|
|
}
|
|
|
|
.passed { background-color: #28a745; }
|
|
.failed { background-color: #dc3545; }
|
|
.pending { background-color: #6c757d; }
|
|
.warning { background-color: #ffc107; }
|
|
|
|
.parameter-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
gap: 20px;
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.parameter-card {
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
}
|
|
|
|
.parameter-card h6 {
|
|
color: #495057;
|
|
border-bottom: 1px solid #dee2e6;
|
|
padding-bottom: 10px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.trend-chart {
|
|
background: #f8f9fa;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
margin-top: 15px;
|
|
text-align: center;
|
|
min-height: 200px;
|
|
}
|
|
|
|
.action-timeline {
|
|
background: #e9ecef;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.timeline-item {
|
|
border-left: 3px solid #007bff;
|
|
padding-left: 15px;
|
|
margin-bottom: 15px;
|
|
position: relative;
|
|
}
|
|
|
|
.timeline-item::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: -6px;
|
|
top: 5px;
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
background-color: #007bff;
|
|
}
|
|
|
|
.capa-section {
|
|
background: #f8d7da;
|
|
border: 2px solid #dc3545;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
margin-top: 15px;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<!-- BEGIN breadcrumb -->
|
|
<ol class="breadcrumb float-xl-end">
|
|
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
|
|
<li class="breadcrumb-item"><a href="{% url 'blood_bank:dashboard' %}">Blood Bank</a></li>
|
|
<li class="breadcrumb-item"><a href="{% url 'blood_bank:quality_control_list' %}">Quality Control</a></li>
|
|
<li class="breadcrumb-item active">{{ qc_record.test_name }}</li>
|
|
</ol>
|
|
<!-- END breadcrumb -->
|
|
|
|
<!-- BEGIN page-header -->
|
|
<h1 class="page-header">
|
|
Quality Control Detail
|
|
<small>{{ qc_record.test_name }}</small>
|
|
</h1>
|
|
<!-- END page-header -->
|
|
|
|
<!-- BEGIN panel -->
|
|
<div class="panel panel-inverse">
|
|
<div class="panel-heading">
|
|
<h4 class="panel-title">
|
|
<i class="fa fa-clipboard-check"></i> QC Test Results
|
|
</h4>
|
|
<div class="panel-heading-btn">
|
|
<span class="badge bg-{% if qc_record.result == 'passed' %}success{% elif qc_record.result == 'failed' %}danger{% else %}warning{% endif %}">
|
|
{{ qc_record.get_result_display }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="panel-body">
|
|
<!-- BEGIN test information -->
|
|
<div class="info-section test-info">
|
|
<h5><i class="fa fa-flask"></i> Test Information</h5>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<table class="table table-borderless mb-0">
|
|
<tr>
|
|
<td class="fw-bold">Test Name:</td>
|
|
<td>{{ qc_record.test_name }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Test Type:</td>
|
|
<td>{{ qc_record.get_test_type_display }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Test Date:</td>
|
|
<td>{{ qc_record.test_date|date:"M d, Y H:i" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Tested By:</td>
|
|
<td>{{ qc_record.tested_by.get_full_name }}</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<table class="table table-borderless mb-0">
|
|
<tr>
|
|
<td class="fw-bold">Equipment:</td>
|
|
<td>{{ qc_record.equipment_used|default:"Not specified" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Lot Numbers:</td>
|
|
<td>{{ qc_record.lot_numbers|default:"Not specified" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Temperature:</td>
|
|
<td>{{ qc_record.temperature|default:"Not recorded" }}°C</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Result:</td>
|
|
<td>
|
|
<span class="result-indicator {% if qc_record.result == 'passed' %}passed{% elif qc_record.result == 'failed' %}failed{% else %}pending{% endif %}"></span>
|
|
{{ qc_record.get_result_display }}
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{% if qc_record.sample_id %}
|
|
<div class="mt-3">
|
|
<h6>Sample Information:</h6>
|
|
<div class="alert alert-info">
|
|
<strong>Sample ID:</strong> {{ qc_record.sample_id }}<br>
|
|
{% if qc_record.blood_unit %}
|
|
<strong>Blood Unit:</strong> {{ qc_record.blood_unit.unit_number }}<br>
|
|
{% endif %}
|
|
<strong>Sample Type:</strong> {{ qc_record.get_test_type_display }}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
<!-- END test information -->
|
|
|
|
<!-- BEGIN test results -->
|
|
<div class="info-section {% if qc_record.result == 'passed' %}results-section{% else %}failed-section{% endif %}">
|
|
<h5><i class="fa fa-chart-bar"></i> Test Results</h5>
|
|
|
|
{% if qc_record.test_type == 'temperature' %}
|
|
<div class="parameter-grid">
|
|
<div class="parameter-card">
|
|
<h6><i class="fa fa-thermometer-half"></i> Temperature Control</h6>
|
|
<table class="table table-sm">
|
|
<tr>
|
|
<td>Measured Temperature:</td>
|
|
<td><strong>{{ qc_record.temperature }}°C</strong></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Acceptable Range:</td>
|
|
<td>2-6°C</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Deviation:</td>
|
|
<td>
|
|
{% if qc_record.temperature %}
|
|
{% if qc_record.temperature >= 2 and qc_record.temperature <= 6 %}
|
|
<span class="text-success">Within range</span>
|
|
{% else %}
|
|
<span class="text-danger">{{ qc_record.temperature|floatformat:1 }}°C deviation</span>
|
|
{% endif %}
|
|
{% else %}
|
|
<span class="text-muted">Not recorded</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{% elif qc_record.test_type == 'ph' %}
|
|
<div class="parameter-grid">
|
|
<div class="parameter-card">
|
|
<h6><i class="fa fa-vial"></i> pH Testing</h6>
|
|
<table class="table table-sm">
|
|
<tr>
|
|
<td>Measured pH:</td>
|
|
<td><strong>{{ qc_record.ph_value|default:"Not recorded" }}</strong></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Acceptable Range:</td>
|
|
<td>6.0-8.0</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Buffer Solution:</td>
|
|
<td>{{ qc_record.buffer_solution|default:"Not specified" }}</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{% elif qc_record.test_type == 'sterility' %}
|
|
<div class="parameter-grid">
|
|
<div class="parameter-card">
|
|
<h6><i class="fa fa-microscope"></i> Sterility Testing</h6>
|
|
<table class="table table-sm">
|
|
<tr>
|
|
<td>Culture Medium:</td>
|
|
<td>{{ qc_record.culture_medium|default:"Not specified" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Incubation Period:</td>
|
|
<td>{{ qc_record.incubation_period|default:"Standard" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Growth Observed:</td>
|
|
<td>
|
|
{% if qc_record.result == 'passed' %}
|
|
<span class="text-success">No growth</span>
|
|
{% else %}
|
|
<span class="text-danger">Growth detected</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{% elif qc_record.test_type == 'hemolysis' %}
|
|
<div class="parameter-grid">
|
|
<div class="parameter-card">
|
|
<h6><i class="fa fa-tint"></i> Hemolysis Testing</h6>
|
|
<table class="table table-sm">
|
|
<tr>
|
|
<td>Hemolysis Percentage:</td>
|
|
<td><strong>{{ qc_record.hemolysis_percentage|default:"Not recorded" }}%</strong></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Acceptable Limit:</td>
|
|
<td>< 0.8%</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Visual Assessment:</td>
|
|
<td>{{ qc_record.visual_assessment|default:"Not recorded" }}</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if qc_record.test_notes %}
|
|
<div class="mt-3">
|
|
<h6>Test Notes:</h6>
|
|
<div class="alert alert-info">{{ qc_record.test_notes }}</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
<!-- END test results -->
|
|
|
|
<!-- BEGIN compliance information -->
|
|
<div class="info-section compliance-section">
|
|
<h5><i class="fa fa-shield-alt"></i> Compliance Information</h5>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<h6>Regulatory Standards</h6>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" checked disabled>
|
|
<label class="form-check-label">FDA 21 CFR Part 606</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" checked disabled>
|
|
<label class="form-check-label">AABB Standards</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" checked disabled>
|
|
<label class="form-check-label">ISO 15189</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" checked disabled>
|
|
<label class="form-check-label">CAP Requirements</label>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<h6>Quality Assurance</h6>
|
|
<table class="table table-sm">
|
|
<tr>
|
|
<td>Equipment Calibrated:</td>
|
|
<td>
|
|
{% if qc_record.equipment_calibrated %}
|
|
<span class="text-success"><i class="fa fa-check"></i> Yes</span>
|
|
{% else %}
|
|
<span class="text-danger"><i class="fa fa-times"></i> No</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>SOP Followed:</td>
|
|
<td>
|
|
{% if qc_record.sop_followed %}
|
|
<span class="text-success"><i class="fa fa-check"></i> Yes</span>
|
|
{% else %}
|
|
<span class="text-danger"><i class="fa fa-times"></i> No</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Controls Passed:</td>
|
|
<td>
|
|
{% if qc_record.controls_passed %}
|
|
<span class="text-success"><i class="fa fa-check"></i> Yes</span>
|
|
{% else %}
|
|
<span class="text-danger"><i class="fa fa-times"></i> No</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- END compliance information -->
|
|
|
|
<!-- BEGIN trend analysis -->
|
|
<div class="info-section">
|
|
<h5><i class="fa fa-chart-line"></i> Trend Analysis</h5>
|
|
<div class="trend-chart">
|
|
<h6>Recent QC Results for {{ qc_record.test_name }}</h6>
|
|
<canvas id="trendChart" width="400" height="200"></canvas>
|
|
<p class="text-muted mt-2">Last 10 test results showing trend over time</p>
|
|
</div>
|
|
</div>
|
|
<!-- END trend analysis -->
|
|
|
|
{% if qc_record.result == 'failed' %}
|
|
<!-- BEGIN CAPA section -->
|
|
<div class="capa-section">
|
|
<h5><i class="fa fa-exclamation-triangle"></i> Corrective and Preventive Action (CAPA)</h5>
|
|
<div class="alert alert-danger">
|
|
<strong>QC Test Failed - Immediate Action Required</strong>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<h6>Immediate Actions</h6>
|
|
<ul>
|
|
<li>Quarantine affected products</li>
|
|
<li>Investigate root cause</li>
|
|
<li>Review equipment calibration</li>
|
|
<li>Check reagent integrity</li>
|
|
<li>Notify quality manager</li>
|
|
</ul>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<h6>Follow-up Required</h6>
|
|
<ul>
|
|
<li>Document investigation findings</li>
|
|
<li>Implement corrective actions</li>
|
|
<li>Verify effectiveness</li>
|
|
<li>Update procedures if needed</li>
|
|
<li>Schedule follow-up testing</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
{% if qc_record.capa_initiated %}
|
|
<div class="alert alert-info mt-3">
|
|
<strong>CAPA Status:</strong> {{ qc_record.get_capa_status_display }}<br>
|
|
<strong>CAPA Number:</strong> {{ qc_record.capa_number }}<br>
|
|
<strong>Initiated By:</strong> {{ qc_record.capa_initiated_by.get_full_name }}<br>
|
|
<strong>Date:</strong> {{ qc_record.capa_date|date:"M d, Y H:i" }}
|
|
</div>
|
|
{% else %}
|
|
<div class="mt-3">
|
|
<a href="#" class="btn btn-warning" onclick="initiateCapa()">
|
|
<i class="fa fa-plus"></i> Initiate CAPA
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
<!-- END CAPA section -->
|
|
{% endif %}
|
|
|
|
<!-- BEGIN action timeline -->
|
|
<div class="action-timeline">
|
|
<h6><i class="fa fa-history"></i> Action Timeline</h6>
|
|
|
|
<div class="timeline-item">
|
|
<strong>{{ qc_record.test_date|date:"M d, Y H:i" }}</strong><br>
|
|
QC test performed by {{ qc_record.tested_by.get_full_name }}<br>
|
|
<small class="text-muted">Result: {{ qc_record.get_result_display }}</small>
|
|
</div>
|
|
|
|
{% if qc_record.reviewed_by %}
|
|
<div class="timeline-item">
|
|
<strong>{{ qc_record.review_date|date:"M d, Y H:i" }}</strong><br>
|
|
Results reviewed by {{ qc_record.reviewed_by.get_full_name }}<br>
|
|
<small class="text-muted">Review completed</small>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if qc_record.capa_initiated %}
|
|
<div class="timeline-item">
|
|
<strong>{{ qc_record.capa_date|date:"M d, Y H:i" }}</strong><br>
|
|
CAPA initiated by {{ qc_record.capa_initiated_by.get_full_name }}<br>
|
|
<small class="text-muted">CAPA #{{ qc_record.capa_number }}</small>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
<!-- END action timeline -->
|
|
|
|
<!-- BEGIN actions -->
|
|
<div class="d-flex justify-content-between mt-4">
|
|
<a href="{% url 'blood_bank:quality_control_list' %}" class="btn btn-secondary">
|
|
<i class="fa fa-arrow-left"></i> Back to QC List
|
|
</a>
|
|
<div>
|
|
<button type="button" class="btn btn-info" onclick="printReport()">
|
|
<i class="fa fa-print"></i> Print Report
|
|
</button>
|
|
{% if qc_record.result == 'failed' and not qc_record.capa_initiated %}
|
|
<button type="button" class="btn btn-warning" onclick="initiateCapa()">
|
|
<i class="fa fa-exclamation-triangle"></i> Initiate CAPA
|
|
</button>
|
|
{% endif %}
|
|
{% if not qc_record.reviewed_by %}
|
|
<button type="button" class="btn btn-success" onclick="reviewResults()">
|
|
<i class="fa fa-check"></i> Review Results
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<!-- END actions -->
|
|
</div>
|
|
</div>
|
|
<!-- END panel -->
|
|
{% endblock %}
|
|
|
|
{% block js %}
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
|
|
<script>
|
|
$(document).ready(function() {
|
|
// Initialize trend chart
|
|
initializeTrendChart();
|
|
});
|
|
|
|
function initializeTrendChart() {
|
|
var ctx = document.getElementById('trendChart').getContext('2d');
|
|
|
|
// Sample data - in real implementation, this would come from the backend
|
|
var chartData = {
|
|
labels: ['Test 1', 'Test 2', 'Test 3', 'Test 4', 'Test 5', 'Test 6', 'Test 7', 'Test 8', 'Test 9', 'Current'],
|
|
datasets: [{
|
|
label: '{{ qc_record.test_name }}',
|
|
data: [98, 97, 99, 96, 98, 97, 99, 98, 97, {{ qc_record.temperature|default:0 }}],
|
|
borderColor: '{% if qc_record.result == "passed" %}#28a745{% else %}#dc3545{% endif %}',
|
|
backgroundColor: '{% if qc_record.result == "passed" %}rgba(40, 167, 69, 0.1){% else %}rgba(220, 53, 69, 0.1){% endif %}',
|
|
tension: 0.4
|
|
}]
|
|
};
|
|
|
|
var chartOptions = {
|
|
responsive: true,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: false,
|
|
min: {% if qc_record.test_type == 'temperature' %}0{% else %}90{% endif %},
|
|
max: {% if qc_record.test_type == 'temperature' %}10{% else %}100{% endif %}
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
}
|
|
}
|
|
};
|
|
|
|
new Chart(ctx, {
|
|
type: 'line',
|
|
data: chartData,
|
|
options: chartOptions
|
|
});
|
|
}
|
|
|
|
function printReport() {
|
|
window.print();
|
|
}
|
|
|
|
function reviewResults() {
|
|
Swal.fire({
|
|
title: 'Review QC Results',
|
|
text: 'Are you sure you want to mark these results as reviewed?',
|
|
icon: 'question',
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Yes, Review',
|
|
cancelButtonText: 'Cancel'
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
// In real implementation, this would make an AJAX call
|
|
Swal.fire('Reviewed!', 'QC results have been marked as reviewed.', 'success');
|
|
}
|
|
});
|
|
}
|
|
|
|
function initiateCapa() {
|
|
Swal.fire({
|
|
title: 'Initiate CAPA',
|
|
html: `
|
|
<div class="text-start">
|
|
<p>This will initiate a Corrective and Preventive Action for the failed QC test.</p>
|
|
<div class="form-group">
|
|
<label>CAPA Priority:</label>
|
|
<select class="form-select" id="capaPriority">
|
|
<option value="high">High</option>
|
|
<option value="medium">Medium</option>
|
|
<option value="low">Low</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group mt-2">
|
|
<label>Initial Assessment:</label>
|
|
<textarea class="form-control" id="capaAssessment" rows="3" placeholder="Describe the issue and immediate actions taken..."></textarea>
|
|
</div>
|
|
</div>
|
|
`,
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Initiate CAPA',
|
|
cancelButtonText: 'Cancel',
|
|
preConfirm: () => {
|
|
const priority = document.getElementById('capaPriority').value;
|
|
const assessment = document.getElementById('capaAssessment').value;
|
|
|
|
if (!assessment.trim()) {
|
|
Swal.showValidationMessage('Please provide an initial assessment');
|
|
return false;
|
|
}
|
|
|
|
return { priority: priority, assessment: assessment };
|
|
}
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
// In real implementation, this would make an AJAX call
|
|
Swal.fire('CAPA Initiated!', 'CAPA #{{ qc_record.id }}-001 has been created.', 'success');
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
|