537 lines
21 KiB
HTML
537 lines
21 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}QC Sample Details - {{ qc_sample.sample_id }}{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.qc-status-pending { color: #ffc107; }
|
|
.qc-status-in-progress { color: #17a2b8; }
|
|
.qc-status-completed { color: #28a745; }
|
|
.qc-status-failed { color: #dc3545; }
|
|
.qc-status-cancelled { color: #6c757d; }
|
|
|
|
.qc-level-level1 { color: #28a745; }
|
|
.qc-level-level2 { color: #ffc107; }
|
|
.qc-level-level3 { color: #fd7e14; }
|
|
|
|
.result-within-range { background-color: #d4edda; color: #155724; }
|
|
.result-out-of-range { background-color: #f8d7da; color: #721c24; }
|
|
.result-critical { background-color: #f5c6cb; color: #721c24; }
|
|
|
|
.info-card {
|
|
background: #f8f9fa;
|
|
border-left: 4px solid #007bff;
|
|
padding: 1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.result-card {
|
|
background: #fff;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.375rem;
|
|
padding: 1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.timeline {
|
|
position: relative;
|
|
padding-left: 2rem;
|
|
}
|
|
|
|
.timeline::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0.5rem;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 2px;
|
|
background: #dee2e6;
|
|
}
|
|
|
|
.timeline-item {
|
|
position: relative;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.timeline-item::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: -0.75rem;
|
|
top: 0.25rem;
|
|
width: 0.75rem;
|
|
height: 0.75rem;
|
|
background: #007bff;
|
|
border-radius: 50%;
|
|
border: 2px solid #fff;
|
|
}
|
|
|
|
.chart-container {
|
|
position: relative;
|
|
height: 300px;
|
|
margin: 1rem 0;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.timeline {
|
|
padding-left: 1.5rem;
|
|
}
|
|
|
|
.timeline-item::before {
|
|
left: -0.5rem;
|
|
}
|
|
}
|
|
</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"><a href="{% url 'laboratory:quality_control_list' %}">QC Samples</a></li>
|
|
<li class="breadcrumb-item active">{{ qc_sample.sample_id }}</li>
|
|
</ol>
|
|
<h1 class="page-header mb-0">
|
|
<i class="fas fa-vial me-2"></i>QC Sample: {{ qc_sample.sample_id }}
|
|
</h1>
|
|
</div>
|
|
<div class="ms-auto">
|
|
<a href="{% url 'laboratory:quality_control_list' %}" class="btn btn-outline-secondary me-2">
|
|
<i class="fas fa-arrow-left me-1"></i>Back to List
|
|
</a>
|
|
{% if qc_sample.status == 'pending' or qc_sample.status == 'in_progress' %}
|
|
<a href="{% url 'laboratory:enter_qc_result' qc_sample.pk %}" class="btn btn-primary">
|
|
<i class="fas fa-edit me-1"></i>Enter Result
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<!-- Sample Information -->
|
|
<div class="col-lg-8">
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-info-circle me-2"></i>Sample Information
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="info-card">
|
|
<h6 class="mb-2">
|
|
<i class="fas fa-vial me-2"></i>Sample Details
|
|
</h6>
|
|
<table class="table table-sm table-borderless">
|
|
<tr>
|
|
<td class="fw-bold">Sample ID:</td>
|
|
<td>{{ qc_sample.sample_id }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Test Type:</td>
|
|
<td>{{ qc_sample.test_type.name }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">QC Level:</td>
|
|
<td>
|
|
<span class="badge qc-level-{{ qc_sample.qc_level }}">
|
|
{{ qc_sample.get_qc_level_display }}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Status:</td>
|
|
<td>
|
|
<span class="badge qc-status-{{ qc_sample.status }}">
|
|
{{ qc_sample.get_status_display }}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="info-card">
|
|
<h6 class="mb-2">
|
|
<i class="fas fa-calendar me-2"></i>Timing Information
|
|
</h6>
|
|
<table class="table table-sm table-borderless">
|
|
<tr>
|
|
<td class="fw-bold">Created:</td>
|
|
<td>{{ qc_sample.created_at|date:"M d, Y H:i" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Run Date:</td>
|
|
<td>{{ qc_sample.run_date|date:"M d, Y" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Run Time:</td>
|
|
<td>{{ qc_sample.run_time|time:"H:i" }}</td>
|
|
</tr>
|
|
{% if qc_sample.completed_at %}
|
|
<tr>
|
|
<td class="fw-bold">Completed:</td>
|
|
<td>{{ qc_sample.completed_at|date:"M d, Y H:i" }}</td>
|
|
</tr>
|
|
{% endif %}
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="info-card">
|
|
<h6 class="mb-2">
|
|
<i class="fas fa-box me-2"></i>Lot Information
|
|
</h6>
|
|
<table class="table table-sm table-borderless">
|
|
<tr>
|
|
<td class="fw-bold">Lot Number:</td>
|
|
<td>{{ qc_sample.lot_number }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Expiry Date:</td>
|
|
<td>{{ qc_sample.expiry_date|date:"M d, Y" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Expected Range:</td>
|
|
<td>{{ qc_sample.expected_range|default:"Not specified" }}</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="info-card">
|
|
<h6 class="mb-2">
|
|
<i class="fas fa-user me-2"></i>Personnel
|
|
</h6>
|
|
<table class="table table-sm table-borderless">
|
|
<tr>
|
|
<td class="fw-bold">Created By:</td>
|
|
<td>{{ qc_sample.created_by.get_full_name }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="fw-bold">Technician:</td>
|
|
<td>{{ qc_sample.technician.get_full_name|default:"Not assigned" }}</td>
|
|
</tr>
|
|
{% if qc_sample.reviewed_by %}
|
|
<tr>
|
|
<td class="fw-bold">Reviewed By:</td>
|
|
<td>{{ qc_sample.reviewed_by.get_full_name }}</td>
|
|
</tr>
|
|
{% endif %}
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if qc_sample.comments %}
|
|
<div class="info-card">
|
|
<h6 class="mb-2">
|
|
<i class="fas fa-comment me-2"></i>Comments
|
|
</h6>
|
|
<p class="mb-0">{{ qc_sample.comments }}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Results Section -->
|
|
{% if qc_sample.result_value %}
|
|
<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 Results
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="result-card result-{{ qc_sample.result_status }}">
|
|
<div class="row align-items-center">
|
|
<div class="col-md-4">
|
|
<h3 class="mb-1">{{ qc_sample.result_value }} {{ qc_sample.unit }}</h3>
|
|
<p class="text-muted mb-0">Measured Value</p>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<h5 class="mb-1">{{ qc_sample.expected_range }}</h5>
|
|
<p class="text-muted mb-0">Expected Range</p>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<span class="badge result-{{ qc_sample.result_status }} fs-6">
|
|
{{ qc_sample.get_result_status_display }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{% if qc_sample.result_comments %}
|
|
<hr>
|
|
<div>
|
|
<h6>Result Comments:</h6>
|
|
<p class="mb-0">{{ qc_sample.result_comments }}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Trend Chart Placeholder -->
|
|
<div class="chart-container">
|
|
<canvas id="qcTrendChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Activity Timeline -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-history me-2"></i>Activity Timeline
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="timeline">
|
|
{% for activity in qc_sample.activities.all %}
|
|
<div class="timeline-item">
|
|
<div class="d-flex justify-content-between">
|
|
<div>
|
|
<h6 class="mb-1">{{ activity.action }}</h6>
|
|
<p class="text-muted mb-1">{{ activity.description }}</p>
|
|
<small class="text-muted">
|
|
by {{ activity.user.get_full_name }} - {{ activity.created_at|date:"M d, Y H:i" }}
|
|
</small>
|
|
</div>
|
|
<div class="text-end">
|
|
<span class="badge bg-secondary">{{ activity.get_action_display }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% empty %}
|
|
<div class="text-muted text-center py-3">
|
|
<i class="fas fa-history fa-2x mb-2"></i>
|
|
<p>No activity recorded yet.</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sidebar -->
|
|
<div class="col-lg-4">
|
|
<!-- Quick Actions -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-bolt me-2"></i>Quick Actions
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="d-grid gap-2">
|
|
{% if qc_sample.status == 'pending' or qc_sample.status == 'in_progress' %}
|
|
<a href="{% url 'laboratory:enter_qc_result' qc_sample.pk %}" class="btn btn-primary">
|
|
<i class="fas fa-edit me-2"></i>Enter Result
|
|
</a>
|
|
{% endif %}
|
|
|
|
<button type="button" class="btn btn-outline-info" onclick="printQCLabel()">
|
|
<i class="fas fa-print me-2"></i>Print Label
|
|
</button>
|
|
|
|
<button type="button" class="btn btn-outline-success" onclick="exportQCData()">
|
|
<i class="fas fa-download me-2"></i>Export Data
|
|
</button>
|
|
|
|
{% if qc_sample.status == 'pending' %}
|
|
<button type="button" class="btn btn-outline-danger" onclick="cancelQCSample()">
|
|
<i class="fas fa-times me-2"></i>Cancel Sample
|
|
</button>
|
|
{% endif %}
|
|
|
|
{% if qc_sample.status == 'completed' and not qc_sample.reviewed_by %}
|
|
<button type="button" class="btn btn-outline-warning" onclick="reviewQCSample()">
|
|
<i class="fas fa-check me-2"></i>Review & Approve
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Related QC Samples -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-link me-2"></i>Related QC Samples
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
{% for related_sample in related_qc_samples %}
|
|
<div class="d-flex align-items-center mb-2">
|
|
<i class="fas fa-vial me-2 text-muted"></i>
|
|
<div class="flex-grow-1">
|
|
<a href="{% url 'laboratory:qc_sample_detail' related_sample.pk %}" class="text-decoration-none">
|
|
{{ related_sample.sample_id }}
|
|
</a>
|
|
<small class="d-block text-muted">{{ related_sample.run_date|date:"M d, Y" }}</small>
|
|
</div>
|
|
<span class="badge qc-status-{{ related_sample.status }}">
|
|
{{ related_sample.get_status_display }}
|
|
</span>
|
|
</div>
|
|
{% empty %}
|
|
<div class="text-muted text-center py-3">
|
|
<i class="fas fa-link fa-2x mb-2"></i>
|
|
<p>No related QC samples found.</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- QC Statistics -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-chart-bar me-2"></i>QC Statistics
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row text-center">
|
|
<div class="col-6">
|
|
<h4 class="text-success">{{ qc_stats.pass_rate }}%</h4>
|
|
<small class="text-muted">Pass Rate</small>
|
|
</div>
|
|
<div class="col-6">
|
|
<h4 class="text-primary">{{ qc_stats.total_runs }}</h4>
|
|
<small class="text-muted">Total Runs</small>
|
|
</div>
|
|
</div>
|
|
<hr>
|
|
<div class="row text-center">
|
|
<div class="col-6">
|
|
<h5 class="text-warning">{{ qc_stats.out_of_range }}</h5>
|
|
<small class="text-muted">Out of Range</small>
|
|
</div>
|
|
<div class="col-6">
|
|
<h5 class="text-danger">{{ qc_stats.failed }}</h5>
|
|
<small class="text-muted">Failed</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script src="{% static 'assets/plugins/chart.js/dist/chart.min.js' %}"></script>
|
|
|
|
<script>
|
|
$(document).ready(function() {
|
|
// Initialize QC Trend Chart
|
|
{% if qc_sample.result_value %}
|
|
initializeQCTrendChart();
|
|
{% endif %}
|
|
});
|
|
|
|
function initializeQCTrendChart() {
|
|
const ctx = document.getElementById('qcTrendChart').getContext('2d');
|
|
|
|
// Sample data - replace with actual data from backend
|
|
const chartData = {
|
|
labels: {{ trend_dates|safe }},
|
|
datasets: [{
|
|
label: 'QC Results',
|
|
data: {{ trend_values|safe }},
|
|
borderColor: 'rgb(75, 192, 192)',
|
|
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
|
tension: 0.1
|
|
}, {
|
|
label: 'Upper Limit',
|
|
data: {{ upper_limits|safe }},
|
|
borderColor: 'rgb(255, 99, 132)',
|
|
borderDash: [5, 5],
|
|
fill: false
|
|
}, {
|
|
label: 'Lower Limit',
|
|
data: {{ lower_limits|safe }},
|
|
borderColor: 'rgb(255, 99, 132)',
|
|
borderDash: [5, 5],
|
|
fill: false
|
|
}]
|
|
};
|
|
|
|
new Chart(ctx, {
|
|
type: 'line',
|
|
data: chartData,
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
title: {
|
|
display: true,
|
|
text: 'QC Trend Analysis - {{ qc_sample.test_type.name }}'
|
|
},
|
|
legend: {
|
|
position: 'top'
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: false,
|
|
title: {
|
|
display: true,
|
|
text: 'Value ({{ qc_sample.unit }})'
|
|
}
|
|
},
|
|
x: {
|
|
title: {
|
|
display: true,
|
|
text: 'Date'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function printQCLabel() {
|
|
window.open('{% url "laboratory:print_qc_label" qc_sample.pk %}', '_blank');
|
|
}
|
|
|
|
function exportQCData() {
|
|
window.location.href = '{% url "laboratory:export_qc_data" qc_sample.pk %}';
|
|
}
|
|
|
|
function cancelQCSample() {
|
|
if (confirm('Are you sure you want to cancel this QC sample?')) {
|
|
$.ajax({
|
|
url: '{% url "laboratory:cancel_qc_sample" qc_sample.pk %}',
|
|
method: 'POST',
|
|
data: {
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
},
|
|
success: function(response) {
|
|
if (response.success) {
|
|
location.reload();
|
|
} else {
|
|
alert('Error cancelling QC sample: ' + response.message);
|
|
}
|
|
},
|
|
error: function() {
|
|
alert('Error cancelling QC sample.');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function reviewQCSample() {
|
|
window.location.href = '{% url "laboratory:review_qc_sample" qc_sample.pk %}';
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
|