This commit is contained in:
Marwan Alwali 2025-08-30 09:45:26 +03:00
parent 25f548825b
commit be70e47e22
54 changed files with 17114 additions and 1089 deletions

View File

@ -2149,12 +2149,12 @@ def payment_download(request, payment_id):
# #
# #
# # Legacy view functions for backward compatibility # # Legacy view functions for backward compatibility
# dashboard = BillingDashboardView.as_view() dashboard = BillingDashboardView.as_view()
# bill_list = MedicalBillListView.as_view() bill_list = MedicalBillListView.as_view()
# bill_detail = MedicalBillDetailView.as_view() bill_detail = MedicalBillDetailView.as_view()
# bill_create = MedicalBillCreateView.as_view() bill_create = MedicalBillCreateView.as_view()
# claim_list = InsuranceClaimListView.as_view() claim_list = InsuranceClaimListView.as_view()
# payment_list = PaymentListView.as_view() payment_list = PaymentListView.as_view()
# #
# #
# #

Binary file not shown.

Binary file not shown.

View File

@ -638,7 +638,7 @@ def add_vital_signs(request, encounter_id):
vital_signs = VitalSigns.objects.create(**vital_signs_data) vital_signs = VitalSigns.objects.create(**vital_signs_data)
# Log the action # Log the action
AuditLogger.log_action( AuditLogger.log_event(
user=request.user, user=request.user,
action='VITAL_SIGNS_RECORDED', action='VITAL_SIGNS_RECORDED',
model='VitalSigns', model='VitalSigns',
@ -687,7 +687,7 @@ def add_problem(request, patient_id):
problem = ProblemList.objects.create(**problem_data) problem = ProblemList.objects.create(**problem_data)
# Log the action # Log the action
AuditLogger.log_action( AuditLogger.log_event(
user=request.user, user=request.user,
action='PROBLEM_ADDED', action='PROBLEM_ADDED',
model='ProblemList', model='ProblemList',
@ -727,7 +727,7 @@ def update_encounter_status(request, encounter_id):
encounter.save() encounter.save()
# Log the action # Log the action
AuditLogger.log_action( AuditLogger.log_event(
user=request.user, user=request.user,
action='ENCOUNTER_STATUS_UPDATED', action='ENCOUNTER_STATUS_UPDATED',
model='Encounter', model='Encounter',
@ -765,7 +765,7 @@ def sign_note(request, note_id):
note.save() note.save()
# Log the action # Log the action
AuditLogger.log_action( AuditLogger.log_event(
user=request.user, user=request.user,
action='NOTE_SIGNED', action='NOTE_SIGNED',
model='ClinicalNote', model='ClinicalNote',

View File

@ -167,7 +167,7 @@ class SpecimenForm(forms.ModelForm):
fields = [ fields = [
'order', 'specimen_type', 'collected_datetime', 'order', 'specimen_type', 'collected_datetime',
'collection_method', 'collection_site', 'volume', 'collection_method', 'collection_site', 'volume',
'container_type', 'quality_notes' 'container_type', 'quality_notes',
] ]
widgets = { widgets = {
'collected_datetime': forms.DateTimeInput( 'collected_datetime': forms.DateTimeInput(
@ -185,7 +185,7 @@ class SpecimenForm(forms.ModelForm):
if user and hasattr(user, 'tenant'): if user and hasattr(user, 'tenant'):
self.fields['order'].queryset = LabOrder.objects.filter( self.fields['order'].queryset = LabOrder.objects.filter(
tenant=user.tenant, tenant=user.tenant,
status__in=['PENDING', 'SCHEDULED'] # status__in=['PENDING', 'SCHEDULED']
).select_related('patient', 'test').order_by('-order_datetime') ).select_related('patient', 'test').order_by('-order_datetime')
# Set default collection time to now # Set default collection time to now

View 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,
),
]

View File

@ -337,7 +337,28 @@ class LabOrder(models.Model):
""" """
Lab order model for test ordering and management. 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 relationship
tenant = models.ForeignKey( tenant = models.ForeignKey(
'core.Tenant', 'core.Tenant',
@ -387,13 +408,7 @@ class LabOrder(models.Model):
) )
priority = models.CharField( priority = models.CharField(
max_length=20, max_length=20,
choices=[ choices=PRIORITY_CHOICES,
('ROUTINE', 'Routine'),
('URGENT', 'Urgent'),
('STAT', 'STAT'),
('ASAP', 'ASAP'),
('TIMED', 'Timed'),
],
default='ROUTINE', default='ROUTINE',
help_text='Order priority' help_text='Order priority'
) )
@ -430,11 +445,7 @@ class LabOrder(models.Model):
) )
fasting_status = models.CharField( fasting_status = models.CharField(
max_length=20, max_length=20,
choices=[ choices=FASTING_STATUS_CHOICES,
('FASTING', 'Fasting'),
('NON_FASTING', 'Non-Fasting'),
('UNKNOWN', 'Unknown'),
],
default='UNKNOWN', default='UNKNOWN',
help_text='Patient fasting status' help_text='Patient fasting status'
) )
@ -442,15 +453,7 @@ class LabOrder(models.Model):
# Status # Status
status = models.CharField( status = models.CharField(
max_length=20, max_length=20,
choices=[ choices=STATUS_CHOICES,
('PENDING', 'Pending'),
('SCHEDULED', 'Scheduled'),
('COLLECTED', 'Collected'),
('IN_PROGRESS', 'In Progress'),
('COMPLETED', 'Completed'),
('CANCELLED', 'Cancelled'),
('ON_HOLD', 'On Hold'),
],
default='PENDING', default='PENDING',
help_text='Order status' help_text='Order status'
) )
@ -527,7 +530,54 @@ class Specimen(models.Model):
""" """
Specimen model for specimen tracking and management. 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 relationship
order = models.ForeignKey( order = models.ForeignKey(
LabOrder, LabOrder,
@ -552,22 +602,7 @@ class Specimen(models.Model):
# Specimen Details # Specimen Details
specimen_type = models.CharField( specimen_type = models.CharField(
max_length=30, max_length=30,
choices=[ choices=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'),
],
help_text='Specimen type' help_text='Specimen type'
) )
container_type = models.CharField( container_type = models.CharField(
@ -611,11 +646,7 @@ class Specimen(models.Model):
# Specimen Quality # Specimen Quality
quality = models.CharField( quality = models.CharField(
max_length=20, max_length=20,
choices=[ choices=QUALITY_CHOICES,
('ACCEPTABLE', 'Acceptable'),
('SUBOPTIMAL', 'Suboptimal'),
('REJECTED', 'Rejected'),
],
default='ACCEPTABLE', default='ACCEPTABLE',
help_text='Specimen quality' help_text='Specimen quality'
) )
@ -623,17 +654,7 @@ class Specimen(models.Model):
max_length=100, max_length=100,
blank=True, blank=True,
null=True, null=True,
choices=[ choices=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'),
],
help_text='Reason for rejection' help_text='Reason for rejection'
) )
quality_notes = models.TextField( quality_notes = models.TextField(
@ -666,12 +687,7 @@ class Specimen(models.Model):
) )
storage_temperature = models.CharField( storage_temperature = models.CharField(
max_length=30, max_length=30,
choices=[ choices=STORAGE_TEMPERATURE_CHOICES,
('ROOM_TEMP', 'Room Temperature'),
('REFRIGERATED', 'Refrigerated (2-8°C)'),
('FROZEN', 'Frozen (-20°C)'),
('DEEP_FROZEN', 'Deep Frozen (-80°C)'),
],
default='ROOM_TEMP', default='ROOM_TEMP',
help_text='Storage temperature' help_text='Storage temperature'
) )
@ -679,15 +695,7 @@ class Specimen(models.Model):
# Status # Status
status = models.CharField( status = models.CharField(
max_length=20, max_length=20,
choices=[ choices=STATUS_CHOICES,
('COLLECTED', 'Collected'),
('IN_TRANSIT', 'In Transit'),
('RECEIVED', 'Received'),
('PROCESSING', 'Processing'),
('COMPLETED', 'Completed'),
('REJECTED', 'Rejected'),
('DISPOSED', 'Disposed'),
],
default='COLLECTED', default='COLLECTED',
help_text='Specimen status' help_text='Specimen status'
) )
@ -757,7 +765,28 @@ class LabResult(models.Model):
""" """
Lab result model for test results and reporting. 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 and Test relationship
order = models.ForeignKey( order = models.ForeignKey(
LabOrder, LabOrder,
@ -800,12 +829,7 @@ class LabResult(models.Model):
) )
result_type = models.CharField( result_type = models.CharField(
max_length=20, max_length=20,
choices=[ choices=RESULT_TYPE_CHOICES,
('NUMERIC', 'Numeric'),
('TEXT', 'Text'),
('CODED', 'Coded'),
('NARRATIVE', 'Narrative'),
],
default='NUMERIC', default='NUMERIC',
help_text='Type of result' help_text='Type of result'
) )
@ -819,14 +843,7 @@ class LabResult(models.Model):
) )
abnormal_flag = models.CharField( abnormal_flag = models.CharField(
max_length=10, max_length=10,
choices=[ choices=ABNORMAL_FLAG_CHOICES,
('N', 'Normal'),
('H', 'High'),
('L', 'Low'),
('HH', 'Critical High'),
('LL', 'Critical Low'),
('A', 'Abnormal'),
],
blank=True, blank=True,
null=True, null=True,
help_text='Abnormal flag' help_text='Abnormal flag'
@ -896,14 +913,7 @@ class LabResult(models.Model):
# Status # Status
status = models.CharField( status = models.CharField(
max_length=20, max_length=20,
choices=[ choices=STATUS_CHOICES,
('PENDING', 'Pending'),
('IN_PROGRESS', 'In Progress'),
('COMPLETED', 'Completed'),
('VERIFIED', 'Verified'),
('AMENDED', 'Amended'),
('CANCELLED', 'Cancelled'),
],
default='PENDING', default='PENDING',
help_text='Result status' help_text='Result status'
) )
@ -994,7 +1004,18 @@ class QualityControl(models.Model):
""" """
Quality control model for lab quality management. 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 relationship
tenant = models.ForeignKey( tenant = models.ForeignKey(
'core.Tenant', 'core.Tenant',
@ -1010,7 +1031,7 @@ class QualityControl(models.Model):
related_name='quality_controls', related_name='quality_controls',
help_text='Lab test' help_text='Lab test'
) )
result = models.ForeignKey(LabResult, on_delete=models.CASCADE, related_name='quality_controls')
# QC Information # QC Information
qc_id = models.UUIDField( qc_id = models.UUIDField(
default=uuid.uuid4, default=uuid.uuid4,
@ -1030,12 +1051,7 @@ class QualityControl(models.Model):
) )
control_level = models.CharField( control_level = models.CharField(
max_length=20, max_length=20,
choices=[ choices=CONTROL_LEVEL_CHOICES,
('NORMAL', 'Normal'),
('LOW', 'Low'),
('HIGH', 'High'),
('CRITICAL', 'Critical'),
],
help_text='Control level' help_text='Control level'
) )
@ -1069,12 +1085,7 @@ class QualityControl(models.Model):
# QC Status # QC Status
status = models.CharField( status = models.CharField(
max_length=20, max_length=20,
choices=[ choices=STATUS_CHOICES,
('PASSED', 'Passed'),
('FAILED', 'Failed'),
('WARNING', 'Warning'),
('PENDING', 'Pending'),
],
help_text='QC status' help_text='QC status'
) )
@ -1166,7 +1177,11 @@ class ReferenceRange(models.Model):
""" """
Reference range model for test normal values. Reference range model for test normal values.
""" """
GENDER_CHOICES = [
('M', 'Male'),
('F', 'Female'),
('ALL', 'All'),
]
# Test relationship # Test relationship
test = models.ForeignKey( test = models.ForeignKey(
LabTest, LabTest,
@ -1186,11 +1201,7 @@ class ReferenceRange(models.Model):
# Demographics # Demographics
gender = models.CharField( gender = models.CharField(
max_length=10, max_length=10,
choices=[ choices=GENDER_CHOICES,
('M', 'Male'),
('F', 'Female'),
('ALL', 'All'),
],
default='ALL', default='ALL',
help_text='Gender' help_text='Gender'
) )

View File

@ -63,23 +63,23 @@ class LaboratoryDashboardView(LoginRequiredMixin, TemplateView):
status='PENDING' status='PENDING'
).count(), ).count(),
'results_completed_today': LabResult.objects.filter( 'results_completed_today': LabResult.objects.filter(
tenant=tenant, order__tenant=tenant,
result_datetime__date=today, analyzed_datetime__date=today,
status='VERIFIED' status='VERIFIED'
).count(), ).count(),
'qc_tests_today': QualityControl.objects.filter( 'qc_tests_today': QualityControl.objects.filter(
tenant=tenant, tenant=tenant,
test_date=today run_datetime=today
).count(), ).count(),
'total_tests_available': LabTest.objects.filter( 'total_tests_available': LabTest.objects.filter(
tenant=tenant, tenant=tenant,
is_active=True is_active=True
).count(), ).count(),
'critical_results': LabResult.objects.filter( 'critical_results': LabResult.objects.filter(
tenant=tenant, order__tenant=tenant,
is_critical=True, is_critical=True,
status='VERIFIED', status='VERIFIED',
result_datetime__date=today analyzed_datetime__date=today
).count(), ).count(),
}) })
@ -90,9 +90,9 @@ class LaboratoryDashboardView(LoginRequiredMixin, TemplateView):
# Recent results # Recent results
context['recent_results'] = LabResult.objects.filter( context['recent_results'] = LabResult.objects.filter(
tenant=tenant, order__tenant=tenant,
status='VERIFIED' status='VERIFIED'
).select_related('order', 'test').order_by('-result_datetime')[:10] ).select_related('order', 'test').order_by('-analyzed_datetime')[:10]
return context return context
@ -281,12 +281,12 @@ class ReferenceRangeListView(LoginRequiredMixin, ListView):
List all reference ranges with filtering. List all reference ranges with filtering.
""" """
model = ReferenceRange model = ReferenceRange
template_name = 'laboratory/reference_range_list.html' template_name = 'laboratory/reference_ranges/reference_range_list.html'
context_object_name = 'reference_ranges' context_object_name = 'reference_ranges'
paginate_by = 25 paginate_by = 25
def get_queryset(self): 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 # Filter by test
test_id = self.request.GET.get('test') test_id = self.request.GET.get('test')
@ -307,7 +307,7 @@ class ReferenceRangeListView(LoginRequiredMixin, ListView):
tenant=self.request.user.tenant, tenant=self.request.user.tenant,
is_active=True is_active=True
).order_by('test_name'), ).order_by('test_name'),
'genders': ReferenceRange._meta.get_field('gender').choices, 'genders': ReferenceRange.GENDER_CHOICES,
}) })
return context return context
@ -317,11 +317,11 @@ class ReferenceRangeDetailView(LoginRequiredMixin, DetailView):
Display detailed information about a reference range. Display detailed information about a reference range.
""" """
model = ReferenceRange model = ReferenceRange
template_name = 'laboratory/reference_range_detail.html' template_name = 'laboratory/reference_ranges/reference_range_detail.html'
context_object_name = 'reference_range' context_object_name = 'reference_range'
def get_queryset(self): 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): class ReferenceRangeCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
@ -330,7 +330,7 @@ class ReferenceRangeCreateView(LoginRequiredMixin, PermissionRequiredMixin, Crea
""" """
model = ReferenceRange model = ReferenceRange
form_class = ReferenceRangeForm form_class = ReferenceRangeForm
template_name = 'laboratory/reference_range_form.html' template_name = 'laboratory/reference_ranges/reference_range_form.html'
permission_required = 'laboratory.add_referencerange' permission_required = 'laboratory.add_referencerange'
success_url = reverse_lazy('laboratory:reference_range_list') success_url = reverse_lazy('laboratory:reference_range_list')
@ -339,7 +339,7 @@ class ReferenceRangeCreateView(LoginRequiredMixin, PermissionRequiredMixin, Crea
response = super().form_valid(form) response = super().form_valid(form)
# Log the action # Log the action
AuditLogger.log_action( AuditLogger.log_event(
user=self.request.user, user=self.request.user,
action='REFERENCE_RANGE_CREATED', action='REFERENCE_RANGE_CREATED',
model='ReferenceRange', model='ReferenceRange',
@ -361,11 +361,11 @@ class ReferenceRangeUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Upda
""" """
model = ReferenceRange model = ReferenceRange
form_class = ReferenceRangeForm form_class = ReferenceRangeForm
template_name = 'laboratory/reference_range_form.html' template_name = 'laboratory/reference_ranges/reference_range_form.html'
permission_required = 'laboratory.change_referencerange' permission_required = 'laboratory.change_referencerange'
def get_queryset(self): 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): def get_success_url(self):
return reverse('laboratory:reference_range_detail', kwargs={'pk': self.object.pk}) 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) response = super().form_valid(form)
# Log the action # Log the action
AuditLogger.log_action( AuditLogger.log_event(
user=self.request.user, user=self.request.user,
action='REFERENCE_RANGE_UPDATED', action='REFERENCE_RANGE_UPDATED',
model='ReferenceRange', model='ReferenceRange',
@ -394,12 +394,12 @@ class ReferenceRangeDeleteView(LoginRequiredMixin, PermissionRequiredMixin, Dele
Delete a reference range. Delete a reference range.
""" """
model = ReferenceRange 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' permission_required = 'laboratory.delete_referencerange'
success_url = reverse_lazy('laboratory:reference_range_list') success_url = reverse_lazy('laboratory:reference_range_list')
def get_queryset(self): 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): def delete(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
@ -408,7 +408,7 @@ class ReferenceRangeDeleteView(LoginRequiredMixin, PermissionRequiredMixin, Dele
response = super().delete(request, *args, **kwargs) response = super().delete(request, *args, **kwargs)
# Log the action # Log the action
AuditLogger.log_action( AuditLogger.log_event(
user=request.user, user=request.user,
action='REFERENCE_RANGE_DELETED', action='REFERENCE_RANGE_DELETED',
model='ReferenceRange', model='ReferenceRange',
@ -430,7 +430,7 @@ class LabOrderListView(LoginRequiredMixin, ListView):
""" """
model = LabOrder model = LabOrder
template_name = 'laboratory/orders/lab_order_list.html' template_name = 'laboratory/orders/lab_order_list.html'
context_object_name = 'lab_orders' context_object_name = 'orders'
paginate_by = 25 paginate_by = 25
def get_queryset(self): def get_queryset(self):
@ -465,7 +465,7 @@ class LabOrderListView(LoginRequiredMixin, ListView):
queryset = queryset.filter(order_datetime__date__lte=date_to) queryset = queryset.filter(order_datetime__date__lte=date_to)
return queryset.select_related( return queryset.select_related(
'patient', 'test', 'ordering_provider' 'patient', 'ordering_provider'
).order_by('-order_datetime') ).order_by('-order_datetime')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -582,7 +582,7 @@ class SpecimenListView(LoginRequiredMixin, ListView):
paginate_by = 25 paginate_by = 25
def get_queryset(self): 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 functionality
search = self.request.GET.get('search') search = self.request.GET.get('search')
@ -604,7 +604,7 @@ class SpecimenListView(LoginRequiredMixin, ListView):
if specimen_type: if specimen_type:
queryset = queryset.filter(specimen_type=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): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -624,7 +624,7 @@ class SpecimenDetailView(LoginRequiredMixin, DetailView):
context_object_name = 'specimen' context_object_name = 'specimen'
def get_queryset(self): 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): class SpecimenCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
@ -643,7 +643,7 @@ class SpecimenCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView
response = super().form_valid(form) response = super().form_valid(form)
# Log the action # Log the action
AuditLogger.log_action( AuditLogger.log_event(
user=self.request.user, user=self.request.user,
action='SPECIMEN_COLLECTED', action='SPECIMEN_COLLECTED',
model='Specimen', model='Specimen',
@ -663,12 +663,12 @@ class SpecimenUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView
Update specimen (limited to status and processing notes). Update specimen (limited to status and processing notes).
""" """
model = Specimen model = Specimen
fields = ['status', 'processing_notes'] # Restricted fields fields = ['status'] # Restricted fields
template_name = 'laboratory/specimens/specimen_form.html' template_name = 'laboratory/specimens/specimen_form.html'
permission_required = 'laboratory.change_specimen' permission_required = 'laboratory.change_specimen'
def get_queryset(self): 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): def get_success_url(self):
return reverse('laboratory:specimen_detail', kwargs={'pk': self.object.pk}) return reverse('laboratory:specimen_detail', kwargs={'pk': self.object.pk})
@ -677,7 +677,7 @@ class SpecimenUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView
response = super().form_valid(form) response = super().form_valid(form)
# Log the action # Log the action
AuditLogger.log_action( AuditLogger.log_event(
user=self.request.user, user=self.request.user,
action='SPECIMEN_UPDATED', action='SPECIMEN_UPDATED',
model='Specimen', model='Specimen',
@ -814,8 +814,8 @@ class QualityControlListView(LoginRequiredMixin, ListView):
List all quality control records. List all quality control records.
""" """
model = QualityControl model = QualityControl
template_name = 'laboratory/quality_control_list.html' template_name = 'laboratory/quality_control/qc_sample_list.html'
context_object_name = 'qc_records' context_object_name = 'qc_samples'
paginate_by = 25 paginate_by = 25
def get_queryset(self): def get_queryset(self):
@ -831,7 +831,7 @@ class QualityControlListView(LoginRequiredMixin, ListView):
if result: if result:
queryset = queryset.filter(result=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): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -850,7 +850,7 @@ class QualityControlDetailView(LoginRequiredMixin, DetailView):
Display detailed information about a quality control record. Display detailed information about a quality control record.
""" """
model = QualityControl model = QualityControl
template_name = 'laboratory/quality_control_detail.html' template_name = 'laboratory/quality_control/qc_sample_detail.html'
context_object_name = 'qc_record' context_object_name = 'qc_record'
def get_queryset(self): def get_queryset(self):
@ -1196,7 +1196,7 @@ def mark_collected(request, order_id):
order.save() order.save()
# Log the action # Log the action
AuditLogger.log_action( AuditLogger.log_event(
user=request.user, user=request.user,
action='SPECIMEN_COLLECTED', action='SPECIMEN_COLLECTED',
model='LabOrder', model='LabOrder',

File diff suppressed because it is too large Load Diff

View File

@ -35,14 +35,15 @@ urlpatterns = [
# HTMX views # HTMX views
path('htmx/patient-search/', views.patient_search, name='patient_search'), path('patient-search/', views.patient_search, name='patient_search'),
path('htmx/patient-stats/', views.patient_stats, name='patient_stats'), path('patient-stats/', views.patient_stats, name='patient_stats'),
path('htmx/emergency-contacts/<int:patient_id>/', views.emergency_contacts_list, name='emergency_contacts_list'), path('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('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('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('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('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('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('appointments/<int:patient_id>/', views.patient_appointment_list, name='patient_appointments'),
path('patient-info/<int:pk>/', views.get_patient_info, name='get_patient_info')
] ]

View File

@ -1315,6 +1315,20 @@ def add_patient_note(request, patient_id):
'patient': patient '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. # Patients app views for hospital management system with comprehensive CRUD operations.

View 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,
),
),
]

View File

@ -17,7 +17,49 @@ class Medication(models.Model):
""" """
Medication model for drug database and formulary management. 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 relationship
tenant = models.ForeignKey( tenant = models.ForeignKey(
'core.Tenant', 'core.Tenant',
@ -68,14 +110,7 @@ class Medication(models.Model):
) )
controlled_substance_schedule = models.CharField( controlled_substance_schedule = models.CharField(
max_length=5, max_length=5,
choices=[ choices=CONTROLLED_SUBSTANCE_SCHEDULE_CHOICES,
('CI', 'Schedule I'),
('CII', 'Schedule II'),
('CIII', 'Schedule III'),
('CIV', 'Schedule IV'),
('CV', 'Schedule V'),
('NON', 'Non-Controlled'),
],
default='NON', default='NON',
help_text='DEA controlled substance schedule' help_text='DEA controlled substance schedule'
) )
@ -83,21 +118,7 @@ class Medication(models.Model):
# Formulation # Formulation
dosage_form = models.CharField( dosage_form = models.CharField(
max_length=50, max_length=50,
choices=[ choices=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'),
],
help_text='Dosage form' help_text='Dosage form'
) )
strength = models.CharField( strength = models.CharField(
@ -106,18 +127,7 @@ class Medication(models.Model):
) )
unit_of_measure = models.CharField( unit_of_measure = models.CharField(
max_length=20, max_length=20,
choices=[ choices=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'),
],
help_text='Unit of measure' help_text='Unit of measure'
) )
@ -188,13 +198,7 @@ class Medication(models.Model):
# Formulary Status # Formulary Status
formulary_status = models.CharField( formulary_status = models.CharField(
max_length=20, max_length=20,
choices=[ choices=FORMULARY_STATUS_CHOICES,
('PREFERRED', 'Preferred'),
('NON_PREFERRED', 'Non-Preferred'),
('RESTRICTED', 'Restricted'),
('NOT_COVERED', 'Not Covered'),
('PRIOR_AUTH', 'Prior Authorization Required'),
],
default='PREFERRED', default='PREFERRED',
help_text='Formulary status' help_text='Formulary status'
) )
@ -290,7 +294,31 @@ class Prescription(models.Model):
""" """
Prescription model for electronic prescription management. 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 relationship
tenant = models.ForeignKey( tenant = models.ForeignKey(
'core.Tenant', 'core.Tenant',
@ -340,19 +368,7 @@ class Prescription(models.Model):
) )
quantity_unit = models.CharField( quantity_unit = models.CharField(
max_length=20, max_length=20,
choices=[ choices=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'),
],
help_text='Unit of quantity' help_text='Unit of quantity'
) )
@ -398,17 +414,7 @@ class Prescription(models.Model):
# Status # Status
status = models.CharField( status = models.CharField(
max_length=20, max_length=20,
choices=[ choices=STATUS_CHOICES,
('PENDING', 'Pending'),
('ACTIVE', 'Active'),
('DISPENSED', 'Dispensed'),
('PARTIALLY_DISPENSED', 'Partially Dispensed'),
('COMPLETED', 'Completed'),
('CANCELLED', 'Cancelled'),
('EXPIRED', 'Expired'),
('ON_HOLD', 'On Hold'),
('TRANSFERRED', 'Transferred'),
],
default='PENDING', default='PENDING',
help_text='Prescription status' help_text='Prescription status'
) )
@ -577,7 +583,15 @@ class InventoryItem(models.Model):
""" """
Inventory item model for pharmacy stock management. Inventory item model for pharmacy stock management.
""" """
STATUS_CHOICES = [
('ACTIVE', 'Active'),
('QUARANTINE', 'Quarantine'),
('EXPIRED', 'Expired'),
('RECALLED', 'Recalled'),
('DAMAGED', 'Damaged'),
('RETURNED', 'Returned'),
]
# Tenant relationship # Tenant relationship
tenant = models.ForeignKey( tenant = models.ForeignKey(
'core.Tenant', 'core.Tenant',
@ -684,14 +698,7 @@ class InventoryItem(models.Model):
# Status # Status
status = models.CharField( status = models.CharField(
max_length=20, max_length=20,
choices=[ choices=STATUS_CHOICES,
('ACTIVE', 'Active'),
('QUARANTINE', 'Quarantine'),
('EXPIRED', 'Expired'),
('RECALLED', 'Recalled'),
('DAMAGED', 'Damaged'),
('RETURNED', 'Returned'),
],
default='ACTIVE', default='ACTIVE',
help_text='Inventory status' help_text='Inventory status'
) )

View File

@ -21,6 +21,7 @@ urlpatterns = [
# path('inventory/<int:pk>/delete/', views.InventoryItemDeleteView.as_view(), name='inventory_delete'), # path('inventory/<int:pk>/delete/', views.InventoryItemDeleteView.as_view(), name='inventory_delete'),
path('dispense-records/', views.DispenseRecordListView.as_view(), name='dispense_record_list'), 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/', 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/create/', views.MedicationCreateView.as_view(), name='medication_create'),
path('medications/<int:pk>/', views.MedicationDetailView.as_view(), name='medication_detail'), 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('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/<int:item_id>/update/', views.update_inventory, name='update_inventory'),
path('inventory-adjustment/<int:item_id>/', views.adjust_inventory, name='adjust_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 # API endpoints
# path('api/', include('pharmacy.api.urls')), # path('api/', include('pharmacy.api.urls')),

View File

@ -322,13 +322,13 @@ class MedicationDetailView(LoginRequiredMixin, DetailView):
context['recent_prescriptions'] = Prescription.objects.filter( context['recent_prescriptions'] = Prescription.objects.filter(
medication=medication, medication=medication,
patient__tenant=self.request.user.tenant 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 # Inventory items
context['inventory_items'] = InventoryItem.objects.filter( context['inventory_items'] = InventoryItem.objects.filter(
medication=medication, medication=medication,
is_active=True status="ACTIVE"
).order_by('expiry_date') ).order_by('expiration_date')
# Drug interactions # Drug interactions
context['interactions'] = DrugInteraction.objects.filter( context['interactions'] = DrugInteraction.objects.filter(
@ -350,7 +350,7 @@ class MedicationDetailView(LoginRequiredMixin, DetailView):
context['total_inventory'] = InventoryItem.objects.filter( context['total_inventory'] = InventoryItem.objects.filter(
medication=medication, medication=medication,
is_active=True status="ACTIVE"
).aggregate(total=Sum('quantity_on_hand'))['total'] or 0 ).aggregate(total=Sum('quantity_on_hand'))['total'] or 0
return context return context
@ -398,8 +398,8 @@ class MedicationListView(LoginRequiredMixin, ListView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context.update({ context.update({
'medication_types': Medication._meta.get_field('medication_type').choices, 'medication_types': Medication.DOSAGE_FORM_CHOICES,
'controlled_schedules': Medication._meta.get_field('controlled_substance_schedule').choices, 'controlled_schedules': Medication.CONTROLLED_SUBSTANCE_SCHEDULE_CHOICES,
}) })
return context return context
@ -517,7 +517,7 @@ class InventoryItemCreateView(LoginRequiredMixin, CreateView):
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user # kwargs['user'] = self.request.user
return kwargs return kwargs
def form_valid(self, form): def form_valid(self, form):
@ -623,7 +623,7 @@ class DrugInteractionListView(LoginRequiredMixin, ListView):
List view for drug interactions. List view for drug interactions.
""" """
model = DrugInteraction model = DrugInteraction
template_name = 'pharmacy/drug_interaction_list.html' template_name = 'pharmacy/interactions/drug_interaction_list.html'
context_object_name = 'interactions' context_object_name = 'interactions'
paginate_by = 25 paginate_by = 25
@ -649,7 +649,7 @@ class DrugInteractionListView(LoginRequiredMixin, ListView):
if interaction_type: if interaction_type:
queryset = queryset.filter(interaction_type=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): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -776,7 +776,7 @@ def drug_interaction_check(request, prescription_id):
tenant=request.user.tenant tenant=request.user.tenant
).select_related('medication_a', 'medication_b') ).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, 'prescription': prescription,
'interactions': interactions 'interactions': interactions
}) })
@ -901,7 +901,7 @@ def update_inventory(request, item_id):
item.save() item.save()
# Log the action # Log the action
AuditLogger.log_action( AuditLogger.log_event(
user=request.user, user=request.user,
action='UPDATE_INVENTORY', action='UPDATE_INVENTORY',
model_name='InventoryItem', model_name='InventoryItem',
@ -918,6 +918,15 @@ def update_inventory(request, item_id):
return JsonResponse({'status': 'error'}) 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. # Views for Pharmacy app with comprehensive CRUD operations following healthcare best practices.
@ -1855,40 +1864,50 @@ def update_inventory(request, item_id):
# return queryset # return queryset
# #
# #
# class DrugInteractionCreateView(LoginRequiredMixin, CreateView): class DrugInteractionCreateView(LoginRequiredMixin, CreateView):
# """ """
# Create view for drug interaction. Create view for drug interaction.
# """ """
# model = DrugInteraction model = DrugInteraction
# form_class = DrugInteractionForm form_class = DrugInteractionForm
# template_name = 'pharmacy/interaction_form.html' template_name = 'pharmacy/interaction_form.html'
# success_url = reverse_lazy('pharmacy:interaction_list') success_url = reverse_lazy('pharmacy:interaction_list')
#
# def get_form_kwargs(self): def get_form_kwargs(self):
# kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
# kwargs['user'] = self.request.user kwargs['user'] = self.request.user
# return kwargs return kwargs
#
# def form_valid(self, form): def form_valid(self, form):
# form.instance.created_by = self.request.user form.instance.created_by = self.request.user
# response = super().form_valid(form) response = super().form_valid(form)
#
# # Create audit log # Create audit log
# AuditLogEntry.objects.create( AuditLogEntry.objects.create(
# tenant=self.request.user.tenant, tenant=self.request.user.tenant,
# user=self.request.user, user=self.request.user,
# action='CREATE', action='CREATE',
# model_name='DrugInteraction', model_name='DrugInteraction',
# object_id=self.object.id, object_id=self.object.id,
# changes={ changes={
# 'medication_1': str(self.object.medication_1), 'medication_1': str(self.object.medication_1),
# 'medication_2': str(self.object.medication_2), 'medication_2': str(self.object.medication_2),
# 'severity': self.object.severity 'severity': self.object.severity
# } }
# ) )
#
# messages.success(self.request, 'Drug interaction created successfully.') messages.success(self.request, 'Drug interaction created successfully.')
# return response return response
class DrugInteractionDetailView(LoginRequiredMixin, DetailView):
model = DrugInteraction
template_name = 'pharmacy/interactions/drug_interaction_detail.html'
context_object_name = 'interaction'
# #
# #
# # HTMX Views # # HTMX Views
@ -2170,3 +2189,8 @@ def adjust_inventory(request, pk):
# #
# return response # 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

Binary file not shown.

View File

@ -42,7 +42,7 @@
</a></li> </a></li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
{% if object.status == 'DRAFT' %} {% 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 <i class="fas fa-paper-plane me-2"></i>Submit Bill
</a></li> </a></li>
{% endif %} {% endif %}

View File

@ -22,6 +22,7 @@
</a> </a>
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown"> <button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown">
<span class="visually-hidden">Toggle Dropdown</span> <span class="visually-hidden">Toggle Dropdown</span>
<i class="fas fa-ellipsis-v"></i>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a class="dropdown-item" href="{% url 'billing:export_bills' %}"> <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 class="d-flex justify-content-between align-items-center">
<div> <div>
<h6 class="card-title mb-1 opacity-75">Total Bills</h6> <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>
<div class="text-white-50"> <div class="text-white-50">
<i class="fas fa-file-invoice fa-2x"></i> <i class="fas fa-file-invoice fa-2x"></i>
@ -282,6 +283,7 @@
<div class="btn-group" role="group"> <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"> <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> <span class="visually-hidden">Toggle Dropdown</span>
<i class="fas fa-ellipsis-v"></i>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a class="dropdown-item" href=""> <li><a class="dropdown-item" href="">
@ -328,48 +330,9 @@
</div> </div>
<!-- Pagination --> <!-- Pagination -->
{% if page_obj.has_other_pages %} {% if is_paginated %}
<div class="card-footer"> {% include 'partial/pagination.html' %}
<div class="d-flex justify-content-between align-items-center"> {% endif %}
<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 %}
</div> </div>
</div> </div>

View File

@ -412,32 +412,32 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
// Bill selection change handler // Bill selection change handler
document.getElementById('{{ form.medical_bill.id_for_label }}').addEventListener('change', function() { {#document.getElementById('{{ form.medical_bill.id_for_label }}').addEventListener('change', function() {#}
const billId = this.value; {# const billId = this.value;#}
if (billId) { {# if (billId) {#}
// Fetch bill details via HTMX or AJAX {# // Fetch bill details via HTMX or AJAX#}
fetch(`{% url 'billing:bill_details_api' %}?bill_id=${billId}`) {# fetch(`{% url 'billing:bill_details_api' %}?bill_id=${billId}`)#}
.then(response => response.json()) {# .then(response => response.json())#}
.then(data => { {# .then(data => {#}
document.getElementById('bill-patient').textContent = data.patient_name; {# document.getElementById('bill-patient').textContent = data.patient_name;#}
document.getElementById('bill-number').textContent = data.bill_number; {# document.getElementById('bill-number').textContent = data.bill_number;#}
document.getElementById('bill-date').textContent = data.bill_date; {# document.getElementById('bill-date').textContent = data.bill_date;#}
document.getElementById('bill-total').textContent = `$${data.total_amount}`; {# document.getElementById('bill-total').textContent = `$${data.total_amount}`;#}
document.getElementById('bill-paid').textContent = `$${data.paid_amount}`; {# document.getElementById('bill-paid').textContent = `$${data.paid_amount}`;#}
document.getElementById('bill-balance').textContent = `$${data.balance_amount}`; {# document.getElementById('bill-balance').textContent = `$${data.balance_amount}`;#}
document.getElementById('bill-details').style.display = 'block'; {# document.getElementById('bill-details').style.display = 'block';#}
{# #}
// Set max payment amount to balance {# // Set max payment amount to balance#}
const paymentAmountField = document.getElementById('{{ form.payment_amount.id_for_label }}'); {# const paymentAmountField = document.getElementById('{{ form.payment_amount.id_for_label }}');#}
paymentAmountField.max = data.balance_amount; {# paymentAmountField.max = data.balance_amount;#}
}) {# })#}
.catch(error => { {# .catch(error => {#}
console.error('Error fetching bill details:', error); {# console.error('Error fetching bill details:', error);#}
}); {# });#}
} else { {# } else {#}
document.getElementById('bill-details').style.display = 'none'; {# document.getElementById('bill-details').style.display = 'none';#}
} {# }#}
}); {# });#}
// Payment amount change handler // Payment amount change handler
document.getElementById('{{ form.payment_amount.id_for_label }}').addEventListener('input', function() { document.getElementById('{{ form.payment_amount.id_for_label }}').addEventListener('input', function() {
@ -479,14 +479,14 @@ function calculatePartialPayment() {
} }
} }
function viewPaymentHistory() { {#function viewPaymentHistory() {#}
const billId = document.getElementById('{{ form.medical_bill.id_for_label }}').value; {# const billId = document.getElementById('{{ form.medical_bill.id_for_label }}').value;#}
if (billId) { {# if (billId) {#}
window.open(`{% url 'billing:payment_history' %}?bill_id=${billId}`, '_blank'); {# window.open(`{% url 'billing:payment_history' %}?bill_id=${billId}`, '_blank');#}
} else { {# } else {#}
alert('Please select a bill first.'); {# alert('Please select a bill first.');#}
} {# }#}
} {# }#}
// Form validation // Form validation
document.getElementById('paymentForm').addEventListener('submit', function(e) { document.getElementById('paymentForm').addEventListener('submit', function(e) {

BIN
templates/laboratory/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -394,16 +394,16 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
// Real-time notifications for critical results // Real-time notifications for critical results
function checkCriticalResults() { {#function checkCriticalResults() {#}
fetch('{% url "laboratory:check_critical_results" %}') {# fetch('{% url "laboratory:check_critical_results" %}')#}
.then(response => response.json()) {# .then(response => response.json())#}
.then(data => { {# .then(data => {#}
if (data.critical_count > 0) { {# if (data.critical_count > 0) {#}
showCriticalAlert(data.critical_count); {# showCriticalAlert(data.critical_count);#}
} {# }#}
}) {# })#}
.catch(error => console.error('Error checking critical results:', error)); {# .catch(error => console.error('Error checking critical results:', error));#}
} {# }#}
function showCriticalAlert(count) { function showCriticalAlert(count) {
const alertHtml = ` const alertHtml = `

View File

@ -4,8 +4,8 @@
{% block title %}Laboratory Orders - Laboratory Management{% endblock %} {% block title %}Laboratory Orders - Laboratory Management{% endblock %}
{% block css %} {% block css %}
<link href="{% static 'assets/plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" /> <link href="{% static '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-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@ -119,7 +119,8 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for order in object_list %}
{% for order in orders %}
<tr data-status="{{ order.status }}" data-priority="{{ order.priority }}"> <tr data-status="{{ order.status }}" data-priority="{{ order.priority }}">
<td> <td>
<div class="form-check"> <div class="form-check">
@ -143,7 +144,7 @@
</td> </td>
<td> <td>
<div class="small"> <div class="small">
{% for test in order.tests.all|slice:":3" %} {% for test in order.tests.all %}
<div>{{ test.name }}</div> <div>{{ test.name }}</div>
{% endfor %} {% endfor %}
{% if order.tests.count > 3 %} {% if order.tests.count > 3 %}
@ -207,45 +208,45 @@
{% endblock %} {% endblock %}
{% block js %} {% block js %}
<script src="{% static 'assets/plugins/datatables.net/js/jquery.dataTables.min.js' %}"></script> <script src="{% static 'plugins/datatables.net/js/dataTables.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script> <script src="{% static 'plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
<script> <script>
$(document).ready(function() { {#$(document).ready(function() {#}
$('#orders-table').DataTable({ {# $('#orders-table').DataTable({#}
responsive: true, {# responsive: true,#}
pageLength: 25, {# pageLength: 25,#}
order: [[6, 'desc']] {# order: [[6, 'desc']]#}
}); {# });#}
}); {# });#}
{##}
function filterByStatus(status) { {#function filterByStatus(status) {#}
$('.btn-group button').removeClass('active'); {# $('.btn-group button').removeClass('active');#}
event.target.classList.add('active'); {# event.target.classList.add('active');#}
{# #}
if (status === 'all') { {# if (status === 'all') {#}
$('tr[data-status]').show(); {# $('tr[data-status]').show();#}
} else { {# } else {#}
$('tr[data-status]').hide(); {# $('tr[data-status]').hide();#}
$(`tr[data-status="${status}"]`).show(); {# $(`tr[data-status="${status}"]`).show();#}
} {# }#}
} {# }#}
{##}
function filterByPriority(priority) { {#function filterByPriority(priority) {#}
$('.btn-group button').removeClass('active'); {# $('.btn-group button').removeClass('active');#}
event.target.classList.add('active'); {# event.target.classList.add('active');#}
{# #}
if (priority === 'all') { {# if (priority === 'all') {#}
$('tr[data-priority]').show(); {# $('tr[data-priority]').show();#}
} else { {# } else {#}
$('tr[data-priority]').hide(); {# $('tr[data-priority]').hide();#}
$(`tr[data-priority="${priority}"]`).show(); {# $(`tr[data-priority="${priority}"]`).show();#}
} {# }#}
} {# }#}
function startProcessing(orderId) { function startProcessing(orderId) {
$.ajax({ $.ajax({
url: '{% url "laboratory:lab_order_start_processing" 0 %}'.replace('0', orderId), url: '{% url "laboratory:start_processing" 0 %}'.replace('0', orderId),
method: 'POST', method: 'POST',
data: { data: {
'csrfmiddlewaretoken': '{{ csrf_token }}' 'csrfmiddlewaretoken': '{{ csrf_token }}'
@ -264,9 +265,9 @@ function startProcessing(orderId) {
}); });
} }
function exportOrders() { {#function exportOrders() {#}
window.open('{% url "laboratory:lab_order_export" %}'); {# window.open('{% url "laboratory:lab_order_export" %}');#}
} {# }#}
</script> </script>
{% endblock %} {% endblock %}

View 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 %}

View File

@ -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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View File

@ -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 %}

View File

@ -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 %}

View 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 %}

View 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>&gt; {{ range.min_value }}</strong>
{% elif range.max_value %}
<strong>&lt; {{ 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 %}&lt;{{ range.critical_low }}{% endif %}
{% if range.critical_low and range.critical_high %}, {% endif %}
{% if range.critical_high %}&gt;{{ 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">&laquo; 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 &raquo;</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 %}

View File

@ -15,11 +15,12 @@
</div> </div>
<div class="ms-auto"> <div class="ms-auto">
<div class="btn-group"> <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 <i class="fas fa-edit me-2"></i>Edit Specimen
</a> </a>
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown"> <button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown">
<span class="visually-hidden">Toggle Dropdown</span> <span class="visually-hidden">Toggle Dropdown</span>
<i class="fas fa-ellipsis-v"></i>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="printLabel()"> <li><a class="dropdown-item" href="#" onclick="printLabel()">
@ -35,9 +36,9 @@
<li><a class="dropdown-item" href="#" onclick="generateReport()"> <li><a class="dropdown-item" href="#" onclick="generateReport()">
<i class="fas fa-chart-line me-2"></i>Generate Report <i class="fas fa-chart-line me-2"></i>Generate Report
</a></li> </a></li>
<li><a class="dropdown-item text-danger" href="{% url 'laboratory:specimen_confirm_delete' object.pk %}"> {# <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 {# <i class="fas fa-trash me-2"></i>Delete Specimen#}
</a></li> {# </a></li>#}
</ul> </ul>
</div> </div>
</div> </div>
@ -76,19 +77,32 @@
</tr> </tr>
<tr> <tr>
<td class="fw-bold">MRN:</td> <td class="fw-bold">MRN:</td>
<td>{{ object.patient.medical_record_number }}</td> <td>{{ object.patient.mrn }}</td>
</tr> </tr>
<tr> <tr>
<td class="fw-bold">Type:</td> <td class="fw-bold">Type:</td>
<td> <td>
<span class="badge bg-light text-dark">
{{ object.get_specimen_type_display }} <i class="fas
</span> 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> </td>
</tr> </tr>
<tr> <tr>
<td class="fw-bold">Collection Date:</td> <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> </tr>
</table> </table>
</div> </div>
@ -113,8 +127,8 @@
<tr> <tr>
<td class="fw-bold">Priority:</td> <td class="fw-bold">Priority:</td>
<td> <td>
<span class="badge bg-{% if object.priority == 'urgent' %}danger{% elif object.priority == 'stat' %}warning{% else %}primary{% endif %}"> <span class="badge bg-{% if object.order.priority == 'STAT' %}danger{% elif object.order.priority == 'URGENT' %}warning{% else %}primary{% endif %}">
{{ object.get_priority_display }} {{ object.order.get_priority_display }}
</span> </span>
</td> </td>
</tr> </tr>
@ -313,7 +327,7 @@
<strong>Gender:</strong> {{ object.patient.get_gender_display }} <strong>Gender:</strong> {{ object.patient.get_gender_display }}
</div> </div>
<div class="mb-2"> <div class="mb-2">
<strong>MRN:</strong> {{ object.patient.medical_record_number }} <strong>MRN:</strong> {{ object.patient.mrn }}
</div> </div>
<div> <div>
<a href="{% url 'patients:patient_detail' object.patient.pk %}" class="btn btn-sm btn-outline-primary"> <a href="{% url 'patients:patient_detail' object.patient.pk %}" class="btn btn-sm btn-outline-primary">

View File

@ -49,9 +49,9 @@
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label for="{{ form.patient.id_for_label }}" class="form-label">Patient *</label> <label for="{{ form.patient.id_for_label }}" class="form-label">Patient *</label>
<select class="form-select {% if form.patient.errors %}is-invalid{% endif %}" <select class="form-select {% if form.patient.errors %}is-invalid{% endif %}"
id="{{ form.patient.id_for_label }}" id="{{ form.patient.id_for_label }}"
name="{{ form.patient.name }}" name="{{ form.patient.name }}"
required> required>
<option value="">Select Patient</option> <option value="">Select Patient</option>
{% for choice in form.patient.field.choices %} {% 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> <label for="{{ form.specimen_id.id_for_label }}" class="form-label">Specimen ID</label>
<div class="input-group"> <div class="input-group">
<input type="text" <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 }}" id="{{ form.specimen_id.id_for_label }}"
name="{{ form.specimen_id.name }}" name="{{ form.specimen_id.name }}"
value="{{ form.specimen_id.value|default:'' }}" value="{{ form.specimen_id.value|default:'' }}"
@ -102,7 +102,7 @@
<div class="col-md-4"> <div class="col-md-4">
<div class="mb-3"> <div class="mb-3">
<label for="{{ form.specimen_type.id_for_label }}" class="form-label">Specimen Type *</label> <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 }}" id="{{ form.specimen_type.id_for_label }}"
name="{{ form.specimen_type.name }}" name="{{ form.specimen_type.name }}"
required> 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; document.getElementById('guidelinesContent').innerHTML = content;
} }
@ -615,9 +616,10 @@ function generateSpecimenId() {
const month = (now.getMonth() + 1).toString().padStart(2, '0'); const month = (now.getMonth() + 1).toString().padStart(2, '0');
const day = now.getDate().toString().padStart(2, '0'); const day = now.getDate().toString().padStart(2, '0');
const time = now.getTime().toString().slice(-6); const time = now.getTime().toString().slice(-6);
const specimenId = `SP${year}${month}${day}${time}`; let specimenId;
document.getElementById('{{ form.specimen_id.id_for_label }}').value = specimenId; specimenId = `SP${year}${month}${day}${time}`;
document.querySelector('.specimen').value = specimenId;
} }
function generateBarcode() { function generateBarcode() {

View File

@ -14,11 +14,12 @@
</div> </div>
<div class="ms-auto"> <div class="ms-auto">
<div class="btn-group"> <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 <i class="fas fa-plus me-2"></i>New Specimen
</a> </a>
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown"> <button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown">
<span class="visually-hidden">Toggle Dropdown</span> <span class="visually-hidden">Toggle Dropdown</span>
<i class="fas fa-ellipsis-v"></i>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="exportSpecimens()"> <li><a class="dropdown-item" href="#" onclick="exportSpecimens()">
@ -211,11 +212,11 @@
<th>Status</th> <th>Status</th>
<th>Priority</th> <th>Priority</th>
<th>Tests</th> <th>Tests</th>
<th width="120">Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for specimen in object_list %} {% for specimen in specimens %}
<tr> <tr>
<td> <td>
<input type="checkbox" class="specimen-checkbox" value="{{ specimen.pk }}"> <input type="checkbox" class="specimen-checkbox" value="{{ specimen.pk }}">
@ -244,20 +245,34 @@
</td> </td>
<td> <td>
<div class="d-flex align-items-center"> <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> </div>
{% if specimen.volume %} {% if specimen.volume %}
<div class="small text-muted">{{ specimen.volume }}</div> <div class="small text-muted">{{ specimen.volume }}</div>
{% endif %} {% endif %}
</td> </td>
<td> <td>
<div class="fw-bold">{{ specimen.collection_date|date:"M d, Y" }}</div> <div class="fw-bold">{{ specimen.collected_datetime|date:"M d, Y" }}</div>
<div class="small text-muted">{{ specimen.collection_date|date:"g:i A" }}</div> <div class="small text-muted">{{ specimen.collected_datetime|date:"g:i A" }}</div>
</td> </td>
<td>{{ specimen.collected_by.get_full_name }}</td> <td>{{ specimen.collected_by.get_full_name }}</td>
<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 }} {{ specimen.get_status_display }}
</span> </span>
{% if specimen.condition and specimen.condition != 'good' %} {% if specimen.condition and specimen.condition != 'good' %}
@ -267,8 +282,8 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
<span class="badge bg-{% if specimen.priority == 'stat' %}danger{% elif specimen.priority == 'urgent' %}warning{% else %}primary{% endif %}"> <span class="badge bg-{% if specimen.order.priority == 'STAT' %}danger{% elif specimen.order.priority == 'URGENT' %}warning{% else %}primary{% endif %}">
{{ specimen.get_priority_display }} {{ specimen.order.get_priority_display }}
</span> </span>
{% if specimen.fasting_status %} {% if specimen.fasting_status %}
<div class="small text-info mt-1"> <div class="small text-info mt-1">
@ -290,7 +305,7 @@
title="View Details"> title="View Details">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</a> </a>
<a href="{% url 'laboratory:specimen_form' specimen.pk %}" <a href="{% url 'laboratory:specimen_update' specimen.pk %}"
class="btn btn-outline-secondary" class="btn btn-outline-secondary"
title="Edit"> title="Edit">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
@ -301,11 +316,6 @@
title="Print Label"> title="Print Label">
<i class="fas fa-print"></i> <i class="fas fa-print"></i>
</button> </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> </div>
</td> </td>
</tr> </tr>
@ -316,38 +326,7 @@
<!-- Pagination --> <!-- Pagination -->
{% if is_paginated %} {% if is_paginated %}
<div class="card-footer"> {% include 'partial/pagination.html' %}
<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>
{% endif %} {% endif %}
<!-- Bulk Actions --> <!-- Bulk Actions -->

View File

@ -148,10 +148,10 @@
</a> </a>
</div> </div>
<div class="col-lg-2 col-md-4 col-sm-6"> <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> <i class="fas fa-search fa-2x mb-2"></i>
<span>Drug Interactions</span> <span>Drug Interactions</span>
</button> </a>
</div> </div>
<div class="col-lg-2 col-md-4 col-sm-6"> <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"> <a href="" class="btn btn-outline-secondary w-100 h-100 d-flex flex-column align-items-center justify-content-center py-3">

View File

@ -515,7 +515,7 @@ function checkInteraction() {
} }
$.ajax({ $.ajax({
url: '{% url "pharmacy:check_interaction" %}', url: '{% url "pharmacy:drug_interaction_check" 0 %}'.replace('0', med1),
method: 'POST', method: 'POST',
data: { data: {
'medication1': med1, 'medication1': med1,
@ -579,7 +579,7 @@ function viewInteraction(interactionId) {
currentInteractionId = interactionId; currentInteractionId = interactionId;
$.ajax({ $.ajax({
url: '{% url "pharmacy:interaction_detail" %}', url: '{% url "pharmacy:drug_interaction_detail" 0 %}'.replace('0', interactionId),
method: 'GET', method: 'GET',
data: {'interaction_id': interactionId}, data: {'interaction_id': interactionId},
success: function(response) { success: function(response) {
@ -592,39 +592,39 @@ function viewInteraction(interactionId) {
}); });
} }
function showManagement(interactionId) { {#function showManagement(interactionId) {#}
$.ajax({ {# $.ajax({#}
url: '{% url "pharmacy:interaction_management" %}', {# url: '{% url "pharmacy:interaction_management" %}',#}
method: 'GET', {# method: 'GET',#}
data: {'interaction_id': interactionId}, {# data: {'interaction_id': interactionId},#}
success: function(response) { {# success: function(response) {#}
toastr.info(response.management, 'Management Recommendation', { {# toastr.info(response.management, 'Management Recommendation', {#}
timeOut: 10000, {# timeOut: 10000,#}
extendedTimeOut: 5000 {# extendedTimeOut: 5000#}
}); {# });#}
}, {# },#}
error: function() { {# error: function() {#}
toastr.error('Failed to load management recommendations'); {# toastr.error('Failed to load management recommendations');#}
} {# }#}
}); {# });#}
} {# }#}
function addToMonitoring(interactionId) { {#function addToMonitoring(interactionId) {#}
$.ajax({ {# $.ajax({#}
url: '{% url "pharmacy:add_to_monitoring" %}', {# url: '{% url "pharmacy:add_to_monitoring" %}',#}
method: 'POST', {# method: 'POST',#}
data: { {# data: {#}
'interaction_id': interactionId, {# 'interaction_id': interactionId,#}
'csrfmiddlewaretoken': '{{ csrf_token }}' {# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
}, {# },#}
success: function(response) { {# success: function(response) {#}
toastr.success('Interaction added to monitoring list'); {# toastr.success('Interaction added to monitoring list');#}
}, {# },#}
error: function() { {# error: function() {#}
toastr.error('Failed to add interaction to monitoring'); {# toastr.error('Failed to add interaction to monitoring');#}
} {# }#}
}); {# });#}
} {# }#}
function addCurrentToMonitoring() { function addCurrentToMonitoring() {
if (currentInteractionId) { if (currentInteractionId) {
@ -633,57 +633,57 @@ function addCurrentToMonitoring() {
} }
} }
function addToMonitoringBulk() { {#function addToMonitoringBulk() {#}
if (selectedInteractions.length === 0) { {# if (selectedInteractions.length === 0) {#}
toastr.warning('Please select interactions to add to monitoring'); {# toastr.warning('Please select interactions to add to monitoring');#}
return; {# return;#}
} {# }#}
{# #}
$.ajax({ {# $.ajax({#}
url: '{% url "pharmacy:add_to_monitoring_bulk" %}', {# url: '{% url "pharmacy:add_to_monitoring_bulk" %}',#}
method: 'POST', {# method: 'POST',#}
data: { {# data: {#}
'interaction_ids': selectedInteractions, {# 'interaction_ids': selectedInteractions,#}
'csrfmiddlewaretoken': '{{ csrf_token }}' {# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
}, {# },#}
success: function(response) { {# success: function(response) {#}
toastr.success(selectedInteractions.length + ' interactions added to monitoring'); {# toastr.success(selectedInteractions.length + ' interactions added to monitoring');#}
$('.row-checkbox:checked').prop('checked', false); {# $('.row-checkbox:checked').prop('checked', false);#}
updateSelectedInteractions(); {# updateSelectedInteractions();#}
}, {# },#}
error: function() { {# error: function() {#}
toastr.error('Failed to add interactions to monitoring'); {# toastr.error('Failed to add interactions to monitoring');#}
} {# }#}
}); {# });#}
} {# }#}
function checkPatientInteractions() { function checkPatientInteractions() {
$('#patientCheckModal').modal('show'); $('#patientCheckModal').modal('show');
} }
function checkPatientMedications() { {#function checkPatientMedications() {#}
var patientId = $('#patient-select').val(); {# var patientId = $('#patient-select').val();#}
{# #}
if (!patientId) { {# if (!patientId) {#}
toastr.warning('Please select a patient'); {# toastr.warning('Please select a patient');#}
return; {# return;#}
} {# }#}
{# #}
$.ajax({ {# $.ajax({#}
url: '{% url "pharmacy:check_patient_interactions" %}', {# url: '{% url "pharmacy:check_patient_interactions" %}',#}
method: 'POST', {# method: 'POST',#}
data: { {# data: {#}
'patient_id': patientId, {# 'patient_id': patientId,#}
'csrfmiddlewaretoken': '{{ csrf_token }}' {# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
}, {# },#}
success: function(response) { {# success: function(response) {#}
$('#patient-interaction-results').html(response.html); {# $('#patient-interaction-results').html(response.html);#}
}, {# },#}
error: function() { {# error: function() {#}
toastr.error('Failed to check patient interactions'); {# toastr.error('Failed to check patient interactions');#}
} {# }#}
}); {# });#}
} {# }#}
function bulkInteractionCheck() { function bulkInteractionCheck() {
// Implementation for bulk interaction checking // Implementation for bulk interaction checking
@ -696,35 +696,35 @@ function exportInteractions() {
window.open('?' + params.toString(), '_blank'); window.open('?' + params.toString(), '_blank');
} }
function exportSelectedInteractions() { {#function exportSelectedInteractions() {#}
if (selectedInteractions.length === 0) { {# if (selectedInteractions.length === 0) {#}
toastr.warning('Please select interactions to export'); {# toastr.warning('Please select interactions to export');#}
return; {# return;#}
} {# }#}
{# #}
var form = $('<form>', { {# var form = $('<form>', {#}
method: 'POST', {# method: 'POST',#}
action: '{% url "pharmacy:export_interactions" %}' {# action: '{% url "pharmacy:export_interactions" %}'#}
}); {# });#}
{# #}
form.append($('<input>', { {# form.append($('<input>', {#}
type: 'hidden', {# type: 'hidden',#}
name: 'csrfmiddlewaretoken', {# name: 'csrfmiddlewaretoken',#}
value: '{{ csrf_token }}' {# value: '{{ csrf_token }}'#}
})); {# }));#}
{# #}
selectedInteractions.forEach(function(id) { {# selectedInteractions.forEach(function(id) {#}
form.append($('<input>', { {# form.append($('<input>', {#}
type: 'hidden', {# type: 'hidden',#}
name: 'interaction_ids', {# name: 'interaction_ids',#}
value: id {# value: id#}
})); {# }));#}
}); {# });#}
{# #}
$('body').append(form); {# $('body').append(form);#}
form.submit(); {# form.submit();#}
form.remove(); {# form.remove();#}
} {# }#}
function generateReport() { function generateReport() {
if (selectedInteractions.length === 0) { if (selectedInteractions.length === 0) {
@ -746,13 +746,13 @@ function createAlert() {
toastr.info('Alert creation feature coming soon'); toastr.info('Alert creation feature coming soon');
} }
function printPatientReport() { {#function printPatientReport() {#}
var patientId = $('#patient-select').val(); {# var patientId = $('#patient-select').val();#}
if (patientId) { {# if (patientId) {#}
var printUrl = '{% url "pharmacy:print_patient_interactions" %}?patient_id=' + patientId; {# var printUrl = '{% url "pharmacy:print_patient_interactions" %}?patient_id=' + patientId;#}
window.open(printUrl, '_blank'); {# window.open(printUrl, '_blank');#}
} {# }#}
} {# }#}
function clearFilters() { function clearFilters() {
$('#filter-form')[0].reset(); $('#filter-form')[0].reset();

View File

@ -474,7 +474,7 @@ function initializeFormBehavior() {
function loadMedicationInfo(medicationId) { function loadMedicationInfo(medicationId) {
$.ajax({ $.ajax({
url: '{% url "pharmacy:medication_info" %}', url: '{% url "pharmacy:get_medication_info" 0 %}'.replace('0', medicationId),
method: 'GET', method: 'GET',
data: {'medication_id': medicationId}, data: {'medication_id': medicationId},
success: function(response) { success: function(response) {
@ -641,50 +641,50 @@ function calculateReorderLevel() {
validateStockLevels(); validateStockLevels();
} }
function checkDuplicates() { {#function checkDuplicates() {#}
var medicationId = $('#{{ form.medication.id_for_label }}').val(); {# var medicationId = $('#{{ form.medication.id_for_label }}').val();#}
var locationId = $('#{{ form.location.id_for_label }}').val(); {# var locationId = $('#{{ form.location.id_for_label }}').val();#}
{# #}
if (!medicationId || !locationId) { {# if (!medicationId || !locationId) {#}
return; {# return;#}
} {# }#}
{# #}
$.ajax({ {# $.ajax({#}
url: '{% url "pharmacy:check_inventory_duplicates" %}', {# url: '{% url "pharmacy:check_inventory_duplicates" %}',#}
method: 'POST', {# method: 'POST',#}
data: { {# data: {#}
'medication_id': medicationId, {# 'medication_id': medicationId,#}
'location_id': locationId, {# 'location_id': locationId,#}
{% if object %}'exclude_id': {{ object.pk }},{% endif %} {# {% if object %}'exclude_id': {{ object.pk }},{% endif %}#}
'csrfmiddlewaretoken': '{{ csrf_token }}' {# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
}, {# },#}
success: function(response) { {# success: function(response) {#}
if (response.exists) { {# if (response.exists) {#}
toastr.warning('Inventory record already exists for this medication at this location'); {# toastr.warning('Inventory record already exists for this medication at this location');#}
} {# }#}
}, {# },#}
error: function() { {# error: function() {#}
toastr.error('Failed to check for duplicates'); {# toastr.error('Failed to check for duplicates');#}
} {# }#}
}); {# });#}
} {# }#}
function saveDraft() { {#function saveDraft() {#}
var formData = $('#inventory-form').serialize(); {# var formData = $('#inventory-form').serialize();#}
{# #}
$.ajax({ {# $.ajax({#}
url: '{% url "pharmacy:save_inventory_draft" %}', {# url: '{% url "pharmacy:save_inventory_draft" %}',#}
method: 'POST', {# method: 'POST',#}
data: formData, {# data: formData,#}
success: function(response) { {# success: function(response) {#}
updateFormStatus('Draft saved', 'success'); {# updateFormStatus('Draft saved', 'success');#}
$('#last-saved').text(new Date().toLocaleTimeString()); {# $('#last-saved').text(new Date().toLocaleTimeString());#}
}, {# },#}
error: function() { {# error: function() {#}
updateFormStatus('Failed to save draft', 'danger'); {# updateFormStatus('Failed to save draft', 'danger');#}
} {# }#}
}); {# });#}
} {# }#}
function updateFormStatus(message, type) { function updateFormStatus(message, type) {
var alertClass = 'alert-' + type; var alertClass = 'alert-' + type;

View File

@ -27,7 +27,7 @@
<div class="panel-heading"> <div class="panel-heading">
<h4 class="panel-title">Medication Information</h4> <h4 class="panel-title">Medication Information</h4>
<div class="panel-heading-btn"> <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 <i class="fa fa-edit"></i> Edit
</a> </a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></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>
<div class="panel-body"> <div class="panel-body">
<div class="d-grid gap-2"> <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 <i class="fa fa-edit me-2"></i>Edit Medication
</a> </a>
<a href="{% url 'pharmacy:prescription_create' %}?medication={{ object.pk }}" class="btn btn-success"> <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()"> <button type="button" class="btn btn-warning" onclick="checkInteractions()">
<i class="fa fa-exclamation-triangle me-2"></i>Check Interactions <i class="fa fa-exclamation-triangle me-2"></i>Check Interactions
</button> </button>
<a href="{% url 'pharmacy:medication_print' object.pk %}" class="btn btn-secondary" target="_blank"> {# <a href="{% url 'pharmacy:medication_print' object.pk %}" class="btn btn-secondary" target="_blank">#}
<i class="fa fa-print me-2"></i>Print Information {# <i class="fa fa-print me-2"></i>Print Information#}
</a> {# </a>#}
{% if perms.pharmacy.delete_medication %} {% if perms.pharmacy.delete_medication %}
<a href="{% url 'pharmacy:medication_delete' object.pk %}" class="btn btn-danger"> <a href="{% url 'pharmacy:medication_delete' object.pk %}" class="btn btn-danger">
<i class="fa fa-trash me-2"></i>Delete Medication <i class="fa fa-trash me-2"></i>Delete Medication
@ -366,154 +366,154 @@ $(document).ready(function() {
}, 30000); }, 30000);
}); });
function loadInventoryStatus() { {#function loadInventoryStatus() {#}
$.ajax({ {# $.ajax({#}
url: '{% url "pharmacy:medication_inventory_status" object.pk %}', {# url: '{% url "pharmacy:medication_inventory_status" object.pk %}',#}
method: 'GET', {# method: 'GET',#}
success: function(response) { {# success: function(response) {#}
var html = ''; {# var html = '';#}
{# #}
if (response.inventory_items && response.inventory_items.length > 0) { {# if (response.inventory_items && response.inventory_items.length > 0) {#}
response.inventory_items.forEach(function(item) { {# response.inventory_items.forEach(function(item) {#}
var statusClass = 'success'; {# var statusClass = 'success';#}
var statusIcon = 'check'; {# var statusIcon = 'check';#}
{# #}
if (item.quantity <= item.reorder_level) { {# if (item.quantity <= item.reorder_level) {#}
statusClass = 'warning'; {# statusClass = 'warning';#}
statusIcon = 'exclamation-triangle'; {# statusIcon = 'exclamation-triangle';#}
} {# }#}
if (item.quantity <= 0) { {# if (item.quantity <= 0) {#}
statusClass = 'danger'; {# statusClass = 'danger';#}
statusIcon = 'times'; {# statusIcon = 'times';#}
} {# }#}
{# #}
html += '<div class="d-flex justify-content-between align-items-center mb-2">'; {# html += '<div class="d-flex justify-content-between align-items-center mb-2">';#}
html += '<div>'; {# html += '<div>';#}
html += '<div class="fw-bold">' + item.location_name + '</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 class="small text-muted">Lot: ' + (item.lot_number || 'N/A') + '</div>';#}
html += '</div>'; {# html += '</div>';#}
html += '<div class="text-end">'; {# html += '<div class="text-end">';#}
html += '<div class="badge bg-' + statusClass + '">'; {# html += '<div class="badge bg-' + statusClass + '">';#}
html += '<i class="fa fa-' + statusIcon + ' me-1"></i>' + item.quantity + ' ' + item.unit; {# html += '<i class="fa fa-' + statusIcon + ' me-1"></i>' + item.quantity + ' ' + item.unit;#}
html += '</div>'; {# html += '</div>';#}
if (item.expiry_date) { {# if (item.expiry_date) {#}
html += '<div class="small text-muted">Exp: ' + item.expiry_date + '</div>'; {# html += '<div class="small text-muted">Exp: ' + item.expiry_date + '</div>';#}
} {# }#}
html += '</div>'; {# html += '</div>';#}
html += '</div>'; {# html += '</div>';#}
}); {# });#}
{# #}
// Total quantity {# // Total quantity#}
html += '<hr>'; {# html += '<hr>';#}
html += '<div class="d-flex justify-content-between align-items-center">'; {# html += '<div class="d-flex justify-content-between align-items-center">';#}
html += '<div class="fw-bold">Total Quantity</div>'; {# html += '<div class="fw-bold">Total Quantity</div>';#}
html += '<div class="fw-bold">' + response.total_quantity + ' units</div>'; {# html += '<div class="fw-bold">' + response.total_quantity + ' units</div>';#}
html += '</div>'; {# html += '</div>';#}
} else { {# } else {#}
html = '<div class="text-center text-muted">'; {# html = '<div class="text-center text-muted">';#}
html += '<i class="fa fa-box-open fa-2x mb-2"></i>'; {# html += '<i class="fa fa-box-open fa-2x mb-2"></i>';#}
html += '<div>No inventory records found</div>'; {# html += '<div>No inventory records found</div>';#}
html += '</div>'; {# html += '</div>';#}
} {# }#}
{# #}
$('#inventory-status').html(html); {# $('#inventory-status').html(html);#}
}, {# },#}
error: function() { {# error: function() {#}
$('#inventory-status').html('<div class="text-danger text-center">Failed to load inventory status</div>'); {# $('#inventory-status').html('<div class="text-danger text-center">Failed to load inventory status</div>');#}
} {# }#}
}); {# });#}
} {# }#}
function loadPrescriptionStats() { {#function loadPrescriptionStats() {#}
$.ajax({ {# $.ajax({#}
url: '{% url "pharmacy:medication_prescription_stats" object.pk %}', {# url: '{% url "pharmacy:medication_prescription_stats" object.pk %}',#}
method: 'GET', {# method: 'GET',#}
success: function(response) { {# success: function(response) {#}
var html = ''; {# var html = '';#}
{# #}
html += '<div class="row text-center">'; {# html += '<div class="row text-center">';#}
html += '<div class="col-6">'; {# html += '<div class="col-6">';#}
html += '<div class="h4 mb-0">' + response.total_prescriptions + '</div>'; {# html += '<div class="h4 mb-0">' + response.total_prescriptions + '</div>';#}
html += '<div class="small text-muted">Total Prescriptions</div>'; {# html += '<div class="small text-muted">Total Prescriptions</div>';#}
html += '</div>'; {# html += '</div>';#}
html += '<div class="col-6">'; {# html += '<div class="col-6">';#}
html += '<div class="h4 mb-0">' + response.active_prescriptions + '</div>'; {# html += '<div class="h4 mb-0">' + response.active_prescriptions + '</div>';#}
html += '<div class="small text-muted">Active</div>'; {# html += '<div class="small text-muted">Active</div>';#}
html += '</div>'; {# html += '</div>';#}
html += '</div>'; {# html += '</div>';#}
{# #}
html += '<hr>'; {# html += '<hr>';#}
{# #}
html += '<div class="row text-center">'; {# html += '<div class="row text-center">';#}
html += '<div class="col-6">'; {# html += '<div class="col-6">';#}
html += '<div class="h5 mb-0">' + response.this_month + '</div>'; {# html += '<div class="h5 mb-0">' + response.this_month + '</div>';#}
html += '<div class="small text-muted">This Month</div>'; {# html += '<div class="small text-muted">This Month</div>';#}
html += '</div>'; {# html += '</div>';#}
html += '<div class="col-6">'; {# html += '<div class="col-6">';#}
html += '<div class="h5 mb-0">' + response.last_30_days + '</div>'; {# html += '<div class="h5 mb-0">' + response.last_30_days + '</div>';#}
html += '<div class="small text-muted">Last 30 Days</div>'; {# html += '<div class="small text-muted">Last 30 Days</div>';#}
html += '</div>'; {# html += '</div>';#}
html += '</div>'; {# html += '</div>';#}
{# #}
if (response.recent_prescriptions && response.recent_prescriptions.length > 0) { {# if (response.recent_prescriptions && response.recent_prescriptions.length > 0) {#}
html += '<hr>'; {# html += '<hr>';#}
html += '<div class="fw-bold mb-2">Recent Prescriptions</div>'; {# html += '<div class="fw-bold mb-2">Recent Prescriptions</div>';#}
response.recent_prescriptions.forEach(function(prescription) { {# response.recent_prescriptions.forEach(function(prescription) {#}
html += '<div class="d-flex justify-content-between align-items-center mb-1">'; {# 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">' + prescription.patient_name + '</div>';#}
html += '<div class="small text-muted">' + prescription.date + '</div>'; {# html += '<div class="small text-muted">' + prescription.date + '</div>';#}
html += '</div>'; {# html += '</div>';#}
}); {# });#}
} {# }#}
{# #}
$('#prescription-stats').html(html); {# $('#prescription-stats').html(html);#}
}, {# },#}
error: function() { {# error: function() {#}
$('#prescription-stats').html('<div class="text-danger text-center">Failed to load statistics</div>'); {# $('#prescription-stats').html('<div class="text-danger text-center">Failed to load statistics</div>');#}
} {# }#}
}); {# });#}
} {# }#}
function checkInteractions() { {#function checkInteractions() {#}
$.ajax({ {# $.ajax({#}
url: '{% url "pharmacy:medication_interactions" object.pk %}', {# url: '{% url "pharmacy:medication_interactions" object.pk %}',#}
method: 'GET', {# method: 'GET',#}
success: function(response) { {# success: function(response) {#}
var html = ''; {# var html = '';#}
{# #}
if (response.interactions && response.interactions.length > 0) { {# if (response.interactions && response.interactions.length > 0) {#}
html += '<div class="alert alert-warning">'; {# html += '<div class="alert alert-warning">';#}
html += '<h6 class="alert-heading">Known Drug Interactions</h6>'; {# html += '<h6 class="alert-heading">Known Drug Interactions</h6>';#}
html += '<ul class="mb-0">'; {# html += '<ul class="mb-0">';#}
response.interactions.forEach(function(interaction) { {# response.interactions.forEach(function(interaction) {#}
html += '<li><strong>' + interaction.drug_name + '</strong>: ' + interaction.description + '</li>'; {# html += '<li><strong>' + interaction.drug_name + '</strong>: ' + interaction.description + '</li>';#}
}); {# });#}
html += '</ul>'; {# html += '</ul>';#}
html += '</div>'; {# html += '</div>';#}
} else { {# } else {#}
html += '<div class="alert alert-success">'; {# html += '<div class="alert alert-success">';#}
html += '<i class="fa fa-check me-2"></i>No known drug interactions found in the database.'; {# html += '<i class="fa fa-check me-2"></i>No known drug interactions found in the database.';#}
html += '</div>'; {# html += '</div>';#}
} {# }#}
{# #}
if (response.contraindications && response.contraindications.length > 0) { {# if (response.contraindications && response.contraindications.length > 0) {#}
html += '<div class="alert alert-danger">'; {# html += '<div class="alert alert-danger">';#}
html += '<h6 class="alert-heading">Contraindications</h6>'; {# html += '<h6 class="alert-heading">Contraindications</h6>';#}
html += '<ul class="mb-0">'; {# html += '<ul class="mb-0">';#}
response.contraindications.forEach(function(contraindication) { {# response.contraindications.forEach(function(contraindication) {#}
html += '<li>' + contraindication + '</li>'; {# html += '<li>' + contraindication + '</li>';#}
}); {# });#}
html += '</ul>'; {# html += '</ul>';#}
html += '</div>'; {# html += '</div>';#}
} {# }#}
{# #}
$('#interactions-content').html(html); {# $('#interactions-content').html(html);#}
}, {# },#}
error: function() { {# error: function() {#}
$('#interactions-content').html('<div class="text-danger text-center">Failed to load interaction data</div>'); {# $('#interactions-content').html('<div class="text-danger text-center">Failed to load interaction data</div>');#}
} {# }#}
}); {# });#}
} {# }#}
function viewInventory() { function viewInventory() {
window.location.href = '{% url "pharmacy:inventory_list" %}?medication=' + {{ object.pk }}; window.location.href = '{% url "pharmacy:inventory_list" %}?medication=' + {{ object.pk }};

View File

@ -614,41 +614,41 @@ function validateForm() {
return isValid; return isValid;
} }
function validateNDC() { {#function validateNDC() {#}
var ndcNumber = $('#{{ form.ndc_number.id_for_label }}').val(); {# var ndcNumber = $('#{{ form.ndc_number.id_for_label }}').val();#}
{# #}
if (!ndcNumber) { {# if (!ndcNumber) {#}
$('#ndc-check').html('<i class="fa fa-times text-danger me-2"></i>NDC number not provided'); {# $('#ndc-check').html('<i class="fa fa-times text-danger me-2"></i>NDC number not provided');#}
return; {# return;#}
} {# }#}
{# #}
if (isValidNDC(ndcNumber)) { {# if (isValidNDC(ndcNumber)) {#}
$('#ndc-check').html('<i class="fa fa-check text-success me-2"></i>NDC format valid'); {# $('#ndc-check').html('<i class="fa fa-check text-success me-2"></i>NDC format valid');#}
{# #}
// Check NDC in database {# // Check NDC in database#}
$.ajax({ {# $.ajax({#}
url: '{% url "pharmacy:validate_ndc" %}', {# url: '{% url "pharmacy:validate_ndc" %}',#}
method: 'POST', {# method: 'POST',#}
data: { {# data: {#}
'ndc_number': ndcNumber, {# 'ndc_number': ndcNumber,#}
'csrfmiddlewaretoken': '{{ csrf_token }}' {# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
}, {# },#}
success: function(response) { {# success: function(response) {#}
if (response.exists) { {# if (response.exists) {#}
toastr.warning('NDC number already exists in database'); {# toastr.warning('NDC number already exists in database');#}
} else { {# } else {#}
toastr.success('NDC number is available'); {# toastr.success('NDC number is available');#}
} {# }#}
}, {# },#}
error: function() { {# error: function() {#}
toastr.error('Failed to validate NDC number'); {# toastr.error('Failed to validate NDC number');#}
} {# }#}
}); {# });#}
} else { {# } else {#}
$('#ndc-check').html('<i class="fa fa-times text-danger me-2"></i>Invalid NDC format'); {# $('#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'); {# toastr.error('NDC number format is invalid. Use format: 12345-678-90');#}
} {# }#}
} {# }#}
function isValidNDC(ndc) { function isValidNDC(ndc) {
// NDC format: 5-4-2 or 5-3-2 digits with hyphens // NDC format: 5-4-2 or 5-3-2 digits with hyphens
@ -682,54 +682,54 @@ function validateStrength() {
} }
} }
function checkDuplicates() { {#function checkDuplicates() {#}
var genericName = $('#{{ form.generic_name.id_for_label }}').val(); {# var genericName = $('#{{ form.generic_name.id_for_label }}').val();#}
var strength = $('#{{ form.strength.id_for_label }}').val(); {# var strength = $('#{{ form.strength.id_for_label }}').val();#}
{# #}
if (!genericName) { {# if (!genericName) {#}
return; {# return;#}
} {# }#}
{# #}
$.ajax({ {# $.ajax({#}
url: '{% url "pharmacy:check_medication_duplicates" %}', {# url: '{% url "pharmacy:check_medication_duplicates" %}',#}
method: 'POST', {# method: 'POST',#}
data: { {# data: {#}
'generic_name': genericName, {# 'generic_name': genericName,#}
'strength': strength, {# 'strength': strength,#}
{% if object %}'exclude_id': {{ object.pk }},{% endif %} {# {% if object %}'exclude_id': {{ object.pk }},{% endif %}#}
'csrfmiddlewaretoken': '{{ csrf_token }}' {# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
}, {# },#}
success: function(response) { {# success: function(response) {#}
if (response.duplicates.length > 0) { {# if (response.duplicates.length > 0) {#}
var message = 'Similar medications found:\n'; {# var message = 'Similar medications found:\n';#}
response.duplicates.forEach(function(med) { {# response.duplicates.forEach(function(med) {#}
message += '- ' + med.name + ' (' + med.strength + ')\n'; {# message += '- ' + med.name + ' (' + med.strength + ')\n';#}
}); {# });#}
toastr.warning(message); {# toastr.warning(message);#}
} {# }#}
}, {# },#}
error: function() { {# error: function() {#}
toastr.error('Failed to check for duplicates'); {# toastr.error('Failed to check for duplicates');#}
} {# }#}
}); {# });#}
} {# }#}
function saveDraft() { {#function saveDraft() {#}
var formData = $('#medication-form').serialize(); {# var formData = $('#medication-form').serialize();#}
{# #}
$.ajax({ {# $.ajax({#}
url: '{% url "pharmacy:save_medication_draft" %}', {# url: '{% url "pharmacy:save_medication_draft" %}',#}
method: 'POST', {# method: 'POST',#}
data: formData, {# data: formData,#}
success: function(response) { {# success: function(response) {#}
updateFormStatus('Draft saved', 'success'); {# updateFormStatus('Draft saved', 'success');#}
$('#last-saved').text(new Date().toLocaleTimeString()); {# $('#last-saved').text(new Date().toLocaleTimeString());#}
}, {# },#}
error: function() { {# error: function() {#}
updateFormStatus('Failed to save draft', 'danger'); {# updateFormStatus('Failed to save draft', 'danger');#}
} {# }#}
}); {# });#}
} {# }#}
function updateFormStatus(message, type) { function updateFormStatus(message, type) {
var alertClass = 'alert-' + type; var alertClass = 'alert-' + type;

View File

@ -112,66 +112,66 @@
<form method="get" class="row g-3" id="filterForm"> <form method="get" class="row g-3" id="filterForm">
<div class="col-lg-4 col-md-6"> <div class="col-lg-4 col-md-6">
<label for="search" class="form-label">Search Medications</label> <label for="search" class="form-label">Search Medications</label>
<div class="input-group"> {# <div class="input-group">#}
<span class="input-group-text"><i class="fas fa-search"></i></span> {# <span class="input-group-text"><i class="fas fa-search"></i></span>#}
<input type="text" {# <input type="text" #}
class="form-control" {# class="form-control" #}
id="search" {# id="search" #}
name="search" {# name="search" #}
value="{{ request.GET.search }}" {# value="{{ request.GET.search }}"#}
placeholder="Generic name, brand name, NDC..." {# placeholder="Generic name, brand name, NDC..."#}
hx-get="{% url 'pharmacy:medication_search' %}" {# hx-get="{% url 'pharmacy:medication_search' %}"#}
hx-target="#medication-list-container" {# hx-target="#medication-list-container"#}
hx-trigger="keyup changed delay:500ms" {# hx-trigger="keyup changed delay:500ms"#}
hx-include="#filterForm"> {# hx-include="#filterForm">#}
</div> {# </div>#}
</div> </div>
<div class="col-lg-2 col-md-6"> {# <div class="col-lg-2 col-md-6">#}
<label for="drug_class" class="form-label">Drug Class</label> {# <label for="drug_class" class="form-label">Drug Class</label>#}
<select class="form-select" id="drug_class" name="drug_class" {# <select class="form-select" id="drug_class" name="drug_class"#}
hx-get="{% url 'pharmacy:medication_search' %}" {# hx-get="{% url 'pharmacy:medication_search' %}"#}
hx-target="#medication-list-container" {# hx-target="#medication-list-container"#}
hx-trigger="change" {# hx-trigger="change"#}
hx-include="#filterForm"> {# hx-include="#filterForm">#}
<option value="">All Classes</option> {# <option value="">All Classes</option>#}
{% for class in drug_classes %} {# {% for class in drug_classes %}#}
<option value="{{ class }}" {% if request.GET.drug_class == class %}selected{% endif %}>{{ class }}</option> {# <option value="{{ class }}" {% if request.GET.drug_class == class %}selected{% endif %}>{{ class }}</option>#}
{% endfor %} {# {% endfor %}#}
</select> {# </select>#}
</div> {# </div>#}
<div class="col-lg-2 col-md-6"> {# <div class="col-lg-2 col-md-6">#}
<label for="controlled_schedule" class="form-label">Schedule</label> {# <label for="controlled_schedule" class="form-label">Schedule</label>#}
<select class="form-select" id="controlled_schedule" name="controlled_schedule" {# <select class="form-select" id="controlled_schedule" name="controlled_schedule"#}
hx-get="{% url 'pharmacy:medication_search' %}" {# hx-get="{% url 'pharmacy:medication_search' %}"#}
hx-target="#medication-list-container" {# hx-target="#medication-list-container"#}
hx-trigger="change" {# hx-trigger="change"#}
hx-include="#filterForm"> {# hx-include="#filterForm">#}
<option value="">All Schedules</option> {# <option value="">All Schedules</option>#}
<option value="NON" {% if request.GET.controlled_schedule == 'NON' %}selected{% endif %}>Non-Controlled</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="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="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="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="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> {# <option value="CV" {% if request.GET.controlled_schedule == 'CV' %}selected{% endif %}>Schedule V</option>#}
</select> {# </select>#}
</div> {# </div>#}
<div class="col-lg-2 col-md-6"> {# <div class="col-lg-2 col-md-6">#}
<label for="formulary_status" class="form-label">Formulary Status</label> {# <label for="formulary_status" class="form-label">Formulary Status</label>#}
<select class="form-select" id="formulary_status" name="formulary_status" {# <select class="form-select" id="formulary_status" name="formulary_status"#}
hx-get="{% url 'pharmacy:medication_search' %}" {# hx-get="{% url 'pharmacy:medication_search' %}"#}
hx-target="#medication-list-container" {# hx-target="#medication-list-container"#}
hx-trigger="change" {# hx-trigger="change"#}
hx-include="#filterForm"> {# hx-include="#filterForm">#}
<option value="">All Status</option> {# <option value="">All Status</option>#}
<option value="PREFERRED" {% if request.GET.formulary_status == 'PREFERRED' %}selected{% endif %}>Preferred</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="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="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> {# <option value="PRIOR_AUTH" {% if request.GET.formulary_status == 'PRIOR_AUTH' %}selected{% endif %}>Prior Auth Required</option>#}
</select> {# </select>#}
</div> {# </div>#}
<div class="col-lg-2 col-md-6"> <div class="col-lg-2 col-md-6">
<label class="form-label">&nbsp;</label> <label class="form-label">&nbsp;</label>
@ -310,7 +310,7 @@
<a href="{% url 'pharmacy:medication_detail' medication.pk %}" class="btn btn-outline-primary btn-sm" title="View Details"> <a href="{% url 'pharmacy:medication_detail' medication.pk %}" class="btn btn-outline-primary btn-sm" title="View Details">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</a> </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> <i class="fas fa-edit"></i>
</a> </a>
<button type="button" class="btn btn-outline-danger btn-sm" onclick="deleteMedication({{ medication.pk }})" title="Delete"> <button type="button" class="btn btn-outline-danger btn-sm" onclick="deleteMedication({{ medication.pk }})" title="Delete">
@ -335,37 +335,9 @@
</div> </div>
<!-- Pagination --> <!-- Pagination -->
{% if page_obj.has_other_pages %} {% if is_paginated %}
<div class="card-footer"> {% include 'partial/pagination.html' %}
<nav aria-label="Medication pagination"> {% endif %}
<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 %}
</div> </div>
</div> </div>
</div> </div>

View File

@ -492,7 +492,7 @@ function initializeFormBehavior() {
function loadPatientInfo(patientId) { function loadPatientInfo(patientId) {
$.ajax({ $.ajax({
url: '{% url "pharmacy:get_patient_info" 0 %}'.replace('0', patientId), url: '{% url "patients:get_patient_info" 0 %}'.replace('0', patientId),
method: 'GET', method: 'GET',
success: function(response) { success: function(response) {
var patient = response.patient; var patient = response.patient;
@ -624,131 +624,131 @@ function validateForm() {
return isValid; return isValid;
} }
function saveDraft() { {#function saveDraft() {#}
var formData = $('#prescription-form').serialize(); {# var formData = $('#prescription-form').serialize();#}
{# #}
$.ajax({ {# $.ajax({#}
url: '{% url "pharmacy:save_prescription_draft" %}', {# url: '{% url "pharmacy:save_prescription_draft" %}',#}
method: 'POST', {# method: 'POST',#}
data: formData, {# data: formData,#}
success: function(response) { {# success: function(response) {#}
updatePrescriptionStatus('Draft saved', 'success'); {# updatePrescriptionStatus('Draft saved', 'success');#}
$('#last-saved').text(new Date().toLocaleTimeString()); {# $('#last-saved').text(new Date().toLocaleTimeString());#}
}, {# },#}
error: function() { {# error: function() {#}
updatePrescriptionStatus('Failed to save draft', 'danger'); {# updatePrescriptionStatus('Failed to save draft', 'danger');#}
} {# }#}
}); {# });#}
} {# }#}
function updatePrescriptionStatus(message, type) { function updatePrescriptionStatus(message, type) {
var alertClass = 'alert-' + type; var alertClass = 'alert-' + type;
$('#prescription-status').html('<div class="alert ' + alertClass + '"><i class="fa fa-info-circle me-2"></i>' + message + '</div>'); $('#prescription-status').html('<div class="alert ' + alertClass + '"><i class="fa fa-info-circle me-2"></i>' + message + '</div>');
} }
function checkInteractions() { {#function checkInteractions() {#}
var patientId = $('#{{ form.patient.id_for_label }}').val(); {# var patientId = $('#{{ form.patient.id_for_label }}').val();#}
var medicationId = $('#{{ form.medication.id_for_label }}').val(); {# var medicationId = $('#{{ form.medication.id_for_label }}').val();#}
{# #}
if (!patientId || !medicationId) { {# if (!patientId || !medicationId) {#}
toastr.warning('Please select both patient and medication first'); {# toastr.warning('Please select both patient and medication first');#}
return; {# return;#}
} {# }#}
{# #}
$.ajax({ {# $.ajax({#}
url: '{% url "pharmacy:check_drug_interactions" %}', {# url: '{% url "pharmacy:check_drug_interactions" %}',#}
method: 'POST', {# method: 'POST',#}
data: { {# data: {#}
'patient_id': patientId, {# 'patient_id': patientId,#}
'medication_id': medicationId, {# 'medication_id': medicationId,#}
'csrfmiddlewaretoken': '{{ csrf_token }}' {# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
}, {# },#}
success: function(response) { {# success: function(response) {#}
if (response.interactions.length > 0) { {# if (response.interactions.length > 0) {#}
var message = 'Drug interactions found:\n'; {# var message = 'Drug interactions found:\n';#}
response.interactions.forEach(function(interaction) { {# response.interactions.forEach(function(interaction) {#}
message += '- ' + interaction.description + '\n'; {# message += '- ' + interaction.description + '\n';#}
}); {# });#}
toastr.warning(message); {# toastr.warning(message);#}
$('#interaction-check').html('<i class="fa fa-exclamation-triangle text-warning me-2"></i>Interactions found'); {# $('#interaction-check').html('<i class="fa fa-exclamation-triangle text-warning me-2"></i>Interactions found');#}
} else { {# } else {#}
toastr.success('No drug interactions found'); {# toastr.success('No drug interactions found');#}
$('#interaction-check').html('<i class="fa fa-check text-success me-2"></i>No interactions found'); {# $('#interaction-check').html('<i class="fa fa-check text-success me-2"></i>No interactions found');#}
} {# }#}
}, {# },#}
error: function() { {# error: function() {#}
toastr.error('Failed to check drug interactions'); {# toastr.error('Failed to check drug interactions');#}
} {# }#}
}); {# });#}
} {# }#}
function checkAllergies() { {#function checkAllergies() {#}
var patientId = $('#{{ form.patient.id_for_label }}').val(); {# var patientId = $('#{{ form.patient.id_for_label }}').val();#}
var medicationId = $('#{{ form.medication.id_for_label }}').val(); {# var medicationId = $('#{{ form.medication.id_for_label }}').val();#}
{# #}
if (!patientId || !medicationId) { {# if (!patientId || !medicationId) {#}
return; {# return;#}
} {# }#}
{# #}
$.ajax({ {# $.ajax({#}
url: '{% url "pharmacy:check_allergies" %}', {# url: '{% url "pharmacy:check_allergies" %}',#}
method: 'POST', {# method: 'POST',#}
data: { {# data: {#}
'patient_id': patientId, {# 'patient_id': patientId,#}
'medication_id': medicationId, {# 'medication_id': medicationId,#}
'csrfmiddlewaretoken': '{{ csrf_token }}' {# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
}, {# },#}
success: function(response) { {# success: function(response) {#}
if (response.allergies.length > 0) { {# if (response.allergies.length > 0) {#}
var message = 'Allergy alert:\n'; {# var message = 'Allergy alert:\n';#}
response.allergies.forEach(function(allergy) { {# response.allergies.forEach(function(allergy) {#}
message += '- ' + allergy.allergen + ' (' + allergy.reaction + ')\n'; {# message += '- ' + allergy.allergen + ' (' + allergy.reaction + ')\n';#}
}); {# });#}
toastr.error(message); {# toastr.error(message);#}
$('#allergy-check').html('<i class="fa fa-exclamation-triangle text-danger me-2"></i>Allergy alert'); {# $('#allergy-check').html('<i class="fa fa-exclamation-triangle text-danger me-2"></i>Allergy alert');#}
} else { {# } else {#}
$('#allergy-check').html('<i class="fa fa-check text-success me-2"></i>No allergies found'); {# $('#allergy-check').html('<i class="fa fa-check text-success me-2"></i>No allergies found');#}
} {# }#}
}, {# },#}
error: function() { {# error: function() {#}
toastr.error('Failed to check allergies'); {# toastr.error('Failed to check allergies');#}
} {# }#}
}); {# });#}
} {# }#}
function verifyDosage() { {#function verifyDosage() {#}
var dosage = $('#{{ form.dosage.id_for_label }}').val(); {# var dosage = $('#{{ form.dosage.id_for_label }}').val();#}
var medicationId = $('#{{ form.medication.id_for_label }}').val(); {# var medicationId = $('#{{ form.medication.id_for_label }}').val();#}
var patientId = $('#{{ form.patient.id_for_label }}').val(); {# var patientId = $('#{{ form.patient.id_for_label }}').val();#}
{# #}
if (!dosage || !medicationId || !patientId) { {# if (!dosage || !medicationId || !patientId) {#}
toastr.warning('Please fill in dosage, medication, and patient first'); {# toastr.warning('Please fill in dosage, medication, and patient first');#}
return; {# return;#}
} {# }#}
{# #}
$.ajax({ {# $.ajax({#}
url: '{% url "pharmacy:verify_dosage" %}', {# url: '{% url "pharmacy:verify_dosage" %}',#}
method: 'POST', {# method: 'POST',#}
data: { {# data: {#}
'dosage': dosage, {# 'dosage': dosage,#}
'medication_id': medicationId, {# 'medication_id': medicationId,#}
'patient_id': patientId, {# 'patient_id': patientId,#}
'csrfmiddlewaretoken': '{{ csrf_token }}' {# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
}, {# },#}
success: function(response) { {# success: function(response) {#}
if (response.is_valid) { {# if (response.is_valid) {#}
toastr.success('Dosage is within normal range'); {# toastr.success('Dosage is within normal range');#}
$('#dosage-check').html('<i class="fa fa-check text-success me-2"></i>Dosage verified'); {# $('#dosage-check').html('<i class="fa fa-check text-success me-2"></i>Dosage verified');#}
} else { {# } else {#}
toastr.warning('Dosage may be outside normal range: ' + response.message); {# 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'); {# $('#dosage-check').html('<i class="fa fa-exclamation-triangle text-warning me-2"></i>Dosage warning');#}
} {# }#}
}, {# },#}
error: function() { {# error: function() {#}
toastr.error('Failed to verify dosage'); {# toastr.error('Failed to verify dosage');#}
} {# }#}
}); {# });#}
} {# }#}
function loadTemplate() { function loadTemplate() {
// Implementation for loading prescription templates // Implementation for loading prescription templates