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
# dashboard = BillingDashboardView.as_view()
# bill_list = MedicalBillListView.as_view()
# bill_detail = MedicalBillDetailView.as_view()
# bill_create = MedicalBillCreateView.as_view()
# claim_list = InsuranceClaimListView.as_view()
# payment_list = PaymentListView.as_view()
dashboard = BillingDashboardView.as_view()
bill_list = MedicalBillListView.as_view()
bill_detail = MedicalBillDetailView.as_view()
bill_create = MedicalBillCreateView.as_view()
claim_list = InsuranceClaimListView.as_view()
payment_list = PaymentListView.as_view()
#
#
#

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

View File

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

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -35,14 +35,15 @@ urlpatterns = [
# HTMX views
path('htmx/patient-search/', views.patient_search, name='patient_search'),
path('htmx/patient-stats/', views.patient_stats, name='patient_stats'),
path('htmx/emergency-contacts/<int:patient_id>/', views.emergency_contacts_list, name='emergency_contacts_list'),
path('htmx/insurance-info/<int:patient_id>/', views.insurance_info_list, name='insurance_info_list'),
path('htmx/consent-forms/<int:patient_id>/', views.consent_forms_list, name='consent_forms_list'),
path('htmx/patient-notes/<int:patient_id>/', views.patient_notes_list, name='patient_notes_list'),
path('htmx/add-patient-note/<int:patient_id>/', views.add_patient_note, name='add_patient_note'),
path('htmx/sign-consent/<int:pk>/', views.sign_consent_form, name='sign_consent_form'),
path('htmx/appointments/<int:patient_id>/', views.patient_appointment_list, name='patient_appointments')
path('patient-search/', views.patient_search, name='patient_search'),
path('patient-stats/', views.patient_stats, name='patient_stats'),
path('emergency-contacts/<int:patient_id>/', views.emergency_contacts_list, name='emergency_contacts_list'),
path('insurance-info/<int:patient_id>/', views.insurance_info_list, name='insurance_info_list'),
path('consent-forms/<int:patient_id>/', views.consent_forms_list, name='consent_forms_list'),
path('patient-notes/<int:patient_id>/', views.patient_notes_list, name='patient_notes_list'),
path('add-patient-note/<int:patient_id>/', views.add_patient_note, name='add_patient_note'),
path('sign-consent/<int:pk>/', views.sign_consent_form, name='sign_consent_form'),
path('appointments/<int:patient_id>/', views.patient_appointment_list, name='patient_appointments'),
path('patient-info/<int:pk>/', views.get_patient_info, name='get_patient_info')
]

View File

@ -1315,6 +1315,20 @@ def add_patient_note(request, patient_id):
'patient': patient
})
def get_patient_info(request, pk):
patient = get_object_or_404(PatientProfile, pk=pk)
patient_info = {
'id': patient.id,
'first_name': patient.first_name,
'last_name': patient.last_name,
'gender': patient.get_gender_display(),
'date_of_birth': patient.date_of_birth,
'phone_number': patient.phone_number,
'email': patient.email,
}
return JsonResponse(patient_info)
#
# """
# Patients app views for hospital management system with comprehensive CRUD operations.

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.
"""
CONTROLLED_SUBSTANCE_SCHEDULE_CHOICES = [
('CI', 'Schedule I'),
('CII', 'Schedule II'),
('CIII', 'Schedule III'),
('CIV', 'Schedule IV'),
('CV', 'Schedule V'),
('NON', 'Non-Controlled'),
]
DOSAGE_FORM_CHOICES = [
('TABLET', 'Tablet'),
('CAPSULE', 'Capsule'),
('LIQUID', 'Liquid'),
('INJECTION', 'Injection'),
('TOPICAL', 'Topical'),
('INHALER', 'Inhaler'),
('PATCH', 'Patch'),
('SUPPOSITORY', 'Suppository'),
('CREAM', 'Cream'),
('OINTMENT', 'Ointment'),
('DROPS', 'Drops'),
('SPRAY', 'Spray'),
('OTHER', 'Other'),
]
UNIT_OF_MEASURE_CHOICES = [
('MG', 'Milligrams'),
('G', 'Grams'),
('MCG', 'Micrograms'),
('ML', 'Milliliters'),
('L', 'Liters'),
('UNITS', 'Units'),
('IU', 'International Units'),
('MEQ', 'Milliequivalents'),
('PERCENT', 'Percent'),
('OTHER', 'Other'),
]
FORMULARY_STATUS_CHOICES = [
('PREFERRED', 'Preferred'),
('NON_PREFERRED', 'Non-Preferred'),
('RESTRICTED', 'Restricted'),
('NOT_COVERED', 'Not Covered'),
('PRIOR_AUTH', 'Prior Authorization Required'),
]
# Tenant relationship
tenant = models.ForeignKey(
'core.Tenant',
@ -68,14 +110,7 @@ class Medication(models.Model):
)
controlled_substance_schedule = models.CharField(
max_length=5,
choices=[
('CI', 'Schedule I'),
('CII', 'Schedule II'),
('CIII', 'Schedule III'),
('CIV', 'Schedule IV'),
('CV', 'Schedule V'),
('NON', 'Non-Controlled'),
],
choices=CONTROLLED_SUBSTANCE_SCHEDULE_CHOICES,
default='NON',
help_text='DEA controlled substance schedule'
)
@ -83,21 +118,7 @@ class Medication(models.Model):
# Formulation
dosage_form = models.CharField(
max_length=50,
choices=[
('TABLET', 'Tablet'),
('CAPSULE', 'Capsule'),
('LIQUID', 'Liquid'),
('INJECTION', 'Injection'),
('TOPICAL', 'Topical'),
('INHALER', 'Inhaler'),
('PATCH', 'Patch'),
('SUPPOSITORY', 'Suppository'),
('CREAM', 'Cream'),
('OINTMENT', 'Ointment'),
('DROPS', 'Drops'),
('SPRAY', 'Spray'),
('OTHER', 'Other'),
],
choices=DOSAGE_FORM_CHOICES,
help_text='Dosage form'
)
strength = models.CharField(
@ -106,18 +127,7 @@ class Medication(models.Model):
)
unit_of_measure = models.CharField(
max_length=20,
choices=[
('MG', 'Milligrams'),
('G', 'Grams'),
('MCG', 'Micrograms'),
('ML', 'Milliliters'),
('L', 'Liters'),
('UNITS', 'Units'),
('IU', 'International Units'),
('MEQ', 'Milliequivalents'),
('PERCENT', 'Percent'),
('OTHER', 'Other'),
],
choices=UNIT_OF_MEASURE_CHOICES,
help_text='Unit of measure'
)
@ -188,13 +198,7 @@ class Medication(models.Model):
# Formulary Status
formulary_status = models.CharField(
max_length=20,
choices=[
('PREFERRED', 'Preferred'),
('NON_PREFERRED', 'Non-Preferred'),
('RESTRICTED', 'Restricted'),
('NOT_COVERED', 'Not Covered'),
('PRIOR_AUTH', 'Prior Authorization Required'),
],
choices=FORMULARY_STATUS_CHOICES,
default='PREFERRED',
help_text='Formulary status'
)
@ -290,7 +294,31 @@ class Prescription(models.Model):
"""
Prescription model for electronic prescription management.
"""
QUANTITY_UNIT_CHOICES = [
('TABLETS', 'Tablets'),
('CAPSULES', 'Capsules'),
('ML', 'Milliliters'),
('GRAMS', 'Grams'),
('UNITS', 'Units'),
('PATCHES', 'Patches'),
('INHALERS', 'Inhalers'),
('BOTTLES', 'Bottles'),
('TUBES', 'Tubes'),
('VIALS', 'Vials'),
('OTHER', 'Other'),
]
STATUS_CHOICES = [
('PENDING', 'Pending'),
('ACTIVE', 'Active'),
('DISPENSED', 'Dispensed'),
('PARTIALLY_DISPENSED', 'Partially Dispensed'),
('COMPLETED', 'Completed'),
('CANCELLED', 'Cancelled'),
('EXPIRED', 'Expired'),
('ON_HOLD', 'On Hold'),
('TRANSFERRED', 'Transferred'),
('DRAFT', 'Draft'),
]
# Tenant relationship
tenant = models.ForeignKey(
'core.Tenant',
@ -340,19 +368,7 @@ class Prescription(models.Model):
)
quantity_unit = models.CharField(
max_length=20,
choices=[
('TABLETS', 'Tablets'),
('CAPSULES', 'Capsules'),
('ML', 'Milliliters'),
('GRAMS', 'Grams'),
('UNITS', 'Units'),
('PATCHES', 'Patches'),
('INHALERS', 'Inhalers'),
('BOTTLES', 'Bottles'),
('TUBES', 'Tubes'),
('VIALS', 'Vials'),
('OTHER', 'Other'),
],
choices=QUANTITY_UNIT_CHOICES,
help_text='Unit of quantity'
)
@ -398,17 +414,7 @@ class Prescription(models.Model):
# Status
status = models.CharField(
max_length=20,
choices=[
('PENDING', 'Pending'),
('ACTIVE', 'Active'),
('DISPENSED', 'Dispensed'),
('PARTIALLY_DISPENSED', 'Partially Dispensed'),
('COMPLETED', 'Completed'),
('CANCELLED', 'Cancelled'),
('EXPIRED', 'Expired'),
('ON_HOLD', 'On Hold'),
('TRANSFERRED', 'Transferred'),
],
choices=STATUS_CHOICES,
default='PENDING',
help_text='Prescription status'
)
@ -577,7 +583,15 @@ class InventoryItem(models.Model):
"""
Inventory item model for pharmacy stock management.
"""
STATUS_CHOICES = [
('ACTIVE', 'Active'),
('QUARANTINE', 'Quarantine'),
('EXPIRED', 'Expired'),
('RECALLED', 'Recalled'),
('DAMAGED', 'Damaged'),
('RETURNED', 'Returned'),
]
# Tenant relationship
tenant = models.ForeignKey(
'core.Tenant',
@ -684,14 +698,7 @@ class InventoryItem(models.Model):
# Status
status = models.CharField(
max_length=20,
choices=[
('ACTIVE', 'Active'),
('QUARANTINE', 'Quarantine'),
('EXPIRED', 'Expired'),
('RECALLED', 'Recalled'),
('DAMAGED', 'Damaged'),
('RETURNED', 'Returned'),
],
choices=STATUS_CHOICES,
default='ACTIVE',
help_text='Inventory status'
)

View File

@ -21,6 +21,7 @@ urlpatterns = [
# path('inventory/<int:pk>/delete/', views.InventoryItemDeleteView.as_view(), name='inventory_delete'),
path('dispense-records/', views.DispenseRecordListView.as_view(), name='dispense_record_list'),
path('drug-interactions/', views.DrugInteractionListView.as_view(), name='drug_interaction_list'),
path('drug-interactions/<int:pk>/', views.DrugInteractionDetailView.as_view(), name='drug_interaction_detail'),
path('medications/create/', views.MedicationCreateView.as_view(), name='medication_create'),
path('medications/<int:pk>/', views.MedicationDetailView.as_view(), name='medication_detail'),
@ -39,6 +40,8 @@ urlpatterns = [
path('prescription/<int:prescription_id>/dispense/', views.dispense_medication, name='dispense_medication'),
path('inventory/<int:item_id>/update/', views.update_inventory, name='update_inventory'),
path('inventory-adjustment/<int:item_id>/', views.adjust_inventory, name='adjust_inventory'),
path('medication/<int:medication_id>/', views.get_medication_info, name='get_medication_info'),
path('prescription/<int:prescription_id>/draft/', views.save_prescription_draft, name='save_prescription_draft'),
# API endpoints
# path('api/', include('pharmacy.api.urls')),

View File

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

BIN
templates/.DS_Store vendored

Binary file not shown.

View File

@ -42,7 +42,7 @@
</a></li>
<li><hr class="dropdown-divider"></li>
{% if object.status == 'DRAFT' %}
<li><a class="dropdown-item text-warning" href="{% url 'billing:bill_submit' object.bill_id %}">
<li><a class="dropdown-item text-warning" href="{% url 'billing:submit_bill' object.bill_id %}">
<i class="fas fa-paper-plane me-2"></i>Submit Bill
</a></li>
{% endif %}

View File

@ -22,6 +22,7 @@
</a>
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown">
<span class="visually-hidden">Toggle Dropdown</span>
<i class="fas fa-ellipsis-v"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{% url 'billing:export_bills' %}">
@ -42,7 +43,7 @@
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="card-title mb-1 opacity-75">Total Bills</h6>
<h3 class="mb-0">{{ stats.total_bills|default:0 }}</h3>
<h3 class="mb-0">{{ stats.total_bills }}</h3>
</div>
<div class="text-white-50">
<i class="fas fa-file-invoice fa-2x"></i>
@ -282,6 +283,7 @@
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown">
<span class="visually-hidden">Toggle Dropdown</span>
<i class="fas fa-ellipsis-v"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="">
@ -328,48 +330,9 @@
</div>
<!-- Pagination -->
{% if page_obj.has_other_pages %}
<div class="card-footer">
<div class="d-flex justify-content-between align-items-center">
<div class="text-muted">
Showing {{ page_obj.start_index }} to {{ page_obj.end_index }} of {{ page_obj.paginator.count }} bills
</div>
<nav aria-label="Bills pagination">
<ul class="pagination pagination-sm mb-0">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page=1">First</a>
</li>
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.previous_page_number }}">Previous</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<span class="page-link">{{ num }}</span>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ num }}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.next_page_number }}">Next</a>
</li>
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.paginator.num_pages }}">Last</a>
</li>
{% endif %}
</ul>
</nav>
</div>
</div>
{% endif %}
{% if is_paginated %}
{% include 'partial/pagination.html' %}
{% endif %}
</div>
</div>

View File

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

BIN
templates/laboratory/.DS_Store vendored Normal file

Binary file not shown.

View File

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

View File

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

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 class="ms-auto">
<div class="btn-group">
<a href="{% url 'laboratory:specimen_form' object.pk %}" class="btn btn-primary">
<a href="{% url 'laboratory:specimen_update' object.pk %}" class="btn btn-primary">
<i class="fas fa-edit me-2"></i>Edit Specimen
</a>
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown">
<span class="visually-hidden">Toggle Dropdown</span>
<i class="fas fa-ellipsis-v"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="printLabel()">
@ -35,9 +36,9 @@
<li><a class="dropdown-item" href="#" onclick="generateReport()">
<i class="fas fa-chart-line me-2"></i>Generate Report
</a></li>
<li><a class="dropdown-item text-danger" href="{% url 'laboratory:specimen_confirm_delete' object.pk %}">
<i class="fas fa-trash me-2"></i>Delete Specimen
</a></li>
{# <li><a class="dropdown-item text-danger" href="{% url 'laboratory:specimen_confirm_delete' object.pk %}">#}
{# <i class="fas fa-trash me-2"></i>Delete Specimen#}
{# </a></li>#}
</ul>
</div>
</div>
@ -76,19 +77,32 @@
</tr>
<tr>
<td class="fw-bold">MRN:</td>
<td>{{ object.patient.medical_record_number }}</td>
<td>{{ object.patient.mrn }}</td>
</tr>
<tr>
<td class="fw-bold">Type:</td>
<td>
<span class="badge bg-light text-dark">
{{ object.get_specimen_type_display }}
</span>
<i class="fas
fa-{% if specimen.specimen_type == 'BLOOD' %}tint text-danger
{% elif specimen.specimen_type == 'URINE' %}flask text-warning
{% elif specimen.specimen_type == 'SPUTUM' %}lungs text-info
{% elif specimen.specimen_type == 'PLASMA' %}tint text-yellow
{% elif specimen.specimen_type == 'SERUM' %}bottle-droplet text-info
{% elif specimen.specimen_type == 'STOOL' %}poop text-cyan
{% elif specimen.specimen_type == 'CSF' %}eye text-primary
{% elif specimen.specimen_type == 'SWAB' %}hand text-secondary
{% elif specimen.specimen_type == 'TISSUE' %}microscope text-green
{% elif specimen.specimen_type == 'FLUID' %}droplet text-info
{% elif specimen.specimen_type == 'SALIVA' %}lungs text-info
{% else %}vial-vertical text-secondary{% endif %}"></i>
<span class="fw-bold">{{ specimen.get_specimen_type_display }}</span>
</td>
</tr>
<tr>
<td class="fw-bold">Collection Date:</td>
<td>{{ object.collection_date|date:"M d, Y g:i A" }}</td>
<td>{{ object.collected_datetime|date:"M d, Y g:i A" }}</td>
</tr>
</table>
</div>
@ -113,8 +127,8 @@
<tr>
<td class="fw-bold">Priority:</td>
<td>
<span class="badge bg-{% if object.priority == 'urgent' %}danger{% elif object.priority == 'stat' %}warning{% else %}primary{% endif %}">
{{ object.get_priority_display }}
<span class="badge bg-{% if object.order.priority == 'STAT' %}danger{% elif object.order.priority == 'URGENT' %}warning{% else %}primary{% endif %}">
{{ object.order.get_priority_display }}
</span>
</td>
</tr>
@ -313,7 +327,7 @@
<strong>Gender:</strong> {{ object.patient.get_gender_display }}
</div>
<div class="mb-2">
<strong>MRN:</strong> {{ object.patient.medical_record_number }}
<strong>MRN:</strong> {{ object.patient.mrn }}
</div>
<div>
<a href="{% url 'patients:patient_detail' object.patient.pk %}" class="btn btn-sm btn-outline-primary">

View File

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

View File

@ -14,11 +14,12 @@
</div>
<div class="ms-auto">
<div class="btn-group">
<a href="{% url 'laboratory:specimen_form' %}" class="btn btn-primary">
<a href="{% url 'laboratory:specimen_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>New Specimen
</a>
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown">
<span class="visually-hidden">Toggle Dropdown</span>
<i class="fas fa-ellipsis-v"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="exportSpecimens()">
@ -211,11 +212,11 @@
<th>Status</th>
<th>Priority</th>
<th>Tests</th>
<th width="120">Actions</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for specimen in object_list %}
{% for specimen in specimens %}
<tr>
<td>
<input type="checkbox" class="specimen-checkbox" value="{{ specimen.pk }}">
@ -244,20 +245,34 @@
</td>
<td>
<div class="d-flex align-items-center">
<i class="fas fa-{% if specimen.specimen_type == 'blood' %}tint text-danger{% elif specimen.specimen_type == 'urine' %}flask text-warning{% elif specimen.specimen_type == 'sputum' %}lungs text-info{% else %}vial text-secondary{% endif %} me-2"></i>
{{ specimen.get_specimen_type_display }}
<i class="fas
fa-{% if specimen.specimen_type == 'BLOOD' %}tint text-danger
{% elif specimen.specimen_type == 'URINE' %}flask text-warning
{% elif specimen.specimen_type == 'SPUTUM' %}lungs text-info
{% elif specimen.specimen_type == 'PLASMA' %}tint text-yellow
{% elif specimen.specimen_type == 'SERUM' %}bottle-droplet text-info
{% elif specimen.specimen_type == 'STOOL' %}poop text-cyan
{% elif specimen.specimen_type == 'CSF' %}eye text-primary
{% elif specimen.specimen_type == 'SWAB' %}hand text-secondary
{% elif specimen.specimen_type == 'TISSUE' %}microscope text-green
{% elif specimen.specimen_type == 'FLUID' %}droplet text-info
{% elif specimen.specimen_type == 'SALIVA' %}lungs text-info
{% else %}vial-vertical text-secondary{% endif %} me-1"></i>
<span class="fw-bold">{{ specimen.get_specimen_type_display }}</span>
</div>
{% if specimen.volume %}
<div class="small text-muted">{{ specimen.volume }}</div>
{% endif %}
</td>
<td>
<div class="fw-bold">{{ specimen.collection_date|date:"M d, Y" }}</div>
<div class="small text-muted">{{ specimen.collection_date|date:"g:i A" }}</div>
<div class="fw-bold">{{ specimen.collected_datetime|date:"M d, Y" }}</div>
<div class="small text-muted">{{ specimen.collected_datetime|date:"g:i A" }}</div>
</td>
<td>{{ specimen.collected_by.get_full_name }}</td>
<td>
<span class="badge bg-{% if specimen.status == 'collected' %}success{% elif specimen.status == 'processing' %}warning{% elif specimen.status == 'completed' %}info{% elif specimen.status == 'rejected' %}danger{% else %}secondary{% endif %}">
<span class="badge bg-{% if specimen.status == 'COLLECTED' %}success{% elif specimen.status == 'PROCESSING' %}warning{% elif specimen.status == 'COMPLETED' %}info{% elif specimen.status == 'REJECTED' %}danger{% else %}secondary{% endif %}">
{{ specimen.get_status_display }}
</span>
{% if specimen.condition and specimen.condition != 'good' %}
@ -267,8 +282,8 @@
{% endif %}
</td>
<td>
<span class="badge bg-{% if specimen.priority == 'stat' %}danger{% elif specimen.priority == 'urgent' %}warning{% else %}primary{% endif %}">
{{ specimen.get_priority_display }}
<span class="badge bg-{% if specimen.order.priority == 'STAT' %}danger{% elif specimen.order.priority == 'URGENT' %}warning{% else %}primary{% endif %}">
{{ specimen.order.get_priority_display }}
</span>
{% if specimen.fasting_status %}
<div class="small text-info mt-1">
@ -290,7 +305,7 @@
title="View Details">
<i class="fas fa-eye"></i>
</a>
<a href="{% url 'laboratory:specimen_form' specimen.pk %}"
<a href="{% url 'laboratory:specimen_update' specimen.pk %}"
class="btn btn-outline-secondary"
title="Edit">
<i class="fas fa-edit"></i>
@ -301,11 +316,6 @@
title="Print Label">
<i class="fas fa-print"></i>
</button>
<a href="{% url 'laboratory:specimen_confirm_delete' specimen.pk %}"
class="btn btn-outline-danger"
title="Delete">
<i class="fas fa-trash"></i>
</a>
</div>
</td>
</tr>
@ -316,38 +326,7 @@
<!-- Pagination -->
{% if is_paginated %}
<div class="card-footer">
<div class="d-flex justify-content-between align-items-center">
<div>
Showing {{ page_obj.start_index }} to {{ page_obj.end_index }} of {{ paginator.count }} specimens
</div>
<nav>
<ul class="pagination pagination-sm mb-0">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.specimen_type %}&specimen_type={{ request.GET.specimen_type }}{% endif %}">First</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.specimen_type %}&specimen_type={{ request.GET.specimen_type }}{% endif %}">Previous</a>
</li>
{% endif %}
<li class="page-item active">
<span class="page-link">{{ page_obj.number }}</span>
</li>
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.specimen_type %}&specimen_type={{ request.GET.specimen_type }}{% endif %}">Next</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ paginator.num_pages }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}{% if request.GET.specimen_type %}&specimen_type={{ request.GET.specimen_type }}{% endif %}">Last</a>
</li>
{% endif %}
</ul>
</nav>
</div>
</div>
{% include 'partial/pagination.html' %}
{% endif %}
<!-- Bulk Actions -->

View File

@ -148,10 +148,10 @@
</a>
</div>
<div class="col-lg-2 col-md-4 col-sm-6">
<button type="button" class="btn btn-outline-warning w-100 h-100 d-flex flex-column align-items-center justify-content-center py-3" onclick="checkInteractions()">
<a href="{% url 'pharmacy:drug_interaction_list' %}" type="button" class="btn btn-outline-warning w-100 h-100 d-flex flex-column align-items-center justify-content-center py-3">
<i class="fas fa-search fa-2x mb-2"></i>
<span>Drug Interactions</span>
</button>
</a>
</div>
<div class="col-lg-2 col-md-4 col-sm-6">
<a href="" class="btn btn-outline-secondary w-100 h-100 d-flex flex-column align-items-center justify-content-center py-3">

View File

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

View File

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

View File

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

View File

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

View File

@ -112,66 +112,66 @@
<form method="get" class="row g-3" id="filterForm">
<div class="col-lg-4 col-md-6">
<label for="search" class="form-label">Search Medications</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-search"></i></span>
<input type="text"
class="form-control"
id="search"
name="search"
value="{{ request.GET.search }}"
placeholder="Generic name, brand name, NDC..."
hx-get="{% url 'pharmacy:medication_search' %}"
hx-target="#medication-list-container"
hx-trigger="keyup changed delay:500ms"
hx-include="#filterForm">
</div>
{# <div class="input-group">#}
{# <span class="input-group-text"><i class="fas fa-search"></i></span>#}
{# <input type="text" #}
{# class="form-control" #}
{# id="search" #}
{# name="search" #}
{# value="{{ request.GET.search }}"#}
{# placeholder="Generic name, brand name, NDC..."#}
{# hx-get="{% url 'pharmacy:medication_search' %}"#}
{# hx-target="#medication-list-container"#}
{# hx-trigger="keyup changed delay:500ms"#}
{# hx-include="#filterForm">#}
{# </div>#}
</div>
<div class="col-lg-2 col-md-6">
<label for="drug_class" class="form-label">Drug Class</label>
<select class="form-select" id="drug_class" name="drug_class"
hx-get="{% url 'pharmacy:medication_search' %}"
hx-target="#medication-list-container"
hx-trigger="change"
hx-include="#filterForm">
<option value="">All Classes</option>
{% for class in drug_classes %}
<option value="{{ class }}" {% if request.GET.drug_class == class %}selected{% endif %}>{{ class }}</option>
{% endfor %}
</select>
</div>
{# <div class="col-lg-2 col-md-6">#}
{# <label for="drug_class" class="form-label">Drug Class</label>#}
{# <select class="form-select" id="drug_class" name="drug_class"#}
{# hx-get="{% url 'pharmacy:medication_search' %}"#}
{# hx-target="#medication-list-container"#}
{# hx-trigger="change"#}
{# hx-include="#filterForm">#}
{# <option value="">All Classes</option>#}
{# {% for class in drug_classes %}#}
{# <option value="{{ class }}" {% if request.GET.drug_class == class %}selected{% endif %}>{{ class }}</option>#}
{# {% endfor %}#}
{# </select>#}
{# </div>#}
<div class="col-lg-2 col-md-6">
<label for="controlled_schedule" class="form-label">Schedule</label>
<select class="form-select" id="controlled_schedule" name="controlled_schedule"
hx-get="{% url 'pharmacy:medication_search' %}"
hx-target="#medication-list-container"
hx-trigger="change"
hx-include="#filterForm">
<option value="">All Schedules</option>
<option value="NON" {% if request.GET.controlled_schedule == 'NON' %}selected{% endif %}>Non-Controlled</option>
<option value="CI" {% if request.GET.controlled_schedule == 'CI' %}selected{% endif %}>Schedule I</option>
<option value="CII" {% if request.GET.controlled_schedule == 'CII' %}selected{% endif %}>Schedule II</option>
<option value="CIII" {% if request.GET.controlled_schedule == 'CIII' %}selected{% endif %}>Schedule III</option>
<option value="CIV" {% if request.GET.controlled_schedule == 'CIV' %}selected{% endif %}>Schedule IV</option>
<option value="CV" {% if request.GET.controlled_schedule == 'CV' %}selected{% endif %}>Schedule V</option>
</select>
</div>
{# <div class="col-lg-2 col-md-6">#}
{# <label for="controlled_schedule" class="form-label">Schedule</label>#}
{# <select class="form-select" id="controlled_schedule" name="controlled_schedule"#}
{# hx-get="{% url 'pharmacy:medication_search' %}"#}
{# hx-target="#medication-list-container"#}
{# hx-trigger="change"#}
{# hx-include="#filterForm">#}
{# <option value="">All Schedules</option>#}
{# <option value="NON" {% if request.GET.controlled_schedule == 'NON' %}selected{% endif %}>Non-Controlled</option>#}
{# <option value="CI" {% if request.GET.controlled_schedule == 'CI' %}selected{% endif %}>Schedule I</option>#}
{# <option value="CII" {% if request.GET.controlled_schedule == 'CII' %}selected{% endif %}>Schedule II</option>#}
{# <option value="CIII" {% if request.GET.controlled_schedule == 'CIII' %}selected{% endif %}>Schedule III</option>#}
{# <option value="CIV" {% if request.GET.controlled_schedule == 'CIV' %}selected{% endif %}>Schedule IV</option>#}
{# <option value="CV" {% if request.GET.controlled_schedule == 'CV' %}selected{% endif %}>Schedule V</option>#}
{# </select>#}
{# </div>#}
<div class="col-lg-2 col-md-6">
<label for="formulary_status" class="form-label">Formulary Status</label>
<select class="form-select" id="formulary_status" name="formulary_status"
hx-get="{% url 'pharmacy:medication_search' %}"
hx-target="#medication-list-container"
hx-trigger="change"
hx-include="#filterForm">
<option value="">All Status</option>
<option value="PREFERRED" {% if request.GET.formulary_status == 'PREFERRED' %}selected{% endif %}>Preferred</option>
<option value="NON_PREFERRED" {% if request.GET.formulary_status == 'NON_PREFERRED' %}selected{% endif %}>Non-Preferred</option>
<option value="RESTRICTED" {% if request.GET.formulary_status == 'RESTRICTED' %}selected{% endif %}>Restricted</option>
<option value="PRIOR_AUTH" {% if request.GET.formulary_status == 'PRIOR_AUTH' %}selected{% endif %}>Prior Auth Required</option>
</select>
</div>
{# <div class="col-lg-2 col-md-6">#}
{# <label for="formulary_status" class="form-label">Formulary Status</label>#}
{# <select class="form-select" id="formulary_status" name="formulary_status"#}
{# hx-get="{% url 'pharmacy:medication_search' %}"#}
{# hx-target="#medication-list-container"#}
{# hx-trigger="change"#}
{# hx-include="#filterForm">#}
{# <option value="">All Status</option>#}
{# <option value="PREFERRED" {% if request.GET.formulary_status == 'PREFERRED' %}selected{% endif %}>Preferred</option>#}
{# <option value="NON_PREFERRED" {% if request.GET.formulary_status == 'NON_PREFERRED' %}selected{% endif %}>Non-Preferred</option>#}
{# <option value="RESTRICTED" {% if request.GET.formulary_status == 'RESTRICTED' %}selected{% endif %}>Restricted</option>#}
{# <option value="PRIOR_AUTH" {% if request.GET.formulary_status == 'PRIOR_AUTH' %}selected{% endif %}>Prior Auth Required</option>#}
{# </select>#}
{# </div>#}
<div class="col-lg-2 col-md-6">
<label class="form-label">&nbsp;</label>
@ -310,7 +310,7 @@
<a href="{% url 'pharmacy:medication_detail' medication.pk %}" class="btn btn-outline-primary btn-sm" title="View Details">
<i class="fas fa-eye"></i>
</a>
<a href="{% url 'pharmacy:medication_edit' medication.pk %}" class="btn btn-outline-secondary btn-sm" title="Edit">
<a href="{% url 'pharmacy:medication_update' medication.pk %}" class="btn btn-outline-secondary btn-sm" title="Edit">
<i class="fas fa-edit"></i>
</a>
<button type="button" class="btn btn-outline-danger btn-sm" onclick="deleteMedication({{ medication.pk }})" title="Delete">
@ -335,37 +335,9 @@
</div>
<!-- Pagination -->
{% if page_obj.has_other_pages %}
<div class="card-footer">
<nav aria-label="Medication pagination">
<ul class="pagination justify-content-center mb-0">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}">Previous</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<span class="page-link">{{ num }}</span>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}">Next</a>
</li>
{% endif %}
</ul>
</nav>
</div>
{% endif %}
{% if is_paginated %}
{% include 'partial/pagination.html' %}
{% endif %}
</div>
</div>
</div>

View File

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