Marwan Alwali be70e47e22 update
2025-08-30 09:45:26 +03:00

674 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends 'base.html' %}
{% load static %}
{% block title %}Enter QC Result - {{ qc_sample.sample_id }}{% endblock %}
{% block extra_css %}
<style>
.result-entry-card {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.sample-info-card {
background: #e3f2fd;
border-left: 4px solid #2196f3;
padding: 1rem;
margin-bottom: 1rem;
}
.result-status-within { background-color: #d4edda; color: #155724; }
.result-status-out-of-range { background-color: #f8d7da; color: #721c24; }
.result-status-critical { background-color: #f5c6cb; color: #721c24; }
.validation-indicator {
padding: 0.5rem;
border-radius: 0.25rem;
margin-top: 0.5rem;
display: none;
}
.validation-indicator.valid {
background-color: #d4edda;
color: #155724;
display: block;
}
.validation-indicator.invalid {
background-color: #f8d7da;
color: #721c24;
display: block;
}
.result-input {
font-size: 1.25rem;
font-weight: bold;
text-align: center;
padding: 1rem;
}
.expected-range-display {
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 0.25rem;
padding: 0.75rem;
margin: 0.5rem 0;
}
.quick-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.trend-chart-container {
height: 200px;
margin: 1rem 0;
}
@media (max-width: 768px) {
.result-input {
font-size: 1rem;
padding: 0.75rem;
}
.quick-actions {
justify-content: center;
}
}
</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:qc_sample_list' %}">QC Samples</a></li>
<li class="breadcrumb-item"><a href="{% url 'laboratory:qc_sample_detail' qc_sample.pk %}">{{ qc_sample.sample_id }}</a></li>
<li class="breadcrumb-item active">Enter Result</li>
</ol>
<h1 class="page-header mb-0">
<i class="fas fa-edit me-2"></i>Enter QC Result
</h1>
</div>
<div class="ms-auto">
<a href="{% url 'laboratory:qc_sample_detail' qc_sample.pk %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>Back to Sample
</a>
</div>
</div>
<div class="row">
<div class="col-lg-8">
<!-- Sample Information -->
<div class="sample-info-card">
<div class="row">
<div class="col-md-6">
<h6 class="mb-2">
<i class="fas fa-vial me-2"></i>{{ qc_sample.sample_id }}
</h6>
<p class="mb-1"><strong>Test:</strong> {{ qc_sample.test_type.name }}</p>
<p class="mb-1"><strong>QC Level:</strong> {{ qc_sample.get_qc_level_display }}</p>
<p class="mb-0"><strong>Lot:</strong> {{ qc_sample.lot_number }}</p>
</div>
<div class="col-md-6">
<p class="mb-1"><strong>Run Date:</strong> {{ qc_sample.run_date|date:"M d, Y" }}</p>
<p class="mb-1"><strong>Run Time:</strong> {{ qc_sample.run_time|time:"H:i" }}</p>
<p class="mb-0"><strong>Expected Range:</strong> {{ qc_sample.expected_range|default:"Not specified" }}</p>
</div>
</div>
</div>
<!-- Result Entry Form -->
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-calculator me-2"></i>Result Entry
</h5>
</div>
<div class="card-body">
<form id="resultForm" method="post">
{% csrf_token %}
<div class="result-entry-card">
<div class="row">
<div class="col-md-6">
<div class="form-group mb-3">
<label class="form-label fw-bold">Result Value *</label>
<div class="input-group">
<input type="number" class="form-control result-input"
name="result_value" id="result-value-input"
value="{{ qc_sample.result_value|default:'' }}"
step="0.001" placeholder="Enter result" required>
<span class="input-group-text">{{ qc_sample.test_type.unit|default:"" }}</span>
</div>
<div id="validation-indicator" class="validation-indicator"></div>
</div>
</div>
<div class="col-md-6">
<div class="form-group mb-3">
<label class="form-label fw-bold">Result Status</label>
<select class="form-select" name="result_status" id="result-status-select">
<option value="within_range" {% if qc_sample.result_status == 'within_range' %}selected{% endif %}>
Within Range
</option>
<option value="out_of_range" {% if qc_sample.result_status == 'out_of_range' %}selected{% endif %}>
Out of Range
</option>
<option value="critical" {% if qc_sample.result_status == 'critical' %}selected{% endif %}>
Critical
</option>
</select>
</div>
</div>
</div>
{% if qc_sample.expected_range %}
<div class="expected-range-display">
<div class="d-flex justify-content-between align-items-center">
<span><strong>Expected Range:</strong> {{ qc_sample.expected_range }}</span>
<span id="range-status" class="badge"></span>
</div>
</div>
{% endif %}
<div class="quick-actions">
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="setTargetValue()">
<i class="fas fa-bullseye me-1"></i>Use Target
</button>
<button type="button" class="btn btn-outline-info btn-sm" onclick="showCalculator()">
<i class="fas fa-calculator me-1"></i>Calculator
</button>
<button type="button" class="btn btn-outline-warning btn-sm" onclick="flagOutlier()">
<i class="fas fa-exclamation-triangle me-1"></i>Flag Outlier
</button>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group mb-3">
<label class="form-label">Instrument Used</label>
<select class="form-select" name="instrument">
<option value="">Select instrument...</option>
{% for instrument in instruments %}
<option value="{{ instrument.id }}"
{% if qc_sample.instrument_id == instrument.id %}selected{% endif %}>
{{ instrument.name }} ({{ instrument.model }})
</option>
{% endfor %}
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group mb-3">
<label class="form-label">Technician</label>
<select class="form-select" name="technician">
<option value="{{ request.user.id }}" selected>
{{ request.user.get_full_name }} (Current User)
</option>
{% for tech in technicians %}
{% if tech.id != request.user.id %}
<option value="{{ tech.id }}">
{{ tech.get_full_name }}
</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
</div>
<div class="form-group mb-3">
<label class="form-label">Result Comments</label>
<textarea class="form-control" name="result_comments" rows="3"
placeholder="Any observations, notes, or comments about this result...">{{ qc_sample.result_comments|default:'' }}</textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" name="requires_review"
id="requires-review" {% if qc_sample.result_status == 'out_of_range' or qc_sample.result_status == 'critical' %}checked{% endif %}>
<label class="form-check-label" for="requires-review">
Requires supervisor review
</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" name="notify_supervisor"
id="notify-supervisor">
<label class="form-check-label" for="notify-supervisor">
Notify supervisor immediately
</label>
</div>
</div>
</div>
<div class="d-flex justify-content-between">
<div>
<a href="{% url 'laboratory:qc_sample_detail' qc_sample.pk %}" class="btn btn-outline-secondary">
<i class="fas fa-times me-1"></i>Cancel
</a>
</div>
<div>
<button type="button" class="btn btn-outline-primary me-2" onclick="saveAsDraft()">
<i class="fas fa-save me-1"></i>Save as Draft
</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-check me-1"></i>Submit Result
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<!-- Sidebar -->
<div class="col-lg-4">
<!-- QC Trend Chart -->
<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
</h5>
</div>
<div class="card-body">
<div class="trend-chart-container">
<canvas id="qcTrendChart"></canvas>
</div>
<div class="text-center">
<small class="text-muted">Last 10 QC results for {{ qc_sample.test_type.name }}</small>
</div>
</div>
</div>
<!-- Recent Results -->
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-history me-2"></i>Recent Results
</h5>
</div>
<div class="card-body">
{% for recent_result in recent_results %}
<div class="d-flex align-items-center mb-2">
<div class="flex-grow-1">
<div class="fw-bold">{{ recent_result.sample_id }}</div>
<small class="text-muted">{{ recent_result.run_date|date:"M d, Y" }}</small>
</div>
<div class="text-end">
<div class="fw-bold">{{ recent_result.result_value }} {{ recent_result.unit }}</div>
<span class="badge result-status-{{ recent_result.result_status }}">
{{ recent_result.get_result_status_display }}
</span>
</div>
</div>
{% empty %}
<div class="text-muted text-center py-3">
<i class="fas fa-history fa-2x mb-2"></i>
<p>No recent results</p>
</div>
{% endfor %}
</div>
</div>
<!-- QC Guidelines -->
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-info-circle me-2"></i>QC Guidelines
</h5>
</div>
<div class="card-body">
<div class="mb-3">
<h6>Result Interpretation:</h6>
<ul class="list-unstyled">
<li class="mb-1">
<span class="badge result-status-within me-2">Within Range</span>
Acceptable result
</li>
<li class="mb-1">
<span class="badge result-status-out-of-range me-2">Out of Range</span>
Requires investigation
</li>
<li class="mb-1">
<span class="badge result-status-critical me-2">Critical</span>
Immediate action required
</li>
</ul>
</div>
<div class="mb-3">
<h6>Actions Required:</h6>
<ul class="list-unstyled">
<li><i class="fas fa-check text-success me-2"></i>Verify instrument calibration</li>
<li><i class="fas fa-check text-success me-2"></i>Check control material expiry</li>
<li><i class="fas fa-check text-success me-2"></i>Review procedure compliance</li>
<li><i class="fas fa-check text-success me-2"></i>Document corrective actions</li>
</ul>
</div>
</div>
</div>
<!-- Quick Reference -->
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-book me-2"></i>Quick Reference
</h5>
</div>
<div class="card-body">
<div class="mb-2">
<strong>Target Value:</strong> {{ qc_sample.target_value|default:"Not set" }}
</div>
<div class="mb-2">
<strong>CV Limit:</strong> {{ qc_sample.cv_limit|default:"Not set" }}%
</div>
<div class="mb-2">
<strong>SD Limit:</strong> {{ qc_sample.sd_limit|default:"Not set" }}
</div>
<div class="mb-2">
<strong>Westgard Rules:</strong>
<a href="#" onclick="showWestgardRules()">View Rules</a>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Calculator Modal -->
<div class="modal fade" id="calculatorModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-calculator me-2"></i>Calculator
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="calculator">
<input type="text" class="form-control mb-3" id="calculator-display" readonly>
<div class="row g-2">
<div class="col-3"><button class="btn btn-outline-secondary w-100" onclick="clearCalculator()">C</button></div>
<div class="col-3"><button class="btn btn-outline-secondary w-100" onclick="appendToCalculator('/')">/</button></div>
<div class="col-3"><button class="btn btn-outline-secondary w-100" onclick="appendToCalculator('*')">×</button></div>
<div class="col-3"><button class="btn btn-outline-secondary w-100" onclick="deleteLast()"></button></div>
<div class="col-3"><button class="btn btn-outline-primary w-100" onclick="appendToCalculator('7')">7</button></div>
<div class="col-3"><button class="btn btn-outline-primary w-100" onclick="appendToCalculator('8')">8</button></div>
<div class="col-3"><button class="btn btn-outline-primary w-100" onclick="appendToCalculator('9')">9</button></div>
<div class="col-3"><button class="btn btn-outline-secondary w-100" onclick="appendToCalculator('-')">-</button></div>
<div class="col-3"><button class="btn btn-outline-primary w-100" onclick="appendToCalculator('4')">4</button></div>
<div class="col-3"><button class="btn btn-outline-primary w-100" onclick="appendToCalculator('5')">5</button></div>
<div class="col-3"><button class="btn btn-outline-primary w-100" onclick="appendToCalculator('6')">6</button></div>
<div class="col-3"><button class="btn btn-outline-secondary w-100" onclick="appendToCalculator('+')">+</button></div>
<div class="col-3"><button class="btn btn-outline-primary w-100" onclick="appendToCalculator('1')">1</button></div>
<div class="col-3"><button class="btn btn-outline-primary w-100" onclick="appendToCalculator('2')">2</button></div>
<div class="col-3"><button class="btn btn-outline-primary w-100" onclick="appendToCalculator('3')">3</button></div>
<div class="col-3 row-span-2"><button class="btn btn-success w-100 h-100" onclick="calculateResult()">=</button></div>
<div class="col-6"><button class="btn btn-outline-primary w-100" onclick="appendToCalculator('0')">0</button></div>
<div class="col-3"><button class="btn btn-outline-primary w-100" onclick="appendToCalculator('.')">.</button></div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" onclick="useCalculatorResult()">Use Result</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'assets/plugins/chart.js/dist/chart.min.js' %}"></script>
<script>
let calculatorValue = '';
let expectedRange = null;
$(document).ready(function() {
// Parse expected range if available
const rangeText = '{{ qc_sample.expected_range|default:"" }}';
if (rangeText) {
parseExpectedRange(rangeText);
}
// Initialize trend chart
initializeTrendChart();
// Real-time validation
$('#result-value-input').on('input', function() {
validateResult();
updateResultStatus();
});
// Form submission
$('#resultForm').on('submit', function(e) {
if (!validateForm()) {
e.preventDefault();
return false;
}
});
// Initial validation
validateResult();
});
function parseExpectedRange(rangeText) {
// Parse range like "4.5-6.0" or "4.5 - 6.0 mg/dL"
const match = rangeText.match(/(\d+\.?\d*)\s*-\s*(\d+\.?\d*)/);
if (match) {
expectedRange = {
min: parseFloat(match[1]),
max: parseFloat(match[2])
};
}
}
function validateResult() {
const resultValue = parseFloat($('#result-value-input').val());
const indicator = $('#validation-indicator');
const rangeStatus = $('#range-status');
if (isNaN(resultValue)) {
indicator.removeClass('valid invalid').hide();
rangeStatus.removeClass().addClass('badge');
return;
}
if (expectedRange) {
if (resultValue >= expectedRange.min && resultValue <= expectedRange.max) {
indicator.removeClass('invalid').addClass('valid')
.html('<i class="fas fa-check me-2"></i>Result is within expected range');
rangeStatus.removeClass().addClass('badge bg-success').text('Within Range');
} else {
indicator.removeClass('valid').addClass('invalid')
.html('<i class="fas fa-exclamation-triangle me-2"></i>Result is outside expected range');
rangeStatus.removeClass().addClass('badge bg-danger').text('Out of Range');
}
}
}
function updateResultStatus() {
const resultValue = parseFloat($('#result-value-input').val());
const statusSelect = $('#result-status-select');
if (isNaN(resultValue) || !expectedRange) return;
if (resultValue >= expectedRange.min && resultValue <= expectedRange.max) {
statusSelect.val('within_range');
$('#requires-review').prop('checked', false);
} else {
const deviation = Math.abs(resultValue - (expectedRange.min + expectedRange.max) / 2);
const rangeSize = expectedRange.max - expectedRange.min;
if (deviation > rangeSize) {
statusSelect.val('critical');
$('#requires-review').prop('checked', true);
$('#notify-supervisor').prop('checked', true);
} else {
statusSelect.val('out_of_range');
$('#requires-review').prop('checked', true);
}
}
}
function validateForm() {
const resultValue = $('#result-value-input').val();
if (!resultValue) {
alert('Please enter a result value.');
$('#result-value-input').focus();
return false;
}
if (isNaN(parseFloat(resultValue))) {
alert('Please enter a valid numeric result.');
$('#result-value-input').focus();
return false;
}
return true;
}
function setTargetValue() {
const targetValue = {{ qc_sample.target_value|default:"null" }};
if (targetValue) {
$('#result-value-input').val(targetValue).trigger('input');
} else {
alert('No target value set for this QC sample.');
}
}
function showCalculator() {
calculatorValue = $('#result-value-input').val() || '';
$('#calculator-display').val(calculatorValue);
$('#calculatorModal').modal('show');
}
function appendToCalculator(value) {
calculatorValue += value;
$('#calculator-display').val(calculatorValue);
}
function clearCalculator() {
calculatorValue = '';
$('#calculator-display').val('');
}
function deleteLast() {
calculatorValue = calculatorValue.slice(0, -1);
$('#calculator-display').val(calculatorValue);
}
function calculateResult() {
try {
const result = eval(calculatorValue);
calculatorValue = result.toString();
$('#calculator-display').val(calculatorValue);
} catch (e) {
alert('Invalid calculation');
}
}
function useCalculatorResult() {
$('#result-value-input').val(calculatorValue).trigger('input');
$('#calculatorModal').modal('hide');
}
function flagOutlier() {
$('#result-status-select').val('critical');
$('#requires-review').prop('checked', true);
$('#notify-supervisor').prop('checked', true);
const comments = $('textarea[name="result_comments"]');
const currentComments = comments.val();
const outlierNote = 'FLAGGED AS OUTLIER: ';
if (!currentComments.includes(outlierNote)) {
comments.val(outlierNote + currentComments);
}
}
function saveAsDraft() {
const form = $('#resultForm');
const originalAction = form.attr('action');
// Add draft parameter
form.append('<input type="hidden" name="save_as_draft" value="1">');
// Submit form
form.submit();
}
function initializeTrendChart() {
const ctx = document.getElementById('qcTrendChart').getContext('2d');
// Sample trend data - replace with actual data from backend
const trendData = {
labels: {{ trend_dates|safe|default:"[]" }},
datasets: [{
label: 'QC Results',
data: {{ trend_values|safe|default:"[]" }},
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.1,
pointRadius: 4,
pointHoverRadius: 6
}]
};
new Chart(ctx, {
type: 'line',
data: trendData,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: false,
title: {
display: true,
text: 'Value'
}
},
x: {
title: {
display: true,
text: 'Date'
}
}
}
}
});
}
function showWestgardRules() {
alert('Westgard Rules:\n\n1₂s: 1 control observation exceeds 2s\n1₃s: 1 control observation exceeds 3s\n2₂s: 2 consecutive control observations exceed 2s\n4₁s: 4 consecutive control observations exceed 1s\nR₄s: Range of 4 consecutive controls exceeds 4s\n10x̄: 10 consecutive controls on same side of mean');
}
</script>
{% endblock %}