update
This commit is contained in:
parent
25f548825b
commit
be70e47e22
Binary file not shown.
@ -2149,12 +2149,12 @@ def payment_download(request, payment_id):
|
||||
#
|
||||
#
|
||||
# # Legacy view functions for backward compatibility
|
||||
# dashboard = BillingDashboardView.as_view()
|
||||
# bill_list = MedicalBillListView.as_view()
|
||||
# bill_detail = MedicalBillDetailView.as_view()
|
||||
# bill_create = MedicalBillCreateView.as_view()
|
||||
# claim_list = InsuranceClaimListView.as_view()
|
||||
# payment_list = PaymentListView.as_view()
|
||||
dashboard = BillingDashboardView.as_view()
|
||||
bill_list = MedicalBillListView.as_view()
|
||||
bill_detail = MedicalBillDetailView.as_view()
|
||||
bill_create = MedicalBillCreateView.as_view()
|
||||
claim_list = InsuranceClaimListView.as_view()
|
||||
payment_list = PaymentListView.as_view()
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -638,7 +638,7 @@ def add_vital_signs(request, encounter_id):
|
||||
vital_signs = VitalSigns.objects.create(**vital_signs_data)
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=request.user,
|
||||
action='VITAL_SIGNS_RECORDED',
|
||||
model='VitalSigns',
|
||||
@ -687,7 +687,7 @@ def add_problem(request, patient_id):
|
||||
problem = ProblemList.objects.create(**problem_data)
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=request.user,
|
||||
action='PROBLEM_ADDED',
|
||||
model='ProblemList',
|
||||
@ -727,7 +727,7 @@ def update_encounter_status(request, encounter_id):
|
||||
encounter.save()
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=request.user,
|
||||
action='ENCOUNTER_STATUS_UPDATED',
|
||||
model='Encounter',
|
||||
@ -765,7 +765,7 @@ def sign_note(request, note_id):
|
||||
note.save()
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=request.user,
|
||||
action='NOTE_SIGNED',
|
||||
model='ClinicalNote',
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -167,7 +167,7 @@ class SpecimenForm(forms.ModelForm):
|
||||
fields = [
|
||||
'order', 'specimen_type', 'collected_datetime',
|
||||
'collection_method', 'collection_site', 'volume',
|
||||
'container_type', 'quality_notes'
|
||||
'container_type', 'quality_notes',
|
||||
]
|
||||
widgets = {
|
||||
'collected_datetime': forms.DateTimeInput(
|
||||
@ -185,7 +185,7 @@ class SpecimenForm(forms.ModelForm):
|
||||
if user and hasattr(user, 'tenant'):
|
||||
self.fields['order'].queryset = LabOrder.objects.filter(
|
||||
tenant=user.tenant,
|
||||
status__in=['PENDING', 'SCHEDULED']
|
||||
# status__in=['PENDING', 'SCHEDULED']
|
||||
).select_related('patient', 'test').order_by('-order_datetime')
|
||||
|
||||
# Set default collection time to now
|
||||
|
||||
25
laboratory/migrations/0002_qualitycontrol_result.py
Normal file
25
laboratory/migrations/0002_qualitycontrol_result.py
Normal file
@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-28 19:46
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("laboratory", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="qualitycontrol",
|
||||
name="result",
|
||||
field=models.ForeignKey(
|
||||
default=1,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="quality_controls",
|
||||
to="laboratory.labresult",
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -337,7 +337,28 @@ class LabOrder(models.Model):
|
||||
"""
|
||||
Lab order model for test ordering and management.
|
||||
"""
|
||||
|
||||
PRIORITY_CHOICES = [
|
||||
('ROUTINE', 'Routine'),
|
||||
('URGENT', 'Urgent'),
|
||||
('STAT', 'STAT'),
|
||||
('ASAP', 'ASAP'),
|
||||
('TIMED', 'Timed'),
|
||||
]
|
||||
STATUS_CHOICES = [
|
||||
('PENDING', 'Pending'),
|
||||
('SCHEDULED', 'Scheduled'),
|
||||
('COLLECTED', 'Collected'),
|
||||
('IN_PROGRESS', 'In Progress'),
|
||||
('COMPLETED', 'Completed'),
|
||||
('CANCELLED', 'Cancelled'),
|
||||
('ON_HOLD', 'On Hold'),
|
||||
]
|
||||
FASTING_STATUS_CHOICES = [
|
||||
('FASTING', 'Fasting'),
|
||||
('NON_FASTING', 'Non-Fasting'),
|
||||
('UNKNOWN', 'Unknown'),
|
||||
]
|
||||
|
||||
# Tenant relationship
|
||||
tenant = models.ForeignKey(
|
||||
'core.Tenant',
|
||||
@ -387,13 +408,7 @@ class LabOrder(models.Model):
|
||||
)
|
||||
priority = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('ROUTINE', 'Routine'),
|
||||
('URGENT', 'Urgent'),
|
||||
('STAT', 'STAT'),
|
||||
('ASAP', 'ASAP'),
|
||||
('TIMED', 'Timed'),
|
||||
],
|
||||
choices=PRIORITY_CHOICES,
|
||||
default='ROUTINE',
|
||||
help_text='Order priority'
|
||||
)
|
||||
@ -430,11 +445,7 @@ class LabOrder(models.Model):
|
||||
)
|
||||
fasting_status = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('FASTING', 'Fasting'),
|
||||
('NON_FASTING', 'Non-Fasting'),
|
||||
('UNKNOWN', 'Unknown'),
|
||||
],
|
||||
choices=FASTING_STATUS_CHOICES,
|
||||
default='UNKNOWN',
|
||||
help_text='Patient fasting status'
|
||||
)
|
||||
@ -442,15 +453,7 @@ class LabOrder(models.Model):
|
||||
# Status
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('PENDING', 'Pending'),
|
||||
('SCHEDULED', 'Scheduled'),
|
||||
('COLLECTED', 'Collected'),
|
||||
('IN_PROGRESS', 'In Progress'),
|
||||
('COMPLETED', 'Completed'),
|
||||
('CANCELLED', 'Cancelled'),
|
||||
('ON_HOLD', 'On Hold'),
|
||||
],
|
||||
choices=STATUS_CHOICES,
|
||||
default='PENDING',
|
||||
help_text='Order status'
|
||||
)
|
||||
@ -527,7 +530,54 @@ class Specimen(models.Model):
|
||||
"""
|
||||
Specimen model for specimen tracking and management.
|
||||
"""
|
||||
|
||||
SPECIMEN_TYPE_CHOICES = [
|
||||
('BLOOD', 'Blood'),
|
||||
('SERUM', 'Serum'),
|
||||
('PLASMA', 'Plasma'),
|
||||
('URINE', 'Urine'),
|
||||
('STOOL', 'Stool'),
|
||||
('CSF', 'Cerebrospinal Fluid'),
|
||||
('SPUTUM', 'Sputum'),
|
||||
('SWAB', 'Swab'),
|
||||
('TISSUE', 'Tissue'),
|
||||
('FLUID', 'Body Fluid'),
|
||||
('SALIVA', 'Saliva'),
|
||||
('HAIR', 'Hair'),
|
||||
('NAIL', 'Nail'),
|
||||
('OTHER', 'Other'),
|
||||
]
|
||||
QUALITY_CHOICES = [
|
||||
('ACCEPTABLE', 'Acceptable'),
|
||||
('SUBOPTIMAL', 'Suboptimal'),
|
||||
('REJECTED', 'Rejected'),
|
||||
]
|
||||
REJECTION_REASON_CHOICES = [
|
||||
('HEMOLYZED', 'Hemolyzed'),
|
||||
('CLOTTED', 'Clotted'),
|
||||
('INSUFFICIENT_VOLUME', 'Insufficient Volume'),
|
||||
('CONTAMINATED', 'Contaminated'),
|
||||
('MISLABELED', 'Mislabeled'),
|
||||
('EXPIRED', 'Expired'),
|
||||
('IMPROPER_STORAGE', 'Improper Storage'),
|
||||
('DAMAGED_CONTAINER', 'Damaged Container'),
|
||||
('OTHER', 'Other'),
|
||||
]
|
||||
STORAGE_TEMPERATURE_CHOICES = [
|
||||
('ROOM_TEMP', 'Room Temperature'),
|
||||
('REFRIGERATED', 'Refrigerated (2-8°C)'),
|
||||
('FROZEN', 'Frozen (-20°C)'),
|
||||
('DEEP_FROZEN', 'Deep Frozen (-80°C)'),
|
||||
]
|
||||
STATUS_CHOICES = [
|
||||
('COLLECTED', 'Collected'),
|
||||
('IN_TRANSIT', 'In Transit'),
|
||||
('RECEIVED', 'Received'),
|
||||
('PROCESSING', 'Processing'),
|
||||
('COMPLETED', 'Completed'),
|
||||
('REJECTED', 'Rejected'),
|
||||
('DISPOSED', 'Disposed'),
|
||||
]
|
||||
|
||||
# Order relationship
|
||||
order = models.ForeignKey(
|
||||
LabOrder,
|
||||
@ -552,22 +602,7 @@ class Specimen(models.Model):
|
||||
# Specimen Details
|
||||
specimen_type = models.CharField(
|
||||
max_length=30,
|
||||
choices=[
|
||||
('BLOOD', 'Blood'),
|
||||
('SERUM', 'Serum'),
|
||||
('PLASMA', 'Plasma'),
|
||||
('URINE', 'Urine'),
|
||||
('STOOL', 'Stool'),
|
||||
('CSF', 'Cerebrospinal Fluid'),
|
||||
('SPUTUM', 'Sputum'),
|
||||
('SWAB', 'Swab'),
|
||||
('TISSUE', 'Tissue'),
|
||||
('FLUID', 'Body Fluid'),
|
||||
('SALIVA', 'Saliva'),
|
||||
('HAIR', 'Hair'),
|
||||
('NAIL', 'Nail'),
|
||||
('OTHER', 'Other'),
|
||||
],
|
||||
choices=SPECIMEN_TYPE_CHOICES,
|
||||
help_text='Specimen type'
|
||||
)
|
||||
container_type = models.CharField(
|
||||
@ -611,11 +646,7 @@ class Specimen(models.Model):
|
||||
# Specimen Quality
|
||||
quality = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('ACCEPTABLE', 'Acceptable'),
|
||||
('SUBOPTIMAL', 'Suboptimal'),
|
||||
('REJECTED', 'Rejected'),
|
||||
],
|
||||
choices=QUALITY_CHOICES,
|
||||
default='ACCEPTABLE',
|
||||
help_text='Specimen quality'
|
||||
)
|
||||
@ -623,17 +654,7 @@ class Specimen(models.Model):
|
||||
max_length=100,
|
||||
blank=True,
|
||||
null=True,
|
||||
choices=[
|
||||
('HEMOLYZED', 'Hemolyzed'),
|
||||
('CLOTTED', 'Clotted'),
|
||||
('INSUFFICIENT_VOLUME', 'Insufficient Volume'),
|
||||
('CONTAMINATED', 'Contaminated'),
|
||||
('MISLABELED', 'Mislabeled'),
|
||||
('EXPIRED', 'Expired'),
|
||||
('IMPROPER_STORAGE', 'Improper Storage'),
|
||||
('DAMAGED_CONTAINER', 'Damaged Container'),
|
||||
('OTHER', 'Other'),
|
||||
],
|
||||
choices=REJECTION_REASON_CHOICES,
|
||||
help_text='Reason for rejection'
|
||||
)
|
||||
quality_notes = models.TextField(
|
||||
@ -666,12 +687,7 @@ class Specimen(models.Model):
|
||||
)
|
||||
storage_temperature = models.CharField(
|
||||
max_length=30,
|
||||
choices=[
|
||||
('ROOM_TEMP', 'Room Temperature'),
|
||||
('REFRIGERATED', 'Refrigerated (2-8°C)'),
|
||||
('FROZEN', 'Frozen (-20°C)'),
|
||||
('DEEP_FROZEN', 'Deep Frozen (-80°C)'),
|
||||
],
|
||||
choices=STORAGE_TEMPERATURE_CHOICES,
|
||||
default='ROOM_TEMP',
|
||||
help_text='Storage temperature'
|
||||
)
|
||||
@ -679,15 +695,7 @@ class Specimen(models.Model):
|
||||
# Status
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('COLLECTED', 'Collected'),
|
||||
('IN_TRANSIT', 'In Transit'),
|
||||
('RECEIVED', 'Received'),
|
||||
('PROCESSING', 'Processing'),
|
||||
('COMPLETED', 'Completed'),
|
||||
('REJECTED', 'Rejected'),
|
||||
('DISPOSED', 'Disposed'),
|
||||
],
|
||||
choices=STATUS_CHOICES,
|
||||
default='COLLECTED',
|
||||
help_text='Specimen status'
|
||||
)
|
||||
@ -757,7 +765,28 @@ class LabResult(models.Model):
|
||||
"""
|
||||
Lab result model for test results and reporting.
|
||||
"""
|
||||
|
||||
RESULT_TYPE_CHOICES = [
|
||||
('NUMERIC', 'Numeric'),
|
||||
('TEXT', 'Text'),
|
||||
('CODED', 'Coded'),
|
||||
('NARRATIVE', 'Narrative'),
|
||||
]
|
||||
ABNORMAL_FLAG_CHOICES = [
|
||||
('N', 'Normal'),
|
||||
('H', 'High'),
|
||||
('L', 'Low'),
|
||||
('HH', 'Critical High'),
|
||||
('LL', 'Critical Low'),
|
||||
('A', 'Abnormal'),
|
||||
]
|
||||
STATUS_CHOICES = [
|
||||
('PENDING', 'Pending'),
|
||||
('IN_PROGRESS', 'In Progress'),
|
||||
('COMPLETED', 'Completed'),
|
||||
('VERIFIED', 'Verified'),
|
||||
('AMENDED', 'Amended'),
|
||||
('CANCELLED', 'Cancelled'),
|
||||
]
|
||||
# Order and Test relationship
|
||||
order = models.ForeignKey(
|
||||
LabOrder,
|
||||
@ -800,12 +829,7 @@ class LabResult(models.Model):
|
||||
)
|
||||
result_type = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('NUMERIC', 'Numeric'),
|
||||
('TEXT', 'Text'),
|
||||
('CODED', 'Coded'),
|
||||
('NARRATIVE', 'Narrative'),
|
||||
],
|
||||
choices=RESULT_TYPE_CHOICES,
|
||||
default='NUMERIC',
|
||||
help_text='Type of result'
|
||||
)
|
||||
@ -819,14 +843,7 @@ class LabResult(models.Model):
|
||||
)
|
||||
abnormal_flag = models.CharField(
|
||||
max_length=10,
|
||||
choices=[
|
||||
('N', 'Normal'),
|
||||
('H', 'High'),
|
||||
('L', 'Low'),
|
||||
('HH', 'Critical High'),
|
||||
('LL', 'Critical Low'),
|
||||
('A', 'Abnormal'),
|
||||
],
|
||||
choices=ABNORMAL_FLAG_CHOICES,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Abnormal flag'
|
||||
@ -896,14 +913,7 @@ class LabResult(models.Model):
|
||||
# Status
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('PENDING', 'Pending'),
|
||||
('IN_PROGRESS', 'In Progress'),
|
||||
('COMPLETED', 'Completed'),
|
||||
('VERIFIED', 'Verified'),
|
||||
('AMENDED', 'Amended'),
|
||||
('CANCELLED', 'Cancelled'),
|
||||
],
|
||||
choices=STATUS_CHOICES,
|
||||
default='PENDING',
|
||||
help_text='Result status'
|
||||
)
|
||||
@ -994,7 +1004,18 @@ class QualityControl(models.Model):
|
||||
"""
|
||||
Quality control model for lab quality management.
|
||||
"""
|
||||
|
||||
CONTROL_LEVEL_CHOICES = [
|
||||
('NORMAL', 'Normal'),
|
||||
('LOW', 'Low'),
|
||||
('HIGH', 'High'),
|
||||
('CRITICAL', 'Critical'),
|
||||
]
|
||||
STATUS_CHOICES = [
|
||||
('PASSED', 'Passed'),
|
||||
('FAILED', 'Failed'),
|
||||
('WARNING', 'Warning'),
|
||||
('PENDING', 'Pending'),
|
||||
]
|
||||
# Tenant relationship
|
||||
tenant = models.ForeignKey(
|
||||
'core.Tenant',
|
||||
@ -1010,7 +1031,7 @@ class QualityControl(models.Model):
|
||||
related_name='quality_controls',
|
||||
help_text='Lab test'
|
||||
)
|
||||
|
||||
result = models.ForeignKey(LabResult, on_delete=models.CASCADE, related_name='quality_controls')
|
||||
# QC Information
|
||||
qc_id = models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
@ -1030,12 +1051,7 @@ class QualityControl(models.Model):
|
||||
)
|
||||
control_level = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('NORMAL', 'Normal'),
|
||||
('LOW', 'Low'),
|
||||
('HIGH', 'High'),
|
||||
('CRITICAL', 'Critical'),
|
||||
],
|
||||
choices=CONTROL_LEVEL_CHOICES,
|
||||
help_text='Control level'
|
||||
)
|
||||
|
||||
@ -1069,12 +1085,7 @@ class QualityControl(models.Model):
|
||||
# QC Status
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('PASSED', 'Passed'),
|
||||
('FAILED', 'Failed'),
|
||||
('WARNING', 'Warning'),
|
||||
('PENDING', 'Pending'),
|
||||
],
|
||||
choices=STATUS_CHOICES,
|
||||
help_text='QC status'
|
||||
)
|
||||
|
||||
@ -1166,7 +1177,11 @@ class ReferenceRange(models.Model):
|
||||
"""
|
||||
Reference range model for test normal values.
|
||||
"""
|
||||
|
||||
GENDER_CHOICES = [
|
||||
('M', 'Male'),
|
||||
('F', 'Female'),
|
||||
('ALL', 'All'),
|
||||
]
|
||||
# Test relationship
|
||||
test = models.ForeignKey(
|
||||
LabTest,
|
||||
@ -1186,11 +1201,7 @@ class ReferenceRange(models.Model):
|
||||
# Demographics
|
||||
gender = models.CharField(
|
||||
max_length=10,
|
||||
choices=[
|
||||
('M', 'Male'),
|
||||
('F', 'Female'),
|
||||
('ALL', 'All'),
|
||||
],
|
||||
choices=GENDER_CHOICES,
|
||||
default='ALL',
|
||||
help_text='Gender'
|
||||
)
|
||||
|
||||
@ -63,23 +63,23 @@ class LaboratoryDashboardView(LoginRequiredMixin, TemplateView):
|
||||
status='PENDING'
|
||||
).count(),
|
||||
'results_completed_today': LabResult.objects.filter(
|
||||
tenant=tenant,
|
||||
result_datetime__date=today,
|
||||
order__tenant=tenant,
|
||||
analyzed_datetime__date=today,
|
||||
status='VERIFIED'
|
||||
).count(),
|
||||
'qc_tests_today': QualityControl.objects.filter(
|
||||
tenant=tenant,
|
||||
test_date=today
|
||||
run_datetime=today
|
||||
).count(),
|
||||
'total_tests_available': LabTest.objects.filter(
|
||||
tenant=tenant,
|
||||
is_active=True
|
||||
).count(),
|
||||
'critical_results': LabResult.objects.filter(
|
||||
tenant=tenant,
|
||||
order__tenant=tenant,
|
||||
is_critical=True,
|
||||
status='VERIFIED',
|
||||
result_datetime__date=today
|
||||
analyzed_datetime__date=today
|
||||
).count(),
|
||||
})
|
||||
|
||||
@ -90,9 +90,9 @@ class LaboratoryDashboardView(LoginRequiredMixin, TemplateView):
|
||||
|
||||
# Recent results
|
||||
context['recent_results'] = LabResult.objects.filter(
|
||||
tenant=tenant,
|
||||
order__tenant=tenant,
|
||||
status='VERIFIED'
|
||||
).select_related('order', 'test').order_by('-result_datetime')[:10]
|
||||
).select_related('order', 'test').order_by('-analyzed_datetime')[:10]
|
||||
|
||||
return context
|
||||
|
||||
@ -281,12 +281,12 @@ class ReferenceRangeListView(LoginRequiredMixin, ListView):
|
||||
List all reference ranges with filtering.
|
||||
"""
|
||||
model = ReferenceRange
|
||||
template_name = 'laboratory/reference_range_list.html'
|
||||
template_name = 'laboratory/reference_ranges/reference_range_list.html'
|
||||
context_object_name = 'reference_ranges'
|
||||
paginate_by = 25
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = ReferenceRange.objects.filter(tenant=self.request.user.tenant)
|
||||
queryset = ReferenceRange.objects.filter(test__tenant=self.request.user.tenant)
|
||||
|
||||
# Filter by test
|
||||
test_id = self.request.GET.get('test')
|
||||
@ -307,7 +307,7 @@ class ReferenceRangeListView(LoginRequiredMixin, ListView):
|
||||
tenant=self.request.user.tenant,
|
||||
is_active=True
|
||||
).order_by('test_name'),
|
||||
'genders': ReferenceRange._meta.get_field('gender').choices,
|
||||
'genders': ReferenceRange.GENDER_CHOICES,
|
||||
})
|
||||
return context
|
||||
|
||||
@ -317,11 +317,11 @@ class ReferenceRangeDetailView(LoginRequiredMixin, DetailView):
|
||||
Display detailed information about a reference range.
|
||||
"""
|
||||
model = ReferenceRange
|
||||
template_name = 'laboratory/reference_range_detail.html'
|
||||
template_name = 'laboratory/reference_ranges/reference_range_detail.html'
|
||||
context_object_name = 'reference_range'
|
||||
|
||||
def get_queryset(self):
|
||||
return ReferenceRange.objects.filter(tenant=self.request.user.tenant)
|
||||
return ReferenceRange.objects.filter(test__tenant=self.request.user.tenant)
|
||||
|
||||
|
||||
class ReferenceRangeCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||||
@ -330,7 +330,7 @@ class ReferenceRangeCreateView(LoginRequiredMixin, PermissionRequiredMixin, Crea
|
||||
"""
|
||||
model = ReferenceRange
|
||||
form_class = ReferenceRangeForm
|
||||
template_name = 'laboratory/reference_range_form.html'
|
||||
template_name = 'laboratory/reference_ranges/reference_range_form.html'
|
||||
permission_required = 'laboratory.add_referencerange'
|
||||
success_url = reverse_lazy('laboratory:reference_range_list')
|
||||
|
||||
@ -339,7 +339,7 @@ class ReferenceRangeCreateView(LoginRequiredMixin, PermissionRequiredMixin, Crea
|
||||
response = super().form_valid(form)
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=self.request.user,
|
||||
action='REFERENCE_RANGE_CREATED',
|
||||
model='ReferenceRange',
|
||||
@ -361,11 +361,11 @@ class ReferenceRangeUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Upda
|
||||
"""
|
||||
model = ReferenceRange
|
||||
form_class = ReferenceRangeForm
|
||||
template_name = 'laboratory/reference_range_form.html'
|
||||
template_name = 'laboratory/reference_ranges/reference_range_form.html'
|
||||
permission_required = 'laboratory.change_referencerange'
|
||||
|
||||
def get_queryset(self):
|
||||
return ReferenceRange.objects.filter(tenant=self.request.user.tenant)
|
||||
return ReferenceRange.objects.filter(test__tenant=self.request.user.tenant)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('laboratory:reference_range_detail', kwargs={'pk': self.object.pk})
|
||||
@ -374,7 +374,7 @@ class ReferenceRangeUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Upda
|
||||
response = super().form_valid(form)
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=self.request.user,
|
||||
action='REFERENCE_RANGE_UPDATED',
|
||||
model='ReferenceRange',
|
||||
@ -394,12 +394,12 @@ class ReferenceRangeDeleteView(LoginRequiredMixin, PermissionRequiredMixin, Dele
|
||||
Delete a reference range.
|
||||
"""
|
||||
model = ReferenceRange
|
||||
template_name = 'laboratory/reference_range_confirm_delete.html'
|
||||
template_name = 'laboratory/reference_ranges/reference_range_confirm_delete.html'
|
||||
permission_required = 'laboratory.delete_referencerange'
|
||||
success_url = reverse_lazy('laboratory:reference_range_list')
|
||||
|
||||
def get_queryset(self):
|
||||
return ReferenceRange.objects.filter(tenant=self.request.user.tenant)
|
||||
return ReferenceRange.objects.filter(test__tenant=self.request.user.tenant)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
@ -408,7 +408,7 @@ class ReferenceRangeDeleteView(LoginRequiredMixin, PermissionRequiredMixin, Dele
|
||||
response = super().delete(request, *args, **kwargs)
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=request.user,
|
||||
action='REFERENCE_RANGE_DELETED',
|
||||
model='ReferenceRange',
|
||||
@ -430,7 +430,7 @@ class LabOrderListView(LoginRequiredMixin, ListView):
|
||||
"""
|
||||
model = LabOrder
|
||||
template_name = 'laboratory/orders/lab_order_list.html'
|
||||
context_object_name = 'lab_orders'
|
||||
context_object_name = 'orders'
|
||||
paginate_by = 25
|
||||
|
||||
def get_queryset(self):
|
||||
@ -465,7 +465,7 @@ class LabOrderListView(LoginRequiredMixin, ListView):
|
||||
queryset = queryset.filter(order_datetime__date__lte=date_to)
|
||||
|
||||
return queryset.select_related(
|
||||
'patient', 'test', 'ordering_provider'
|
||||
'patient', 'ordering_provider'
|
||||
).order_by('-order_datetime')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@ -582,7 +582,7 @@ class SpecimenListView(LoginRequiredMixin, ListView):
|
||||
paginate_by = 25
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = Specimen.objects.filter(tenant=self.request.user.tenant)
|
||||
queryset = Specimen.objects.filter(order__tenant=self.request.user.tenant)
|
||||
|
||||
# Search functionality
|
||||
search = self.request.GET.get('search')
|
||||
@ -604,7 +604,7 @@ class SpecimenListView(LoginRequiredMixin, ListView):
|
||||
if specimen_type:
|
||||
queryset = queryset.filter(specimen_type=specimen_type)
|
||||
|
||||
return queryset.select_related('order__patient', 'order__test').order_by('-collection_datetime')
|
||||
return queryset.select_related('order__patient').order_by('-collected_datetime')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@ -624,7 +624,7 @@ class SpecimenDetailView(LoginRequiredMixin, DetailView):
|
||||
context_object_name = 'specimen'
|
||||
|
||||
def get_queryset(self):
|
||||
return Specimen.objects.filter(tenant=self.request.user.tenant)
|
||||
return Specimen.objects.filter(order__tenant=self.request.user.tenant)
|
||||
|
||||
|
||||
class SpecimenCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||||
@ -643,7 +643,7 @@ class SpecimenCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView
|
||||
response = super().form_valid(form)
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=self.request.user,
|
||||
action='SPECIMEN_COLLECTED',
|
||||
model='Specimen',
|
||||
@ -663,12 +663,12 @@ class SpecimenUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
Update specimen (limited to status and processing notes).
|
||||
"""
|
||||
model = Specimen
|
||||
fields = ['status', 'processing_notes'] # Restricted fields
|
||||
fields = ['status'] # Restricted fields
|
||||
template_name = 'laboratory/specimens/specimen_form.html'
|
||||
permission_required = 'laboratory.change_specimen'
|
||||
|
||||
def get_queryset(self):
|
||||
return Specimen.objects.filter(tenant=self.request.user.tenant)
|
||||
return Specimen.objects.filter(order__tenant=self.request.user.tenant)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('laboratory:specimen_detail', kwargs={'pk': self.object.pk})
|
||||
@ -677,7 +677,7 @@ class SpecimenUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
response = super().form_valid(form)
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=self.request.user,
|
||||
action='SPECIMEN_UPDATED',
|
||||
model='Specimen',
|
||||
@ -814,8 +814,8 @@ class QualityControlListView(LoginRequiredMixin, ListView):
|
||||
List all quality control records.
|
||||
"""
|
||||
model = QualityControl
|
||||
template_name = 'laboratory/quality_control_list.html'
|
||||
context_object_name = 'qc_records'
|
||||
template_name = 'laboratory/quality_control/qc_sample_list.html'
|
||||
context_object_name = 'qc_samples'
|
||||
paginate_by = 25
|
||||
|
||||
def get_queryset(self):
|
||||
@ -831,7 +831,7 @@ class QualityControlListView(LoginRequiredMixin, ListView):
|
||||
if result:
|
||||
queryset = queryset.filter(result=result)
|
||||
|
||||
return queryset.select_related('test', 'performed_by').order_by('-test_date')
|
||||
return queryset.select_related('test', 'performed_by')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@ -850,7 +850,7 @@ class QualityControlDetailView(LoginRequiredMixin, DetailView):
|
||||
Display detailed information about a quality control record.
|
||||
"""
|
||||
model = QualityControl
|
||||
template_name = 'laboratory/quality_control_detail.html'
|
||||
template_name = 'laboratory/quality_control/qc_sample_detail.html'
|
||||
context_object_name = 'qc_record'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -1196,7 +1196,7 @@ def mark_collected(request, order_id):
|
||||
order.save()
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=request.user,
|
||||
action='SPECIMEN_COLLECTED',
|
||||
model='LabOrder',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@ -35,14 +35,15 @@ urlpatterns = [
|
||||
|
||||
|
||||
# HTMX views
|
||||
path('htmx/patient-search/', views.patient_search, name='patient_search'),
|
||||
path('htmx/patient-stats/', views.patient_stats, name='patient_stats'),
|
||||
path('htmx/emergency-contacts/<int:patient_id>/', views.emergency_contacts_list, name='emergency_contacts_list'),
|
||||
path('htmx/insurance-info/<int:patient_id>/', views.insurance_info_list, name='insurance_info_list'),
|
||||
path('htmx/consent-forms/<int:patient_id>/', views.consent_forms_list, name='consent_forms_list'),
|
||||
path('htmx/patient-notes/<int:patient_id>/', views.patient_notes_list, name='patient_notes_list'),
|
||||
path('htmx/add-patient-note/<int:patient_id>/', views.add_patient_note, name='add_patient_note'),
|
||||
path('htmx/sign-consent/<int:pk>/', views.sign_consent_form, name='sign_consent_form'),
|
||||
path('htmx/appointments/<int:patient_id>/', views.patient_appointment_list, name='patient_appointments')
|
||||
path('patient-search/', views.patient_search, name='patient_search'),
|
||||
path('patient-stats/', views.patient_stats, name='patient_stats'),
|
||||
path('emergency-contacts/<int:patient_id>/', views.emergency_contacts_list, name='emergency_contacts_list'),
|
||||
path('insurance-info/<int:patient_id>/', views.insurance_info_list, name='insurance_info_list'),
|
||||
path('consent-forms/<int:patient_id>/', views.consent_forms_list, name='consent_forms_list'),
|
||||
path('patient-notes/<int:patient_id>/', views.patient_notes_list, name='patient_notes_list'),
|
||||
path('add-patient-note/<int:patient_id>/', views.add_patient_note, name='add_patient_note'),
|
||||
path('sign-consent/<int:pk>/', views.sign_consent_form, name='sign_consent_form'),
|
||||
path('appointments/<int:patient_id>/', views.patient_appointment_list, name='patient_appointments'),
|
||||
path('patient-info/<int:pk>/', views.get_patient_info, name='get_patient_info')
|
||||
]
|
||||
|
||||
|
||||
@ -1315,6 +1315,20 @@ def add_patient_note(request, patient_id):
|
||||
'patient': patient
|
||||
})
|
||||
|
||||
|
||||
def get_patient_info(request, pk):
|
||||
patient = get_object_or_404(PatientProfile, pk=pk)
|
||||
patient_info = {
|
||||
'id': patient.id,
|
||||
'first_name': patient.first_name,
|
||||
'last_name': patient.last_name,
|
||||
'gender': patient.get_gender_display(),
|
||||
'date_of_birth': patient.date_of_birth,
|
||||
'phone_number': patient.phone_number,
|
||||
'email': patient.email,
|
||||
}
|
||||
return JsonResponse(patient_info)
|
||||
|
||||
#
|
||||
# """
|
||||
# Patients app views for hospital management system with comprehensive CRUD operations.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
34
pharmacy/migrations/0002_alter_prescription_status.py
Normal file
34
pharmacy/migrations/0002_alter_prescription_status.py
Normal file
@ -0,0 +1,34 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-28 19:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pharmacy", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="prescription",
|
||||
name="status",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("PENDING", "Pending"),
|
||||
("ACTIVE", "Active"),
|
||||
("DISPENSED", "Dispensed"),
|
||||
("PARTIALLY_DISPENSED", "Partially Dispensed"),
|
||||
("COMPLETED", "Completed"),
|
||||
("CANCELLED", "Cancelled"),
|
||||
("EXPIRED", "Expired"),
|
||||
("ON_HOLD", "On Hold"),
|
||||
("TRANSFERRED", "Transferred"),
|
||||
("DRAFT", "Draft"),
|
||||
],
|
||||
default="PENDING",
|
||||
help_text="Prescription status",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -17,7 +17,49 @@ class Medication(models.Model):
|
||||
"""
|
||||
Medication model for drug database and formulary management.
|
||||
"""
|
||||
|
||||
CONTROLLED_SUBSTANCE_SCHEDULE_CHOICES = [
|
||||
('CI', 'Schedule I'),
|
||||
('CII', 'Schedule II'),
|
||||
('CIII', 'Schedule III'),
|
||||
('CIV', 'Schedule IV'),
|
||||
('CV', 'Schedule V'),
|
||||
('NON', 'Non-Controlled'),
|
||||
]
|
||||
DOSAGE_FORM_CHOICES = [
|
||||
('TABLET', 'Tablet'),
|
||||
('CAPSULE', 'Capsule'),
|
||||
('LIQUID', 'Liquid'),
|
||||
('INJECTION', 'Injection'),
|
||||
('TOPICAL', 'Topical'),
|
||||
('INHALER', 'Inhaler'),
|
||||
('PATCH', 'Patch'),
|
||||
('SUPPOSITORY', 'Suppository'),
|
||||
('CREAM', 'Cream'),
|
||||
('OINTMENT', 'Ointment'),
|
||||
('DROPS', 'Drops'),
|
||||
('SPRAY', 'Spray'),
|
||||
('OTHER', 'Other'),
|
||||
]
|
||||
UNIT_OF_MEASURE_CHOICES = [
|
||||
('MG', 'Milligrams'),
|
||||
('G', 'Grams'),
|
||||
('MCG', 'Micrograms'),
|
||||
('ML', 'Milliliters'),
|
||||
('L', 'Liters'),
|
||||
('UNITS', 'Units'),
|
||||
('IU', 'International Units'),
|
||||
('MEQ', 'Milliequivalents'),
|
||||
('PERCENT', 'Percent'),
|
||||
('OTHER', 'Other'),
|
||||
]
|
||||
FORMULARY_STATUS_CHOICES = [
|
||||
('PREFERRED', 'Preferred'),
|
||||
('NON_PREFERRED', 'Non-Preferred'),
|
||||
('RESTRICTED', 'Restricted'),
|
||||
('NOT_COVERED', 'Not Covered'),
|
||||
('PRIOR_AUTH', 'Prior Authorization Required'),
|
||||
]
|
||||
|
||||
# Tenant relationship
|
||||
tenant = models.ForeignKey(
|
||||
'core.Tenant',
|
||||
@ -68,14 +110,7 @@ class Medication(models.Model):
|
||||
)
|
||||
controlled_substance_schedule = models.CharField(
|
||||
max_length=5,
|
||||
choices=[
|
||||
('CI', 'Schedule I'),
|
||||
('CII', 'Schedule II'),
|
||||
('CIII', 'Schedule III'),
|
||||
('CIV', 'Schedule IV'),
|
||||
('CV', 'Schedule V'),
|
||||
('NON', 'Non-Controlled'),
|
||||
],
|
||||
choices=CONTROLLED_SUBSTANCE_SCHEDULE_CHOICES,
|
||||
default='NON',
|
||||
help_text='DEA controlled substance schedule'
|
||||
)
|
||||
@ -83,21 +118,7 @@ class Medication(models.Model):
|
||||
# Formulation
|
||||
dosage_form = models.CharField(
|
||||
max_length=50,
|
||||
choices=[
|
||||
('TABLET', 'Tablet'),
|
||||
('CAPSULE', 'Capsule'),
|
||||
('LIQUID', 'Liquid'),
|
||||
('INJECTION', 'Injection'),
|
||||
('TOPICAL', 'Topical'),
|
||||
('INHALER', 'Inhaler'),
|
||||
('PATCH', 'Patch'),
|
||||
('SUPPOSITORY', 'Suppository'),
|
||||
('CREAM', 'Cream'),
|
||||
('OINTMENT', 'Ointment'),
|
||||
('DROPS', 'Drops'),
|
||||
('SPRAY', 'Spray'),
|
||||
('OTHER', 'Other'),
|
||||
],
|
||||
choices=DOSAGE_FORM_CHOICES,
|
||||
help_text='Dosage form'
|
||||
)
|
||||
strength = models.CharField(
|
||||
@ -106,18 +127,7 @@ class Medication(models.Model):
|
||||
)
|
||||
unit_of_measure = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('MG', 'Milligrams'),
|
||||
('G', 'Grams'),
|
||||
('MCG', 'Micrograms'),
|
||||
('ML', 'Milliliters'),
|
||||
('L', 'Liters'),
|
||||
('UNITS', 'Units'),
|
||||
('IU', 'International Units'),
|
||||
('MEQ', 'Milliequivalents'),
|
||||
('PERCENT', 'Percent'),
|
||||
('OTHER', 'Other'),
|
||||
],
|
||||
choices=UNIT_OF_MEASURE_CHOICES,
|
||||
help_text='Unit of measure'
|
||||
)
|
||||
|
||||
@ -188,13 +198,7 @@ class Medication(models.Model):
|
||||
# Formulary Status
|
||||
formulary_status = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('PREFERRED', 'Preferred'),
|
||||
('NON_PREFERRED', 'Non-Preferred'),
|
||||
('RESTRICTED', 'Restricted'),
|
||||
('NOT_COVERED', 'Not Covered'),
|
||||
('PRIOR_AUTH', 'Prior Authorization Required'),
|
||||
],
|
||||
choices=FORMULARY_STATUS_CHOICES,
|
||||
default='PREFERRED',
|
||||
help_text='Formulary status'
|
||||
)
|
||||
@ -290,7 +294,31 @@ class Prescription(models.Model):
|
||||
"""
|
||||
Prescription model for electronic prescription management.
|
||||
"""
|
||||
|
||||
QUANTITY_UNIT_CHOICES = [
|
||||
('TABLETS', 'Tablets'),
|
||||
('CAPSULES', 'Capsules'),
|
||||
('ML', 'Milliliters'),
|
||||
('GRAMS', 'Grams'),
|
||||
('UNITS', 'Units'),
|
||||
('PATCHES', 'Patches'),
|
||||
('INHALERS', 'Inhalers'),
|
||||
('BOTTLES', 'Bottles'),
|
||||
('TUBES', 'Tubes'),
|
||||
('VIALS', 'Vials'),
|
||||
('OTHER', 'Other'),
|
||||
]
|
||||
STATUS_CHOICES = [
|
||||
('PENDING', 'Pending'),
|
||||
('ACTIVE', 'Active'),
|
||||
('DISPENSED', 'Dispensed'),
|
||||
('PARTIALLY_DISPENSED', 'Partially Dispensed'),
|
||||
('COMPLETED', 'Completed'),
|
||||
('CANCELLED', 'Cancelled'),
|
||||
('EXPIRED', 'Expired'),
|
||||
('ON_HOLD', 'On Hold'),
|
||||
('TRANSFERRED', 'Transferred'),
|
||||
('DRAFT', 'Draft'),
|
||||
]
|
||||
# Tenant relationship
|
||||
tenant = models.ForeignKey(
|
||||
'core.Tenant',
|
||||
@ -340,19 +368,7 @@ class Prescription(models.Model):
|
||||
)
|
||||
quantity_unit = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('TABLETS', 'Tablets'),
|
||||
('CAPSULES', 'Capsules'),
|
||||
('ML', 'Milliliters'),
|
||||
('GRAMS', 'Grams'),
|
||||
('UNITS', 'Units'),
|
||||
('PATCHES', 'Patches'),
|
||||
('INHALERS', 'Inhalers'),
|
||||
('BOTTLES', 'Bottles'),
|
||||
('TUBES', 'Tubes'),
|
||||
('VIALS', 'Vials'),
|
||||
('OTHER', 'Other'),
|
||||
],
|
||||
choices=QUANTITY_UNIT_CHOICES,
|
||||
help_text='Unit of quantity'
|
||||
)
|
||||
|
||||
@ -398,17 +414,7 @@ class Prescription(models.Model):
|
||||
# Status
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('PENDING', 'Pending'),
|
||||
('ACTIVE', 'Active'),
|
||||
('DISPENSED', 'Dispensed'),
|
||||
('PARTIALLY_DISPENSED', 'Partially Dispensed'),
|
||||
('COMPLETED', 'Completed'),
|
||||
('CANCELLED', 'Cancelled'),
|
||||
('EXPIRED', 'Expired'),
|
||||
('ON_HOLD', 'On Hold'),
|
||||
('TRANSFERRED', 'Transferred'),
|
||||
],
|
||||
choices=STATUS_CHOICES,
|
||||
default='PENDING',
|
||||
help_text='Prescription status'
|
||||
)
|
||||
@ -577,7 +583,15 @@ class InventoryItem(models.Model):
|
||||
"""
|
||||
Inventory item model for pharmacy stock management.
|
||||
"""
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('ACTIVE', 'Active'),
|
||||
('QUARANTINE', 'Quarantine'),
|
||||
('EXPIRED', 'Expired'),
|
||||
('RECALLED', 'Recalled'),
|
||||
('DAMAGED', 'Damaged'),
|
||||
('RETURNED', 'Returned'),
|
||||
]
|
||||
|
||||
# Tenant relationship
|
||||
tenant = models.ForeignKey(
|
||||
'core.Tenant',
|
||||
@ -684,14 +698,7 @@ class InventoryItem(models.Model):
|
||||
# Status
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('ACTIVE', 'Active'),
|
||||
('QUARANTINE', 'Quarantine'),
|
||||
('EXPIRED', 'Expired'),
|
||||
('RECALLED', 'Recalled'),
|
||||
('DAMAGED', 'Damaged'),
|
||||
('RETURNED', 'Returned'),
|
||||
],
|
||||
choices=STATUS_CHOICES,
|
||||
default='ACTIVE',
|
||||
help_text='Inventory status'
|
||||
)
|
||||
|
||||
@ -21,6 +21,7 @@ urlpatterns = [
|
||||
# path('inventory/<int:pk>/delete/', views.InventoryItemDeleteView.as_view(), name='inventory_delete'),
|
||||
path('dispense-records/', views.DispenseRecordListView.as_view(), name='dispense_record_list'),
|
||||
path('drug-interactions/', views.DrugInteractionListView.as_view(), name='drug_interaction_list'),
|
||||
path('drug-interactions/<int:pk>/', views.DrugInteractionDetailView.as_view(), name='drug_interaction_detail'),
|
||||
|
||||
path('medications/create/', views.MedicationCreateView.as_view(), name='medication_create'),
|
||||
path('medications/<int:pk>/', views.MedicationDetailView.as_view(), name='medication_detail'),
|
||||
@ -39,6 +40,8 @@ urlpatterns = [
|
||||
path('prescription/<int:prescription_id>/dispense/', views.dispense_medication, name='dispense_medication'),
|
||||
path('inventory/<int:item_id>/update/', views.update_inventory, name='update_inventory'),
|
||||
path('inventory-adjustment/<int:item_id>/', views.adjust_inventory, name='adjust_inventory'),
|
||||
path('medication/<int:medication_id>/', views.get_medication_info, name='get_medication_info'),
|
||||
path('prescription/<int:prescription_id>/draft/', views.save_prescription_draft, name='save_prescription_draft'),
|
||||
|
||||
# API endpoints
|
||||
# path('api/', include('pharmacy.api.urls')),
|
||||
|
||||
@ -322,13 +322,13 @@ class MedicationDetailView(LoginRequiredMixin, DetailView):
|
||||
context['recent_prescriptions'] = Prescription.objects.filter(
|
||||
medication=medication,
|
||||
patient__tenant=self.request.user.tenant
|
||||
).select_related('patient', 'prescriber').order_by('-prescribed_date')[:10]
|
||||
).select_related('patient', 'prescriber').order_by('-date_prescribed')[:10]
|
||||
|
||||
# Inventory items
|
||||
context['inventory_items'] = InventoryItem.objects.filter(
|
||||
medication=medication,
|
||||
is_active=True
|
||||
).order_by('expiry_date')
|
||||
status="ACTIVE"
|
||||
).order_by('expiration_date')
|
||||
|
||||
# Drug interactions
|
||||
context['interactions'] = DrugInteraction.objects.filter(
|
||||
@ -350,7 +350,7 @@ class MedicationDetailView(LoginRequiredMixin, DetailView):
|
||||
|
||||
context['total_inventory'] = InventoryItem.objects.filter(
|
||||
medication=medication,
|
||||
is_active=True
|
||||
status="ACTIVE"
|
||||
).aggregate(total=Sum('quantity_on_hand'))['total'] or 0
|
||||
|
||||
return context
|
||||
@ -398,8 +398,8 @@ class MedicationListView(LoginRequiredMixin, ListView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context.update({
|
||||
'medication_types': Medication._meta.get_field('medication_type').choices,
|
||||
'controlled_schedules': Medication._meta.get_field('controlled_substance_schedule').choices,
|
||||
'medication_types': Medication.DOSAGE_FORM_CHOICES,
|
||||
'controlled_schedules': Medication.CONTROLLED_SUBSTANCE_SCHEDULE_CHOICES,
|
||||
})
|
||||
return context
|
||||
|
||||
@ -517,7 +517,7 @@ class InventoryItemCreateView(LoginRequiredMixin, CreateView):
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['user'] = self.request.user
|
||||
# kwargs['user'] = self.request.user
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
@ -623,7 +623,7 @@ class DrugInteractionListView(LoginRequiredMixin, ListView):
|
||||
List view for drug interactions.
|
||||
"""
|
||||
model = DrugInteraction
|
||||
template_name = 'pharmacy/drug_interaction_list.html'
|
||||
template_name = 'pharmacy/interactions/drug_interaction_list.html'
|
||||
context_object_name = 'interactions'
|
||||
paginate_by = 25
|
||||
|
||||
@ -649,7 +649,7 @@ class DrugInteractionListView(LoginRequiredMixin, ListView):
|
||||
if interaction_type:
|
||||
queryset = queryset.filter(interaction_type=interaction_type)
|
||||
|
||||
return queryset.select_related('medication_a', 'medication_b').order_by('-severity', 'medication_a__name')
|
||||
return queryset.select_related('medication_1', 'medication_2').order_by('-severity', 'medication_1')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@ -776,7 +776,7 @@ def drug_interaction_check(request, prescription_id):
|
||||
tenant=request.user.tenant
|
||||
).select_related('medication_a', 'medication_b')
|
||||
|
||||
return render(request, 'pharmacy/partials/drug_interactions.html', {
|
||||
return render(request, 'pharmacy/drug_interactions/drug_interactions.html', {
|
||||
'prescription': prescription,
|
||||
'interactions': interactions
|
||||
})
|
||||
@ -901,7 +901,7 @@ def update_inventory(request, item_id):
|
||||
item.save()
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=request.user,
|
||||
action='UPDATE_INVENTORY',
|
||||
model_name='InventoryItem',
|
||||
@ -918,6 +918,15 @@ def update_inventory(request, item_id):
|
||||
|
||||
return JsonResponse({'status': 'error'})
|
||||
|
||||
|
||||
def get_medication_info(request, pk):
|
||||
medication = get_object_or_404(Medication, pk=pk)
|
||||
medication_info = {
|
||||
'generic_name': medication.generic_name,
|
||||
'brand_name': medication.brand_name,
|
||||
|
||||
}
|
||||
return JsonResponse(medication_info)
|
||||
#
|
||||
# """
|
||||
# Views for Pharmacy app with comprehensive CRUD operations following healthcare best practices.
|
||||
@ -1855,40 +1864,50 @@ def update_inventory(request, item_id):
|
||||
# return queryset
|
||||
#
|
||||
#
|
||||
# class DrugInteractionCreateView(LoginRequiredMixin, CreateView):
|
||||
# """
|
||||
# Create view for drug interaction.
|
||||
# """
|
||||
# model = DrugInteraction
|
||||
# form_class = DrugInteractionForm
|
||||
# template_name = 'pharmacy/interaction_form.html'
|
||||
# success_url = reverse_lazy('pharmacy:interaction_list')
|
||||
#
|
||||
# def get_form_kwargs(self):
|
||||
# kwargs = super().get_form_kwargs()
|
||||
# kwargs['user'] = self.request.user
|
||||
# return kwargs
|
||||
#
|
||||
# def form_valid(self, form):
|
||||
# form.instance.created_by = self.request.user
|
||||
# response = super().form_valid(form)
|
||||
#
|
||||
# # Create audit log
|
||||
# AuditLogEntry.objects.create(
|
||||
# tenant=self.request.user.tenant,
|
||||
# user=self.request.user,
|
||||
# action='CREATE',
|
||||
# model_name='DrugInteraction',
|
||||
# object_id=self.object.id,
|
||||
# changes={
|
||||
# 'medication_1': str(self.object.medication_1),
|
||||
# 'medication_2': str(self.object.medication_2),
|
||||
# 'severity': self.object.severity
|
||||
# }
|
||||
# )
|
||||
#
|
||||
# messages.success(self.request, 'Drug interaction created successfully.')
|
||||
# return response
|
||||
class DrugInteractionCreateView(LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
Create view for drug interaction.
|
||||
"""
|
||||
model = DrugInteraction
|
||||
form_class = DrugInteractionForm
|
||||
template_name = 'pharmacy/interaction_form.html'
|
||||
success_url = reverse_lazy('pharmacy:interaction_list')
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['user'] = self.request.user
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.created_by = self.request.user
|
||||
response = super().form_valid(form)
|
||||
|
||||
# Create audit log
|
||||
AuditLogEntry.objects.create(
|
||||
tenant=self.request.user.tenant,
|
||||
user=self.request.user,
|
||||
action='CREATE',
|
||||
model_name='DrugInteraction',
|
||||
object_id=self.object.id,
|
||||
changes={
|
||||
'medication_1': str(self.object.medication_1),
|
||||
'medication_2': str(self.object.medication_2),
|
||||
'severity': self.object.severity
|
||||
}
|
||||
)
|
||||
|
||||
messages.success(self.request, 'Drug interaction created successfully.')
|
||||
return response
|
||||
|
||||
class DrugInteractionDetailView(LoginRequiredMixin, DetailView):
|
||||
model = DrugInteraction
|
||||
template_name = 'pharmacy/interactions/drug_interaction_detail.html'
|
||||
context_object_name = 'interaction'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
# # HTMX Views
|
||||
@ -2170,3 +2189,8 @@ def adjust_inventory(request, pk):
|
||||
#
|
||||
# return response
|
||||
#
|
||||
def save_prescription_draft(request, pk):
|
||||
prescription = get_object_or_404(Prescription, pk=pk)
|
||||
prescription.status = "DRAFT"
|
||||
prescription.save()
|
||||
return redirect('pharmacy:prescription_detail', pk=pk)
|
||||
|
||||
BIN
templates/.DS_Store
vendored
BIN
templates/.DS_Store
vendored
Binary file not shown.
@ -42,7 +42,7 @@
|
||||
</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
{% if object.status == 'DRAFT' %}
|
||||
<li><a class="dropdown-item text-warning" href="{% url 'billing:bill_submit' object.bill_id %}">
|
||||
<li><a class="dropdown-item text-warning" href="{% url 'billing:submit_bill' object.bill_id %}">
|
||||
<i class="fas fa-paper-plane me-2"></i>Submit Bill
|
||||
</a></li>
|
||||
{% endif %}
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown">
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="{% url 'billing:export_bills' %}">
|
||||
@ -42,7 +43,7 @@
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="card-title mb-1 opacity-75">Total Bills</h6>
|
||||
<h3 class="mb-0">{{ stats.total_bills|default:0 }}</h3>
|
||||
<h3 class="mb-0">{{ stats.total_bills }}</h3>
|
||||
</div>
|
||||
<div class="text-white-50">
|
||||
<i class="fas fa-file-invoice fa-2x"></i>
|
||||
@ -282,6 +283,7 @@
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown">
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="">
|
||||
@ -328,48 +330,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if page_obj.has_other_pages %}
|
||||
<div class="card-footer">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="text-muted">
|
||||
Showing {{ page_obj.start_index }} to {{ page_obj.end_index }} of {{ page_obj.paginator.count }} bills
|
||||
</div>
|
||||
<nav aria-label="Bills pagination">
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page=1">First</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.previous_page_number }}">Previous</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ num }}</span>
|
||||
</li>
|
||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ num }}">{{ num }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.next_page_number }}">Next</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.paginator.num_pages }}">Last</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if is_paginated %}
|
||||
{% include 'partial/pagination.html' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -412,32 +412,32 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
|
||||
// Bill selection change handler
|
||||
document.getElementById('{{ form.medical_bill.id_for_label }}').addEventListener('change', function() {
|
||||
const billId = this.value;
|
||||
if (billId) {
|
||||
// Fetch bill details via HTMX or AJAX
|
||||
fetch(`{% url 'billing:bill_details_api' %}?bill_id=${billId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById('bill-patient').textContent = data.patient_name;
|
||||
document.getElementById('bill-number').textContent = data.bill_number;
|
||||
document.getElementById('bill-date').textContent = data.bill_date;
|
||||
document.getElementById('bill-total').textContent = `$${data.total_amount}`;
|
||||
document.getElementById('bill-paid').textContent = `$${data.paid_amount}`;
|
||||
document.getElementById('bill-balance').textContent = `$${data.balance_amount}`;
|
||||
document.getElementById('bill-details').style.display = 'block';
|
||||
|
||||
// Set max payment amount to balance
|
||||
const paymentAmountField = document.getElementById('{{ form.payment_amount.id_for_label }}');
|
||||
paymentAmountField.max = data.balance_amount;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching bill details:', error);
|
||||
});
|
||||
} else {
|
||||
document.getElementById('bill-details').style.display = 'none';
|
||||
}
|
||||
});
|
||||
{#document.getElementById('{{ form.medical_bill.id_for_label }}').addEventListener('change', function() {#}
|
||||
{# const billId = this.value;#}
|
||||
{# if (billId) {#}
|
||||
{# // Fetch bill details via HTMX or AJAX#}
|
||||
{# fetch(`{% url 'billing:bill_details_api' %}?bill_id=${billId}`)#}
|
||||
{# .then(response => response.json())#}
|
||||
{# .then(data => {#}
|
||||
{# document.getElementById('bill-patient').textContent = data.patient_name;#}
|
||||
{# document.getElementById('bill-number').textContent = data.bill_number;#}
|
||||
{# document.getElementById('bill-date').textContent = data.bill_date;#}
|
||||
{# document.getElementById('bill-total').textContent = `$${data.total_amount}`;#}
|
||||
{# document.getElementById('bill-paid').textContent = `$${data.paid_amount}`;#}
|
||||
{# document.getElementById('bill-balance').textContent = `$${data.balance_amount}`;#}
|
||||
{# document.getElementById('bill-details').style.display = 'block';#}
|
||||
{# #}
|
||||
{# // Set max payment amount to balance#}
|
||||
{# const paymentAmountField = document.getElementById('{{ form.payment_amount.id_for_label }}');#}
|
||||
{# paymentAmountField.max = data.balance_amount;#}
|
||||
{# })#}
|
||||
{# .catch(error => {#}
|
||||
{# console.error('Error fetching bill details:', error);#}
|
||||
{# });#}
|
||||
{# } else {#}
|
||||
{# document.getElementById('bill-details').style.display = 'none';#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
|
||||
// Payment amount change handler
|
||||
document.getElementById('{{ form.payment_amount.id_for_label }}').addEventListener('input', function() {
|
||||
@ -479,14 +479,14 @@ function calculatePartialPayment() {
|
||||
}
|
||||
}
|
||||
|
||||
function viewPaymentHistory() {
|
||||
const billId = document.getElementById('{{ form.medical_bill.id_for_label }}').value;
|
||||
if (billId) {
|
||||
window.open(`{% url 'billing:payment_history' %}?bill_id=${billId}`, '_blank');
|
||||
} else {
|
||||
alert('Please select a bill first.');
|
||||
}
|
||||
}
|
||||
{#function viewPaymentHistory() {#}
|
||||
{# const billId = document.getElementById('{{ form.medical_bill.id_for_label }}').value;#}
|
||||
{# if (billId) {#}
|
||||
{# window.open(`{% url 'billing:payment_history' %}?bill_id=${billId}`, '_blank');#}
|
||||
{# } else {#}
|
||||
{# alert('Please select a bill first.');#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
|
||||
// Form validation
|
||||
document.getElementById('paymentForm').addEventListener('submit', function(e) {
|
||||
|
||||
BIN
templates/laboratory/.DS_Store
vendored
Normal file
BIN
templates/laboratory/.DS_Store
vendored
Normal file
Binary file not shown.
@ -394,16 +394,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
|
||||
// Real-time notifications for critical results
|
||||
function checkCriticalResults() {
|
||||
fetch('{% url "laboratory:check_critical_results" %}')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.critical_count > 0) {
|
||||
showCriticalAlert(data.critical_count);
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error checking critical results:', error));
|
||||
}
|
||||
{#function checkCriticalResults() {#}
|
||||
{# fetch('{% url "laboratory:check_critical_results" %}')#}
|
||||
{# .then(response => response.json())#}
|
||||
{# .then(data => {#}
|
||||
{# if (data.critical_count > 0) {#}
|
||||
{# showCriticalAlert(data.critical_count);#}
|
||||
{# }#}
|
||||
{# })#}
|
||||
{# .catch(error => console.error('Error checking critical results:', error));#}
|
||||
{# }#}
|
||||
|
||||
function showCriticalAlert(count) {
|
||||
const alertHtml = `
|
||||
|
||||
@ -4,8 +4,8 @@
|
||||
{% block title %}Laboratory Orders - Laboratory Management{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link href="{% static 'assets/plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'assets/plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@ -119,7 +119,8 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for order in object_list %}
|
||||
|
||||
{% for order in orders %}
|
||||
<tr data-status="{{ order.status }}" data-priority="{{ order.priority }}">
|
||||
<td>
|
||||
<div class="form-check">
|
||||
@ -143,7 +144,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<div class="small">
|
||||
{% for test in order.tests.all|slice:":3" %}
|
||||
{% for test in order.tests.all %}
|
||||
<div>{{ test.name }}</div>
|
||||
{% endfor %}
|
||||
{% if order.tests.count > 3 %}
|
||||
@ -207,45 +208,45 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static 'assets/plugins/datatables.net/js/jquery.dataTables.min.js' %}"></script>
|
||||
<script src="{% static 'assets/plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/datatables.net/js/dataTables.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#orders-table').DataTable({
|
||||
responsive: true,
|
||||
pageLength: 25,
|
||||
order: [[6, 'desc']]
|
||||
});
|
||||
});
|
||||
|
||||
function filterByStatus(status) {
|
||||
$('.btn-group button').removeClass('active');
|
||||
event.target.classList.add('active');
|
||||
|
||||
if (status === 'all') {
|
||||
$('tr[data-status]').show();
|
||||
} else {
|
||||
$('tr[data-status]').hide();
|
||||
$(`tr[data-status="${status}"]`).show();
|
||||
}
|
||||
}
|
||||
|
||||
function filterByPriority(priority) {
|
||||
$('.btn-group button').removeClass('active');
|
||||
event.target.classList.add('active');
|
||||
|
||||
if (priority === 'all') {
|
||||
$('tr[data-priority]').show();
|
||||
} else {
|
||||
$('tr[data-priority]').hide();
|
||||
$(`tr[data-priority="${priority}"]`).show();
|
||||
}
|
||||
}
|
||||
{#$(document).ready(function() {#}
|
||||
{# $('#orders-table').DataTable({#}
|
||||
{# responsive: true,#}
|
||||
{# pageLength: 25,#}
|
||||
{# order: [[6, 'desc']]#}
|
||||
{# });#}
|
||||
{# });#}
|
||||
{##}
|
||||
{#function filterByStatus(status) {#}
|
||||
{# $('.btn-group button').removeClass('active');#}
|
||||
{# event.target.classList.add('active');#}
|
||||
{# #}
|
||||
{# if (status === 'all') {#}
|
||||
{# $('tr[data-status]').show();#}
|
||||
{# } else {#}
|
||||
{# $('tr[data-status]').hide();#}
|
||||
{# $(`tr[data-status="${status}"]`).show();#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
{##}
|
||||
{#function filterByPriority(priority) {#}
|
||||
{# $('.btn-group button').removeClass('active');#}
|
||||
{# event.target.classList.add('active');#}
|
||||
{# #}
|
||||
{# if (priority === 'all') {#}
|
||||
{# $('tr[data-priority]').show();#}
|
||||
{# } else {#}
|
||||
{# $('tr[data-priority]').hide();#}
|
||||
{# $(`tr[data-priority="${priority}"]`).show();#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
|
||||
function startProcessing(orderId) {
|
||||
$.ajax({
|
||||
url: '{% url "laboratory:lab_order_start_processing" 0 %}'.replace('0', orderId),
|
||||
url: '{% url "laboratory:start_processing" 0 %}'.replace('0', orderId),
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
@ -264,9 +265,9 @@ function startProcessing(orderId) {
|
||||
});
|
||||
}
|
||||
|
||||
function exportOrders() {
|
||||
window.open('{% url "laboratory:lab_order_export" %}');
|
||||
}
|
||||
{#function exportOrders() {#}
|
||||
{# window.open('{% url "laboratory:lab_order_export" %}');#}
|
||||
{# }#}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
673
templates/laboratory/quality_control/qc_result_entry.html
Normal file
673
templates/laboratory/quality_control/qc_result_entry.html
Normal file
@ -0,0 +1,673 @@
|
||||
{% 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 %}
|
||||
|
||||
@ -0,0 +1,521 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Delete QC Sample - {{ qc_sample.sample_id }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.delete-warning {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.impact-analysis {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.confirmation-input {
|
||||
background: #fff5f5;
|
||||
border: 2px solid #fed7d7;
|
||||
}
|
||||
|
||||
.confirmation-input:focus {
|
||||
border-color: #fc8181;
|
||||
box-shadow: 0 0 0 0.2rem rgba(252, 129, 129, 0.25);
|
||||
}
|
||||
|
||||
.danger-zone {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.alternative-action {
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #bae6fd;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.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; }
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.info-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.info-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.info-item .text-end {
|
||||
text-align: left !important;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
}
|
||||
</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">Delete</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-trash text-danger me-2"></i>Delete QC Sample
|
||||
</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">
|
||||
<!-- Warning Message -->
|
||||
<div class="delete-warning">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-exclamation-triangle text-warning me-3 fa-2x"></i>
|
||||
<div>
|
||||
<h5 class="mb-1">Warning: Permanent Deletion</h5>
|
||||
<p class="mb-0">
|
||||
You are about to permanently delete this QC sample. This action cannot be undone
|
||||
and will remove all associated data including results, comments, and audit trail.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QC Sample Information -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-vial me-2"></i>QC Sample Information
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="info-item">
|
||||
<span class="fw-bold">Sample ID:</span>
|
||||
<span>{{ qc_sample.sample_id }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="fw-bold">Test Type:</span>
|
||||
<span>{{ qc_sample.test_type.name }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="fw-bold">QC Level:</span>
|
||||
<span class="badge qc-level-{{ qc_sample.qc_level }}">
|
||||
{{ qc_sample.get_qc_level_display }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="fw-bold">Status:</span>
|
||||
<span class="badge qc-status-{{ qc_sample.status }}">
|
||||
{{ qc_sample.get_status_display }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="info-item">
|
||||
<span class="fw-bold">Created:</span>
|
||||
<span>{{ qc_sample.created_at|date:"M d, Y H:i" }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="fw-bold">Run Date:</span>
|
||||
<span>{{ qc_sample.run_date|date:"M d, Y" }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="fw-bold">Lot Number:</span>
|
||||
<span>{{ qc_sample.lot_number }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="fw-bold">Created By:</span>
|
||||
<span>{{ qc_sample.created_by.get_full_name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if qc_sample.result_value %}
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h6 class="mb-2">Result Information:</h6>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="fw-bold me-3">Result Value:</span>
|
||||
<span class="me-3">{{ qc_sample.result_value }} {{ qc_sample.unit }}</span>
|
||||
<span class="badge result-{{ qc_sample.result_status }}">
|
||||
{{ qc_sample.get_result_status_display }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Impact Analysis -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-chart-line me-2"></i>Deletion Impact Analysis
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="impact-analysis">
|
||||
<h6 class="mb-3">The following data will be permanently deleted:</h6>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<ul class="list-unstyled">
|
||||
<li><i class="fas fa-check text-danger me-2"></i>QC sample record</li>
|
||||
<li><i class="fas fa-check text-danger me-2"></i>All result data</li>
|
||||
<li><i class="fas fa-check text-danger me-2"></i>Comments and notes</li>
|
||||
<li><i class="fas fa-check text-danger me-2"></i>Activity timeline</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<ul class="list-unstyled">
|
||||
<li><i class="fas fa-check text-danger me-2"></i>Audit trail entries</li>
|
||||
<li><i class="fas fa-check text-danger me-2"></i>Associated attachments</li>
|
||||
<li><i class="fas fa-check text-danger me-2"></i>QC trend data point</li>
|
||||
<li><i class="fas fa-check text-danger me-2"></i>Statistical calculations</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if blocking_conditions %}
|
||||
<div class="alert alert-danger">
|
||||
<h6 class="alert-heading">
|
||||
<i class="fas fa-ban me-2"></i>Deletion Blocked
|
||||
</h6>
|
||||
<p class="mb-2">This QC sample cannot be deleted due to the following conditions:</p>
|
||||
<ul class="mb-0">
|
||||
{% for condition in blocking_conditions %}
|
||||
<li>{{ condition }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-4 text-center">
|
||||
<h4 class="text-danger">{{ related_activities.count }}</h4>
|
||||
<small class="text-muted">Activity Records</small>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<h4 class="text-danger">{{ related_attachments.count }}</h4>
|
||||
<small class="text-muted">Attachments</small>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<h4 class="text-danger">{{ related_trend_points }}</h4>
|
||||
<small class="text-muted">Trend Data Points</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Deletion Confirmation -->
|
||||
{% if not blocking_conditions %}
|
||||
<div class="danger-zone">
|
||||
<h5 class="text-danger mb-3">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>Confirm Deletion
|
||||
</h5>
|
||||
|
||||
<form id="deleteForm" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">
|
||||
Type "{{ qc_sample.sample_id }}" to confirm deletion:
|
||||
</label>
|
||||
<input type="text" class="form-control confirmation-input"
|
||||
id="confirmationInput" placeholder="Enter sample ID to confirm"
|
||||
autocomplete="off">
|
||||
<div class="form-text text-danger">
|
||||
This confirmation is required to prevent accidental deletion.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Reason for Deletion:</label>
|
||||
<select class="form-select" name="deletion_reason" required>
|
||||
<option value="">Select reason...</option>
|
||||
<option value="duplicate">Duplicate sample</option>
|
||||
<option value="error">Created in error</option>
|
||||
<option value="invalid">Invalid sample</option>
|
||||
<option value="expired">Expired control material</option>
|
||||
<option value="other">Other (specify in comments)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Additional Comments:</label>
|
||||
<textarea class="form-control" name="deletion_comments" rows="3"
|
||||
placeholder="Provide additional details about why this sample is being deleted..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="confirmDeletion" required>
|
||||
<label class="form-check-label text-danger" for="confirmDeletion">
|
||||
I understand this action cannot be undone
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-danger" id="deleteButton" disabled>
|
||||
<i class="fas fa-trash me-1"></i>Delete QC Sample
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="col-lg-4">
|
||||
<!-- Alternative Actions -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-lightbulb me-2"></i>Alternative Actions
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alternative-action">
|
||||
<h6 class="mb-2">
|
||||
<i class="fas fa-pause me-2"></i>Cancel Sample
|
||||
</h6>
|
||||
<p class="mb-2">
|
||||
Mark the sample as cancelled instead of deleting it.
|
||||
This preserves the record for audit purposes.
|
||||
</p>
|
||||
<button type="button" class="btn btn-outline-warning btn-sm" onclick="cancelSample()">
|
||||
Cancel Sample
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="alternative-action">
|
||||
<h6 class="mb-2">
|
||||
<i class="fas fa-archive me-2"></i>Archive Sample
|
||||
</h6>
|
||||
<p class="mb-2">
|
||||
Archive the sample to hide it from active lists
|
||||
while keeping all data intact.
|
||||
</p>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="archiveSample()">
|
||||
Archive Sample
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="alternative-action">
|
||||
<h6 class="mb-2">
|
||||
<i class="fas fa-download me-2"></i>Export Data
|
||||
</h6>
|
||||
<p class="mb-2">
|
||||
Export all sample data before deletion
|
||||
for backup or transfer purposes.
|
||||
</p>
|
||||
<button type="button" class="btn btn-outline-info btn-sm" onclick="exportSampleData()">
|
||||
Export Data
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Related QC Samples -->
|
||||
{% if 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>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Help & Support -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-question-circle me-2"></i>Need Help?
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-3">
|
||||
If you're unsure about deleting this QC sample,
|
||||
consider contacting your laboratory supervisor or IT support.
|
||||
</p>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="contactSupport()">
|
||||
<i class="fas fa-phone me-1"></i>Contact Support
|
||||
</button>
|
||||
<a href="{% url 'laboratory:qc_guidelines' %}" class="btn btn-outline-info btn-sm">
|
||||
<i class="fas fa-book me-1"></i>QC Guidelines
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
const confirmationInput = $('#confirmationInput');
|
||||
const deleteButton = $('#deleteButton');
|
||||
const confirmCheckbox = $('#confirmDeletion');
|
||||
const expectedText = '{{ qc_sample.sample_id }}';
|
||||
|
||||
// Enable/disable delete button based on confirmation
|
||||
function updateDeleteButton() {
|
||||
const textMatches = confirmationInput.val() === expectedText;
|
||||
const checkboxChecked = confirmCheckbox.is(':checked');
|
||||
|
||||
deleteButton.prop('disabled', !(textMatches && checkboxChecked));
|
||||
|
||||
if (textMatches) {
|
||||
confirmationInput.removeClass('is-invalid').addClass('is-valid');
|
||||
} else if (confirmationInput.val().length > 0) {
|
||||
confirmationInput.removeClass('is-valid').addClass('is-invalid');
|
||||
} else {
|
||||
confirmationInput.removeClass('is-valid is-invalid');
|
||||
}
|
||||
}
|
||||
|
||||
confirmationInput.on('input', updateDeleteButton);
|
||||
confirmCheckbox.on('change', updateDeleteButton);
|
||||
|
||||
// Form submission
|
||||
$('#deleteForm').on('submit', function(e) {
|
||||
if (confirmationInput.val() !== expectedText) {
|
||||
e.preventDefault();
|
||||
alert('Please enter the correct sample ID to confirm deletion.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!confirmCheckbox.is(':checked')) {
|
||||
e.preventDefault();
|
||||
alert('Please confirm that you understand this action cannot be undone.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Final confirmation
|
||||
if (!confirm('Are you absolutely sure you want to delete this QC sample? This action is permanent and cannot be undone.')) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
deleteButton.prop('disabled', true).html('<i class="fas fa-spinner fa-spin me-1"></i>Deleting...');
|
||||
});
|
||||
});
|
||||
|
||||
function cancelSample() {
|
||||
if (confirm('Cancel this QC sample instead of deleting it?')) {
|
||||
$.ajax({
|
||||
url: '{% url "laboratory:cancel_qc_sample" qc_sample.pk %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
window.location.href = '{% url "laboratory:qc_sample_detail" qc_sample.pk %}';
|
||||
} else {
|
||||
alert('Error cancelling QC sample: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error cancelling QC sample.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function archiveSample() {
|
||||
if (confirm('Archive this QC sample instead of deleting it?')) {
|
||||
$.ajax({
|
||||
url: '{% url "laboratory:archive_qc_sample" qc_sample.pk %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
window.location.href = '{% url "laboratory:qc_sample_detail" qc_sample.pk %}';
|
||||
} else {
|
||||
alert('Error archiving QC sample: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error archiving QC sample.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function exportSampleData() {
|
||||
window.location.href = '{% url "laboratory:export_qc_sample" qc_sample.pk %}';
|
||||
}
|
||||
|
||||
function contactSupport() {
|
||||
// Implement support contact functionality
|
||||
alert('Contact your laboratory supervisor or IT support for assistance with QC sample management.');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
536
templates/laboratory/quality_control/qc_sample_detail.html
Normal file
536
templates/laboratory/quality_control/qc_sample_detail.html
Normal file
@ -0,0 +1,536 @@
|
||||
{% 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 %}
|
||||
|
||||
598
templates/laboratory/quality_control/qc_sample_form.html
Normal file
598
templates/laboratory/quality_control/qc_sample_form.html
Normal file
@ -0,0 +1,598 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}
|
||||
{% if qc_sample.pk %}Edit QC Sample - {{ qc_sample.sample_id }}{% else %}Create QC Sample{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.form-section {
|
||||
background: #f8f9fa;
|
||||
border-left: 4px solid #007bff;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.form-section h5 {
|
||||
color: #007bff;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.required-field::after {
|
||||
content: " *";
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.form-help {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.preview-card {
|
||||
background: #fff;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.qc-level-preview {
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.qc-level-level1 { background-color: #d4edda; color: #155724; }
|
||||
.qc-level-level2 { background-color: #fff3cd; color: #856404; }
|
||||
.qc-level-level3 { background-color: #f8d7da; color: #721c24; }
|
||||
|
||||
.validation-feedback {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-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"><a href="{% url 'laboratory:qc_sample_list' %}">QC Samples</a></li>
|
||||
<li class="breadcrumb-item active">
|
||||
{% if qc_sample.pk %}Edit Sample{% else %}Create Sample{% endif %}
|
||||
</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-{% if qc_sample.pk %}edit{% else %}plus{% endif %} me-2"></i>
|
||||
{% if qc_sample.pk %}Edit QC Sample{% else %}Create QC Sample{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a href="{% url 'laboratory:qc_sample_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to List
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<form id="qcSampleForm" method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Basic Information -->
|
||||
<div class="form-section">
|
||||
<h5><i class="fas fa-info-circle me-2"></i>Basic Information</h5>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label required-field">Sample ID</label>
|
||||
<input type="text" class="form-control" name="sample_id"
|
||||
value="{{ qc_sample.sample_id|default:'' }}"
|
||||
{% if not qc_sample.pk %}placeholder="Auto-generated if left blank"{% endif %}
|
||||
{% if qc_sample.pk %}readonly{% endif %}>
|
||||
<div class="form-help">
|
||||
{% if not qc_sample.pk %}
|
||||
Leave blank to auto-generate based on test type and date
|
||||
{% else %}
|
||||
Sample ID cannot be changed after creation
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if form.sample_id.errors %}
|
||||
<div class="validation-feedback">{{ form.sample_id.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label required-field">Test Type</label>
|
||||
<select class="form-select" name="test_type" id="test-type-select" required>
|
||||
<option value="">Select test type...</option>
|
||||
{% for test_type in test_types %}
|
||||
<option value="{{ test_type.id }}"
|
||||
{% if qc_sample.test_type_id == test_type.id %}selected{% endif %}
|
||||
data-category="{{ test_type.category }}"
|
||||
data-unit="{{ test_type.unit }}">
|
||||
{{ test_type.name }} ({{ test_type.category }})
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="form-help">Select the laboratory test for quality control</div>
|
||||
{% if form.test_type.errors %}
|
||||
<div class="validation-feedback">{{ form.test_type.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label required-field">QC Level</label>
|
||||
<select class="form-select" name="qc_level" id="qc-level-select" required>
|
||||
<option value="">Select QC level...</option>
|
||||
<option value="level1" {% if qc_sample.qc_level == 'level1' %}selected{% endif %}>
|
||||
Level 1 (Normal Control)
|
||||
</option>
|
||||
<option value="level2" {% if qc_sample.qc_level == 'level2' %}selected{% endif %}>
|
||||
Level 2 (Abnormal Low Control)
|
||||
</option>
|
||||
<option value="level3" {% if qc_sample.qc_level == 'level3' %}selected{% endif %}>
|
||||
Level 3 (Abnormal High Control)
|
||||
</option>
|
||||
</select>
|
||||
<div class="form-help">Quality control level determines expected value ranges</div>
|
||||
{% if form.qc_level.errors %}
|
||||
<div class="validation-feedback">{{ form.qc_level.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Priority</label>
|
||||
<select class="form-select" name="priority">
|
||||
<option value="routine" {% if qc_sample.priority == 'routine' %}selected{% endif %}>
|
||||
Routine
|
||||
</option>
|
||||
<option value="urgent" {% if qc_sample.priority == 'urgent' %}selected{% endif %}>
|
||||
Urgent
|
||||
</option>
|
||||
<option value="stat" {% if qc_sample.priority == 'stat' %}selected{% endif %}>
|
||||
STAT
|
||||
</option>
|
||||
</select>
|
||||
<div class="form-help">Processing priority for this QC sample</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lot Information -->
|
||||
<div class="form-section">
|
||||
<h5><i class="fas fa-box me-2"></i>Lot Information</h5>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label required-field">Lot Number</label>
|
||||
<input type="text" class="form-control" name="lot_number"
|
||||
value="{{ qc_sample.lot_number|default:'' }}"
|
||||
placeholder="Enter control material lot number" required>
|
||||
<div class="form-help">Lot number from control material packaging</div>
|
||||
{% if form.lot_number.errors %}
|
||||
<div class="validation-feedback">{{ form.lot_number.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label required-field">Expiry Date</label>
|
||||
<input type="date" class="form-control" name="expiry_date"
|
||||
value="{{ qc_sample.expiry_date|date:'Y-m-d' }}" required>
|
||||
<div class="form-help">Control material expiration date</div>
|
||||
{% if form.expiry_date.errors %}
|
||||
<div class="validation-feedback">{{ form.expiry_date.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Expected Range</label>
|
||||
<input type="text" class="form-control" name="expected_range"
|
||||
value="{{ qc_sample.expected_range|default:'' }}"
|
||||
placeholder="e.g., 4.5-6.0 mg/dL">
|
||||
<div class="form-help">Expected value range for this QC level</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Target Value</label>
|
||||
<input type="number" class="form-control" name="target_value"
|
||||
value="{{ qc_sample.target_value|default:'' }}"
|
||||
step="0.01" placeholder="Target value">
|
||||
<div class="form-help">Expected target value for this control</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scheduling Information -->
|
||||
<div class="form-section">
|
||||
<h5><i class="fas fa-calendar me-2"></i>Scheduling Information</h5>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label required-field">Run Date</label>
|
||||
<input type="date" class="form-control" name="run_date"
|
||||
value="{{ qc_sample.run_date|date:'Y-m-d'|default:today }}" required>
|
||||
<div class="form-help">Date when QC sample will be processed</div>
|
||||
{% if form.run_date.errors %}
|
||||
<div class="validation-feedback">{{ form.run_date.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label required-field">Run Time</label>
|
||||
<input type="time" class="form-control" name="run_time"
|
||||
value="{{ qc_sample.run_time|time:'H:i'|default:current_time }}" required>
|
||||
<div class="form-help">Scheduled processing time</div>
|
||||
{% if form.run_time.errors %}
|
||||
<div class="validation-feedback">{{ form.run_time.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Assign Technician</label>
|
||||
<select class="form-select" name="technician">
|
||||
<option value="">Select technician...</option>
|
||||
{% for technician in technicians %}
|
||||
<option value="{{ technician.id }}"
|
||||
{% if qc_sample.technician_id == technician.id %}selected{% endif %}>
|
||||
{{ technician.get_full_name }} - {{ technician.department }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="form-help">Technician responsible for processing this QC sample</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Instrument</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 class="form-help">Laboratory instrument for processing</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Information -->
|
||||
<div class="form-section">
|
||||
<h5><i class="fas fa-comment me-2"></i>Additional Information</h5>
|
||||
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Comments</label>
|
||||
<textarea class="form-control" name="comments" rows="4"
|
||||
placeholder="Additional comments, special instructions, or notes...">{{ qc_sample.comments|default:'' }}</textarea>
|
||||
<div class="form-help">Any additional information about this QC sample</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" name="notify_technician"
|
||||
id="notify-technician" {% if not qc_sample.pk %}checked{% endif %}>
|
||||
<label class="form-check-label" for="notify-technician">
|
||||
Notify assigned technician
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" name="auto_schedule"
|
||||
id="auto-schedule" {% if not qc_sample.pk %}checked{% endif %}>
|
||||
<label class="form-check-label" for="auto-schedule">
|
||||
Add to processing schedule
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<a href="{% url 'laboratory:qc_sample_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times me-1"></i>Cancel
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
{% if qc_sample.pk %}
|
||||
<button type="button" class="btn btn-outline-danger me-2" onclick="deleteQCSample()">
|
||||
<i class="fas fa-trash me-1"></i>Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-1"></i>
|
||||
{% if qc_sample.pk %}Update QC Sample{% else %}Create QC Sample{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="col-lg-4">
|
||||
<!-- Preview Card -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-eye me-2"></i>Sample Preview
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="sample-preview">
|
||||
<div class="text-muted text-center py-3">
|
||||
<i class="fas fa-vial fa-2x mb-2"></i>
|
||||
<p>Fill in the form to see preview</p>
|
||||
</div>
|
||||
</div>
|
||||
</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>QC Levels:</h6>
|
||||
<div class="qc-level-preview qc-level-level1">
|
||||
<strong>Level 1:</strong> Normal control values
|
||||
</div>
|
||||
<div class="qc-level-preview qc-level-level2">
|
||||
<strong>Level 2:</strong> Abnormal low control values
|
||||
</div>
|
||||
<div class="qc-level-preview qc-level-level3">
|
||||
<strong>Level 3:</strong> Abnormal high control values
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<h6>Best Practices:</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li><i class="fas fa-check text-success me-2"></i>Run QC before patient samples</li>
|
||||
<li><i class="fas fa-check text-success me-2"></i>Use fresh control materials</li>
|
||||
<li><i class="fas fa-check text-success me-2"></i>Check expiry dates</li>
|
||||
<li><i class="fas fa-check text-success me-2"></i>Document all results</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent QC Samples -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-history me-2"></i>Recent QC Samples
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% for recent_sample in recent_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">
|
||||
<div class="fw-bold">{{ recent_sample.sample_id }}</div>
|
||||
<small class="text-muted">{{ recent_sample.test_type.name }}</small>
|
||||
</div>
|
||||
<small class="text-muted">{{ recent_sample.run_date|date:"M d" }}</small>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="text-muted text-center py-3">
|
||||
<i class="fas fa-history fa-2x mb-2"></i>
|
||||
<p>No recent QC samples</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Form validation
|
||||
$('#qcSampleForm').on('submit', function(e) {
|
||||
if (!validateForm()) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Real-time preview updates
|
||||
$('#qcSampleForm input, #qcSampleForm select, #qcSampleForm textarea').on('input change', function() {
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
// Test type change handler
|
||||
$('#test-type-select').on('change', function() {
|
||||
const selectedOption = $(this).find('option:selected');
|
||||
const unit = selectedOption.data('unit');
|
||||
const category = selectedOption.data('category');
|
||||
|
||||
// Update expected range placeholder if unit is available
|
||||
if (unit) {
|
||||
$('input[name="expected_range"]').attr('placeholder', `e.g., 4.5-6.0 ${unit}`);
|
||||
}
|
||||
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
// Initialize preview
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
function validateForm() {
|
||||
let isValid = true;
|
||||
|
||||
// Clear previous validation feedback
|
||||
$('.validation-feedback').remove();
|
||||
$('.is-invalid').removeClass('is-invalid');
|
||||
|
||||
// Required field validation
|
||||
const requiredFields = [
|
||||
{ name: 'test_type', message: 'Test type is required' },
|
||||
{ name: 'qc_level', message: 'QC level is required' },
|
||||
{ name: 'lot_number', message: 'Lot number is required' },
|
||||
{ name: 'expiry_date', message: 'Expiry date is required' },
|
||||
{ name: 'run_date', message: 'Run date is required' },
|
||||
{ name: 'run_time', message: 'Run time is required' }
|
||||
];
|
||||
|
||||
requiredFields.forEach(field => {
|
||||
const element = $(`[name="${field.name}"]`);
|
||||
if (!element.val()) {
|
||||
element.addClass('is-invalid');
|
||||
element.after(`<div class="validation-feedback">${field.message}</div>`);
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Date validation
|
||||
const expiryDate = new Date($('input[name="expiry_date"]').val());
|
||||
const runDate = new Date($('input[name="run_date"]').val());
|
||||
|
||||
if (expiryDate <= runDate) {
|
||||
const expiryField = $('input[name="expiry_date"]');
|
||||
expiryField.addClass('is-invalid');
|
||||
expiryField.after('<div class="validation-feedback">Expiry date must be after run date</div>');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
function updatePreview() {
|
||||
const formData = {
|
||||
sample_id: $('input[name="sample_id"]').val() || 'Auto-generated',
|
||||
test_type: $('#test-type-select option:selected').text(),
|
||||
qc_level: $('#qc-level-select option:selected').text(),
|
||||
lot_number: $('input[name="lot_number"]').val(),
|
||||
run_date: $('input[name="run_date"]').val(),
|
||||
run_time: $('input[name="run_time"]').val(),
|
||||
technician: $('select[name="technician"] option:selected').text()
|
||||
};
|
||||
|
||||
if (!formData.test_type || formData.test_type === 'Select test type...') {
|
||||
$('#sample-preview').html(`
|
||||
<div class="text-muted text-center py-3">
|
||||
<i class="fas fa-vial fa-2x mb-2"></i>
|
||||
<p>Fill in the form to see preview</p>
|
||||
</div>
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
||||
const qcLevelClass = $('#qc-level-select').val() ? `qc-level-${$('#qc-level-select').val()}` : '';
|
||||
|
||||
$('#sample-preview').html(`
|
||||
<div class="preview-card">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<i class="fas fa-vial me-2 text-primary"></i>
|
||||
<div>
|
||||
<h6 class="mb-0">${formData.sample_id}</h6>
|
||||
<small class="text-muted">QC Sample</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-sm table-borderless">
|
||||
<tr>
|
||||
<td class="fw-bold">Test Type:</td>
|
||||
<td>${formData.test_type}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">QC Level:</td>
|
||||
<td><span class="badge ${qcLevelClass}">${formData.qc_level}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">Lot Number:</td>
|
||||
<td>${formData.lot_number || 'Not specified'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">Run Date:</td>
|
||||
<td>${formData.run_date ? new Date(formData.run_date).toLocaleDateString() : 'Not set'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">Run Time:</td>
|
||||
<td>${formData.run_time || 'Not set'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">Technician:</td>
|
||||
<td>${formData.technician && formData.technician !== 'Select technician...' ? formData.technician : 'Not assigned'}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
function deleteQCSample() {
|
||||
if (confirm('Are you sure you want to delete this QC sample? This action cannot be undone.')) {
|
||||
$.ajax({
|
||||
url: '{% url "laboratory:delete_qc_sample" qc_sample.pk %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
window.location.href = '{% url "laboratory:qc_sample_list" %}';
|
||||
} else {
|
||||
alert('Error deleting QC sample: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error deleting QC sample.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
612
templates/laboratory/quality_control/qc_sample_list.html
Normal file
612
templates/laboratory/quality_control/qc_sample_list.html
Normal file
@ -0,0 +1,612 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Quality Control Samples{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link href="{% static 'plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
|
||||
<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; }
|
||||
|
||||
.qc-card {
|
||||
transition: all 0.3s ease;
|
||||
border-left: 4px solid #dee2e6;
|
||||
}
|
||||
|
||||
.qc-card.pending { border-left-color: #ffc107; }
|
||||
.qc-card.in-progress { border-left-color: #17a2b8; }
|
||||
.qc-card.completed { border-left-color: #28a745; }
|
||||
.qc-card.failed { border-left-color: #dc3545; }
|
||||
|
||||
.qc-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.stats-card.warning {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
}
|
||||
|
||||
.stats-card.success {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
}
|
||||
|
||||
.stats-card.info {
|
||||
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.qc-card {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
</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">Quality Control Samples</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-vial me-2"></i>Quality Control Samples
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createQCSampleModal">
|
||||
<i class="fas fa-plus me-1"></i>Create QC Sample
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Cards -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="card stats-card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-grow-1">
|
||||
<div class="text-white-75 mb-1">Total QC Samples</div>
|
||||
<div class="h3 mb-0 text-white">{{ stats.total_samples|default:0 }}</div>
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<i class="fas fa-vial fa-2x text-white-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="card stats-card warning">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-grow-1">
|
||||
<div class="text-white-75 mb-1">Pending</div>
|
||||
<div class="h3 mb-0 text-white">{{ stats.pending_samples|default:0 }}</div>
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<i class="fas fa-clock fa-2x text-white-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="card stats-card success">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-grow-1">
|
||||
<div class="text-white-75 mb-1">Completed</div>
|
||||
<div class="h3 mb-0 text-white">{{ stats.completed_samples|default:0 }}</div>
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<i class="fas fa-check-circle fa-2x text-white-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="card stats-card info">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-grow-1">
|
||||
<div class="text-white-75 mb-1">Failed</div>
|
||||
<div class="h3 mb-0 text-white">{{ stats.failed_samples|default:0 }}</div>
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<i class="fas fa-exclamation-triangle fa-2x text-white-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters and Search -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<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 class="col-md-3">
|
||||
<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</option>
|
||||
<option value="level2">Level 2</option>
|
||||
<option value="level3">Level 3</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Status</label>
|
||||
<select class="form-select" id="status-filter">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="in_progress">In Progress</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="failed">Failed</option>
|
||||
<option value="cancelled">Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Date Range</label>
|
||||
<select class="form-select" id="date-filter">
|
||||
<option value="">All Dates</option>
|
||||
<option value="today">Today</option>
|
||||
<option value="week">This Week</option>
|
||||
<option value="month">This Month</option>
|
||||
<option value="quarter">This Quarter</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
||||
<input type="text" class="form-control" id="search-input" placeholder="Search QC samples, lot numbers, or comments...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 text-end">
|
||||
<button type="button" class="btn btn-outline-secondary me-2" onclick="clearFilters()">
|
||||
<i class="fas fa-times me-1"></i>Clear Filters
|
||||
</button>
|
||||
<button type="button" class="btn btn-success" onclick="exportQCReport()">
|
||||
<i class="fas fa-file-excel me-1"></i>Export Report
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QC Samples Table -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-list me-2"></i>Quality Control Samples
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table id="qcSamplesTable" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Sample ID</th>
|
||||
<th>Test Type</th>
|
||||
<th>QC Level</th>
|
||||
<th>Lot Number</th>
|
||||
<th>Run Date</th>
|
||||
<th>Status</th>
|
||||
<th>Result</th>
|
||||
<th>Technician</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for sample in qc_samples %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-vial me-2 text-muted"></i>
|
||||
<div>
|
||||
<div class="fw-bold">{{ sample.test.test_name }}</div>
|
||||
|
||||
<small class="text-muted">{{ sample.qc_id }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
<div class="fw-bold">{{ sample.test.test_type }}</div>
|
||||
<small class="text-muted">{{ sample.test.test_type }}</small>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge qc-level-{{ sample.control_level }}">
|
||||
{{ sample.get_control_level_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
<div class="fw-bold">{{ sample.lot_number }}</div>
|
||||
<small class="text-muted">Exp: {{ sample.expiry_date|date:"M d, Y" }}</small>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
<div>{{ sample.run_date|date:"M d, Y" }}</div>
|
||||
<small class="text-muted">{{ sample.run_time|time:"H:i" }}</small>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge qc-status-{{ sample.status }}">
|
||||
{{ sample.get_status_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if sample.result_value %}
|
||||
<div class="result-{{ sample.result_status }}">
|
||||
<div class="fw-bold">{{ sample.result_value }} {{ sample.unit }}</div>
|
||||
<small class="text-muted">Range: {{ sample.expected_range }}</small>
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="text-muted">Pending</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if sample.technician %}
|
||||
<div>
|
||||
<div class="fw-bold">{{ sample.technician.get_full_name }}</div>
|
||||
<small class="text-muted">{{ sample.technician.department }}</small>
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="text-muted">Not assigned</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
onclick="viewQCSample('{{ sample.id }}')">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
{% if sample.status == 'pending' or sample.status == 'in_progress' %}
|
||||
<button type="button" class="btn btn-sm btn-outline-success"
|
||||
onclick="enterResult('{{ sample.id }}')">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-sm btn-outline-info"
|
||||
onclick="printQCLabel('{{ sample.id }}')">
|
||||
<i class="fas fa-print"></i>
|
||||
</button>
|
||||
{% if sample.status == 'pending' %}
|
||||
<button type="button" class="btn btn-sm btn-outline-danger"
|
||||
onclick="cancelQCSample('{{ sample.id }}')">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="9" class="text-center py-4">
|
||||
<div class="text-muted">
|
||||
<i class="fas fa-vial fa-3x mb-3"></i>
|
||||
<p>No QC samples found.</p>
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createQCSampleModal">
|
||||
<i class="fas fa-plus me-1"></i>Create First QC Sample
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create QC Sample Modal -->
|
||||
<div class="modal fade" id="createQCSampleModal" tabindex="-1" aria-labelledby="createQCSampleModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="createQCSampleModalLabel">
|
||||
<i class="fas fa-plus me-2"></i>Create QC Sample
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form id="createQCSampleForm" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Test Type <span class="text-danger">*</span></label>
|
||||
<select class="form-select" name="test_type" required>
|
||||
<option value="">Select test type...</option>
|
||||
{% for test_type in test_types %}
|
||||
<option value="{{ test_type.id }}">{{ test_type.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">QC Level <span class="text-danger">*</span></label>
|
||||
<select class="form-select" name="qc_level" required>
|
||||
<option value="">Select QC level...</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>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Lot Number <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="lot_number" required
|
||||
placeholder="Enter lot number">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Expiry Date <span class="text-danger">*</span></label>
|
||||
<input type="date" class="form-control" name="expiry_date" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Run Date <span class="text-danger">*</span></label>
|
||||
<input type="date" class="form-control" name="run_date" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Run Time <span class="text-danger">*</span></label>
|
||||
<input type="time" class="form-control" name="run_time" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Expected Range</label>
|
||||
<input type="text" class="form-control" name="expected_range"
|
||||
placeholder="e.g., 4.5-6.0 mg/dL">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Assign Technician</label>
|
||||
<select class="form-select" name="technician">
|
||||
<option value="">Select technician...</option>
|
||||
{% for technician in technicians %}
|
||||
<option value="{{ technician.id }}">{{ technician.get_full_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Comments</label>
|
||||
<textarea class="form-control" name="comments" rows="3"
|
||||
placeholder="Additional comments or notes..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="auto_generate_id" id="auto-generate-id" checked>
|
||||
<label class="form-check-label" for="auto-generate-id">
|
||||
Auto-generate sample ID
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<i class="fas fa-times me-1"></i>Cancel
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-1"></i>Create QC Sample
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- View QC Sample Modal -->
|
||||
<div class="modal fade" id="viewQCSampleModal" tabindex="-1" aria-labelledby="viewQCSampleModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="viewQCSampleModalLabel">
|
||||
<i class="fas fa-eye me-2"></i>QC Sample Details
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="qc-sample-details">
|
||||
<!-- Content loaded via AJAX -->
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<i class="fas fa-times me-1"></i>Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="{% static 'assets/plugins/datatables.net/js/jquery.dataTables.min.js' %}"></script>
|
||||
<script src="{% static 'assets/plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
|
||||
<script src="{% static 'assets/plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
|
||||
<script src="{% static 'assets/plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize DataTable
|
||||
const table = $('#qcSamplesTable').DataTable({
|
||||
responsive: true,
|
||||
pageLength: 25,
|
||||
order: [[4, 'desc']], // Sort by run date
|
||||
columnDefs: [
|
||||
{ orderable: false, targets: [8] } // Actions column
|
||||
]
|
||||
});
|
||||
|
||||
// Filter functionality
|
||||
$('#test-type-filter, #qc-level-filter, #status-filter, #date-filter').on('change', function() {
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
$('#search-input').on('keyup', function() {
|
||||
table.search(this.value).draw();
|
||||
});
|
||||
|
||||
// Form submission
|
||||
$('#createQCSampleForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
|
||||
$.ajax({
|
||||
url: '{% url "laboratory:create_qc_sample" %}',
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
$('#createQCSampleModal').modal('hide');
|
||||
location.reload(); // Refresh page to show new sample
|
||||
} else {
|
||||
alert('Error creating QC sample: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error creating QC sample. Please try again.');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function applyFilters() {
|
||||
const testType = $('#test-type-filter').val();
|
||||
const qcLevel = $('#qc-level-filter').val();
|
||||
const status = $('#status-filter').val();
|
||||
const dateRange = $('#date-filter').val();
|
||||
|
||||
// Apply filters to DataTable
|
||||
const params = new URLSearchParams();
|
||||
if (testType) params.append('test_type', testType);
|
||||
if (qcLevel) params.append('qc_level', qcLevel);
|
||||
if (status) params.append('status', status);
|
||||
if (dateRange) params.append('date_range', dateRange);
|
||||
|
||||
const url = window.location.pathname + (params.toString() ? '?' + params.toString() : '');
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
$('#test-type-filter, #qc-level-filter, #status-filter, #date-filter').val('');
|
||||
$('#search-input').val('');
|
||||
window.location.href = window.location.pathname;
|
||||
}
|
||||
|
||||
function viewQCSample(sampleId) {
|
||||
$.ajax({
|
||||
url: '{% url "laboratory:qc_sample_detail" pk="0" %}'.replace('0', sampleId),
|
||||
success: function(data) {
|
||||
$('#qc-sample-details').html(data);
|
||||
$('#viewQCSampleModal').modal('show');
|
||||
},
|
||||
error: function() {
|
||||
alert('Error loading QC sample details.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function enterResult(sampleId) {
|
||||
window.location.href = '{% url "laboratory:enter_qc_result" pk="0" %}'.replace('0', sampleId);
|
||||
}
|
||||
|
||||
function printQCLabel(sampleId) {
|
||||
window.open('{% url "laboratory:print_qc_label" pk="0" %}'.replace('0', sampleId), '_blank');
|
||||
}
|
||||
|
||||
function cancelQCSample(sampleId) {
|
||||
if (confirm('Are you sure you want to cancel this QC sample?')) {
|
||||
$.ajax({
|
||||
url: '{% url "laboratory:cancel_qc_sample" pk="0" %}'.replace('0', sampleId),
|
||||
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 exportQCReport() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.append('export', 'excel');
|
||||
window.location.href = '{% url "laboratory:qc_report" %}?' + params.toString();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
810
templates/laboratory/quality_control/qc_trend_analysis.html
Normal file
810
templates/laboratory/quality_control/qc_trend_analysis.html
Normal file
@ -0,0 +1,810 @@
|
||||
{% 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 %}
|
||||
|
||||
@ -0,0 +1,585 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Delete Reference Range - {{ range.test_type.name }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.delete-warning {
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);
|
||||
color: white;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.range-info {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.impact-section {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.impact-item {
|
||||
display: flex;
|
||||
align-items-center;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: #fff;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.impact-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 1rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.impact-icon.warning {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.impact-icon.danger {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.impact-icon.info {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.alternative-actions {
|
||||
background: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: #fff;
|
||||
border: 1px solid #28a745;
|
||||
border-radius: 0.25rem;
|
||||
text-decoration: none;
|
||||
color: #155724;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.confirmation-section {
|
||||
background: #f8f9fa;
|
||||
border: 2px solid #dc3545;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.type-to-confirm {
|
||||
background: #fff;
|
||||
border: 2px solid #dc3545;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.confirmation-input {
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.related-ranges {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.range-item {
|
||||
display: flex;
|
||||
align-items-center;
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.delete-warning, .range-info, .impact-section, .alternative-actions, .confirmation-section {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.impact-item, .action-button {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.impact-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
font-size: 1rem;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
}
|
||||
</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:reference_range_list' %}">Reference Ranges</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'laboratory:reference_range_detail' range.pk %}">{{ range.test_type.name }}</a></li>
|
||||
<li class="breadcrumb-item active">Delete</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-trash me-2"></i>Delete Reference Range
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a href="{% url 'laboratory:reference_range_detail' range.pk %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to Range
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Warning -->
|
||||
<div class="delete-warning">
|
||||
<i class="fas fa-exclamation-triangle fa-3x mb-3"></i>
|
||||
<h4>Permanent Deletion Warning</h4>
|
||||
<p class="mb-0">
|
||||
You are about to permanently delete this reference range. This action cannot be undone and may affect laboratory result interpretations.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<!-- Range Information -->
|
||||
<div class="range-info">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-info-circle me-2"></i>Reference Range to be Deleted
|
||||
</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-2">
|
||||
<strong>Test Type:</strong> {{ range.test_type.name }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Test Code:</strong> {{ range.test_type.code }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Age Group:</strong> {{ range.get_age_group_display }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Gender:</strong> {{ range.get_gender_display }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-2">
|
||||
<strong>Reference Range:</strong>
|
||||
{% if range.min_value and range.max_value %}
|
||||
{{ range.min_value }} - {{ range.max_value }} {{ range.unit }}
|
||||
{% elif range.min_value %}
|
||||
> {{ range.min_value }} {{ range.unit }}
|
||||
{% elif range.max_value %}
|
||||
< {{ range.max_value }} {{ range.unit }}
|
||||
{% else %}
|
||||
Not specified
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Status:</strong>
|
||||
{% if range.is_active %}
|
||||
<span class="badge bg-success">Active</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">Inactive</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Created:</strong> {{ range.created_at|date:"M d, Y" }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Last Updated:</strong> {{ range.updated_at|date:"M d, Y" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Impact Analysis -->
|
||||
<div class="impact-section">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>Deletion Impact Analysis
|
||||
</h5>
|
||||
|
||||
<div class="impact-item">
|
||||
<div class="impact-icon warning">
|
||||
<i class="fas fa-flask"></i>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="fw-bold">Laboratory Results</div>
|
||||
<div class="text-muted">
|
||||
{{ impact.recent_results }} recent test results use this reference range
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
{% if impact.recent_results > 0 %}
|
||||
<span class="badge bg-warning">{{ impact.recent_results }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-success">0</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="impact-item">
|
||||
<div class="impact-icon info">
|
||||
<i class="fas fa-chart-line"></i>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="fw-bold">Historical Data</div>
|
||||
<div class="text-muted">
|
||||
{{ impact.historical_results }} historical results reference this range
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<span class="badge bg-info">{{ impact.historical_results }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="impact-item">
|
||||
<div class="impact-icon danger">
|
||||
<i class="fas fa-bell"></i>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="fw-bold">Active Alerts</div>
|
||||
<div class="text-muted">
|
||||
{{ impact.active_alerts }} active alerts depend on this range
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
{% if impact.active_alerts > 0 %}
|
||||
<span class="badge bg-danger">{{ impact.active_alerts }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-success">0</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="impact-item">
|
||||
<div class="impact-icon info">
|
||||
<i class="fas fa-users"></i>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="fw-bold">Related Ranges</div>
|
||||
<div class="text-muted">
|
||||
{{ related_ranges.count }} other ranges for the same test type
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<span class="badge bg-info">{{ related_ranges.count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blocking Conditions -->
|
||||
{% if blocking_conditions %}
|
||||
<div class="alert alert-danger">
|
||||
<h6 class="alert-heading">
|
||||
<i class="fas fa-stop-circle me-2"></i>Cannot Delete - Blocking Conditions
|
||||
</h6>
|
||||
<ul class="mb-0">
|
||||
{% for condition in blocking_conditions %}
|
||||
<li>{{ condition }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<hr>
|
||||
<p class="mb-0">
|
||||
Please resolve these conditions before attempting to delete this reference range.
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<!-- Alternative Actions -->
|
||||
<div class="alternative-actions">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-lightbulb me-2"></i>Consider These Alternatives
|
||||
</h5>
|
||||
|
||||
<a href="#" class="action-button" onclick="deactivateRange()">
|
||||
<i class="fas fa-pause me-2"></i>
|
||||
<div>
|
||||
<div class="fw-bold">Deactivate Range</div>
|
||||
<small>Keep data but stop using for new results</small>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="#" class="action-button" onclick="archiveRange()">
|
||||
<i class="fas fa-archive me-2"></i>
|
||||
<div>
|
||||
<div class="fw-bold">Archive Range</div>
|
||||
<small>Move to archived status for future reference</small>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="{% url 'laboratory:reference_range_edit' range.pk %}" class="action-button">
|
||||
<i class="fas fa-edit me-2"></i>
|
||||
<div>
|
||||
<div class="fw-bold">Modify Range</div>
|
||||
<small>Update values instead of deleting</small>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="#" class="action-button" onclick="transferData()">
|
||||
<i class="fas fa-exchange-alt me-2"></i>
|
||||
<div>
|
||||
<div class="fw-bold">Transfer to New Range</div>
|
||||
<small>Create replacement and migrate data</small>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Related Ranges -->
|
||||
{% if related_ranges %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-link me-2"></i>Related Ranges
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="related-ranges">
|
||||
{% for range in related_ranges %}
|
||||
<div class="range-item">
|
||||
<div class="flex-grow-1">
|
||||
<div class="fw-bold">{{ range.get_age_group_display }}</div>
|
||||
<small class="text-muted">{{ range.get_gender_display }}</small>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
{% if range.is_active %}
|
||||
<span class="badge bg-success">Active</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Inactive</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirmation Section -->
|
||||
{% if not blocking_conditions %}
|
||||
<div class="confirmation-section">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-shield-alt me-2"></i>Deletion Confirmation
|
||||
</h5>
|
||||
|
||||
<form method="post" id="deleteForm">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Reason for Deletion</label>
|
||||
<select class="form-select" name="deletion_reason" required>
|
||||
<option value="">Select a reason...</option>
|
||||
<option value="outdated">Outdated reference values</option>
|
||||
<option value="incorrect">Incorrect range values</option>
|
||||
<option value="duplicate">Duplicate range</option>
|
||||
<option value="methodology_change">Methodology change</option>
|
||||
<option value="regulatory">Regulatory requirement</option>
|
||||
<option value="other">Other (specify in notes)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Notify Users</label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="notify_users"
|
||||
id="notify-users" checked>
|
||||
<label class="form-check-label" for="notify-users">
|
||||
Send notification to laboratory staff
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Additional Notes</label>
|
||||
<textarea class="form-control" name="deletion_notes" rows="3"
|
||||
placeholder="Provide additional context for this deletion..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="type-to-confirm">
|
||||
<p class="mb-2">
|
||||
<strong>To confirm deletion, type the test name exactly as shown:</strong>
|
||||
</p>
|
||||
<code class="fs-5">{{ range.test_type.name }}</code>
|
||||
<input type="text" class="form-control confirmation-input"
|
||||
id="confirmationInput" name="confirmation_text"
|
||||
placeholder="Type test name here..." required>
|
||||
<div class="form-text">
|
||||
This helps prevent accidental deletions
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mt-4">
|
||||
<div>
|
||||
<a href="{% url 'laboratory:reference_range_detail' range.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-warning me-2" onclick="saveBackup()">
|
||||
<i class="fas fa-download me-1"></i>Download Backup
|
||||
</button>
|
||||
<button type="submit" class="btn btn-danger" id="deleteButton" disabled>
|
||||
<i class="fas fa-trash me-1"></i>Permanently Delete Range
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Enable delete button only when confirmation text matches
|
||||
$('#confirmationInput').on('input', function() {
|
||||
const expectedText = '{{ range.test_type.name }}';
|
||||
const enteredText = $(this).val();
|
||||
const deleteButton = $('#deleteButton');
|
||||
|
||||
if (enteredText === expectedText) {
|
||||
deleteButton.prop('disabled', false);
|
||||
$(this).removeClass('is-invalid').addClass('is-valid');
|
||||
} else {
|
||||
deleteButton.prop('disabled', true);
|
||||
$(this).removeClass('is-valid');
|
||||
if (enteredText.length > 0) {
|
||||
$(this).addClass('is-invalid');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Form submission confirmation
|
||||
$('#deleteForm').on('submit', function(e) {
|
||||
const reason = $('select[name="deletion_reason"]').val();
|
||||
if (!reason) {
|
||||
alert('Please select a reason for deletion');
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!confirm('Are you absolutely sure you want to permanently delete this reference range? This action cannot be undone.')) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
$('#deleteButton').prop('disabled', true).html('<i class="fas fa-spinner fa-spin me-1"></i>Deleting...');
|
||||
});
|
||||
});
|
||||
|
||||
function deactivateRange() {
|
||||
if (confirm('Deactivate this reference range? It will no longer be used for new results but historical data will be preserved.')) {
|
||||
$.ajax({
|
||||
url: '{% url "laboratory:deactivate_reference_range" range.pk %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert('Reference range deactivated successfully');
|
||||
window.location.href = '{% url "laboratory:reference_range_detail" range.pk %}';
|
||||
} else {
|
||||
alert('Error deactivating range: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error deactivating range');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function archiveRange() {
|
||||
if (confirm('Archive this reference range? It will be moved to archived status.')) {
|
||||
$.ajax({
|
||||
url: '{% url "laboratory:archive_reference_range" range.pk %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert('Reference range archived successfully');
|
||||
window.location.href = '{% url "laboratory:reference_range_list" %}';
|
||||
} else {
|
||||
alert('Error archiving range: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error archiving range');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function transferData() {
|
||||
const newRangeUrl = '{% url "laboratory:reference_range_create" %}?transfer_from={{ range.pk }}';
|
||||
if (confirm('Create a new reference range and transfer data from this one?')) {
|
||||
window.location.href = newRangeUrl;
|
||||
}
|
||||
}
|
||||
|
||||
function saveBackup() {
|
||||
window.location.href = '{% url "laboratory:export_reference_range" range.pk %}';
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -0,0 +1,605 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Reference Range - {{ reference_range.test_type.name }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.range-info-card {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.range-visual {
|
||||
background: #fff;
|
||||
border: 2px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 2rem;
|
||||
margin: 1rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.range-bar {
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
position: relative;
|
||||
margin: 2rem 0;
|
||||
background: linear-gradient(to right,
|
||||
#dc3545 0%, #dc3545 10%,
|
||||
#ffc107 10%, #ffc107 20%,
|
||||
#28a745 20%, #28a745 80%,
|
||||
#ffc107 80%, #ffc107 90%,
|
||||
#dc3545 90%, #dc3545 100%);
|
||||
}
|
||||
|
||||
.range-marker {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
width: 2px;
|
||||
height: 60px;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.range-marker::after {
|
||||
content: attr(data-value);
|
||||
position: absolute;
|
||||
top: 65px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.range-legend {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: #fff;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.history-timeline {
|
||||
position: relative;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.history-timeline::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0.75rem;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background: #dee2e6;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.timeline-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -2.25rem;
|
||||
top: 0.5rem;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: #007bff;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 0 0 2px #dee2e6;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
background: #fff;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.badge-custom {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.badge-adult { background: #d4edda; color: #155724; }
|
||||
.badge-pediatric { background: #d1ecf1; color: #0c5460; }
|
||||
.badge-geriatric { background: #fff3cd; color: #856404; }
|
||||
.badge-male { background: #cce5ff; color: #004085; }
|
||||
.badge-female { background: #f8d7da; color: #721c24; }
|
||||
.badge-both { background: #e2e3e5; color: #383d41; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.range-visual {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.stat-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
}
|
||||
|
||||
.range-bar {
|
||||
height: 30px;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.range-marker {
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
</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:reference_range_list' %}">Reference Ranges</a></li>
|
||||
<li class="breadcrumb-item active">{{ reference_range.test_type.name }}</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-ruler me-2"></i>{{ reference_range.test_type.name }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'laboratory:reference_range_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to List
|
||||
</a>
|
||||
<a href="{% url 'laboratory:reference_range_update' reference_range.pk %}" class="btn btn-outline-primary">
|
||||
<i class="fas fa-edit me-1"></i>Edit
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-info" onclick="duplicateRange()">
|
||||
<i class="fas fa-copy me-1"></i>Duplicate
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-success" onclick="printRange()">
|
||||
<i class="fas fa-print me-1"></i>Print
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<!-- Range Information -->
|
||||
<div class="range-info-card">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-flask me-2"></i>Test Information
|
||||
</h5>
|
||||
<div class="mb-2">
|
||||
<strong>Test Name:</strong> {{ reference_range.test_type.name }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Test Code:</strong> {{ reference_range.test_type.code }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Category:</strong> {{ reference_range.test_type.category.name }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Method:</strong> {{ reference_range.test_type.method|default:"Not specified" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-users me-2"></i>Demographics
|
||||
</h5>
|
||||
<div class="mb-2">
|
||||
<strong>Age Group:</strong>
|
||||
<span class="badge-custom badge-{{ reference_range.age_group }}">
|
||||
{{ reference_range.get_age_group_display }}
|
||||
</span>
|
||||
</div>
|
||||
{% if reference_range.age_min or reference_range.age_max %}
|
||||
<div class="mb-2">
|
||||
<strong>Age Range:</strong>
|
||||
{% if reference_range.age_min %}{{ reference_range.age_min }}{% endif %}
|
||||
{% if reference_range.age_min and reference_range.age_max %} - {% endif %}
|
||||
{% if reference_range.age_max %}{{ reference_range.age_max }}{% endif %}
|
||||
{{ reference_range.age_unit }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mb-2">
|
||||
<strong>Gender:</strong>
|
||||
<span class="badge-custom badge-{{ reference_range.gender }}">
|
||||
{{ reference_range.get_gender_display }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Status:</strong>
|
||||
{% if reference_range.is_active %}
|
||||
<span class="badge bg-success">Active</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">Inactive</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Range Visualization -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-chart-bar me-2"></i>Reference Range Visualization
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="range-visual">
|
||||
<h6 class="mb-3">{{ reference_range.test_type.name }} Reference Range</h6>
|
||||
|
||||
<div class="stat-grid">
|
||||
{% if reference_range.critical_low %}
|
||||
<div class="stat-item">
|
||||
<div class="stat-value text-danger">{{ reference_range.critical_low }}</div>
|
||||
<div class="stat-label">Critical Low</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if reference_range.min_value %}
|
||||
<div class="stat-item">
|
||||
<div class="stat-value text-warning">{{ reference_range.min_value }}</div>
|
||||
<div class="stat-label">Normal Low</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if reference_range.max_value %}
|
||||
<div class="stat-item">
|
||||
<div class="stat-value text-warning">{{ reference_range.max_value }}</div>
|
||||
<div class="stat-label">Normal High</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if reference_range.critical_high %}
|
||||
<div class="stat-item">
|
||||
<div class="stat-value text-danger">{{ reference_range.critical_high }}</div>
|
||||
<div class="stat-label">Critical High</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if reference_range.min_value and reference_range.max_value %}
|
||||
<div class="range-bar" id="rangeBar">
|
||||
<!-- Range markers will be added by JavaScript -->
|
||||
</div>
|
||||
|
||||
<div class="range-legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #dc3545;"></div>
|
||||
<span>Critical</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #ffc107;"></div>
|
||||
<span>Abnormal</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #28a745;"></div>
|
||||
<span>Normal</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-3">
|
||||
<strong>Unit:</strong> {{ reference_range.unit|default:"Not specified" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Information -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>Additional Information
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6>Clinical Information</h6>
|
||||
{% if reference_range.clinical_significance %}
|
||||
<div class="mb-3">
|
||||
<strong>Clinical Significance:</strong>
|
||||
<p class="mt-1">{{ reference_range.clinical_significance }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if reference_range.interpretation_notes %}
|
||||
<div class="mb-3">
|
||||
<strong>Interpretation Notes:</strong>
|
||||
<p class="mt-1">{{ reference_range.interpretation_notes }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>Technical Information</h6>
|
||||
{% if reference_range.methodology %}
|
||||
<div class="mb-3">
|
||||
<strong>Methodology:</strong>
|
||||
<p class="mt-1">{{ reference_range.methodology }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if reference_range.reference_source %}
|
||||
<div class="mb-3">
|
||||
<strong>Reference Source:</strong>
|
||||
<p class="mt-1">{{ reference_range.reference_source }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if reference_range.conditions %}
|
||||
<div class="mt-3">
|
||||
<h6>Special Conditions</h6>
|
||||
<p>{{ reference_range.conditions }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="col-lg-4">
|
||||
<!-- Quick Stats -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-chart-pie me-2"></i>Usage Statistics
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="stat-item mb-3">
|
||||
<div class="stat-value">{{ usage_stats.total_tests }}</div>
|
||||
<div class="stat-label">Total Tests (Last 30 days)</div>
|
||||
</div>
|
||||
<div class="stat-item mb-3">
|
||||
<div class="stat-value">{{ usage_stats.normal_results }}%</div>
|
||||
<div class="stat-label">Normal Results</div>
|
||||
</div>
|
||||
<div class="stat-item mb-3">
|
||||
<div class="stat-value">{{ usage_stats.abnormal_results }}%</div>
|
||||
<div class="stat-label">Abnormal Results</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ usage_stats.critical_results }}%</div>
|
||||
<div class="stat-label">Critical Results</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Related Ranges -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-link me-2"></i>Related Ranges
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% for related_range in related_ranges %}
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<div class="flex-grow-1">
|
||||
<div class="fw-bold">{{ related_range.test_type.name }}</div>
|
||||
<small class="text-muted">
|
||||
{{ related_range.get_age_group_display }} - {{ related_range.get_gender_display }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<a href="{% url 'laboratory:reference_range_detail' related_range.pk %}"
|
||||
class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="text-muted text-center py-3">
|
||||
<i class="fas fa-link fa-2x mb-2"></i>
|
||||
<p>No related ranges</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Change History -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-history me-2"></i>Change History
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="history-timeline">
|
||||
{% for change in change_history %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-content">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<strong>{{ change.action }}</strong>
|
||||
<small class="text-muted">{{ change.created_at|date:"M d, Y H:i" }}</small>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>By:</strong> {{ change.user.get_full_name|default:change.user.username }}
|
||||
</div>
|
||||
{% if change.changes %}
|
||||
<div class="small">
|
||||
<strong>Changes:</strong>
|
||||
<ul class="mb-0">
|
||||
{% for field, values in change.changes.items %}
|
||||
<li>{{ field }}: {{ values.old }} → {{ values.new }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if change.notes %}
|
||||
<div class="small mt-2">
|
||||
<strong>Notes:</strong> {{ change.notes }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="text-muted text-center py-3">
|
||||
<i class="fas fa-history fa-2x mb-2"></i>
|
||||
<p>No change history</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize range visualization
|
||||
initializeRangeVisualization();
|
||||
});
|
||||
|
||||
function initializeRangeVisualization() {
|
||||
const rangeBar = document.getElementById('rangeBar');
|
||||
if (!rangeBar) return;
|
||||
|
||||
const criticalLow = {{ reference_range.critical_low|default:"null" }};
|
||||
const minValue = {{ reference_range.min_value|default:"null" }};
|
||||
const maxValue = {{ reference_range.max_value|default:"null" }};
|
||||
const criticalHigh = {{ reference_range.critical_high|default:"null" }};
|
||||
|
||||
// Calculate the full range for positioning
|
||||
const values = [criticalLow, minValue, maxValue, criticalHigh].filter(v => v !== null);
|
||||
if (values.length < 2) return;
|
||||
|
||||
const minRange = Math.min(...values);
|
||||
const maxRange = Math.max(...values);
|
||||
const rangeSpan = maxRange - minRange;
|
||||
|
||||
// Add markers for each value
|
||||
if (criticalLow !== null) {
|
||||
addRangeMarker(rangeBar, criticalLow, minRange, rangeSpan, 'Critical Low');
|
||||
}
|
||||
|
||||
if (minValue !== null) {
|
||||
addRangeMarker(rangeBar, minValue, minRange, rangeSpan, 'Normal Low');
|
||||
}
|
||||
|
||||
if (maxValue !== null) {
|
||||
addRangeMarker(rangeBar, maxValue, minRange, rangeSpan, 'Normal High');
|
||||
}
|
||||
|
||||
if (criticalHigh !== null) {
|
||||
addRangeMarker(rangeBar, criticalHigh, minRange, rangeSpan, 'Critical High');
|
||||
}
|
||||
}
|
||||
|
||||
function addRangeMarker(container, value, minRange, rangeSpan, label) {
|
||||
const position = ((value - minRange) / rangeSpan) * 100;
|
||||
|
||||
const marker = document.createElement('div');
|
||||
marker.className = 'range-marker';
|
||||
marker.style.left = position + '%';
|
||||
marker.setAttribute('data-value', value + ' ' + '{{ reference_range.unit|default:"" }}');
|
||||
marker.title = label + ': ' + value;
|
||||
|
||||
container.appendChild(marker);
|
||||
}
|
||||
|
||||
function duplicateRange() {
|
||||
if (confirm('Create a duplicate of this reference range?')) {
|
||||
$.ajax({
|
||||
url: '{% url "laboratory:duplicate_reference_range" %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'range_id': '{{ reference_range.pk }}',
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
window.location.href = '{% url "laboratory:reference_range_detail" %}' + response.new_range_id + '/';
|
||||
} else {
|
||||
alert('Error duplicating range: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error duplicating range');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function printRange() {
|
||||
window.print();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@media print {
|
||||
.btn-group, .breadcrumb {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 1rem !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid #000 !important;
|
||||
break-inside: avoid;
|
||||
}
|
||||
|
||||
.range-visual {
|
||||
border: 2px solid #000 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
765
templates/laboratory/reference_ranges/reference_range_form.html
Normal file
765
templates/laboratory/reference_ranges/reference_range_form.html
Normal file
@ -0,0 +1,765 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}
|
||||
{% if reference_range.pk %}Edit Reference Range{% else %}Add Reference Range{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.form-section {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-section h5 {
|
||||
color: #495057;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.range-preview {
|
||||
background: #fff;
|
||||
border: 2px solid #007bff;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.range-bar-preview {
|
||||
height: 30px;
|
||||
border-radius: 15px;
|
||||
position: relative;
|
||||
margin: 1rem 0;
|
||||
background: linear-gradient(to right,
|
||||
#dc3545 0%, #dc3545 10%,
|
||||
#ffc107 10%, #ffc107 20%,
|
||||
#28a745 20%, #28a745 80%,
|
||||
#ffc107 80%, #ffc107 90%,
|
||||
#dc3545 90%, #dc3545 100%);
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
background: #e9ecef;
|
||||
border-color: #ced4da;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
.validation-feedback {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.validation-feedback.valid {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.validation-feedback.invalid {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.age-range-inputs {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr auto;
|
||||
gap: 0.5rem;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.quick-values {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.quick-value-btn {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.template-selector {
|
||||
background: #e3f2fd;
|
||||
border: 1px solid #bbdefb;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-section {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.age-range-inputs {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.quick-values {
|
||||
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:reference_range_list' %}">Reference Ranges</a></li>
|
||||
<li class="breadcrumb-item active">
|
||||
{% if reference_range.pk %}Edit{% else %}Add{% endif %}
|
||||
</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-ruler me-2"></i>
|
||||
{% if reference_range.pk %}Edit Reference Range{% else %}Add Reference Range{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a href="{% url 'laboratory:reference_range_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to List
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" id="rangeForm" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<!-- Template Selector -->
|
||||
{% if not reference_range.pk %}
|
||||
<div class="template-selector">
|
||||
<h6 class="mb-3">
|
||||
<i class="fas fa-magic me-2"></i>Quick Start Templates
|
||||
</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm w-100 mb-2"
|
||||
onclick="loadTemplate('adult_basic')">
|
||||
<i class="fas fa-user me-1"></i>Adult Basic
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button type="button" class="btn btn-outline-info btn-sm w-100 mb-2"
|
||||
onclick="loadTemplate('pediatric')">
|
||||
<i class="fas fa-child me-1"></i>Pediatric
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button type="button" class="btn btn-outline-success btn-sm w-100 mb-2"
|
||||
onclick="loadTemplate('gender_specific')">
|
||||
<i class="fas fa-venus-mars me-1"></i>Gender Specific
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Test Information -->
|
||||
<div class="form-section">
|
||||
<h5>
|
||||
<i class="fas fa-flask me-2"></i>Test Information
|
||||
</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label required">Test Type</label>
|
||||
{{ form.test_type }}
|
||||
{% if form.test_type.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.test_type.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Select the laboratory test this range applies to</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Unit of Measurement</label>
|
||||
<div class="input-group">
|
||||
{{ form.unit }}
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
|
||||
data-bs-toggle="dropdown">
|
||||
<i class="fas fa-list"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="#" onclick="setUnit('mg/dL')">mg/dL</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="setUnit('g/dL')">g/dL</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="setUnit('mmol/L')">mmol/L</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="setUnit('IU/L')">IU/L</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="setUnit('ng/mL')">ng/mL</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="setUnit('pg/mL')">pg/mL</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="setUnit('%')">%</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="setUnit('ratio')">ratio</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% if form.unit.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.unit.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Demographics -->
|
||||
<div class="form-section">
|
||||
<h5>
|
||||
<i class="fas fa-users me-2"></i>Demographics
|
||||
</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label required">Age Group</label>
|
||||
{{ form.age_group }}
|
||||
{% if form.age_group.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.age_group.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label required">Gender</label>
|
||||
{{ form.gender }}
|
||||
{% if form.gender.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.gender.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Specific Age Range (Optional)</label>
|
||||
<div class="age-range-inputs">
|
||||
<div>
|
||||
{{ form.age_min }}
|
||||
<label class="form-label small">Min Age</label>
|
||||
</div>
|
||||
<div class="align-self-center">
|
||||
<span class="text-muted">to</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ form.age_max }}
|
||||
<label class="form-label small">Max Age</label>
|
||||
</div>
|
||||
<div>
|
||||
{{ form.age_unit }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-text">Leave blank to use the general age group</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reference Values -->
|
||||
<div class="form-section">
|
||||
<h5>
|
||||
<i class="fas fa-chart-bar me-2"></i>Reference Values
|
||||
</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Normal Range - Minimum</label>
|
||||
<div class="input-group">
|
||||
{{ form.min_value }}
|
||||
<span class="input-group-text" id="min-unit">{{ form.unit.value|default:"unit" }}</span>
|
||||
</div>
|
||||
{% if form.min_value.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.min_value.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="quick-values">
|
||||
<button type="button" class="btn btn-outline-secondary quick-value-btn"
|
||||
onclick="setMinValue('0')">0</button>
|
||||
<button type="button" class="btn btn-outline-secondary quick-value-btn"
|
||||
onclick="setMinValue('1')">1</button>
|
||||
<button type="button" class="btn btn-outline-secondary quick-value-btn"
|
||||
onclick="setMinValue('5')">5</button>
|
||||
<button type="button" class="btn btn-outline-secondary quick-value-btn"
|
||||
onclick="setMinValue('10')">10</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Normal Range - Maximum</label>
|
||||
<div class="input-group">
|
||||
{{ form.max_value }}
|
||||
<span class="input-group-text" id="max-unit">{{ form.unit.value|default:"unit" }}</span>
|
||||
</div>
|
||||
{% if form.max_value.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.max_value.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="quick-values">
|
||||
<button type="button" class="btn btn-outline-secondary quick-value-btn"
|
||||
onclick="setMaxValue('50')">50</button>
|
||||
<button type="button" class="btn btn-outline-secondary quick-value-btn"
|
||||
onclick="setMaxValue('100')">100</button>
|
||||
<button type="button" class="btn btn-outline-secondary quick-value-btn"
|
||||
onclick="setMaxValue('200')">200</button>
|
||||
<button type="button" class="btn btn-outline-secondary quick-value-btn"
|
||||
onclick="setMaxValue('500')">500</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Critical Low</label>
|
||||
<div class="input-group">
|
||||
{{ form.critical_low }}
|
||||
<span class="input-group-text" id="crit-low-unit">{{ form.unit.value|default:"unit" }}</span>
|
||||
</div>
|
||||
{% if form.critical_low.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.critical_low.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Values below this require immediate attention</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Critical High</label>
|
||||
<div class="input-group">
|
||||
{{ form.critical_high }}
|
||||
<span class="input-group-text" id="crit-high-unit">{{ form.unit.value|default:"unit" }}</span>
|
||||
</div>
|
||||
{% if form.critical_high.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.critical_high.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Values above this require immediate attention</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Information -->
|
||||
<div class="form-section">
|
||||
<h5>
|
||||
<i class="fas fa-info-circle me-2"></i>Additional Information
|
||||
</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Clinical Significance</label>
|
||||
{{ form.clinical_significance }}
|
||||
{% if form.clinical_significance.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.clinical_significance.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Clinical importance of this test</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Methodology</label>
|
||||
{{ form.methodology }}
|
||||
{% if form.methodology.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.methodology.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Testing method or technique used</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Interpretation Notes</label>
|
||||
{{ form.interpretation_notes }}
|
||||
{% if form.interpretation_notes.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.interpretation_notes.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Guidelines for interpreting results</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Reference Source</label>
|
||||
{{ form.reference_source }}
|
||||
{% if form.reference_source.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.reference_source.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Source of reference values (e.g., literature, manufacturer)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Special Conditions</label>
|
||||
{{ form.conditions }}
|
||||
{% if form.conditions.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.conditions.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Any special conditions that affect this reference range</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="form-section">
|
||||
<h5>
|
||||
<i class="fas fa-toggle-on me-2"></i>Status & Activation
|
||||
</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-check form-switch mb-3">
|
||||
{{ form.is_active }}
|
||||
<label class="form-check-label" for="{{ form.is_active.id_for_label }}">
|
||||
Active Range
|
||||
</label>
|
||||
<div class="form-text">Only active ranges are used for result interpretation</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Effective Date</label>
|
||||
{{ form.effective_date }}
|
||||
{% if form.effective_date.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.effective_date.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">When this range becomes effective</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="col-lg-4">
|
||||
<!-- Range Preview -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-eye me-2"></i>Range Preview
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="range-preview" id="rangePreview">
|
||||
<h6 class="mb-3">Reference Range</h6>
|
||||
<div id="previewContent">
|
||||
<p class="text-muted">Enter values to see preview</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-check-circle me-2"></i>Validation Status
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="validationStatus">
|
||||
<div class="validation-item mb-2">
|
||||
<i class="fas fa-circle text-secondary me-2"></i>
|
||||
<span>Test type selected</span>
|
||||
<span id="testTypeStatus" class="float-end"></span>
|
||||
</div>
|
||||
<div class="validation-item mb-2">
|
||||
<i class="fas fa-circle text-secondary me-2"></i>
|
||||
<span>Demographics specified</span>
|
||||
<span id="demographicsStatus" class="float-end"></span>
|
||||
</div>
|
||||
<div class="validation-item mb-2">
|
||||
<i class="fas fa-circle text-secondary me-2"></i>
|
||||
<span>Reference values set</span>
|
||||
<span id="valuesStatus" class="float-end"></span>
|
||||
</div>
|
||||
<div class="validation-item mb-2">
|
||||
<i class="fas fa-circle text-secondary me-2"></i>
|
||||
<span>Logical range order</span>
|
||||
<span id="logicStatus" class="float-end"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="card">
|
||||
<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">
|
||||
<button type="button" class="btn btn-outline-info" onclick="validateForm()">
|
||||
<i class="fas fa-check me-1"></i>Validate Form
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-warning" onclick="resetForm()">
|
||||
<i class="fas fa-undo me-1"></i>Reset Form
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="saveAsDraft()">
|
||||
<i class="fas fa-save me-1"></i>Save as Draft
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<a href="{% url 'laboratory:reference_range_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times me-1"></i>Cancel
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" name="save_and_continue" class="btn btn-outline-primary me-2">
|
||||
<i class="fas fa-save me-1"></i>Save & Continue Editing
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-check me-1"></i>
|
||||
{% if reference_range.pk %}Update Range{% else %}Create Range{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize form
|
||||
updatePreview();
|
||||
updateValidationStatus();
|
||||
updateUnitLabels();
|
||||
|
||||
// Bind events
|
||||
$('#rangeForm input, #rangeForm select, #rangeForm textarea').on('input change', function() {
|
||||
updatePreview();
|
||||
updateValidationStatus();
|
||||
});
|
||||
|
||||
$('#id_unit').on('input change', function() {
|
||||
updateUnitLabels();
|
||||
});
|
||||
|
||||
// Form validation
|
||||
$('#rangeForm').on('submit', function(e) {
|
||||
if (!validateForm()) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function updatePreview() {
|
||||
const testType = $('#id_test_type option:selected').text();
|
||||
const ageGroup = $('#id_age_group option:selected').text();
|
||||
const gender = $('#id_gender option:selected').text();
|
||||
const minValue = $('#id_min_value').val();
|
||||
const maxValue = $('#id_max_value').val();
|
||||
const criticalLow = $('#id_critical_low').val();
|
||||
const criticalHigh = $('#id_critical_high').val();
|
||||
const unit = $('#id_unit').val();
|
||||
|
||||
let previewHtml = '';
|
||||
|
||||
if (testType && testType !== '---------') {
|
||||
previewHtml += `<div class="mb-2"><strong>${testType}</strong></div>`;
|
||||
}
|
||||
|
||||
if (ageGroup && gender) {
|
||||
previewHtml += `<div class="mb-2">${ageGroup} - ${gender}</div>`;
|
||||
}
|
||||
|
||||
if (minValue || maxValue) {
|
||||
previewHtml += '<div class="mb-2">';
|
||||
if (minValue && maxValue) {
|
||||
previewHtml += `<strong>Normal: ${minValue} - ${maxValue} ${unit}</strong>`;
|
||||
} else if (minValue) {
|
||||
previewHtml += `<strong>Normal: > ${minValue} ${unit}</strong>`;
|
||||
} else if (maxValue) {
|
||||
previewHtml += `<strong>Normal: < ${maxValue} ${unit}</strong>`;
|
||||
}
|
||||
previewHtml += '</div>';
|
||||
}
|
||||
|
||||
if (criticalLow || criticalHigh) {
|
||||
previewHtml += '<div class="text-danger small">';
|
||||
previewHtml += 'Critical: ';
|
||||
if (criticalLow) previewHtml += `< ${criticalLow}`;
|
||||
if (criticalLow && criticalHigh) previewHtml += ', ';
|
||||
if (criticalHigh) previewHtml += `> ${criticalHigh}`;
|
||||
previewHtml += ` ${unit}</div>`;
|
||||
}
|
||||
|
||||
if (!previewHtml) {
|
||||
previewHtml = '<p class="text-muted">Enter values to see preview</p>';
|
||||
}
|
||||
|
||||
$('#previewContent').html(previewHtml);
|
||||
}
|
||||
|
||||
function updateValidationStatus() {
|
||||
// Test type validation
|
||||
const testType = $('#id_test_type').val();
|
||||
updateStatusIcon('testTypeStatus', testType ? 'valid' : 'invalid');
|
||||
|
||||
// Demographics validation
|
||||
const ageGroup = $('#id_age_group').val();
|
||||
const gender = $('#id_gender').val();
|
||||
updateStatusIcon('demographicsStatus', (ageGroup && gender) ? 'valid' : 'invalid');
|
||||
|
||||
// Values validation
|
||||
const minValue = $('#id_min_value').val();
|
||||
const maxValue = $('#id_max_value').val();
|
||||
updateStatusIcon('valuesStatus', (minValue || maxValue) ? 'valid' : 'invalid');
|
||||
|
||||
// Logic validation
|
||||
const criticalLow = parseFloat($('#id_critical_low').val()) || 0;
|
||||
const minVal = parseFloat(minValue) || 0;
|
||||
const maxVal = parseFloat(maxValue) || 0;
|
||||
const criticalHigh = parseFloat($('#id_critical_high').val()) || 0;
|
||||
|
||||
let logicValid = true;
|
||||
if (minValue && maxValue && minVal >= maxVal) logicValid = false;
|
||||
if (criticalLow && minValue && criticalLow >= minVal) logicValid = false;
|
||||
if (maxValue && criticalHigh && maxVal >= criticalHigh) logicValid = false;
|
||||
|
||||
updateStatusIcon('logicStatus', logicValid ? 'valid' : 'invalid');
|
||||
}
|
||||
|
||||
function updateStatusIcon(elementId, status) {
|
||||
const element = $('#' + elementId);
|
||||
if (status === 'valid') {
|
||||
element.html('<i class="fas fa-check text-success"></i>');
|
||||
} else {
|
||||
element.html('<i class="fas fa-times text-danger"></i>');
|
||||
}
|
||||
}
|
||||
|
||||
function updateUnitLabels() {
|
||||
const unit = $('#id_unit').val() || 'unit';
|
||||
$('#min-unit, #max-unit, #crit-low-unit, #crit-high-unit').text(unit);
|
||||
}
|
||||
|
||||
function setUnit(unit) {
|
||||
$('#id_unit').val(unit);
|
||||
updateUnitLabels();
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
function setMinValue(value) {
|
||||
$('#id_min_value').val(value);
|
||||
updatePreview();
|
||||
updateValidationStatus();
|
||||
}
|
||||
|
||||
function setMaxValue(value) {
|
||||
$('#id_max_value').val(value);
|
||||
updatePreview();
|
||||
updateValidationStatus();
|
||||
}
|
||||
|
||||
function loadTemplate(templateType) {
|
||||
switch(templateType) {
|
||||
case 'adult_basic':
|
||||
$('#id_age_group').val('adult');
|
||||
$('#id_gender').val('both');
|
||||
break;
|
||||
case 'pediatric':
|
||||
$('#id_age_group').val('pediatric');
|
||||
$('#id_gender').val('both');
|
||||
$('#id_age_min').val('0');
|
||||
$('#id_age_max').val('18');
|
||||
$('#id_age_unit').val('years');
|
||||
break;
|
||||
case 'gender_specific':
|
||||
$('#id_age_group').val('adult');
|
||||
$('#id_gender').val('male');
|
||||
break;
|
||||
}
|
||||
updatePreview();
|
||||
updateValidationStatus();
|
||||
}
|
||||
|
||||
function validateForm() {
|
||||
let isValid = true;
|
||||
const errors = [];
|
||||
|
||||
// Check required fields
|
||||
if (!$('#id_test_type').val()) {
|
||||
errors.push('Test type is required');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!$('#id_age_group').val()) {
|
||||
errors.push('Age group is required');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!$('#id_gender').val()) {
|
||||
errors.push('Gender is required');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Check logical consistency
|
||||
const minValue = parseFloat($('#id_min_value').val());
|
||||
const maxValue = parseFloat($('#id_max_value').val());
|
||||
const criticalLow = parseFloat($('#id_critical_low').val());
|
||||
const criticalHigh = parseFloat($('#id_critical_high').val());
|
||||
|
||||
if (minValue && maxValue && minValue >= maxValue) {
|
||||
errors.push('Minimum value must be less than maximum value');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (criticalLow && minValue && criticalLow >= minValue) {
|
||||
errors.push('Critical low must be less than normal minimum');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (maxValue && criticalHigh && maxValue >= criticalHigh) {
|
||||
errors.push('Critical high must be greater than normal maximum');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
alert('Validation errors:\n' + errors.join('\n'));
|
||||
} else {
|
||||
alert('Form validation passed!');
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
if (confirm('Reset all form fields? This will clear all entered data.')) {
|
||||
$('#rangeForm')[0].reset();
|
||||
updatePreview();
|
||||
updateValidationStatus();
|
||||
updateUnitLabels();
|
||||
}
|
||||
}
|
||||
|
||||
function saveAsDraft() {
|
||||
// Add a hidden field to indicate draft save
|
||||
$('<input>').attr({
|
||||
type: 'hidden',
|
||||
name: 'save_as_draft',
|
||||
value: 'true'
|
||||
}).appendTo('#rangeForm');
|
||||
|
||||
$('#rangeForm').submit();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
775
templates/laboratory/reference_ranges/reference_range_list.html
Normal file
775
templates/laboratory/reference_ranges/reference_range_list.html
Normal file
@ -0,0 +1,775 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Reference Ranges{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.range-card {
|
||||
background: #fff;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.range-card:hover {
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.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.info {
|
||||
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
|
||||
}
|
||||
|
||||
.range-badge {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.range-badge.adult {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.range-badge.pediatric {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.range-badge.geriatric {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.range-badge.male {
|
||||
background: #cce5ff;
|
||||
color: #004085;
|
||||
}
|
||||
|
||||
.range-badge.female {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.range-badge.both {
|
||||
background: #e2e3e5;
|
||||
color: #383d41;
|
||||
}
|
||||
|
||||
.range-values {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.filter-section {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.range-card {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
}
|
||||
</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">Reference Ranges</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-ruler me-2"></i>Reference Ranges
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-outline-primary" onclick="exportRanges()">
|
||||
<i class="fas fa-download me-1"></i>Export
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-info" onclick="importRanges()">
|
||||
<i class="fas fa-upload me-1"></i>Import
|
||||
</button>
|
||||
<a href="{% url 'laboratory:reference_range_create' %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-1"></i>Add Range
|
||||
</a>
|
||||
</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">{{ stats.total_ranges }}</h3>
|
||||
<p class="mb-0">Total Ranges</p>
|
||||
</div>
|
||||
<i class="fas fa-ruler 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">{{ stats.active_ranges }}</h3>
|
||||
<p class="mb-0">Active Ranges</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">{{ stats.test_types_covered }}</h3>
|
||||
<p class="mb-0">Test Types</p>
|
||||
</div>
|
||||
<i class="fas fa-flask fa-2x opacity-75"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="stat-card info">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h3 class="mb-1">{{ stats.pending_review }}</h3>
|
||||
<p class="mb-0">Pending Review</p>
|
||||
</div>
|
||||
<i class="fas fa-clock fa-2x opacity-75"></i>
|
||||
</div>
|
||||
</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">Age Group</label>
|
||||
<select class="form-select" id="age-group-filter">
|
||||
<option value="">All Age Groups</option>
|
||||
<option value="adult">Adult</option>
|
||||
<option value="pediatric">Pediatric</option>
|
||||
<option value="geriatric">Geriatric</option>
|
||||
<option value="neonatal">Neonatal</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Gender</label>
|
||||
<select class="form-select" id="gender-filter">
|
||||
<option value="">All Genders</option>
|
||||
<option value="male">Male</option>
|
||||
<option value="female">Female</option>
|
||||
<option value="both">Both</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Status</label>
|
||||
<select class="form-select" id="status-filter">
|
||||
<option value="">All Status</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
<option value="pending">Pending Review</option>
|
||||
<option value="archived">Archived</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Search</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="search-input"
|
||||
placeholder="Search by test name, unit, or notes...">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="applyFilters()">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Quick Filters</label>
|
||||
<div class="quick-actions">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="filterByStatus('active')">
|
||||
<i class="fas fa-check me-1"></i>Active Only
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-warning btn-sm" onclick="filterByStatus('pending')">
|
||||
<i class="fas fa-clock me-1"></i>Pending Review
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-info btn-sm" onclick="filterByAgeGroup('pediatric')">
|
||||
<i class="fas fa-child me-1"></i>Pediatric
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="clearFilters()">
|
||||
<i class="fas fa-times me-1"></i>Clear All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reference Ranges Table -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-table me-2"></i>Reference Ranges
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped" id="rangesTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="select-all">
|
||||
</div>
|
||||
</th>
|
||||
<th>Test Type</th>
|
||||
<th>Age Group</th>
|
||||
<th>Gender</th>
|
||||
<th>Reference Range</th>
|
||||
<th>Unit</th>
|
||||
<th>Status</th>
|
||||
<th>Last Updated</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for range in reference_ranges %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input range-checkbox" type="checkbox"
|
||||
value="{{ range.id }}">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-flask text-primary me-2"></i>
|
||||
<div>
|
||||
<div class="fw-bold">{{ range.test_type.name }}</div>
|
||||
<small class="text-muted">{{ range.test_type.code }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="range-badge {{ range.age_group }}">
|
||||
{{ range.get_age_group_display }}
|
||||
</span>
|
||||
{% if range.age_min or range.age_max %}
|
||||
<div class="small text-muted">
|
||||
{% if range.age_min %}{{ range.age_min }}{% endif %}
|
||||
{% if range.age_min and range.age_max %} - {% endif %}
|
||||
{% if range.age_max %}{{ range.age_max }}{% endif %}
|
||||
{{ range.age_unit }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="range-badge {{ range.gender }}">
|
||||
{{ range.get_gender_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="range-values">
|
||||
{% if range.min_value and range.max_value %}
|
||||
<strong>{{ range.min_value }} - {{ range.max_value }}</strong>
|
||||
{% elif range.min_value %}
|
||||
<strong>> {{ range.min_value }}</strong>
|
||||
{% elif range.max_value %}
|
||||
<strong>< {{ range.max_value }}</strong>
|
||||
{% else %}
|
||||
<span class="text-muted">Not specified</span>
|
||||
{% endif %}
|
||||
{% if range.critical_low or range.critical_high %}
|
||||
<div class="small text-danger">
|
||||
Critical:
|
||||
{% if range.critical_low %}<{{ range.critical_low }}{% endif %}
|
||||
{% if range.critical_low and range.critical_high %}, {% endif %}
|
||||
{% if range.critical_high %}>{{ range.critical_high }}{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="fw-bold">{{ range.unit|default:"--" }}</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if range.is_active %}
|
||||
<span class="badge bg-success">Active</span>
|
||||
{% elif range.status == 'pending' %}
|
||||
<span class="badge bg-warning">Pending</span>
|
||||
{% elif range.status == 'archived' %}
|
||||
<span class="badge bg-secondary">Archived</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">Inactive</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div>{{ range.updated_at|date:"M d, Y" }}</div>
|
||||
<small class="text-muted">{{ range }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'laboratory:reference_range_detail' range.pk %}"
|
||||
class="btn btn-outline-primary btn-sm" title="View Details">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{% url 'laboratory:reference_range_update' range.pk %}"
|
||||
class="btn btn-outline-secondary btn-sm" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-info btn-sm"
|
||||
onclick="duplicateRange('{{ range.pk }}')" title="Duplicate">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
<a href="{% url 'laboratory:reference_range_delete' range.pk %}"
|
||||
class="btn btn-outline-danger btn-sm" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="9" class="text-center py-4">
|
||||
<div class="text-muted">
|
||||
<i class="fas fa-ruler fa-3x mb-3"></i>
|
||||
<h5>No Reference Ranges Found</h5>
|
||||
<p>Start by adding your first reference range.</p>
|
||||
<a href="{% url 'laboratory:reference_range_create' %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-1"></i>Add Reference Range
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Reference ranges pagination">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1">« First</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">Previous</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ num }}</span>
|
||||
</li>
|
||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}">Next</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">Last »</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bulk Actions -->
|
||||
<div class="card mt-3" id="bulk-actions" style="display: none;">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<span id="selected-count">0</span> ranges selected
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-outline-success" onclick="bulkActivate()">
|
||||
<i class="fas fa-check me-1"></i>Activate
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-warning" onclick="bulkDeactivate()">
|
||||
<i class="fas fa-pause me-1"></i>Deactivate
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-info" onclick="bulkExport()">
|
||||
<i class="fas fa-download me-1"></i>Export Selected
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger" onclick="bulkDelete()">
|
||||
<i class="fas fa-trash me-1"></i>Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import Modal -->
|
||||
<div class="modal fade" id="importModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-upload me-2"></i>Import Reference Ranges
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="importForm" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Select File</label>
|
||||
<input type="file" class="form-control" name="import_file"
|
||||
accept=".xlsx,.xls,.csv" required>
|
||||
<div class="form-text">
|
||||
Supported formats: Excel (.xlsx, .xls) or CSV (.csv)
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" name="update_existing"
|
||||
id="update-existing">
|
||||
<label class="form-check-label" for="update-existing">
|
||||
Update existing ranges with same test type and demographics
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" name="validate_only"
|
||||
id="validate-only">
|
||||
<label class="form-check-label" for="validate-only">
|
||||
Validate only (don't import)
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
<div class="alert alert-info">
|
||||
<h6>Import Template:</h6>
|
||||
<p class="mb-2">Your file should contain the following columns:</p>
|
||||
<ul class="mb-0">
|
||||
<li>test_type_name</li>
|
||||
<li>age_group</li>
|
||||
<li>gender</li>
|
||||
<li>min_value</li>
|
||||
<li>max_value</li>
|
||||
<li>unit</li>
|
||||
<li>critical_low</li>
|
||||
<li>critical_high</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" onclick="submitImport()">
|
||||
<i class="fas fa-upload me-1"></i>Import
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize DataTable
|
||||
$('#rangesTable').DataTable({
|
||||
responsive: true,
|
||||
pageLength: 25,
|
||||
order: [[1, 'asc']],
|
||||
columnDefs: [
|
||||
{ orderable: false, targets: [0, 8] }
|
||||
]
|
||||
});
|
||||
|
||||
// Handle select all checkbox
|
||||
$('#select-all').on('change', function() {
|
||||
$('.range-checkbox').prop('checked', this.checked);
|
||||
updateBulkActions();
|
||||
});
|
||||
|
||||
// Handle individual checkboxes
|
||||
$('.range-checkbox').on('change', function() {
|
||||
updateBulkActions();
|
||||
|
||||
// Update select all checkbox
|
||||
const totalCheckboxes = $('.range-checkbox').length;
|
||||
const checkedCheckboxes = $('.range-checkbox:checked').length;
|
||||
|
||||
$('#select-all').prop('indeterminate', checkedCheckboxes > 0 && checkedCheckboxes < totalCheckboxes);
|
||||
$('#select-all').prop('checked', checkedCheckboxes === totalCheckboxes);
|
||||
});
|
||||
|
||||
// Auto-apply filters on change
|
||||
$('#test-type-filter, #age-group-filter, #gender-filter, #status-filter').on('change', function() {
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
// Search on enter
|
||||
$('#search-input').on('keypress', function(e) {
|
||||
if (e.which === 13) {
|
||||
applyFilters();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function updateBulkActions() {
|
||||
const selectedCount = $('.range-checkbox:checked').length;
|
||||
$('#selected-count').text(selectedCount);
|
||||
|
||||
if (selectedCount > 0) {
|
||||
$('#bulk-actions').show();
|
||||
} else {
|
||||
$('#bulk-actions').hide();
|
||||
}
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
const filters = {
|
||||
test_type: $('#test-type-filter').val(),
|
||||
age_group: $('#age-group-filter').val(),
|
||||
gender: $('#gender-filter').val(),
|
||||
status: $('#status-filter').val(),
|
||||
search: $('#search-input').val()
|
||||
};
|
||||
|
||||
const params = new URLSearchParams();
|
||||
Object.keys(filters).forEach(key => {
|
||||
if (filters[key]) {
|
||||
params.append(key, filters[key]);
|
||||
}
|
||||
});
|
||||
|
||||
window.location.href = '?' + params.toString();
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
$('#test-type-filter').val('');
|
||||
$('#age-group-filter').val('');
|
||||
$('#gender-filter').val('');
|
||||
$('#status-filter').val('');
|
||||
$('#search-input').val('');
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function filterByStatus(status) {
|
||||
$('#status-filter').val(status);
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function filterByAgeGroup(ageGroup) {
|
||||
$('#age-group-filter').val(ageGroup);
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function duplicateRange(rangeId) {
|
||||
if (confirm('Create a duplicate of this reference range?')) {
|
||||
$.ajax({
|
||||
url: '{% url "laboratory:duplicate_reference_range" %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'range_id': rangeId,
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error duplicating range: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error duplicating range');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function bulkActivate() {
|
||||
const selectedIds = getSelectedIds();
|
||||
if (selectedIds.length === 0) return;
|
||||
|
||||
if (confirm(`Activate ${selectedIds.length} selected ranges?`)) {
|
||||
bulkAction('activate', selectedIds);
|
||||
}
|
||||
}
|
||||
|
||||
function bulkDeactivate() {
|
||||
const selectedIds = getSelectedIds();
|
||||
if (selectedIds.length === 0) return;
|
||||
|
||||
if (confirm(`Deactivate ${selectedIds.length} selected ranges?`)) {
|
||||
bulkAction('deactivate', selectedIds);
|
||||
}
|
||||
}
|
||||
|
||||
function bulkDelete() {
|
||||
const selectedIds = getSelectedIds();
|
||||
if (selectedIds.length === 0) return;
|
||||
|
||||
if (confirm(`Delete ${selectedIds.length} selected ranges? This action cannot be undone.`)) {
|
||||
bulkAction('delete', selectedIds);
|
||||
}
|
||||
}
|
||||
|
||||
function bulkExport() {
|
||||
const selectedIds = getSelectedIds();
|
||||
if (selectedIds.length === 0) return;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
selectedIds.forEach(id => params.append('ids', id));
|
||||
|
||||
window.location.href = '{% url "laboratory:export_reference_ranges" %}?' + params.toString();
|
||||
}
|
||||
|
||||
function getSelectedIds() {
|
||||
return $('.range-checkbox:checked').map(function() {
|
||||
return this.value;
|
||||
}).get();
|
||||
}
|
||||
|
||||
function bulkAction(action, ids) {
|
||||
$.ajax({
|
||||
url: '{% url "laboratory:bulk_reference_range_action" %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'action': action,
|
||||
'ids': ids,
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error performing bulk action: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error performing bulk action');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function exportRanges() {
|
||||
const filters = {
|
||||
test_type: $('#test-type-filter').val(),
|
||||
age_group: $('#age-group-filter').val(),
|
||||
gender: $('#gender-filter').val(),
|
||||
status: $('#status-filter').val(),
|
||||
search: $('#search-input').val()
|
||||
};
|
||||
|
||||
const params = new URLSearchParams();
|
||||
Object.keys(filters).forEach(key => {
|
||||
if (filters[key]) {
|
||||
params.append(key, filters[key]);
|
||||
}
|
||||
});
|
||||
|
||||
window.location.href = '{% url "laboratory:export_reference_ranges" %}?' + params.toString();
|
||||
}
|
||||
|
||||
function importRanges() {
|
||||
$('#importModal').modal('show');
|
||||
}
|
||||
|
||||
function submitImport() {
|
||||
const formData = new FormData($('#importForm')[0]);
|
||||
|
||||
$.ajax({
|
||||
url: '{% url "laboratory:import_reference_ranges" %}',
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
$('#importModal').modal('hide');
|
||||
alert(`Import completed successfully. ${response.imported_count} ranges imported.`);
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Import failed: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error importing ranges');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -15,11 +15,12 @@
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'laboratory:specimen_form' object.pk %}" class="btn btn-primary">
|
||||
<a href="{% url 'laboratory:specimen_update' object.pk %}" class="btn btn-primary">
|
||||
<i class="fas fa-edit me-2"></i>Edit Specimen
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown">
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="#" onclick="printLabel()">
|
||||
@ -35,9 +36,9 @@
|
||||
<li><a class="dropdown-item" href="#" onclick="generateReport()">
|
||||
<i class="fas fa-chart-line me-2"></i>Generate Report
|
||||
</a></li>
|
||||
<li><a class="dropdown-item text-danger" href="{% url 'laboratory:specimen_confirm_delete' object.pk %}">
|
||||
<i class="fas fa-trash me-2"></i>Delete Specimen
|
||||
</a></li>
|
||||
{# <li><a class="dropdown-item text-danger" href="{% url 'laboratory:specimen_confirm_delete' object.pk %}">#}
|
||||
{# <i class="fas fa-trash me-2"></i>Delete Specimen#}
|
||||
{# </a></li>#}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -76,19 +77,32 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">MRN:</td>
|
||||
<td>{{ object.patient.medical_record_number }}</td>
|
||||
<td>{{ object.patient.mrn }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">Type:</td>
|
||||
<td>
|
||||
<span class="badge bg-light text-dark">
|
||||
{{ object.get_specimen_type_display }}
|
||||
</span>
|
||||
|
||||
<i class="fas
|
||||
fa-{% if specimen.specimen_type == 'BLOOD' %}tint text-danger
|
||||
{% elif specimen.specimen_type == 'URINE' %}flask text-warning
|
||||
{% elif specimen.specimen_type == 'SPUTUM' %}lungs text-info
|
||||
{% elif specimen.specimen_type == 'PLASMA' %}tint text-yellow
|
||||
{% elif specimen.specimen_type == 'SERUM' %}bottle-droplet text-info
|
||||
{% elif specimen.specimen_type == 'STOOL' %}poop text-cyan
|
||||
{% elif specimen.specimen_type == 'CSF' %}eye text-primary
|
||||
{% elif specimen.specimen_type == 'SWAB' %}hand text-secondary
|
||||
{% elif specimen.specimen_type == 'TISSUE' %}microscope text-green
|
||||
{% elif specimen.specimen_type == 'FLUID' %}droplet text-info
|
||||
{% elif specimen.specimen_type == 'SALIVA' %}lungs text-info
|
||||
{% else %}vial-vertical text-secondary{% endif %}"></i>
|
||||
<span class="fw-bold">{{ specimen.get_specimen_type_display }}</span>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">Collection Date:</td>
|
||||
<td>{{ object.collection_date|date:"M d, Y g:i A" }}</td>
|
||||
<td>{{ object.collected_datetime|date:"M d, Y g:i A" }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@ -113,8 +127,8 @@
|
||||
<tr>
|
||||
<td class="fw-bold">Priority:</td>
|
||||
<td>
|
||||
<span class="badge bg-{% if object.priority == 'urgent' %}danger{% elif object.priority == 'stat' %}warning{% else %}primary{% endif %}">
|
||||
{{ object.get_priority_display }}
|
||||
<span class="badge bg-{% if object.order.priority == 'STAT' %}danger{% elif object.order.priority == 'URGENT' %}warning{% else %}primary{% endif %}">
|
||||
{{ object.order.get_priority_display }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@ -313,7 +327,7 @@
|
||||
<strong>Gender:</strong> {{ object.patient.get_gender_display }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>MRN:</strong> {{ object.patient.medical_record_number }}
|
||||
<strong>MRN:</strong> {{ object.patient.mrn }}
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'patients:patient_detail' object.patient.pk %}" class="btn btn-sm btn-outline-primary">
|
||||
|
||||
@ -49,9 +49,9 @@
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.patient.id_for_label }}" class="form-label">Patient *</label>
|
||||
<select class="form-select {% if form.patient.errors %}is-invalid{% endif %}"
|
||||
id="{{ form.patient.id_for_label }}"
|
||||
name="{{ form.patient.name }}"
|
||||
<select class="form-select {% if form.patient.errors %}is-invalid{% endif %}"
|
||||
id="{{ form.patient.id_for_label }}"
|
||||
name="{{ form.patient.name }}"
|
||||
required>
|
||||
<option value="">Select Patient</option>
|
||||
{% for choice in form.patient.field.choices %}
|
||||
@ -79,7 +79,7 @@
|
||||
<label for="{{ form.specimen_id.id_for_label }}" class="form-label">Specimen ID</label>
|
||||
<div class="input-group">
|
||||
<input type="text"
|
||||
class="form-control {% if form.specimen_id.errors %}is-invalid{% endif %}"
|
||||
class="specimen form-control {% if form.specimen_id.errors %}is-invalid{% endif %}"
|
||||
id="{{ form.specimen_id.id_for_label }}"
|
||||
name="{{ form.specimen_id.name }}"
|
||||
value="{{ form.specimen_id.value|default:'' }}"
|
||||
@ -102,7 +102,7 @@
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.specimen_type.id_for_label }}" class="form-label">Specimen Type *</label>
|
||||
<select class="form-select {% if form.specimen_type.errors %}is-invalid{% endif %}"
|
||||
<select class="form-select {% if form.specimen_type.errors %}is-invalid{% endif %}"
|
||||
id="{{ form.specimen_type.id_for_label }}"
|
||||
name="{{ form.specimen_type.name }}"
|
||||
required>
|
||||
@ -585,7 +585,8 @@ function updateGuidelines(specimenType) {
|
||||
`
|
||||
};
|
||||
|
||||
const content = guidelines[specimenType] || '<p class="text-muted">Select a specimen type to view collection guidelines.</p>';
|
||||
let content
|
||||
content = guidelines[specimenType] || '<p class="text-muted">Select a specimen type to view collection guidelines.</p>';
|
||||
document.getElementById('guidelinesContent').innerHTML = content;
|
||||
}
|
||||
|
||||
@ -615,9 +616,10 @@ function generateSpecimenId() {
|
||||
const month = (now.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = now.getDate().toString().padStart(2, '0');
|
||||
const time = now.getTime().toString().slice(-6);
|
||||
|
||||
const specimenId = `SP${year}${month}${day}${time}`;
|
||||
document.getElementById('{{ form.specimen_id.id_for_label }}').value = specimenId;
|
||||
|
||||
let specimenId;
|
||||
specimenId = `SP${year}${month}${day}${time}`;
|
||||
document.querySelector('.specimen').value = specimenId;
|
||||
}
|
||||
|
||||
function generateBarcode() {
|
||||
|
||||
@ -14,11 +14,12 @@
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'laboratory:specimen_form' %}" class="btn btn-primary">
|
||||
<a href="{% url 'laboratory:specimen_create' %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-2"></i>New Specimen
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown">
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="#" onclick="exportSpecimens()">
|
||||
@ -211,11 +212,11 @@
|
||||
<th>Status</th>
|
||||
<th>Priority</th>
|
||||
<th>Tests</th>
|
||||
<th width="120">Actions</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for specimen in object_list %}
|
||||
{% for specimen in specimens %}
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" class="specimen-checkbox" value="{{ specimen.pk }}">
|
||||
@ -244,20 +245,34 @@
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-{% if specimen.specimen_type == 'blood' %}tint text-danger{% elif specimen.specimen_type == 'urine' %}flask text-warning{% elif specimen.specimen_type == 'sputum' %}lungs text-info{% else %}vial text-secondary{% endif %} me-2"></i>
|
||||
{{ specimen.get_specimen_type_display }}
|
||||
|
||||
<i class="fas
|
||||
fa-{% if specimen.specimen_type == 'BLOOD' %}tint text-danger
|
||||
{% elif specimen.specimen_type == 'URINE' %}flask text-warning
|
||||
{% elif specimen.specimen_type == 'SPUTUM' %}lungs text-info
|
||||
{% elif specimen.specimen_type == 'PLASMA' %}tint text-yellow
|
||||
{% elif specimen.specimen_type == 'SERUM' %}bottle-droplet text-info
|
||||
{% elif specimen.specimen_type == 'STOOL' %}poop text-cyan
|
||||
{% elif specimen.specimen_type == 'CSF' %}eye text-primary
|
||||
{% elif specimen.specimen_type == 'SWAB' %}hand text-secondary
|
||||
{% elif specimen.specimen_type == 'TISSUE' %}microscope text-green
|
||||
{% elif specimen.specimen_type == 'FLUID' %}droplet text-info
|
||||
{% elif specimen.specimen_type == 'SALIVA' %}lungs text-info
|
||||
{% else %}vial-vertical text-secondary{% endif %} me-1"></i>
|
||||
<span class="fw-bold">{{ specimen.get_specimen_type_display }}</span>
|
||||
|
||||
</div>
|
||||
{% if specimen.volume %}
|
||||
<div class="small text-muted">{{ specimen.volume }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold">{{ specimen.collection_date|date:"M d, Y" }}</div>
|
||||
<div class="small text-muted">{{ specimen.collection_date|date:"g:i A" }}</div>
|
||||
<div class="fw-bold">{{ specimen.collected_datetime|date:"M d, Y" }}</div>
|
||||
<div class="small text-muted">{{ specimen.collected_datetime|date:"g:i A" }}</div>
|
||||
</td>
|
||||
<td>{{ specimen.collected_by.get_full_name }}</td>
|
||||
<td>
|
||||
<span class="badge bg-{% if specimen.status == 'collected' %}success{% elif specimen.status == 'processing' %}warning{% elif specimen.status == 'completed' %}info{% elif specimen.status == 'rejected' %}danger{% else %}secondary{% endif %}">
|
||||
<span class="badge bg-{% if specimen.status == 'COLLECTED' %}success{% elif specimen.status == 'PROCESSING' %}warning{% elif specimen.status == 'COMPLETED' %}info{% elif specimen.status == 'REJECTED' %}danger{% else %}secondary{% endif %}">
|
||||
{{ specimen.get_status_display }}
|
||||
</span>
|
||||
{% if specimen.condition and specimen.condition != 'good' %}
|
||||
@ -267,8 +282,8 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-{% if specimen.priority == 'stat' %}danger{% elif specimen.priority == 'urgent' %}warning{% else %}primary{% endif %}">
|
||||
{{ specimen.get_priority_display }}
|
||||
<span class="badge bg-{% if specimen.order.priority == 'STAT' %}danger{% elif specimen.order.priority == 'URGENT' %}warning{% else %}primary{% endif %}">
|
||||
{{ specimen.order.get_priority_display }}
|
||||
</span>
|
||||
{% if specimen.fasting_status %}
|
||||
<div class="small text-info mt-1">
|
||||
@ -290,7 +305,7 @@
|
||||
title="View Details">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{% url 'laboratory:specimen_form' specimen.pk %}"
|
||||
<a href="{% url 'laboratory:specimen_update' specimen.pk %}"
|
||||
class="btn btn-outline-secondary"
|
||||
title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
@ -301,11 +316,6 @@
|
||||
title="Print Label">
|
||||
<i class="fas fa-print"></i>
|
||||
</button>
|
||||
<a href="{% url 'laboratory:specimen_confirm_delete' specimen.pk %}"
|
||||
class="btn btn-outline-danger"
|
||||
title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -316,38 +326,7 @@
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<div class="card-footer">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
Showing {{ page_obj.start_index }} to {{ page_obj.end_index }} of {{ paginator.count }} specimens
|
||||
</div>
|
||||
<nav>
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.specimen_type %}&specimen_type={{ request.GET.specimen_type }}{% endif %}">First</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.specimen_type %}&specimen_type={{ request.GET.specimen_type }}{% endif %}">Previous</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ page_obj.number }}</span>
|
||||
</li>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.specimen_type %}&specimen_type={{ request.GET.specimen_type }}{% endif %}">Next</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ paginator.num_pages }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.specimen_type %}&specimen_type={{ request.GET.specimen_type }}{% endif %}">Last</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'partial/pagination.html' %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Bulk Actions -->
|
||||
|
||||
@ -148,10 +148,10 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-4 col-sm-6">
|
||||
<button type="button" class="btn btn-outline-warning w-100 h-100 d-flex flex-column align-items-center justify-content-center py-3" onclick="checkInteractions()">
|
||||
<a href="{% url 'pharmacy:drug_interaction_list' %}" type="button" class="btn btn-outline-warning w-100 h-100 d-flex flex-column align-items-center justify-content-center py-3">
|
||||
<i class="fas fa-search fa-2x mb-2"></i>
|
||||
<span>Drug Interactions</span>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-4 col-sm-6">
|
||||
<a href="" class="btn btn-outline-secondary w-100 h-100 d-flex flex-column align-items-center justify-content-center py-3">
|
||||
|
||||
@ -515,7 +515,7 @@ function checkInteraction() {
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '{% url "pharmacy:check_interaction" %}',
|
||||
url: '{% url "pharmacy:drug_interaction_check" 0 %}'.replace('0', med1),
|
||||
method: 'POST',
|
||||
data: {
|
||||
'medication1': med1,
|
||||
@ -579,7 +579,7 @@ function viewInteraction(interactionId) {
|
||||
currentInteractionId = interactionId;
|
||||
|
||||
$.ajax({
|
||||
url: '{% url "pharmacy:interaction_detail" %}',
|
||||
url: '{% url "pharmacy:drug_interaction_detail" 0 %}'.replace('0', interactionId),
|
||||
method: 'GET',
|
||||
data: {'interaction_id': interactionId},
|
||||
success: function(response) {
|
||||
@ -592,39 +592,39 @@ function viewInteraction(interactionId) {
|
||||
});
|
||||
}
|
||||
|
||||
function showManagement(interactionId) {
|
||||
$.ajax({
|
||||
url: '{% url "pharmacy:interaction_management" %}',
|
||||
method: 'GET',
|
||||
data: {'interaction_id': interactionId},
|
||||
success: function(response) {
|
||||
toastr.info(response.management, 'Management Recommendation', {
|
||||
timeOut: 10000,
|
||||
extendedTimeOut: 5000
|
||||
});
|
||||
},
|
||||
error: function() {
|
||||
toastr.error('Failed to load management recommendations');
|
||||
}
|
||||
});
|
||||
}
|
||||
{#function showManagement(interactionId) {#}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "pharmacy:interaction_management" %}',#}
|
||||
{# method: 'GET',#}
|
||||
{# data: {'interaction_id': interactionId},#}
|
||||
{# success: function(response) {#}
|
||||
{# toastr.info(response.management, 'Management Recommendation', {#}
|
||||
{# timeOut: 10000,#}
|
||||
{# extendedTimeOut: 5000#}
|
||||
{# });#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# toastr.error('Failed to load management recommendations');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
|
||||
function addToMonitoring(interactionId) {
|
||||
$.ajax({
|
||||
url: '{% url "pharmacy:add_to_monitoring" %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'interaction_id': interactionId,
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
toastr.success('Interaction added to monitoring list');
|
||||
},
|
||||
error: function() {
|
||||
toastr.error('Failed to add interaction to monitoring');
|
||||
}
|
||||
});
|
||||
}
|
||||
{#function addToMonitoring(interactionId) {#}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "pharmacy:add_to_monitoring" %}',#}
|
||||
{# method: 'POST',#}
|
||||
{# data: {#}
|
||||
{# 'interaction_id': interactionId,#}
|
||||
{# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
|
||||
{# },#}
|
||||
{# success: function(response) {#}
|
||||
{# toastr.success('Interaction added to monitoring list');#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# toastr.error('Failed to add interaction to monitoring');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
|
||||
function addCurrentToMonitoring() {
|
||||
if (currentInteractionId) {
|
||||
@ -633,57 +633,57 @@ function addCurrentToMonitoring() {
|
||||
}
|
||||
}
|
||||
|
||||
function addToMonitoringBulk() {
|
||||
if (selectedInteractions.length === 0) {
|
||||
toastr.warning('Please select interactions to add to monitoring');
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '{% url "pharmacy:add_to_monitoring_bulk" %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'interaction_ids': selectedInteractions,
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
toastr.success(selectedInteractions.length + ' interactions added to monitoring');
|
||||
$('.row-checkbox:checked').prop('checked', false);
|
||||
updateSelectedInteractions();
|
||||
},
|
||||
error: function() {
|
||||
toastr.error('Failed to add interactions to monitoring');
|
||||
}
|
||||
});
|
||||
}
|
||||
{#function addToMonitoringBulk() {#}
|
||||
{# if (selectedInteractions.length === 0) {#}
|
||||
{# toastr.warning('Please select interactions to add to monitoring');#}
|
||||
{# return;#}
|
||||
{# }#}
|
||||
{# #}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "pharmacy:add_to_monitoring_bulk" %}',#}
|
||||
{# method: 'POST',#}
|
||||
{# data: {#}
|
||||
{# 'interaction_ids': selectedInteractions,#}
|
||||
{# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
|
||||
{# },#}
|
||||
{# success: function(response) {#}
|
||||
{# toastr.success(selectedInteractions.length + ' interactions added to monitoring');#}
|
||||
{# $('.row-checkbox:checked').prop('checked', false);#}
|
||||
{# updateSelectedInteractions();#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# toastr.error('Failed to add interactions to monitoring');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
|
||||
function checkPatientInteractions() {
|
||||
$('#patientCheckModal').modal('show');
|
||||
}
|
||||
|
||||
function checkPatientMedications() {
|
||||
var patientId = $('#patient-select').val();
|
||||
|
||||
if (!patientId) {
|
||||
toastr.warning('Please select a patient');
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '{% url "pharmacy:check_patient_interactions" %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'patient_id': patientId,
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
$('#patient-interaction-results').html(response.html);
|
||||
},
|
||||
error: function() {
|
||||
toastr.error('Failed to check patient interactions');
|
||||
}
|
||||
});
|
||||
}
|
||||
{#function checkPatientMedications() {#}
|
||||
{# var patientId = $('#patient-select').val();#}
|
||||
{# #}
|
||||
{# if (!patientId) {#}
|
||||
{# toastr.warning('Please select a patient');#}
|
||||
{# return;#}
|
||||
{# }#}
|
||||
{# #}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "pharmacy:check_patient_interactions" %}',#}
|
||||
{# method: 'POST',#}
|
||||
{# data: {#}
|
||||
{# 'patient_id': patientId,#}
|
||||
{# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
|
||||
{# },#}
|
||||
{# success: function(response) {#}
|
||||
{# $('#patient-interaction-results').html(response.html);#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# toastr.error('Failed to check patient interactions');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
|
||||
function bulkInteractionCheck() {
|
||||
// Implementation for bulk interaction checking
|
||||
@ -696,35 +696,35 @@ function exportInteractions() {
|
||||
window.open('?' + params.toString(), '_blank');
|
||||
}
|
||||
|
||||
function exportSelectedInteractions() {
|
||||
if (selectedInteractions.length === 0) {
|
||||
toastr.warning('Please select interactions to export');
|
||||
return;
|
||||
}
|
||||
|
||||
var form = $('<form>', {
|
||||
method: 'POST',
|
||||
action: '{% url "pharmacy:export_interactions" %}'
|
||||
});
|
||||
|
||||
form.append($('<input>', {
|
||||
type: 'hidden',
|
||||
name: 'csrfmiddlewaretoken',
|
||||
value: '{{ csrf_token }}'
|
||||
}));
|
||||
|
||||
selectedInteractions.forEach(function(id) {
|
||||
form.append($('<input>', {
|
||||
type: 'hidden',
|
||||
name: 'interaction_ids',
|
||||
value: id
|
||||
}));
|
||||
});
|
||||
|
||||
$('body').append(form);
|
||||
form.submit();
|
||||
form.remove();
|
||||
}
|
||||
{#function exportSelectedInteractions() {#}
|
||||
{# if (selectedInteractions.length === 0) {#}
|
||||
{# toastr.warning('Please select interactions to export');#}
|
||||
{# return;#}
|
||||
{# }#}
|
||||
{# #}
|
||||
{# var form = $('<form>', {#}
|
||||
{# method: 'POST',#}
|
||||
{# action: '{% url "pharmacy:export_interactions" %}'#}
|
||||
{# });#}
|
||||
{# #}
|
||||
{# form.append($('<input>', {#}
|
||||
{# type: 'hidden',#}
|
||||
{# name: 'csrfmiddlewaretoken',#}
|
||||
{# value: '{{ csrf_token }}'#}
|
||||
{# }));#}
|
||||
{# #}
|
||||
{# selectedInteractions.forEach(function(id) {#}
|
||||
{# form.append($('<input>', {#}
|
||||
{# type: 'hidden',#}
|
||||
{# name: 'interaction_ids',#}
|
||||
{# value: id#}
|
||||
{# }));#}
|
||||
{# });#}
|
||||
{# #}
|
||||
{# $('body').append(form);#}
|
||||
{# form.submit();#}
|
||||
{# form.remove();#}
|
||||
{# }#}
|
||||
|
||||
function generateReport() {
|
||||
if (selectedInteractions.length === 0) {
|
||||
@ -746,13 +746,13 @@ function createAlert() {
|
||||
toastr.info('Alert creation feature coming soon');
|
||||
}
|
||||
|
||||
function printPatientReport() {
|
||||
var patientId = $('#patient-select').val();
|
||||
if (patientId) {
|
||||
var printUrl = '{% url "pharmacy:print_patient_interactions" %}?patient_id=' + patientId;
|
||||
window.open(printUrl, '_blank');
|
||||
}
|
||||
}
|
||||
{#function printPatientReport() {#}
|
||||
{# var patientId = $('#patient-select').val();#}
|
||||
{# if (patientId) {#}
|
||||
{# var printUrl = '{% url "pharmacy:print_patient_interactions" %}?patient_id=' + patientId;#}
|
||||
{# window.open(printUrl, '_blank');#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
|
||||
function clearFilters() {
|
||||
$('#filter-form')[0].reset();
|
||||
|
||||
@ -474,7 +474,7 @@ function initializeFormBehavior() {
|
||||
|
||||
function loadMedicationInfo(medicationId) {
|
||||
$.ajax({
|
||||
url: '{% url "pharmacy:medication_info" %}',
|
||||
url: '{% url "pharmacy:get_medication_info" 0 %}'.replace('0', medicationId),
|
||||
method: 'GET',
|
||||
data: {'medication_id': medicationId},
|
||||
success: function(response) {
|
||||
@ -641,50 +641,50 @@ function calculateReorderLevel() {
|
||||
validateStockLevels();
|
||||
}
|
||||
|
||||
function checkDuplicates() {
|
||||
var medicationId = $('#{{ form.medication.id_for_label }}').val();
|
||||
var locationId = $('#{{ form.location.id_for_label }}').val();
|
||||
|
||||
if (!medicationId || !locationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '{% url "pharmacy:check_inventory_duplicates" %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'medication_id': medicationId,
|
||||
'location_id': locationId,
|
||||
{% if object %}'exclude_id': {{ object.pk }},{% endif %}
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.exists) {
|
||||
toastr.warning('Inventory record already exists for this medication at this location');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
toastr.error('Failed to check for duplicates');
|
||||
}
|
||||
});
|
||||
}
|
||||
{#function checkDuplicates() {#}
|
||||
{# var medicationId = $('#{{ form.medication.id_for_label }}').val();#}
|
||||
{# var locationId = $('#{{ form.location.id_for_label }}').val();#}
|
||||
{# #}
|
||||
{# if (!medicationId || !locationId) {#}
|
||||
{# return;#}
|
||||
{# }#}
|
||||
{# #}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "pharmacy:check_inventory_duplicates" %}',#}
|
||||
{# method: 'POST',#}
|
||||
{# data: {#}
|
||||
{# 'medication_id': medicationId,#}
|
||||
{# 'location_id': locationId,#}
|
||||
{# {% if object %}'exclude_id': {{ object.pk }},{% endif %}#}
|
||||
{# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
|
||||
{# },#}
|
||||
{# success: function(response) {#}
|
||||
{# if (response.exists) {#}
|
||||
{# toastr.warning('Inventory record already exists for this medication at this location');#}
|
||||
{# }#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# toastr.error('Failed to check for duplicates');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
|
||||
function saveDraft() {
|
||||
var formData = $('#inventory-form').serialize();
|
||||
|
||||
$.ajax({
|
||||
url: '{% url "pharmacy:save_inventory_draft" %}',
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
success: function(response) {
|
||||
updateFormStatus('Draft saved', 'success');
|
||||
$('#last-saved').text(new Date().toLocaleTimeString());
|
||||
},
|
||||
error: function() {
|
||||
updateFormStatus('Failed to save draft', 'danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
{#function saveDraft() {#}
|
||||
{# var formData = $('#inventory-form').serialize();#}
|
||||
{# #}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "pharmacy:save_inventory_draft" %}',#}
|
||||
{# method: 'POST',#}
|
||||
{# data: formData,#}
|
||||
{# success: function(response) {#}
|
||||
{# updateFormStatus('Draft saved', 'success');#}
|
||||
{# $('#last-saved').text(new Date().toLocaleTimeString());#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# updateFormStatus('Failed to save draft', 'danger');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
|
||||
function updateFormStatus(message, type) {
|
||||
var alertClass = 'alert-' + type;
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Medication Information</h4>
|
||||
<div class="panel-heading-btn">
|
||||
<a href="{% url 'pharmacy:medication_edit' object.pk %}" class="btn btn-xs btn-primary me-2">
|
||||
<a href="{% url 'pharmacy:medication_update' object.pk %}" class="btn btn-xs btn-primary me-2">
|
||||
<i class="fa fa-edit"></i> Edit
|
||||
</a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||
@ -256,7 +256,7 @@
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="d-grid gap-2">
|
||||
<a href="{% url 'pharmacy:medication_edit' object.pk %}" class="btn btn-primary">
|
||||
<a href="{% url 'pharmacy:medication_update' object.pk %}" class="btn btn-primary">
|
||||
<i class="fa fa-edit me-2"></i>Edit Medication
|
||||
</a>
|
||||
<a href="{% url 'pharmacy:prescription_create' %}?medication={{ object.pk }}" class="btn btn-success">
|
||||
@ -268,9 +268,9 @@
|
||||
<button type="button" class="btn btn-warning" onclick="checkInteractions()">
|
||||
<i class="fa fa-exclamation-triangle me-2"></i>Check Interactions
|
||||
</button>
|
||||
<a href="{% url 'pharmacy:medication_print' object.pk %}" class="btn btn-secondary" target="_blank">
|
||||
<i class="fa fa-print me-2"></i>Print Information
|
||||
</a>
|
||||
{# <a href="{% url 'pharmacy:medication_print' object.pk %}" class="btn btn-secondary" target="_blank">#}
|
||||
{# <i class="fa fa-print me-2"></i>Print Information#}
|
||||
{# </a>#}
|
||||
{% if perms.pharmacy.delete_medication %}
|
||||
<a href="{% url 'pharmacy:medication_delete' object.pk %}" class="btn btn-danger">
|
||||
<i class="fa fa-trash me-2"></i>Delete Medication
|
||||
@ -366,154 +366,154 @@ $(document).ready(function() {
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
function loadInventoryStatus() {
|
||||
$.ajax({
|
||||
url: '{% url "pharmacy:medication_inventory_status" object.pk %}',
|
||||
method: 'GET',
|
||||
success: function(response) {
|
||||
var html = '';
|
||||
|
||||
if (response.inventory_items && response.inventory_items.length > 0) {
|
||||
response.inventory_items.forEach(function(item) {
|
||||
var statusClass = 'success';
|
||||
var statusIcon = 'check';
|
||||
|
||||
if (item.quantity <= item.reorder_level) {
|
||||
statusClass = 'warning';
|
||||
statusIcon = 'exclamation-triangle';
|
||||
}
|
||||
if (item.quantity <= 0) {
|
||||
statusClass = 'danger';
|
||||
statusIcon = 'times';
|
||||
}
|
||||
|
||||
html += '<div class="d-flex justify-content-between align-items-center mb-2">';
|
||||
html += '<div>';
|
||||
html += '<div class="fw-bold">' + item.location_name + '</div>';
|
||||
html += '<div class="small text-muted">Lot: ' + (item.lot_number || 'N/A') + '</div>';
|
||||
html += '</div>';
|
||||
html += '<div class="text-end">';
|
||||
html += '<div class="badge bg-' + statusClass + '">';
|
||||
html += '<i class="fa fa-' + statusIcon + ' me-1"></i>' + item.quantity + ' ' + item.unit;
|
||||
html += '</div>';
|
||||
if (item.expiry_date) {
|
||||
html += '<div class="small text-muted">Exp: ' + item.expiry_date + '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
// Total quantity
|
||||
html += '<hr>';
|
||||
html += '<div class="d-flex justify-content-between align-items-center">';
|
||||
html += '<div class="fw-bold">Total Quantity</div>';
|
||||
html += '<div class="fw-bold">' + response.total_quantity + ' units</div>';
|
||||
html += '</div>';
|
||||
} else {
|
||||
html = '<div class="text-center text-muted">';
|
||||
html += '<i class="fa fa-box-open fa-2x mb-2"></i>';
|
||||
html += '<div>No inventory records found</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
$('#inventory-status').html(html);
|
||||
},
|
||||
error: function() {
|
||||
$('#inventory-status').html('<div class="text-danger text-center">Failed to load inventory status</div>');
|
||||
}
|
||||
});
|
||||
}
|
||||
{#function loadInventoryStatus() {#}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "pharmacy:medication_inventory_status" object.pk %}',#}
|
||||
{# method: 'GET',#}
|
||||
{# success: function(response) {#}
|
||||
{# var html = '';#}
|
||||
{# #}
|
||||
{# if (response.inventory_items && response.inventory_items.length > 0) {#}
|
||||
{# response.inventory_items.forEach(function(item) {#}
|
||||
{# var statusClass = 'success';#}
|
||||
{# var statusIcon = 'check';#}
|
||||
{# #}
|
||||
{# if (item.quantity <= item.reorder_level) {#}
|
||||
{# statusClass = 'warning';#}
|
||||
{# statusIcon = 'exclamation-triangle';#}
|
||||
{# }#}
|
||||
{# if (item.quantity <= 0) {#}
|
||||
{# statusClass = 'danger';#}
|
||||
{# statusIcon = 'times';#}
|
||||
{# }#}
|
||||
{# #}
|
||||
{# html += '<div class="d-flex justify-content-between align-items-center mb-2">';#}
|
||||
{# html += '<div>';#}
|
||||
{# html += '<div class="fw-bold">' + item.location_name + '</div>';#}
|
||||
{# html += '<div class="small text-muted">Lot: ' + (item.lot_number || 'N/A') + '</div>';#}
|
||||
{# html += '</div>';#}
|
||||
{# html += '<div class="text-end">';#}
|
||||
{# html += '<div class="badge bg-' + statusClass + '">';#}
|
||||
{# html += '<i class="fa fa-' + statusIcon + ' me-1"></i>' + item.quantity + ' ' + item.unit;#}
|
||||
{# html += '</div>';#}
|
||||
{# if (item.expiry_date) {#}
|
||||
{# html += '<div class="small text-muted">Exp: ' + item.expiry_date + '</div>';#}
|
||||
{# }#}
|
||||
{# html += '</div>';#}
|
||||
{# html += '</div>';#}
|
||||
{# });#}
|
||||
{# #}
|
||||
{# // Total quantity#}
|
||||
{# html += '<hr>';#}
|
||||
{# html += '<div class="d-flex justify-content-between align-items-center">';#}
|
||||
{# html += '<div class="fw-bold">Total Quantity</div>';#}
|
||||
{# html += '<div class="fw-bold">' + response.total_quantity + ' units</div>';#}
|
||||
{# html += '</div>';#}
|
||||
{# } else {#}
|
||||
{# html = '<div class="text-center text-muted">';#}
|
||||
{# html += '<i class="fa fa-box-open fa-2x mb-2"></i>';#}
|
||||
{# html += '<div>No inventory records found</div>';#}
|
||||
{# html += '</div>';#}
|
||||
{# }#}
|
||||
{# #}
|
||||
{# $('#inventory-status').html(html);#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# $('#inventory-status').html('<div class="text-danger text-center">Failed to load inventory status</div>');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
|
||||
function loadPrescriptionStats() {
|
||||
$.ajax({
|
||||
url: '{% url "pharmacy:medication_prescription_stats" object.pk %}',
|
||||
method: 'GET',
|
||||
success: function(response) {
|
||||
var html = '';
|
||||
|
||||
html += '<div class="row text-center">';
|
||||
html += '<div class="col-6">';
|
||||
html += '<div class="h4 mb-0">' + response.total_prescriptions + '</div>';
|
||||
html += '<div class="small text-muted">Total Prescriptions</div>';
|
||||
html += '</div>';
|
||||
html += '<div class="col-6">';
|
||||
html += '<div class="h4 mb-0">' + response.active_prescriptions + '</div>';
|
||||
html += '<div class="small text-muted">Active</div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<hr>';
|
||||
|
||||
html += '<div class="row text-center">';
|
||||
html += '<div class="col-6">';
|
||||
html += '<div class="h5 mb-0">' + response.this_month + '</div>';
|
||||
html += '<div class="small text-muted">This Month</div>';
|
||||
html += '</div>';
|
||||
html += '<div class="col-6">';
|
||||
html += '<div class="h5 mb-0">' + response.last_30_days + '</div>';
|
||||
html += '<div class="small text-muted">Last 30 Days</div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
if (response.recent_prescriptions && response.recent_prescriptions.length > 0) {
|
||||
html += '<hr>';
|
||||
html += '<div class="fw-bold mb-2">Recent Prescriptions</div>';
|
||||
response.recent_prescriptions.forEach(function(prescription) {
|
||||
html += '<div class="d-flex justify-content-between align-items-center mb-1">';
|
||||
html += '<div class="small">' + prescription.patient_name + '</div>';
|
||||
html += '<div class="small text-muted">' + prescription.date + '</div>';
|
||||
html += '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
$('#prescription-stats').html(html);
|
||||
},
|
||||
error: function() {
|
||||
$('#prescription-stats').html('<div class="text-danger text-center">Failed to load statistics</div>');
|
||||
}
|
||||
});
|
||||
}
|
||||
{#function loadPrescriptionStats() {#}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "pharmacy:medication_prescription_stats" object.pk %}',#}
|
||||
{# method: 'GET',#}
|
||||
{# success: function(response) {#}
|
||||
{# var html = '';#}
|
||||
{# #}
|
||||
{# html += '<div class="row text-center">';#}
|
||||
{# html += '<div class="col-6">';#}
|
||||
{# html += '<div class="h4 mb-0">' + response.total_prescriptions + '</div>';#}
|
||||
{# html += '<div class="small text-muted">Total Prescriptions</div>';#}
|
||||
{# html += '</div>';#}
|
||||
{# html += '<div class="col-6">';#}
|
||||
{# html += '<div class="h4 mb-0">' + response.active_prescriptions + '</div>';#}
|
||||
{# html += '<div class="small text-muted">Active</div>';#}
|
||||
{# html += '</div>';#}
|
||||
{# html += '</div>';#}
|
||||
{# #}
|
||||
{# html += '<hr>';#}
|
||||
{# #}
|
||||
{# html += '<div class="row text-center">';#}
|
||||
{# html += '<div class="col-6">';#}
|
||||
{# html += '<div class="h5 mb-0">' + response.this_month + '</div>';#}
|
||||
{# html += '<div class="small text-muted">This Month</div>';#}
|
||||
{# html += '</div>';#}
|
||||
{# html += '<div class="col-6">';#}
|
||||
{# html += '<div class="h5 mb-0">' + response.last_30_days + '</div>';#}
|
||||
{# html += '<div class="small text-muted">Last 30 Days</div>';#}
|
||||
{# html += '</div>';#}
|
||||
{# html += '</div>';#}
|
||||
{# #}
|
||||
{# if (response.recent_prescriptions && response.recent_prescriptions.length > 0) {#}
|
||||
{# html += '<hr>';#}
|
||||
{# html += '<div class="fw-bold mb-2">Recent Prescriptions</div>';#}
|
||||
{# response.recent_prescriptions.forEach(function(prescription) {#}
|
||||
{# html += '<div class="d-flex justify-content-between align-items-center mb-1">';#}
|
||||
{# html += '<div class="small">' + prescription.patient_name + '</div>';#}
|
||||
{# html += '<div class="small text-muted">' + prescription.date + '</div>';#}
|
||||
{# html += '</div>';#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
{# #}
|
||||
{# $('#prescription-stats').html(html);#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# $('#prescription-stats').html('<div class="text-danger text-center">Failed to load statistics</div>');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
|
||||
function checkInteractions() {
|
||||
$.ajax({
|
||||
url: '{% url "pharmacy:medication_interactions" object.pk %}',
|
||||
method: 'GET',
|
||||
success: function(response) {
|
||||
var html = '';
|
||||
|
||||
if (response.interactions && response.interactions.length > 0) {
|
||||
html += '<div class="alert alert-warning">';
|
||||
html += '<h6 class="alert-heading">Known Drug Interactions</h6>';
|
||||
html += '<ul class="mb-0">';
|
||||
response.interactions.forEach(function(interaction) {
|
||||
html += '<li><strong>' + interaction.drug_name + '</strong>: ' + interaction.description + '</li>';
|
||||
});
|
||||
html += '</ul>';
|
||||
html += '</div>';
|
||||
} else {
|
||||
html += '<div class="alert alert-success">';
|
||||
html += '<i class="fa fa-check me-2"></i>No known drug interactions found in the database.';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (response.contraindications && response.contraindications.length > 0) {
|
||||
html += '<div class="alert alert-danger">';
|
||||
html += '<h6 class="alert-heading">Contraindications</h6>';
|
||||
html += '<ul class="mb-0">';
|
||||
response.contraindications.forEach(function(contraindication) {
|
||||
html += '<li>' + contraindication + '</li>';
|
||||
});
|
||||
html += '</ul>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
$('#interactions-content').html(html);
|
||||
},
|
||||
error: function() {
|
||||
$('#interactions-content').html('<div class="text-danger text-center">Failed to load interaction data</div>');
|
||||
}
|
||||
});
|
||||
}
|
||||
{#function checkInteractions() {#}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "pharmacy:medication_interactions" object.pk %}',#}
|
||||
{# method: 'GET',#}
|
||||
{# success: function(response) {#}
|
||||
{# var html = '';#}
|
||||
{# #}
|
||||
{# if (response.interactions && response.interactions.length > 0) {#}
|
||||
{# html += '<div class="alert alert-warning">';#}
|
||||
{# html += '<h6 class="alert-heading">Known Drug Interactions</h6>';#}
|
||||
{# html += '<ul class="mb-0">';#}
|
||||
{# response.interactions.forEach(function(interaction) {#}
|
||||
{# html += '<li><strong>' + interaction.drug_name + '</strong>: ' + interaction.description + '</li>';#}
|
||||
{# });#}
|
||||
{# html += '</ul>';#}
|
||||
{# html += '</div>';#}
|
||||
{# } else {#}
|
||||
{# html += '<div class="alert alert-success">';#}
|
||||
{# html += '<i class="fa fa-check me-2"></i>No known drug interactions found in the database.';#}
|
||||
{# html += '</div>';#}
|
||||
{# }#}
|
||||
{# #}
|
||||
{# if (response.contraindications && response.contraindications.length > 0) {#}
|
||||
{# html += '<div class="alert alert-danger">';#}
|
||||
{# html += '<h6 class="alert-heading">Contraindications</h6>';#}
|
||||
{# html += '<ul class="mb-0">';#}
|
||||
{# response.contraindications.forEach(function(contraindication) {#}
|
||||
{# html += '<li>' + contraindication + '</li>';#}
|
||||
{# });#}
|
||||
{# html += '</ul>';#}
|
||||
{# html += '</div>';#}
|
||||
{# }#}
|
||||
{# #}
|
||||
{# $('#interactions-content').html(html);#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# $('#interactions-content').html('<div class="text-danger text-center">Failed to load interaction data</div>');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
|
||||
function viewInventory() {
|
||||
window.location.href = '{% url "pharmacy:inventory_list" %}?medication=' + {{ object.pk }};
|
||||
|
||||
@ -614,41 +614,41 @@ function validateForm() {
|
||||
return isValid;
|
||||
}
|
||||
|
||||
function validateNDC() {
|
||||
var ndcNumber = $('#{{ form.ndc_number.id_for_label }}').val();
|
||||
|
||||
if (!ndcNumber) {
|
||||
$('#ndc-check').html('<i class="fa fa-times text-danger me-2"></i>NDC number not provided');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isValidNDC(ndcNumber)) {
|
||||
$('#ndc-check').html('<i class="fa fa-check text-success me-2"></i>NDC format valid');
|
||||
|
||||
// Check NDC in database
|
||||
$.ajax({
|
||||
url: '{% url "pharmacy:validate_ndc" %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'ndc_number': ndcNumber,
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.exists) {
|
||||
toastr.warning('NDC number already exists in database');
|
||||
} else {
|
||||
toastr.success('NDC number is available');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
toastr.error('Failed to validate NDC number');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$('#ndc-check').html('<i class="fa fa-times text-danger me-2"></i>Invalid NDC format');
|
||||
toastr.error('NDC number format is invalid. Use format: 12345-678-90');
|
||||
}
|
||||
}
|
||||
{#function validateNDC() {#}
|
||||
{# var ndcNumber = $('#{{ form.ndc_number.id_for_label }}').val();#}
|
||||
{# #}
|
||||
{# if (!ndcNumber) {#}
|
||||
{# $('#ndc-check').html('<i class="fa fa-times text-danger me-2"></i>NDC number not provided');#}
|
||||
{# return;#}
|
||||
{# }#}
|
||||
{# #}
|
||||
{# if (isValidNDC(ndcNumber)) {#}
|
||||
{# $('#ndc-check').html('<i class="fa fa-check text-success me-2"></i>NDC format valid');#}
|
||||
{# #}
|
||||
{# // Check NDC in database#}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "pharmacy:validate_ndc" %}',#}
|
||||
{# method: 'POST',#}
|
||||
{# data: {#}
|
||||
{# 'ndc_number': ndcNumber,#}
|
||||
{# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
|
||||
{# },#}
|
||||
{# success: function(response) {#}
|
||||
{# if (response.exists) {#}
|
||||
{# toastr.warning('NDC number already exists in database');#}
|
||||
{# } else {#}
|
||||
{# toastr.success('NDC number is available');#}
|
||||
{# }#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# toastr.error('Failed to validate NDC number');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# } else {#}
|
||||
{# $('#ndc-check').html('<i class="fa fa-times text-danger me-2"></i>Invalid NDC format');#}
|
||||
{# toastr.error('NDC number format is invalid. Use format: 12345-678-90');#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
|
||||
function isValidNDC(ndc) {
|
||||
// NDC format: 5-4-2 or 5-3-2 digits with hyphens
|
||||
@ -682,54 +682,54 @@ function validateStrength() {
|
||||
}
|
||||
}
|
||||
|
||||
function checkDuplicates() {
|
||||
var genericName = $('#{{ form.generic_name.id_for_label }}').val();
|
||||
var strength = $('#{{ form.strength.id_for_label }}').val();
|
||||
|
||||
if (!genericName) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '{% url "pharmacy:check_medication_duplicates" %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'generic_name': genericName,
|
||||
'strength': strength,
|
||||
{% if object %}'exclude_id': {{ object.pk }},{% endif %}
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.duplicates.length > 0) {
|
||||
var message = 'Similar medications found:\n';
|
||||
response.duplicates.forEach(function(med) {
|
||||
message += '- ' + med.name + ' (' + med.strength + ')\n';
|
||||
});
|
||||
toastr.warning(message);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
toastr.error('Failed to check for duplicates');
|
||||
}
|
||||
});
|
||||
}
|
||||
{#function checkDuplicates() {#}
|
||||
{# var genericName = $('#{{ form.generic_name.id_for_label }}').val();#}
|
||||
{# var strength = $('#{{ form.strength.id_for_label }}').val();#}
|
||||
{# #}
|
||||
{# if (!genericName) {#}
|
||||
{# return;#}
|
||||
{# }#}
|
||||
{# #}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "pharmacy:check_medication_duplicates" %}',#}
|
||||
{# method: 'POST',#}
|
||||
{# data: {#}
|
||||
{# 'generic_name': genericName,#}
|
||||
{# 'strength': strength,#}
|
||||
{# {% if object %}'exclude_id': {{ object.pk }},{% endif %}#}
|
||||
{# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
|
||||
{# },#}
|
||||
{# success: function(response) {#}
|
||||
{# if (response.duplicates.length > 0) {#}
|
||||
{# var message = 'Similar medications found:\n';#}
|
||||
{# response.duplicates.forEach(function(med) {#}
|
||||
{# message += '- ' + med.name + ' (' + med.strength + ')\n';#}
|
||||
{# });#}
|
||||
{# toastr.warning(message);#}
|
||||
{# }#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# toastr.error('Failed to check for duplicates');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
|
||||
function saveDraft() {
|
||||
var formData = $('#medication-form').serialize();
|
||||
|
||||
$.ajax({
|
||||
url: '{% url "pharmacy:save_medication_draft" %}',
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
success: function(response) {
|
||||
updateFormStatus('Draft saved', 'success');
|
||||
$('#last-saved').text(new Date().toLocaleTimeString());
|
||||
},
|
||||
error: function() {
|
||||
updateFormStatus('Failed to save draft', 'danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
{#function saveDraft() {#}
|
||||
{# var formData = $('#medication-form').serialize();#}
|
||||
{# #}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "pharmacy:save_medication_draft" %}',#}
|
||||
{# method: 'POST',#}
|
||||
{# data: formData,#}
|
||||
{# success: function(response) {#}
|
||||
{# updateFormStatus('Draft saved', 'success');#}
|
||||
{# $('#last-saved').text(new Date().toLocaleTimeString());#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# updateFormStatus('Failed to save draft', 'danger');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
|
||||
function updateFormStatus(message, type) {
|
||||
var alertClass = 'alert-' + type;
|
||||
|
||||
@ -112,66 +112,66 @@
|
||||
<form method="get" class="row g-3" id="filterForm">
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<label for="search" class="form-label">Search Medications</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="search"
|
||||
name="search"
|
||||
value="{{ request.GET.search }}"
|
||||
placeholder="Generic name, brand name, NDC..."
|
||||
hx-get="{% url 'pharmacy:medication_search' %}"
|
||||
hx-target="#medication-list-container"
|
||||
hx-trigger="keyup changed delay:500ms"
|
||||
hx-include="#filterForm">
|
||||
</div>
|
||||
{# <div class="input-group">#}
|
||||
{# <span class="input-group-text"><i class="fas fa-search"></i></span>#}
|
||||
{# <input type="text" #}
|
||||
{# class="form-control" #}
|
||||
{# id="search" #}
|
||||
{# name="search" #}
|
||||
{# value="{{ request.GET.search }}"#}
|
||||
{# placeholder="Generic name, brand name, NDC..."#}
|
||||
{# hx-get="{% url 'pharmacy:medication_search' %}"#}
|
||||
{# hx-target="#medication-list-container"#}
|
||||
{# hx-trigger="keyup changed delay:500ms"#}
|
||||
{# hx-include="#filterForm">#}
|
||||
{# </div>#}
|
||||
</div>
|
||||
|
||||
<div class="col-lg-2 col-md-6">
|
||||
<label for="drug_class" class="form-label">Drug Class</label>
|
||||
<select class="form-select" id="drug_class" name="drug_class"
|
||||
hx-get="{% url 'pharmacy:medication_search' %}"
|
||||
hx-target="#medication-list-container"
|
||||
hx-trigger="change"
|
||||
hx-include="#filterForm">
|
||||
<option value="">All Classes</option>
|
||||
{% for class in drug_classes %}
|
||||
<option value="{{ class }}" {% if request.GET.drug_class == class %}selected{% endif %}>{{ class }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{# <div class="col-lg-2 col-md-6">#}
|
||||
{# <label for="drug_class" class="form-label">Drug Class</label>#}
|
||||
{# <select class="form-select" id="drug_class" name="drug_class"#}
|
||||
{# hx-get="{% url 'pharmacy:medication_search' %}"#}
|
||||
{# hx-target="#medication-list-container"#}
|
||||
{# hx-trigger="change"#}
|
||||
{# hx-include="#filterForm">#}
|
||||
{# <option value="">All Classes</option>#}
|
||||
{# {% for class in drug_classes %}#}
|
||||
{# <option value="{{ class }}" {% if request.GET.drug_class == class %}selected{% endif %}>{{ class }}</option>#}
|
||||
{# {% endfor %}#}
|
||||
{# </select>#}
|
||||
{# </div>#}
|
||||
|
||||
<div class="col-lg-2 col-md-6">
|
||||
<label for="controlled_schedule" class="form-label">Schedule</label>
|
||||
<select class="form-select" id="controlled_schedule" name="controlled_schedule"
|
||||
hx-get="{% url 'pharmacy:medication_search' %}"
|
||||
hx-target="#medication-list-container"
|
||||
hx-trigger="change"
|
||||
hx-include="#filterForm">
|
||||
<option value="">All Schedules</option>
|
||||
<option value="NON" {% if request.GET.controlled_schedule == 'NON' %}selected{% endif %}>Non-Controlled</option>
|
||||
<option value="CI" {% if request.GET.controlled_schedule == 'CI' %}selected{% endif %}>Schedule I</option>
|
||||
<option value="CII" {% if request.GET.controlled_schedule == 'CII' %}selected{% endif %}>Schedule II</option>
|
||||
<option value="CIII" {% if request.GET.controlled_schedule == 'CIII' %}selected{% endif %}>Schedule III</option>
|
||||
<option value="CIV" {% if request.GET.controlled_schedule == 'CIV' %}selected{% endif %}>Schedule IV</option>
|
||||
<option value="CV" {% if request.GET.controlled_schedule == 'CV' %}selected{% endif %}>Schedule V</option>
|
||||
</select>
|
||||
</div>
|
||||
{# <div class="col-lg-2 col-md-6">#}
|
||||
{# <label for="controlled_schedule" class="form-label">Schedule</label>#}
|
||||
{# <select class="form-select" id="controlled_schedule" name="controlled_schedule"#}
|
||||
{# hx-get="{% url 'pharmacy:medication_search' %}"#}
|
||||
{# hx-target="#medication-list-container"#}
|
||||
{# hx-trigger="change"#}
|
||||
{# hx-include="#filterForm">#}
|
||||
{# <option value="">All Schedules</option>#}
|
||||
{# <option value="NON" {% if request.GET.controlled_schedule == 'NON' %}selected{% endif %}>Non-Controlled</option>#}
|
||||
{# <option value="CI" {% if request.GET.controlled_schedule == 'CI' %}selected{% endif %}>Schedule I</option>#}
|
||||
{# <option value="CII" {% if request.GET.controlled_schedule == 'CII' %}selected{% endif %}>Schedule II</option>#}
|
||||
{# <option value="CIII" {% if request.GET.controlled_schedule == 'CIII' %}selected{% endif %}>Schedule III</option>#}
|
||||
{# <option value="CIV" {% if request.GET.controlled_schedule == 'CIV' %}selected{% endif %}>Schedule IV</option>#}
|
||||
{# <option value="CV" {% if request.GET.controlled_schedule == 'CV' %}selected{% endif %}>Schedule V</option>#}
|
||||
{# </select>#}
|
||||
{# </div>#}
|
||||
|
||||
<div class="col-lg-2 col-md-6">
|
||||
<label for="formulary_status" class="form-label">Formulary Status</label>
|
||||
<select class="form-select" id="formulary_status" name="formulary_status"
|
||||
hx-get="{% url 'pharmacy:medication_search' %}"
|
||||
hx-target="#medication-list-container"
|
||||
hx-trigger="change"
|
||||
hx-include="#filterForm">
|
||||
<option value="">All Status</option>
|
||||
<option value="PREFERRED" {% if request.GET.formulary_status == 'PREFERRED' %}selected{% endif %}>Preferred</option>
|
||||
<option value="NON_PREFERRED" {% if request.GET.formulary_status == 'NON_PREFERRED' %}selected{% endif %}>Non-Preferred</option>
|
||||
<option value="RESTRICTED" {% if request.GET.formulary_status == 'RESTRICTED' %}selected{% endif %}>Restricted</option>
|
||||
<option value="PRIOR_AUTH" {% if request.GET.formulary_status == 'PRIOR_AUTH' %}selected{% endif %}>Prior Auth Required</option>
|
||||
</select>
|
||||
</div>
|
||||
{# <div class="col-lg-2 col-md-6">#}
|
||||
{# <label for="formulary_status" class="form-label">Formulary Status</label>#}
|
||||
{# <select class="form-select" id="formulary_status" name="formulary_status"#}
|
||||
{# hx-get="{% url 'pharmacy:medication_search' %}"#}
|
||||
{# hx-target="#medication-list-container"#}
|
||||
{# hx-trigger="change"#}
|
||||
{# hx-include="#filterForm">#}
|
||||
{# <option value="">All Status</option>#}
|
||||
{# <option value="PREFERRED" {% if request.GET.formulary_status == 'PREFERRED' %}selected{% endif %}>Preferred</option>#}
|
||||
{# <option value="NON_PREFERRED" {% if request.GET.formulary_status == 'NON_PREFERRED' %}selected{% endif %}>Non-Preferred</option>#}
|
||||
{# <option value="RESTRICTED" {% if request.GET.formulary_status == 'RESTRICTED' %}selected{% endif %}>Restricted</option>#}
|
||||
{# <option value="PRIOR_AUTH" {% if request.GET.formulary_status == 'PRIOR_AUTH' %}selected{% endif %}>Prior Auth Required</option>#}
|
||||
{# </select>#}
|
||||
{# </div>#}
|
||||
|
||||
<div class="col-lg-2 col-md-6">
|
||||
<label class="form-label"> </label>
|
||||
@ -310,7 +310,7 @@
|
||||
<a href="{% url 'pharmacy:medication_detail' medication.pk %}" class="btn btn-outline-primary btn-sm" title="View Details">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{% url 'pharmacy:medication_edit' medication.pk %}" class="btn btn-outline-secondary btn-sm" title="Edit">
|
||||
<a href="{% url 'pharmacy:medication_update' medication.pk %}" class="btn btn-outline-secondary btn-sm" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" onclick="deleteMedication({{ medication.pk }})" title="Delete">
|
||||
@ -335,37 +335,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if page_obj.has_other_pages %}
|
||||
<div class="card-footer">
|
||||
<nav aria-label="Medication pagination">
|
||||
<ul class="pagination justify-content-center mb-0">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}">Previous</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ num }}</span>
|
||||
</li>
|
||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ num }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}">{{ num }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}">Next</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if is_paginated %}
|
||||
{% include 'partial/pagination.html' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -492,7 +492,7 @@ function initializeFormBehavior() {
|
||||
|
||||
function loadPatientInfo(patientId) {
|
||||
$.ajax({
|
||||
url: '{% url "pharmacy:get_patient_info" 0 %}'.replace('0', patientId),
|
||||
url: '{% url "patients:get_patient_info" 0 %}'.replace('0', patientId),
|
||||
method: 'GET',
|
||||
success: function(response) {
|
||||
var patient = response.patient;
|
||||
@ -624,131 +624,131 @@ function validateForm() {
|
||||
return isValid;
|
||||
}
|
||||
|
||||
function saveDraft() {
|
||||
var formData = $('#prescription-form').serialize();
|
||||
|
||||
$.ajax({
|
||||
url: '{% url "pharmacy:save_prescription_draft" %}',
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
success: function(response) {
|
||||
updatePrescriptionStatus('Draft saved', 'success');
|
||||
$('#last-saved').text(new Date().toLocaleTimeString());
|
||||
},
|
||||
error: function() {
|
||||
updatePrescriptionStatus('Failed to save draft', 'danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
{#function saveDraft() {#}
|
||||
{# var formData = $('#prescription-form').serialize();#}
|
||||
{# #}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "pharmacy:save_prescription_draft" %}',#}
|
||||
{# method: 'POST',#}
|
||||
{# data: formData,#}
|
||||
{# success: function(response) {#}
|
||||
{# updatePrescriptionStatus('Draft saved', 'success');#}
|
||||
{# $('#last-saved').text(new Date().toLocaleTimeString());#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# updatePrescriptionStatus('Failed to save draft', 'danger');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
|
||||
function updatePrescriptionStatus(message, type) {
|
||||
var alertClass = 'alert-' + type;
|
||||
$('#prescription-status').html('<div class="alert ' + alertClass + '"><i class="fa fa-info-circle me-2"></i>' + message + '</div>');
|
||||
}
|
||||
|
||||
function checkInteractions() {
|
||||
var patientId = $('#{{ form.patient.id_for_label }}').val();
|
||||
var medicationId = $('#{{ form.medication.id_for_label }}').val();
|
||||
|
||||
if (!patientId || !medicationId) {
|
||||
toastr.warning('Please select both patient and medication first');
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '{% url "pharmacy:check_drug_interactions" %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'patient_id': patientId,
|
||||
'medication_id': medicationId,
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.interactions.length > 0) {
|
||||
var message = 'Drug interactions found:\n';
|
||||
response.interactions.forEach(function(interaction) {
|
||||
message += '- ' + interaction.description + '\n';
|
||||
});
|
||||
toastr.warning(message);
|
||||
$('#interaction-check').html('<i class="fa fa-exclamation-triangle text-warning me-2"></i>Interactions found');
|
||||
} else {
|
||||
toastr.success('No drug interactions found');
|
||||
$('#interaction-check').html('<i class="fa fa-check text-success me-2"></i>No interactions found');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
toastr.error('Failed to check drug interactions');
|
||||
}
|
||||
});
|
||||
}
|
||||
{#function checkInteractions() {#}
|
||||
{# var patientId = $('#{{ form.patient.id_for_label }}').val();#}
|
||||
{# var medicationId = $('#{{ form.medication.id_for_label }}').val();#}
|
||||
{# #}
|
||||
{# if (!patientId || !medicationId) {#}
|
||||
{# toastr.warning('Please select both patient and medication first');#}
|
||||
{# return;#}
|
||||
{# }#}
|
||||
{# #}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "pharmacy:check_drug_interactions" %}',#}
|
||||
{# method: 'POST',#}
|
||||
{# data: {#}
|
||||
{# 'patient_id': patientId,#}
|
||||
{# 'medication_id': medicationId,#}
|
||||
{# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
|
||||
{# },#}
|
||||
{# success: function(response) {#}
|
||||
{# if (response.interactions.length > 0) {#}
|
||||
{# var message = 'Drug interactions found:\n';#}
|
||||
{# response.interactions.forEach(function(interaction) {#}
|
||||
{# message += '- ' + interaction.description + '\n';#}
|
||||
{# });#}
|
||||
{# toastr.warning(message);#}
|
||||
{# $('#interaction-check').html('<i class="fa fa-exclamation-triangle text-warning me-2"></i>Interactions found');#}
|
||||
{# } else {#}
|
||||
{# toastr.success('No drug interactions found');#}
|
||||
{# $('#interaction-check').html('<i class="fa fa-check text-success me-2"></i>No interactions found');#}
|
||||
{# }#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# toastr.error('Failed to check drug interactions');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
|
||||
function checkAllergies() {
|
||||
var patientId = $('#{{ form.patient.id_for_label }}').val();
|
||||
var medicationId = $('#{{ form.medication.id_for_label }}').val();
|
||||
|
||||
if (!patientId || !medicationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '{% url "pharmacy:check_allergies" %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'patient_id': patientId,
|
||||
'medication_id': medicationId,
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.allergies.length > 0) {
|
||||
var message = 'Allergy alert:\n';
|
||||
response.allergies.forEach(function(allergy) {
|
||||
message += '- ' + allergy.allergen + ' (' + allergy.reaction + ')\n';
|
||||
});
|
||||
toastr.error(message);
|
||||
$('#allergy-check').html('<i class="fa fa-exclamation-triangle text-danger me-2"></i>Allergy alert');
|
||||
} else {
|
||||
$('#allergy-check').html('<i class="fa fa-check text-success me-2"></i>No allergies found');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
toastr.error('Failed to check allergies');
|
||||
}
|
||||
});
|
||||
}
|
||||
{#function checkAllergies() {#}
|
||||
{# var patientId = $('#{{ form.patient.id_for_label }}').val();#}
|
||||
{# var medicationId = $('#{{ form.medication.id_for_label }}').val();#}
|
||||
{# #}
|
||||
{# if (!patientId || !medicationId) {#}
|
||||
{# return;#}
|
||||
{# }#}
|
||||
{# #}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "pharmacy:check_allergies" %}',#}
|
||||
{# method: 'POST',#}
|
||||
{# data: {#}
|
||||
{# 'patient_id': patientId,#}
|
||||
{# 'medication_id': medicationId,#}
|
||||
{# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
|
||||
{# },#}
|
||||
{# success: function(response) {#}
|
||||
{# if (response.allergies.length > 0) {#}
|
||||
{# var message = 'Allergy alert:\n';#}
|
||||
{# response.allergies.forEach(function(allergy) {#}
|
||||
{# message += '- ' + allergy.allergen + ' (' + allergy.reaction + ')\n';#}
|
||||
{# });#}
|
||||
{# toastr.error(message);#}
|
||||
{# $('#allergy-check').html('<i class="fa fa-exclamation-triangle text-danger me-2"></i>Allergy alert');#}
|
||||
{# } else {#}
|
||||
{# $('#allergy-check').html('<i class="fa fa-check text-success me-2"></i>No allergies found');#}
|
||||
{# }#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# toastr.error('Failed to check allergies');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
|
||||
function verifyDosage() {
|
||||
var dosage = $('#{{ form.dosage.id_for_label }}').val();
|
||||
var medicationId = $('#{{ form.medication.id_for_label }}').val();
|
||||
var patientId = $('#{{ form.patient.id_for_label }}').val();
|
||||
|
||||
if (!dosage || !medicationId || !patientId) {
|
||||
toastr.warning('Please fill in dosage, medication, and patient first');
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '{% url "pharmacy:verify_dosage" %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'dosage': dosage,
|
||||
'medication_id': medicationId,
|
||||
'patient_id': patientId,
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.is_valid) {
|
||||
toastr.success('Dosage is within normal range');
|
||||
$('#dosage-check').html('<i class="fa fa-check text-success me-2"></i>Dosage verified');
|
||||
} else {
|
||||
toastr.warning('Dosage may be outside normal range: ' + response.message);
|
||||
$('#dosage-check').html('<i class="fa fa-exclamation-triangle text-warning me-2"></i>Dosage warning');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
toastr.error('Failed to verify dosage');
|
||||
}
|
||||
});
|
||||
}
|
||||
{#function verifyDosage() {#}
|
||||
{# var dosage = $('#{{ form.dosage.id_for_label }}').val();#}
|
||||
{# var medicationId = $('#{{ form.medication.id_for_label }}').val();#}
|
||||
{# var patientId = $('#{{ form.patient.id_for_label }}').val();#}
|
||||
{# #}
|
||||
{# if (!dosage || !medicationId || !patientId) {#}
|
||||
{# toastr.warning('Please fill in dosage, medication, and patient first');#}
|
||||
{# return;#}
|
||||
{# }#}
|
||||
{# #}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "pharmacy:verify_dosage" %}',#}
|
||||
{# method: 'POST',#}
|
||||
{# data: {#}
|
||||
{# 'dosage': dosage,#}
|
||||
{# 'medication_id': medicationId,#}
|
||||
{# 'patient_id': patientId,#}
|
||||
{# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
|
||||
{# },#}
|
||||
{# success: function(response) {#}
|
||||
{# if (response.is_valid) {#}
|
||||
{# toastr.success('Dosage is within normal range');#}
|
||||
{# $('#dosage-check').html('<i class="fa fa-check text-success me-2"></i>Dosage verified');#}
|
||||
{# } else {#}
|
||||
{# toastr.warning('Dosage may be outside normal range: ' + response.message);#}
|
||||
{# $('#dosage-check').html('<i class="fa fa-exclamation-triangle text-warning me-2"></i>Dosage warning');#}
|
||||
{# }#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# toastr.error('Failed to verify dosage');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
|
||||
function loadTemplate() {
|
||||
// Implementation for loading prescription templates
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user