This commit is contained in:
Marwan Alwali 2025-09-04 19:19:52 +03:00
parent 73c9e2e921
commit 610e165e17
159 changed files with 92985 additions and 5487 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -10,7 +10,8 @@ from django.views.generic import (
) )
from django.http import JsonResponse from django.http import JsonResponse
from django.contrib import messages from django.contrib import messages
from django.db.models import Q, Count, Avg from django.db.models.functions import Now
from django.db.models import Q, Count, Avg, Case, When, Value, DurationField, FloatField, F, ExpressionWrapper, IntegerField
from django.utils import timezone from django.utils import timezone
from django.urls import reverse_lazy, reverse from django.urls import reverse_lazy, reverse
from django.core.paginator import Paginator from django.core.paginator import Paginator
@ -29,6 +30,7 @@ from accounts.models import User
from core.utils import AuditLogger from core.utils import AuditLogger
# ============================================================================ # ============================================================================
# DASHBOARD VIEW # DASHBOARD VIEW
# ============================================================================ # ============================================================================
@ -1334,6 +1336,7 @@ def available_slots(request):
def queue_status(request, queue_id): def queue_status(request, queue_id):
""" """
HTMX view for queue status. HTMX view for queue status.
Shows queue entries plus aggregated stats with DB-side wait calculations.
""" """
tenant = getattr(request, 'tenant', None) tenant = getattr(request, 'tenant', None)
if not tenant: if not tenant:
@ -1341,25 +1344,66 @@ def queue_status(request, queue_id):
queue = get_object_or_404(WaitingQueue, id=queue_id, tenant=tenant) queue = get_object_or_404(WaitingQueue, id=queue_id, tenant=tenant)
# Get queue entries queue_entries = (
queue_entries = QueueEntry.objects.filter( QueueEntry.objects
queue=queue .filter(queue=queue)
).order_by('position', 'created_at') .annotate(
wait_duration=Case(
When(status='WAITING', then=Now() - F('joined_at')),
When(served_at__isnull=False, then=F('served_at') - F('joined_at')),
default=Value(None),
output_field=DurationField(),
),
)
.annotate(
wait_minutes=Case(
When(wait_duration__isnull=False,
then=ExpressionWrapper(
F('wait_duration') / Value(timedelta(minutes=1)),
output_field=FloatField()
)),
default=Value(None),
output_field=FloatField(),
),
waiting_rank=Case(
When(status='WAITING', then=F('queue_position')),
default=Value(None), output_field=IntegerField()
),
)
.select_related('assigned_provider', 'patient', 'appointment') # adjust if you need more
.order_by('queue_position', 'updated_at')
)
# Aggregates & stats
total_entries = queue_entries.count()
waiting_entries = queue_entries.filter(status='WAITING').count()
called_entries = queue_entries.filter(status='CALLED').count()
in_service_entries = queue_entries.filter(status='IN_SERVICE').count()
completed_entries = queue_entries.filter(status='COMPLETED').count()
avg_completed_wait = (
queue_entries
.filter(status='COMPLETED')
.aggregate(avg_wait=Avg('wait_minutes'))
.get('avg_wait') or 0
)
# Calculate statistics
stats = { stats = {
'total_entries': queue_entries.count(), 'total_entries': total_entries,
'waiting_entries': queue_entries.filter(status='WAITING').count(), 'waiting_entries': waiting_entries,
'in_progress_entries': queue_entries.filter(status='IN_PROGRESS').count(), 'called_entries': called_entries,
'average_wait_time': queue_entries.filter( 'in_service_entries': in_service_entries,
status='COMPLETED' 'completed_entries': completed_entries,
).aggregate(avg_wait=Avg('actual_wait_time_minutes'))['avg_wait'] or 0, # Average from COMPLETED cases only (rounded to 1 decimal)
'average_wait_time_minutes': round(avg_completed_wait, 1),
# Quick estimate based on queue config
'estimated_queue_wait_minutes': waiting_entries * queue.average_service_time_minutes,
} }
return render(request, 'appointments/partials/queue_status.html', { return render(request, 'appointments/partials/queue_status.html', {
'queue': queue, 'queue': queue,
'queue_entries': queue_entries, 'queue_entries': queue_entries, # each has .wait_minutes
'stats': stats 'stats': stats,
}) })
@ -1536,7 +1580,7 @@ def next_in_queue(request, queue_id):
return JsonResponse({ return JsonResponse({
'status': 'success', 'status': 'success',
'patient': str(next_entry.patient), 'patient': str(next_entry.patient),
'position': next_entry.position 'position': next_entry.queue_position
}) })
else: else:
return JsonResponse({'status': 'no_patients'}) return JsonResponse({'status': 'no_patients'})
@ -1708,12 +1752,19 @@ class SchedulingCalendarView(LoginRequiredMixin, TemplateView):
return context return context
class QueueManagementView(LoginRequiredMixin, TemplateView): class QueueManagementView(LoginRequiredMixin, ListView):
""" """
Queue management view for appointments. Queue management view for appointments.
""" """
model = QueueEntry
template_name = 'appointments/queue_management.html' template_name = 'appointments/queue_management.html'
context_object_name = 'queues'
def get_queryset(self):
return QueueEntry.objects.filter(
appointment__tenant=self.request.user.tenant
)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)

0
blood_bank/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

387
blood_bank/admin.py Normal file
View File

@ -0,0 +1,387 @@
from django.contrib import admin
from django.utils.html import format_html
from django.urls import reverse
from django.utils import timezone
from .models import (
BloodGroup, Donor, BloodComponent, BloodUnit, BloodTest, CrossMatch,
BloodRequest, BloodIssue, Transfusion, AdverseReaction, InventoryLocation,
QualityControl
)
@admin.register(BloodGroup)
class BloodGroupAdmin(admin.ModelAdmin):
list_display = ['abo_type', 'rh_factor', 'display_name']
list_filter = ['abo_type', 'rh_factor']
ordering = ['abo_type', 'rh_factor']
@admin.register(Donor)
class DonorAdmin(admin.ModelAdmin):
list_display = [
'donor_id', 'full_name', 'blood_group', 'age', 'status',
'total_donations', 'last_donation_date', 'is_eligible_for_donation'
]
list_filter = [
'status', 'donor_type', 'blood_group', 'gender',
'registration_date', 'last_donation_date'
]
search_fields = ['donor_id', 'first_name', 'last_name', 'phone', 'email']
readonly_fields = ['registration_date', 'created_at', 'updated_at', 'age']
fieldsets = (
('Personal Information', {
'fields': (
'donor_id', 'first_name', 'last_name', 'date_of_birth',
'gender', 'blood_group', 'weight', 'height'
)
}),
('Contact Information', {
'fields': (
'phone', 'email', 'address',
'emergency_contact_name', 'emergency_contact_phone'
)
}),
('Donation Information', {
'fields': (
'donor_type', 'status', 'total_donations',
'last_donation_date', 'notes'
)
}),
('System Information', {
'fields': ('created_by', 'registration_date', 'created_at', 'updated_at'),
'classes': ['collapse']
})
)
def is_eligible_for_donation(self, obj):
if obj.is_eligible_for_donation:
return format_html('<span style="color: green;">✓ Eligible</span>')
else:
return format_html('<span style="color: red;">✗ Not Eligible</span>')
is_eligible_for_donation.short_description = 'Eligible'
@admin.register(BloodComponent)
class BloodComponentAdmin(admin.ModelAdmin):
list_display = ['name', 'shelf_life_days', 'storage_temperature', 'volume_ml', 'is_active']
list_filter = ['is_active', 'shelf_life_days']
search_fields = ['name', 'description']
class BloodTestInline(admin.TabularInline):
model = BloodTest
extra = 0
readonly_fields = ['test_date', 'verified_at']
class CrossMatchInline(admin.TabularInline):
model = CrossMatch
extra = 0
readonly_fields = ['test_date', 'verified_at']
@admin.register(BloodUnit)
class BloodUnitAdmin(admin.ModelAdmin):
list_display = [
'unit_number', 'donor', 'component', 'blood_group',
'collection_date', 'expiry_date', 'status', 'days_to_expiry',
'is_available'
]
list_filter = [
'status', 'component', 'blood_group', 'collection_date',
'expiry_date', 'collection_site'
]
search_fields = ['unit_number', 'donor__donor_id', 'donor__first_name', 'donor__last_name']
readonly_fields = ['created_at', 'updated_at', 'days_to_expiry', 'is_expired']
inlines = [BloodTestInline, CrossMatchInline]
fieldsets = (
('Unit Information', {
'fields': (
'unit_number', 'donor', 'component', 'blood_group',
'volume_ml', 'status', 'location'
)
}),
('Collection Details', {
'fields': (
'collection_date', 'expiry_date', 'collection_site',
'bag_type', 'anticoagulant', 'collected_by'
)
}),
('Additional Information', {
'fields': ('notes', 'created_at', 'updated_at'),
'classes': ['collapse']
})
)
def days_to_expiry(self, obj):
days = obj.days_to_expiry
if days == 0:
return format_html('<span style="color: red;">Expired</span>')
elif days <= 3:
return format_html('<span style="color: orange;">{} days</span>', days)
else:
return f"{days} days"
days_to_expiry.short_description = 'Days to Expiry'
def is_available(self, obj):
if obj.is_available:
return format_html('<span style="color: green;">✓ Available</span>')
else:
return format_html('<span style="color: red;">✗ Not Available</span>')
is_available.short_description = 'Available'
@admin.register(BloodTest)
class BloodTestAdmin(admin.ModelAdmin):
list_display = [
'blood_unit', 'test_type', 'result', 'test_date',
'tested_by', 'verified_by', 'verified_at'
]
list_filter = ['test_type', 'result', 'test_date', 'verified_at']
search_fields = ['blood_unit__unit_number', 'equipment_used', 'lot_number']
readonly_fields = ['test_date', 'verified_at']
@admin.register(CrossMatch)
class CrossMatchAdmin(admin.ModelAdmin):
list_display = [
'blood_unit', 'recipient', 'test_type', 'compatibility',
'test_date', 'tested_by', 'verified_by'
]
list_filter = ['test_type', 'compatibility', 'test_date']
search_fields = [
'blood_unit__unit_number', 'recipient__first_name',
'recipient__last_name', 'recipient__patient_id'
]
readonly_fields = ['test_date', 'verified_at']
class BloodIssueInline(admin.TabularInline):
model = BloodIssue
extra = 0
readonly_fields = ['issue_date', 'expiry_time']
@admin.register(BloodRequest)
class BloodRequestAdmin(admin.ModelAdmin):
list_display = [
'request_number', 'patient', 'component_requested',
'units_requested', 'urgency', 'status', 'request_date',
'required_by', 'is_overdue'
]
list_filter = [
'urgency', 'status', 'component_requested',
'requesting_department', 'request_date', 'required_by'
]
search_fields = [
'request_number', 'patient__first_name', 'patient__last_name',
'patient__patient_id', 'indication'
]
readonly_fields = ['request_date', 'processed_at']
inlines = [BloodIssueInline]
fieldsets = (
('Request Information', {
'fields': (
'request_number', 'patient', 'requesting_department',
'requesting_physician', 'urgency', 'status'
)
}),
('Blood Requirements', {
'fields': (
'component_requested', 'units_requested', 'patient_blood_group',
'special_requirements'
)
}),
('Clinical Information', {
'fields': (
'indication', 'hemoglobin_level', 'platelet_count'
)
}),
('Timeline', {
'fields': (
'request_date', 'required_by', 'processed_by', 'processed_at'
)
}),
('Additional Information', {
'fields': ('notes',),
'classes': ['collapse']
})
)
def is_overdue(self, obj):
if obj.is_overdue:
return format_html('<span style="color: red;">✗ Overdue</span>')
else:
return format_html('<span style="color: green;">✓ On Time</span>')
is_overdue.short_description = 'Status'
@admin.register(BloodIssue)
class BloodIssueAdmin(admin.ModelAdmin):
list_display = [
'blood_unit', 'blood_request', 'issued_by', 'issued_to',
'issue_date', 'expiry_time', 'returned', 'is_expired'
]
list_filter = ['returned', 'issue_date', 'expiry_time']
search_fields = [
'blood_unit__unit_number', 'blood_request__request_number',
'blood_request__patient__first_name', 'blood_request__patient__last_name'
]
readonly_fields = ['issue_date', 'is_expired']
def is_expired(self, obj):
if obj.is_expired:
return format_html('<span style="color: red;">✗ Expired</span>')
else:
return format_html('<span style="color: green;">✓ Valid</span>')
is_expired.short_description = 'Status'
class AdverseReactionInline(admin.TabularInline):
model = AdverseReaction
extra = 0
readonly_fields = ['onset_time', 'report_date']
@admin.register(Transfusion)
class TransfusionAdmin(admin.ModelAdmin):
list_display = [
'blood_issue', 'start_time', 'end_time', 'status',
'volume_transfused', 'administered_by', 'duration_minutes'
]
list_filter = ['status', 'start_time', 'patient_consent']
search_fields = [
'blood_issue__blood_unit__unit_number',
'blood_issue__blood_request__patient__first_name',
'blood_issue__blood_request__patient__last_name'
]
readonly_fields = ['duration_minutes']
inlines = [AdverseReactionInline]
fieldsets = (
('Transfusion Information', {
'fields': (
'blood_issue', 'start_time', 'end_time', 'status',
'volume_transfused', 'transfusion_rate'
)
}),
('Personnel', {
'fields': ('administered_by', 'witnessed_by')
}),
('Consent', {
'fields': ('patient_consent', 'consent_date')
}),
('Vital Signs', {
'fields': ('pre_transfusion_vitals', 'post_transfusion_vitals'),
'classes': ['collapse']
}),
('Additional Information', {
'fields': ('notes',),
'classes': ['collapse']
})
)
@admin.register(AdverseReaction)
class AdverseReactionAdmin(admin.ModelAdmin):
list_display = [
'transfusion', 'reaction_type', 'severity', 'onset_time',
'reported_by', 'regulatory_reported'
]
list_filter = [
'reaction_type', 'severity', 'onset_time',
'regulatory_reported'
]
search_fields = [
'transfusion__blood_issue__blood_unit__unit_number',
'symptoms', 'treatment_given'
]
readonly_fields = ['onset_time', 'report_date']
fieldsets = (
('Reaction Information', {
'fields': (
'transfusion', 'reaction_type', 'severity',
'onset_time', 'symptoms'
)
}),
('Treatment', {
'fields': ('treatment_given', 'outcome')
}),
('Reporting', {
'fields': (
'reported_by', 'investigated_by', 'investigation_notes',
'regulatory_reported', 'report_date'
)
})
)
@admin.register(InventoryLocation)
class InventoryLocationAdmin(admin.ModelAdmin):
list_display = [
'name', 'location_type', 'temperature_range',
'current_stock', 'capacity', 'utilization_percentage', 'is_active'
]
list_filter = ['location_type', 'is_active']
search_fields = ['name', 'notes']
def utilization_percentage(self, obj):
percentage = obj.utilization_percentage
if percentage >= 90:
color = 'red'
elif percentage >= 75:
color = 'orange'
else:
color = 'green'
return format_html(
'<span style="color: {};">{:.1f}%</span>',
color, percentage
)
utilization_percentage.short_description = 'Utilization'
@admin.register(QualityControl)
class QualityControlAdmin(admin.ModelAdmin):
list_display = [
'test_type', 'test_date', 'equipment_tested',
'status', 'performed_by', 'reviewed_by', 'next_test_date'
]
list_filter = ['test_type', 'status', 'test_date', 'next_test_date']
search_fields = ['equipment_tested', 'parameters_tested', 'corrective_action']
readonly_fields = ['test_date']
fieldsets = (
('Test Information', {
'fields': (
'test_type', 'test_date', 'equipment_tested',
'parameters_tested'
)
}),
('Results', {
'fields': (
'expected_results', 'actual_results', 'status'
)
}),
('Personnel', {
'fields': ('performed_by', 'reviewed_by')
}),
('Follow-up', {
'fields': ('corrective_action', 'next_test_date')
})
)
# Custom admin site configuration
admin.site.site_header = "Blood Bank Management System"
admin.site.site_title = "Blood Bank Admin"
admin.site.index_title = "Blood Bank Administration"

8
blood_bank/apps.py Normal file
View File

@ -0,0 +1,8 @@
from django.apps import AppConfig
class BloodBankConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'blood_bank'
verbose_name = 'Blood Bank Management'

429
blood_bank/forms.py Normal file
View File

@ -0,0 +1,429 @@
from django import forms
from django.contrib.auth.models import User
from django.utils import timezone
from datetime import timedelta
from patients.models import PatientProfile
from core.models import Department
from .models import (
BloodGroup, Donor, BloodComponent, BloodUnit, BloodTest, CrossMatch,
BloodRequest, BloodIssue, Transfusion, AdverseReaction, InventoryLocation,
QualityControl
)
class DonorForm(forms.ModelForm):
"""Form for donor registration and updates"""
class Meta:
model = Donor
fields = [
'donor_id', 'first_name', 'last_name', 'date_of_birth',
'gender', 'blood_group', 'phone', 'email', 'address',
'emergency_contact_name', 'emergency_contact_phone',
'donor_type', 'status', 'weight', 'height', 'notes'
]
widgets = {
'date_of_birth': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
'donor_id': forms.TextInput(attrs={'class': 'form-control'}),
'gender': forms.Select(attrs={'class': 'form-select'}),
'blood_group': forms.Select(attrs={'class': 'form-select'}),
'phone': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
'address': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'emergency_contact_name': forms.TextInput(attrs={'class': 'form-control'}),
'emergency_contact_phone': forms.TextInput(attrs={'class': 'form-control'}),
'donor_type': forms.Select(attrs={'class': 'form-select'}),
'status': forms.Select(attrs={'class': 'form-select'}),
'weight': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1'}),
'height': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1'}),
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
}
def clean_weight(self):
weight = self.cleaned_data.get('weight')
if weight and weight < 45.0:
raise forms.ValidationError("Minimum weight for donation is 45 kg.")
return weight
def clean_date_of_birth(self):
dob = self.cleaned_data.get('date_of_birth')
if dob:
today = timezone.now().date()
age = today.year - dob.year - ((today.month, today.day) < (dob.month, dob.day))
if age < 18:
raise forms.ValidationError("Donor must be at least 18 years old.")
if age > 65:
raise forms.ValidationError("Donor must be under 65 years old.")
return dob
class BloodUnitForm(forms.ModelForm):
"""Form for blood unit registration"""
class Meta:
model = BloodUnit
fields = [
'unit_number', 'donor', 'component', 'blood_group',
'collection_date', 'volume_ml', 'location', 'bag_type',
'anticoagulant', 'collection_site', 'notes'
]
widgets = {
'unit_number': forms.TextInput(attrs={'class': 'form-control'}),
'donor': forms.Select(attrs={'class': 'form-select'}),
'component': forms.Select(attrs={'class': 'form-select'}),
'blood_group': forms.Select(attrs={'class': 'form-select'}),
'collection_date': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}),
'volume_ml': forms.NumberInput(attrs={'class': 'form-control'}),
'location': forms.TextInput(attrs={'class': 'form-control'}),
'bag_type': forms.TextInput(attrs={'class': 'form-control'}),
'anticoagulant': forms.TextInput(attrs={'class': 'form-control'}),
'collection_site': forms.TextInput(attrs={'class': 'form-control'}),
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Filter active donors only
self.fields['donor'].queryset = Donor.objects.filter(status='active')
self.fields['component'].queryset = BloodComponent.objects.filter(is_active=True)
def save(self, commit=True):
instance = super().save(commit=False)
if not instance.expiry_date:
# Calculate expiry date based on component
component = instance.component
instance.expiry_date = instance.collection_date + timedelta(days=component.shelf_life_days)
if commit:
instance.save()
return instance
class BloodTestForm(forms.ModelForm):
"""Form for blood test results"""
class Meta:
model = BloodTest
fields = [
'blood_unit', 'test_type', 'result', 'test_date',
'equipment_used', 'lot_number', 'notes'
]
widgets = {
'blood_unit': forms.Select(attrs={'class': 'form-select'}),
'test_type': forms.Select(attrs={'class': 'form-select'}),
'result': forms.Select(attrs={'class': 'form-select'}),
'test_date': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}),
'equipment_used': forms.TextInput(attrs={'class': 'form-control'}),
'lot_number': forms.TextInput(attrs={'class': 'form-control'}),
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Filter blood units that are in testing or quarantine
self.fields['blood_unit'].queryset = BloodUnit.objects.filter(
status__in=['collected', 'testing', 'quarantine']
)
class CrossMatchForm(forms.ModelForm):
"""Form for crossmatch testing"""
class Meta:
model = CrossMatch
fields = [
'blood_unit', 'recipient', 'test_type', 'compatibility',
'test_date', 'temperature', 'incubation_time', 'notes'
]
widgets = {
'blood_unit': forms.Select(attrs={'class': 'form-select'}),
'recipient': forms.Select(attrs={'class': 'form-select'}),
'test_type': forms.Select(attrs={'class': 'form-select'}),
'compatibility': forms.Select(attrs={'class': 'form-select'}),
'test_date': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}),
'temperature': forms.TextInput(attrs={'class': 'form-control'}),
'incubation_time': forms.NumberInput(attrs={'class': 'form-control'}),
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Filter available blood units
self.fields['blood_unit'].queryset = BloodUnit.objects.filter(
status__in=['available', 'quarantine']
)
class BloodRequestForm(forms.ModelForm):
"""Form for blood transfusion requests"""
class Meta:
model = BloodRequest
fields = [
'patient', 'requesting_department', 'component_requested',
'units_requested', 'urgency', 'indication', 'special_requirements',
'patient_blood_group', 'hemoglobin_level', 'platelet_count',
'required_by'
]
widgets = {
'patient': forms.Select(attrs={'class': 'form-select'}),
'requesting_department': forms.Select(attrs={'class': 'form-select'}),
'component_requested': forms.Select(attrs={'class': 'form-select'}),
'units_requested': forms.NumberInput(attrs={'class': 'form-control', 'min': '1'}),
'urgency': forms.Select(attrs={'class': 'form-select'}),
'indication': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'special_requirements': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}),
'patient_blood_group': forms.Select(attrs={'class': 'form-select'}),
'hemoglobin_level': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1'}),
'platelet_count': forms.NumberInput(attrs={'class': 'form-control'}),
'required_by': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['component_requested'].queryset = BloodComponent.objects.filter(is_active=True)
def clean_required_by(self):
required_by = self.cleaned_data.get('required_by')
if required_by and required_by <= timezone.now():
raise forms.ValidationError("Required by date must be in the future.")
return required_by
class BloodIssueForm(forms.ModelForm):
"""Form for blood unit issuance"""
class Meta:
model = BloodIssue
fields = [
'blood_request', 'blood_unit', 'crossmatch',
'issued_to', 'expiry_time', 'notes'
]
widgets = {
'blood_request': forms.Select(attrs={'class': 'form-select'}),
'blood_unit': forms.Select(attrs={'class': 'form-select'}),
'crossmatch': forms.Select(attrs={'class': 'form-select'}),
'issued_to': forms.Select(attrs={'class': 'form-select'}),
'expiry_time': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}),
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Filter pending/processing blood requests
self.fields['blood_request'].queryset = BloodRequest.objects.filter(
status__in=['pending', 'processing', 'ready']
)
# Filter available blood units
self.fields['blood_unit'].queryset = BloodUnit.objects.filter(status='available')
# Filter staff users
self.fields['issued_to'].queryset = User.objects.filter(is_staff=True)
class TransfusionForm(forms.ModelForm):
"""Form for transfusion administration"""
class Meta:
model = Transfusion
fields = [
'blood_issue', 'start_time', 'end_time', 'status',
'volume_transfused', 'transfusion_rate', 'witnessed_by',
'patient_consent', 'consent_date', 'notes'
]
widgets = {
'blood_issue': forms.Select(attrs={'class': 'form-select'}),
'start_time': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}),
'end_time': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}),
'status': forms.Select(attrs={'class': 'form-select'}),
'volume_transfused': forms.NumberInput(attrs={'class': 'form-control'}),
'transfusion_rate': forms.TextInput(attrs={'class': 'form-control'}),
'witnessed_by': forms.Select(attrs={'class': 'form-select'}),
'patient_consent': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'consent_date': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}),
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Filter issued blood units
self.fields['blood_issue'].queryset = BloodIssue.objects.filter(returned=False)
# Filter staff users for witness
self.fields['witnessed_by'].queryset = User.objects.filter(is_staff=True)
class AdverseReactionForm(forms.ModelForm):
"""Form for adverse reaction reporting"""
class Meta:
model = AdverseReaction
fields = [
'transfusion', 'reaction_type', 'severity', 'onset_time',
'symptoms', 'treatment_given', 'outcome', 'investigation_notes',
'regulatory_reported'
]
widgets = {
'transfusion': forms.Select(attrs={'class': 'form-select'}),
'reaction_type': forms.Select(attrs={'class': 'form-select'}),
'severity': forms.Select(attrs={'class': 'form-select'}),
'onset_time': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}),
'symptoms': forms.Textarea(attrs={'class': 'form-control', 'rows': 4}),
'treatment_given': forms.Textarea(attrs={'class': 'form-control', 'rows': 4}),
'outcome': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'investigation_notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'regulatory_reported': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
}
class InventoryLocationForm(forms.ModelForm):
"""Form for inventory location management"""
class Meta:
model = InventoryLocation
fields = [
'name', 'location_type', 'temperature_range',
'capacity', 'is_active', 'notes'
]
widgets = {
'name': forms.TextInput(attrs={'class': 'form-control'}),
'location_type': forms.Select(attrs={'class': 'form-select'}),
'temperature_range': forms.TextInput(attrs={'class': 'form-control'}),
'capacity': forms.NumberInput(attrs={'class': 'form-control', 'min': '1'}),
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
}
class QualityControlForm(forms.ModelForm):
"""Form for quality control testing"""
class Meta:
model = QualityControl
fields = [
'test_type', 'test_date', 'equipment_tested',
'parameters_tested', 'expected_results', 'actual_results',
'status', 'corrective_action', 'next_test_date'
]
widgets = {
'test_type': forms.Select(attrs={'class': 'form-select'}),
'test_date': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}),
'equipment_tested': forms.TextInput(attrs={'class': 'form-control'}),
'parameters_tested': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'expected_results': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'actual_results': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'status': forms.Select(attrs={'class': 'form-select'}),
'corrective_action': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'next_test_date': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}),
}
class DonorEligibilityForm(forms.Form):
"""Form for donor eligibility screening"""
# Basic health questions
feeling_well = forms.BooleanField(
label="Are you feeling well today?",
widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})
)
adequate_sleep = forms.BooleanField(
label="Did you get adequate sleep last night?",
widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})
)
eaten_today = forms.BooleanField(
label="Have you eaten within the last 4 hours?",
widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})
)
# Medical history
recent_illness = forms.BooleanField(
required=False,
label="Have you had any illness in the past 2 weeks?",
widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})
)
medications = forms.BooleanField(
required=False,
label="Are you currently taking any medications?",
widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})
)
recent_travel = forms.BooleanField(
required=False,
label="Have you traveled outside the country in the past 3 months?",
widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})
)
# Risk factors
recent_tattoo = forms.BooleanField(
required=False,
label="Have you had a tattoo or piercing in the past 12 months?",
widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})
)
recent_surgery = forms.BooleanField(
required=False,
label="Have you had any surgery in the past 12 months?",
widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})
)
# Additional notes
notes = forms.CharField(
required=False,
label="Additional notes or concerns",
widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 3})
)
def clean(self):
cleaned_data = super().clean()
# Check mandatory requirements
if not cleaned_data.get('feeling_well'):
raise forms.ValidationError("Donor must be feeling well to donate.")
if not cleaned_data.get('adequate_sleep'):
raise forms.ValidationError("Donor must have adequate sleep before donation.")
if not cleaned_data.get('eaten_today'):
raise forms.ValidationError("Donor must have eaten within the last 4 hours.")
return cleaned_data
class BloodInventorySearchForm(forms.Form):
"""Form for searching blood inventory"""
blood_group = forms.ModelChoiceField(
queryset=BloodGroup.objects.all(),
required=False,
empty_label="All Blood Groups",
widget=forms.Select(attrs={'class': 'form-select'})
)
component = forms.ModelChoiceField(
queryset=BloodComponent.objects.filter(is_active=True),
required=False,
empty_label="All Components",
widget=forms.Select(attrs={'class': 'form-select'})
)
status = forms.ChoiceField(
choices=[('', 'All Statuses')] + BloodUnit.STATUS_CHOICES,
required=False,
widget=forms.Select(attrs={'class': 'form-select'})
)
location = forms.ModelChoiceField(
queryset=InventoryLocation.objects.filter(is_active=True),
required=False,
empty_label="All Locations",
widget=forms.Select(attrs={'class': 'form-select'})
)
expiry_days = forms.IntegerField(
required=False,
label="Expiring within (days)",
widget=forms.NumberInput(attrs={'class': 'form-control', 'min': '0'})
)

View File

View File

@ -0,0 +1,339 @@
import random
from datetime import datetime, timedelta
from django.core.management.base import BaseCommand
from django.utils import timezone
from django.db import transaction
from accounts.models import User
from blood_bank.models import (
BloodGroup, Donor, BloodComponent, BloodUnit, BloodTest,
BloodRequest, InventoryLocation, QualityControl
)
from patients.models import PatientProfile
from core.models import Department
class Command(BaseCommand):
help = 'Generate Saudi-influenced blood bank test data'
def add_arguments(self, parser):
parser.add_argument('--donors', type=int, default=50, help='Number of donors to create (default: 50)')
parser.add_argument('--units', type=int, default=100, help='Number of blood units to create (default: 100)')
def handle(self, *args, **options):
self.stdout.write(self.style.SUCCESS('Starting Saudi blood bank data generation...'))
try:
with transaction.atomic():
users = list(User.objects.all())
patients = list(PatientProfile.objects.all())
departments = list(Department.objects.all())
if not users:
self.stdout.write(self.style.ERROR('No users found. Please create users first.'))
return
self.create_blood_groups()
self.create_blood_components()
self.create_inventory_locations()
donors = self.create_saudi_donors(options['donors'], users)
blood_units = self.create_blood_units(options['units'], donors, users)
self.create_blood_tests(blood_units, users)
if patients and departments:
self.create_blood_requests(20, patients, departments, users)
self.create_quality_control_records(users)
except Exception as e:
self.stdout.write(self.style.ERROR(f'Error: {str(e)}'))
return
self.stdout.write(self.style.SUCCESS('Saudi blood bank data generation completed!'))
# ---------- seeders ----------
def create_blood_groups(self):
"""Create ABO/Rh blood groups with correct choices (POS/NEG)."""
blood_groups = [
('A', 'POS'), ('A', 'NEG'),
('B', 'POS'), ('B', 'NEG'),
('AB', 'POS'), ('AB', 'NEG'),
('O', 'POS'), ('O', 'NEG'),
]
for abo, rh in blood_groups:
BloodGroup.objects.get_or_create(abo_type=abo, rh_factor=rh)
self.stdout.write('✓ Created blood groups')
def create_blood_components(self):
"""Create blood components with required fields."""
components = [
# (name, volume_ml, shelf_life_days, storage_temperature, description)
('whole_blood', 450, 35, '26°C', 'Whole blood unit'),
('packed_rbc', 300, 42, '26°C', 'Packed red blood cells'),
('fresh_frozen_plasma', 250, 365, '≤ -18°C', 'Fresh frozen plasma'),
('platelets', 50, 5, '2024°C with agitation', 'Platelet concentrate'),
# Optional extras (your model allows them)
('cryoprecipitate', 15, 365, '≤ -18°C', 'Cryoprecipitated AHF'),
('granulocytes', 200, 1, '2024°C', 'Granulocyte concentrate'),
]
for name, volume, shelf_life, temp, desc in components:
BloodComponent.objects.get_or_create(
name=name,
defaults={
'description': desc,
'shelf_life_days': shelf_life,
'storage_temperature': temp,
'volume_ml': volume,
'is_active': True
}
)
self.stdout.write('✓ Created blood components')
def create_inventory_locations(self):
"""Create storage locations with required fields."""
locations = [
# (name, type, capacity, temperature_range)
('Main Refrigerator A', 'refrigerator', 100, '26°C'),
('Main Refrigerator B', 'refrigerator', 100, '26°C'),
('Platelet Agitator 1', 'platelet_agitator', 30, '2024°C'),
('Plasma Freezer A', 'freezer', 200, '≤ -18°C'),
('Quarantine Storage', 'quarantine', 50, '26°C'),
('Testing Area 1', 'testing', 20, 'Room temp'),
]
for name, loc_type, capacity, temp_range in locations:
InventoryLocation.objects.get_or_create(
name=name,
defaults={
'location_type': loc_type,
'temperature_range': temp_range,
'capacity': capacity,
'is_active': True,
'current_stock': 0,
}
)
self.stdout.write('✓ Created inventory locations')
def create_saudi_donors(self, count, users):
"""Create Saudi-influenced donors aligned with Donor model fields."""
saudi_male_names = [
'Abdullah', 'Mohammed', 'Ahmed', 'Ali', 'Omar', 'Khalid', 'Fahd', 'Salman',
'Faisal', 'Turki', 'Nasser', 'Saud', 'Bandar', 'Majid', 'Waleed', 'Yazeed',
'Abdulaziz', 'Abdulrahman', 'Ibrahim', 'Hassan', 'Hussein', 'Mansour'
]
saudi_female_names = [
'Fatima', 'Aisha', 'Maryam', 'Khadija', 'Zainab', 'Noura', 'Sarah', 'Hala',
'Reem', 'Lama', 'Nada', 'Rana', 'Dina', 'Lina', 'Maha', 'Wafa', 'Amal',
'Najla', 'Huda', 'Layla', 'Nour', 'Ghada', 'Rania'
]
saudi_family_names = [
'Al-Saud', 'Al-Rashid', 'Al-Otaibi', 'Al-Dosari', 'Al-Harbi', 'Al-Zahrani',
'Al-Ghamdi', 'Al-Qahtani', 'Al-Mutairi', 'Al-Malki', 'Al-Subai', 'Al-Shehri',
'Al-Dawsari', 'Al-Enezi', 'Al-Rasheed', 'Al-Faraj', 'Al-Mansour', 'Al-Nasser'
]
blood_groups = list(BloodGroup.objects.all())
created_by = random.choice(users)
donors = []
for i in range(count):
try:
gender = random.choice(['M', 'F'])
first_name = random.choice(saudi_male_names if gender == 'M' else saudi_female_names)
last_name = random.choice(saudi_family_names)
national_id = f"{random.choice([1, 2])}{random.randint(100000000, 999999999)}" # 10 digits
donor_id = f"SA{random.randint(100000, 999999)}"
age = random.randint(18, 65)
birth_date = timezone.now().date() - timedelta(days=age * 365)
phone = f"+966{random.choice([50, 51, 52, 53, 54, 55])}{random.randint(1000000, 9999999)}"
weight = random.randint(60, 120) if gender == 'M' else random.randint(45, 90)
height = random.randint(150, 190)
blood_group = random.choice(blood_groups)
donor = Donor.objects.create(
donor_id=donor_id,
first_name=first_name,
last_name=last_name,
date_of_birth=birth_date,
gender=gender,
blood_group=blood_group,
national_id=national_id,
phone=phone, # field is `phone`
email=f"{first_name.lower()}.{last_name.lower().replace('-', '').replace(' ', '')}@gmail.com",
address=f"{random.randint(1, 999)} King Fahd Road, Saudi Arabia",
emergency_contact_name=f"{random.choice(saudi_male_names + saudi_female_names)} {random.choice(saudi_family_names)}",
emergency_contact_phone=f"+966{random.choice([50, 51, 52, 53, 54, 55])}{random.randint(1000000, 9999999)}",
donor_type='voluntary',
status='active',
last_donation_date=timezone.now() - timedelta(days=random.randint(60, 365)) if random.random() < 0.6 else None,
total_donations=random.randint(0, 10),
weight=weight,
height=height,
notes="Saudi donor",
created_by=created_by, # required
)
donors.append(donor)
except Exception as e:
self.stdout.write(f"Error creating donor {i}: {str(e)}")
continue
self.stdout.write(f'✓ Created {len(donors)} Saudi donors')
return donors
def create_blood_units(self, count, donors, users):
"""Create blood units; save location as string and update stock."""
if not donors:
self.stdout.write('No donors available for blood units')
return []
components = list(BloodComponent.objects.filter(is_active=True))
locations = list(InventoryLocation.objects.filter(is_active=True))
blood_units = []
for i in range(count):
try:
donor = random.choice(donors)
component = random.choice(components)
inv = random.choice(locations)
unit_number = f"SA{timezone.now().year}{random.randint(100000, 999999)}"
collection_date = timezone.now() - timedelta(days=random.randint(0, 30))
expiry_date = collection_date + timedelta(days=component.shelf_life_days)
status = random.choice(['available', 'reserved', 'issued', 'collected', 'testing', 'quarantine'])
blood_unit = BloodUnit.objects.create(
unit_number=unit_number,
donor=donor,
blood_group=donor.blood_group,
component=component,
volume_ml=component.volume_ml,
collection_date=collection_date,
expiry_date=expiry_date,
status=status,
location=inv.name, # CharField on model
collected_by=random.choice(users),
collection_site='King Fahd Medical City Blood Bank',
bag_type='single',
anticoagulant='CPDA-1',
notes=f"Collected from donor {donor.donor_id}"
)
blood_units.append(blood_unit)
# Update inventory stock (bounded by capacity)
inv.current_stock = min(inv.capacity, inv.current_stock + 1)
inv.save(update_fields=['current_stock'])
except Exception as e:
self.stdout.write(f"Error creating blood unit {i}: {str(e)}")
continue
self.stdout.write(f'✓ Created {len(blood_units)} blood units')
return blood_units
def create_blood_tests(self, blood_units, users):
"""Create infectious disease tests for each unit."""
test_types = ['hiv', 'hbv', 'hcv', 'syphilis'] # must match TEST_TYPE_CHOICES keys
for unit in blood_units:
try:
for test_type in test_types:
result = random.choices(['negative', 'positive'], weights=[0.95, 0.05])[0]
BloodTest.objects.create(
blood_unit=unit,
test_type=test_type,
result=result,
test_date=unit.collection_date + timedelta(hours=random.randint(2, 24)),
tested_by=random.choice(users),
equipment_used=f"Abbott PRISM {random.randint(1000, 9999)}",
lot_number=f"LOT{random.randint(100000, 999999)}",
notes="Routine infectious disease screening"
)
except Exception as e:
self.stdout.write(f"Error creating tests for unit {unit.unit_number}: {str(e)}")
continue
self.stdout.write(f'✓ Created blood tests for {len(blood_units)} units')
def create_blood_requests(self, count, patients, departments, users):
"""Create blood requests from departments."""
components = list(BloodComponent.objects.filter(is_active=True))
blood_groups = list(BloodGroup.objects.all())
for i in range(count):
try:
patient = random.choice(patients)
department = random.choice(departments)
component = random.choice(components)
request_number = f"REQ{timezone.now().year}{random.randint(10000, 99999)}"
indications = [
'Surgical blood loss during cardiac surgery',
'Anemia secondary to chronic kidney disease',
'Postpartum hemorrhage',
'Trauma-related blood loss',
'Chemotherapy-induced anemia'
]
urgency = random.choice(['routine', 'urgent', 'emergency'])
status = random.choice(['pending', 'processing', 'ready'])
request_date = timezone.now() - timedelta(days=random.randint(0, 7))
required_by = request_date + timedelta(hours=random.randint(2, 48))
BloodRequest.objects.create(
request_number=request_number,
patient=patient,
requesting_department=department,
requesting_physician=random.choice(users),
component_requested=component,
units_requested=random.randint(1, 3),
urgency=urgency,
indication=random.choice(indications),
patient_blood_group=random.choice(blood_groups),
hemoglobin_level=round(random.uniform(7.0, 12.0), 1),
status=status,
# auto_now_add request_date on model; we still set required_by
required_by=required_by,
notes=f"Request from {department.name}"
)
except Exception as e:
self.stdout.write(f"Error creating blood request {i}: {str(e)}")
continue
self.stdout.write('✓ Created blood requests')
def create_quality_control_records(self, users):
"""Create quality control records."""
test_types = ['temperature_monitoring', 'equipment_calibration', 'reagent_testing']
for i in range(10):
try:
test_type = random.choice(test_types)
status = random.choice(['pass', 'fail', 'pending'])
performed_by = random.choice(users)
qc = QualityControl.objects.create(
test_type=test_type,
test_date=timezone.now() - timedelta(days=random.randint(0, 30)),
equipment_tested=f"Equipment {random.randint(1000, 9999)}",
parameters_tested="Temperature range, accuracy",
expected_results="Within acceptable limits",
actual_results="Pass" if status == 'pass' else "Out of range" if status == 'fail' else "Pending",
status=status,
performed_by=performed_by,
corrective_action="Recalibration performed" if status == 'fail' else "",
next_test_date=timezone.now() + timedelta(days=30)
)
# Optionally auto-review passes
if status == 'pass' and random.random() < 0.5:
qc.reviewed_by = random.choice(users)
qc.review_date = timezone.now()
qc.review_notes = "Reviewed and verified."
qc.save(update_fields=['reviewed_by', 'review_date', 'review_notes'])
except Exception as e:
self.stdout.write(f"Error creating QC record {i}: {str(e)}")
continue
self.stdout.write('✓ Created quality control records')

View File

@ -0,0 +1,925 @@
# Generated by Django 5.2.4 on 2025-09-04 15:11
import django.core.validators
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("core", "0001_initial"),
("patients", "0007_alter_consenttemplate_category"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="BloodComponent",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"name",
models.CharField(
choices=[
("whole_blood", "Whole Blood"),
("packed_rbc", "Packed Red Blood Cells"),
("fresh_frozen_plasma", "Fresh Frozen Plasma"),
("platelets", "Platelets"),
("cryoprecipitate", "Cryoprecipitate"),
("granulocytes", "Granulocytes"),
],
max_length=50,
unique=True,
),
),
("description", models.TextField()),
("shelf_life_days", models.PositiveIntegerField()),
("storage_temperature", models.CharField(max_length=50)),
("volume_ml", models.PositiveIntegerField()),
("is_active", models.BooleanField(default=True)),
],
options={
"ordering": ["name"],
},
),
migrations.CreateModel(
name="InventoryLocation",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100, unique=True)),
(
"location_type",
models.CharField(
choices=[
("refrigerator", "Refrigerator"),
("freezer", "Freezer"),
("platelet_agitator", "Platelet Agitator"),
("quarantine", "Quarantine"),
("testing", "Testing Area"),
],
max_length=20,
),
),
("temperature_range", models.CharField(max_length=50)),
("temperature", models.FloatField(blank=True, null=True)),
("capacity", models.PositiveIntegerField()),
("current_stock", models.PositiveIntegerField(default=0)),
("is_active", models.BooleanField(default=True)),
("notes", models.TextField(blank=True)),
],
options={
"ordering": ["name"],
},
),
migrations.CreateModel(
name="BloodGroup",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"abo_type",
models.CharField(
choices=[("A", "A"), ("B", "B"), ("AB", "AB"), ("O", "O")],
max_length=2,
),
),
(
"rh_factor",
models.CharField(
choices=[("POS", "Positive"), ("NEG", "Negative")], max_length=8
),
),
],
options={
"ordering": ["abo_type", "rh_factor"],
"unique_together": {("abo_type", "rh_factor")},
},
),
migrations.CreateModel(
name="BloodRequest",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("request_number", models.CharField(max_length=20, unique=True)),
(
"units_requested",
models.PositiveIntegerField(
validators=[django.core.validators.MinValueValidator(1)]
),
),
(
"urgency",
models.CharField(
choices=[
("routine", "Routine"),
("urgent", "Urgent"),
("emergency", "Emergency"),
],
default="routine",
max_length=10,
),
),
("indication", models.TextField()),
("special_requirements", models.TextField(blank=True)),
("hemoglobin_level", models.FloatField(blank=True, null=True)),
("platelet_count", models.IntegerField(blank=True, null=True)),
(
"status",
models.CharField(
choices=[
("pending", "Pending"),
("processing", "Processing"),
("ready", "Ready"),
("issued", "Issued"),
("completed", "Completed"),
("cancelled", "Cancelled"),
],
default="pending",
max_length=15,
),
),
("request_date", models.DateTimeField(auto_now_add=True)),
("required_by", models.DateTimeField()),
("processed_at", models.DateTimeField(blank=True, null=True)),
("notes", models.TextField(blank=True)),
("cancellation_reason", models.TextField(blank=True)),
("cancellation_date", models.DateTimeField(blank=True, null=True)),
(
"cancelled_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="cancelled_requests",
to=settings.AUTH_USER_MODEL,
),
),
(
"component_requested",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="blood_bank.bloodcomponent",
),
),
(
"patient",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="blood_requests",
to="patients.patientprofile",
),
),
(
"patient_blood_group",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="blood_bank.bloodgroup",
),
),
(
"processed_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="processed_requests",
to=settings.AUTH_USER_MODEL,
),
),
(
"requesting_department",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="core.department",
),
),
(
"requesting_physician",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="blood_requests",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["-request_date"],
},
),
migrations.CreateModel(
name="BloodUnit",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("unit_number", models.CharField(max_length=20, unique=True)),
("collection_date", models.DateTimeField()),
("expiry_date", models.DateTimeField()),
("volume_ml", models.PositiveIntegerField()),
(
"status",
models.CharField(
choices=[
("collected", "Collected"),
("testing", "Testing"),
("quarantine", "Quarantine"),
("available", "Available"),
("reserved", "Reserved"),
("issued", "Issued"),
("transfused", "Transfused"),
("expired", "Expired"),
("discarded", "Discarded"),
],
default="collected",
max_length=20,
),
),
("location", models.CharField(max_length=100)),
("bag_type", models.CharField(max_length=50)),
("anticoagulant", models.CharField(default="CPDA-1", max_length=50)),
("collection_site", models.CharField(max_length=100)),
("notes", models.TextField(blank=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"blood_group",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="blood_bank.bloodgroup",
),
),
(
"collected_by",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="collected_units",
to=settings.AUTH_USER_MODEL,
),
),
(
"component",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="blood_bank.bloodcomponent",
),
),
],
options={
"ordering": ["-collection_date"],
},
),
migrations.CreateModel(
name="CrossMatch",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"test_type",
models.CharField(
choices=[
("major", "Major Crossmatch"),
("minor", "Minor Crossmatch"),
("immediate_spin", "Immediate Spin"),
("antiglobulin", "Antiglobulin Test"),
],
max_length=20,
),
),
(
"compatibility",
models.CharField(
choices=[
("compatible", "Compatible"),
("incompatible", "Incompatible"),
("pending", "Pending"),
],
default="pending",
max_length=15,
),
),
("test_date", models.DateTimeField()),
("temperature", models.CharField(default="37°C", max_length=20)),
("incubation_time", models.PositiveIntegerField(default=15)),
("notes", models.TextField(blank=True)),
("verified_at", models.DateTimeField(blank=True, null=True)),
(
"blood_unit",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="crossmatches",
to="blood_bank.bloodunit",
),
),
(
"recipient",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="patients.patientprofile",
),
),
(
"tested_by",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to=settings.AUTH_USER_MODEL,
),
),
(
"verified_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="verified_crossmatches",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["-test_date"],
},
),
migrations.CreateModel(
name="BloodIssue",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("issue_date", models.DateTimeField(auto_now_add=True)),
("expiry_time", models.DateTimeField()),
("returned", models.BooleanField(default=False)),
("return_date", models.DateTimeField(blank=True, null=True)),
("return_reason", models.TextField(blank=True)),
("notes", models.TextField(blank=True)),
(
"issued_by",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="issued_units",
to=settings.AUTH_USER_MODEL,
),
),
(
"issued_to",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="received_units",
to=settings.AUTH_USER_MODEL,
),
),
(
"blood_request",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="issues",
to="blood_bank.bloodrequest",
),
),
(
"blood_unit",
models.OneToOneField(
on_delete=django.db.models.deletion.PROTECT,
related_name="issue",
to="blood_bank.bloodunit",
),
),
(
"crossmatch",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="blood_bank.crossmatch",
),
),
],
options={
"ordering": ["-issue_date"],
},
),
migrations.CreateModel(
name="Donor",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("donor_id", models.CharField(max_length=20, unique=True)),
("first_name", models.CharField(max_length=100)),
("last_name", models.CharField(max_length=100)),
("date_of_birth", models.DateField()),
(
"gender",
models.CharField(
choices=[
("male", "Male"),
("female", "Female"),
("other", "Other"),
],
max_length=10,
),
),
("phone", models.CharField(max_length=20)),
("email", models.EmailField(blank=True, max_length=254)),
("address", models.TextField()),
("emergency_contact_name", models.CharField(max_length=100)),
("emergency_contact_phone", models.CharField(max_length=20)),
(
"donor_type",
models.CharField(
choices=[
("voluntary", "Voluntary"),
("replacement", "Replacement"),
("autologous", "Autologous"),
("directed", "Directed"),
],
default="voluntary",
max_length=20,
),
),
(
"status",
models.CharField(
choices=[
("active", "Active"),
("deferred", "Deferred"),
("permanently_deferred", "Permanently Deferred"),
("inactive", "Inactive"),
],
default="active",
max_length=20,
),
),
("registration_date", models.DateTimeField(auto_now_add=True)),
("last_donation_date", models.DateTimeField(blank=True, null=True)),
("total_donations", models.PositiveIntegerField(default=0)),
(
"weight",
models.FloatField(
validators=[django.core.validators.MinValueValidator(45.0)]
),
),
(
"height",
models.FloatField(
validators=[django.core.validators.MinValueValidator(140.0)]
),
),
("notes", models.TextField(blank=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"blood_group",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="blood_bank.bloodgroup",
),
),
(
"created_by",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="created_donors",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["-registration_date"],
},
),
migrations.AddField(
model_name="bloodunit",
name="donor",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="blood_units",
to="blood_bank.donor",
),
),
migrations.CreateModel(
name="QualityControl",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"test_type",
models.CharField(
choices=[
("temperature_monitoring", "Temperature Monitoring"),
("equipment_calibration", "Equipment Calibration"),
("reagent_testing", "Reagent Testing"),
("proficiency_testing", "Proficiency Testing"),
("process_validation", "Process Validation"),
],
max_length=30,
),
),
("test_date", models.DateTimeField()),
("equipment_tested", models.CharField(blank=True, max_length=100)),
("parameters_tested", models.TextField()),
("expected_results", models.TextField()),
("actual_results", models.TextField()),
(
"status",
models.CharField(
choices=[
("pass", "Pass"),
("fail", "Fail"),
("pending", "Pending"),
],
max_length=10,
),
),
("review_date", models.DateTimeField(blank=True, null=True)),
("review_notes", models.TextField(blank=True)),
("corrective_action", models.TextField(blank=True)),
("next_test_date", models.DateTimeField(blank=True, null=True)),
("capa_initiated", models.BooleanField(default=False)),
("capa_number", models.CharField(blank=True, max_length=50)),
(
"capa_priority",
models.CharField(
blank=True,
choices=[
("low", "Low"),
("medium", "Medium"),
("high", "High"),
],
max_length=10,
),
),
("capa_date", models.DateTimeField(blank=True, null=True)),
("capa_assessment", models.TextField(blank=True)),
(
"capa_status",
models.CharField(
blank=True,
choices=[
("open", "Open"),
("in_progress", "In Progress"),
("closed", "Closed"),
],
max_length=20,
),
),
(
"capa_initiated_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="initiated_capas",
to=settings.AUTH_USER_MODEL,
),
),
(
"performed_by",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="qc_tests",
to=settings.AUTH_USER_MODEL,
),
),
(
"reviewed_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="reviewed_qc_tests",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["-test_date"],
},
),
migrations.CreateModel(
name="Transfusion",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("start_time", models.DateTimeField()),
("end_time", models.DateTimeField(blank=True, null=True)),
(
"status",
models.CharField(
choices=[
("started", "Started"),
("in_progress", "In Progress"),
("completed", "Completed"),
("stopped", "Stopped"),
("adverse_reaction", "Adverse Reaction"),
],
default="started",
max_length=20,
),
),
(
"volume_transfused",
models.PositiveIntegerField(blank=True, null=True),
),
("transfusion_rate", models.CharField(blank=True, max_length=50)),
("pre_transfusion_vitals", models.JSONField(default=dict)),
("post_transfusion_vitals", models.JSONField(default=dict)),
("vital_signs_history", models.JSONField(default=list)),
("current_blood_pressure", models.CharField(blank=True, max_length=20)),
("current_heart_rate", models.IntegerField(blank=True, null=True)),
("current_temperature", models.FloatField(blank=True, null=True)),
(
"current_respiratory_rate",
models.IntegerField(blank=True, null=True),
),
(
"current_oxygen_saturation",
models.IntegerField(blank=True, null=True),
),
("last_vitals_check", models.DateTimeField(blank=True, null=True)),
("patient_consent", models.BooleanField(default=False)),
("consent_date", models.DateTimeField(blank=True, null=True)),
("notes", models.TextField(blank=True)),
("stop_reason", models.TextField(blank=True)),
("completion_notes", models.TextField(blank=True)),
(
"administered_by",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="administered_transfusions",
to=settings.AUTH_USER_MODEL,
),
),
(
"blood_issue",
models.OneToOneField(
on_delete=django.db.models.deletion.PROTECT,
related_name="transfusion",
to="blood_bank.bloodissue",
),
),
(
"completed_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="completed_transfusions",
to=settings.AUTH_USER_MODEL,
),
),
(
"stopped_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="stopped_transfusions",
to=settings.AUTH_USER_MODEL,
),
),
(
"witnessed_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="witnessed_transfusions",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["-start_time"],
},
),
migrations.CreateModel(
name="AdverseReaction",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"reaction_type",
models.CharField(
choices=[
("febrile", "Febrile Non-Hemolytic"),
("allergic", "Allergic"),
("hemolytic_acute", "Acute Hemolytic"),
("hemolytic_delayed", "Delayed Hemolytic"),
("anaphylactic", "Anaphylactic"),
("septic", "Septic"),
("circulatory_overload", "Circulatory Overload"),
("lung_injury", "Transfusion-Related Acute Lung Injury"),
("other", "Other"),
],
max_length=30,
),
),
(
"severity",
models.CharField(
choices=[
("mild", "Mild"),
("moderate", "Moderate"),
("severe", "Severe"),
("life_threatening", "Life Threatening"),
],
max_length=20,
),
),
("onset_time", models.DateTimeField()),
("symptoms", models.TextField()),
("treatment_given", models.TextField()),
("outcome", models.TextField()),
("investigation_notes", models.TextField(blank=True)),
("regulatory_reported", models.BooleanField(default=False)),
("report_date", models.DateTimeField(blank=True, null=True)),
(
"investigated_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="investigated_reactions",
to=settings.AUTH_USER_MODEL,
),
),
(
"reported_by",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="reported_reactions",
to=settings.AUTH_USER_MODEL,
),
),
(
"transfusion",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="adverse_reactions",
to="blood_bank.transfusion",
),
),
],
options={
"ordering": ["-onset_time"],
},
),
migrations.CreateModel(
name="BloodTest",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"test_type",
models.CharField(
choices=[
("abo_rh", "ABO/Rh Typing"),
("antibody_screen", "Antibody Screening"),
("hiv", "HIV"),
("hbv", "Hepatitis B"),
("hcv", "Hepatitis C"),
("syphilis", "Syphilis"),
("htlv", "HTLV"),
("cmv", "CMV"),
("malaria", "Malaria"),
],
max_length=20,
),
),
(
"result",
models.CharField(
choices=[
("positive", "Positive"),
("negative", "Negative"),
("indeterminate", "Indeterminate"),
("pending", "Pending"),
],
default="pending",
max_length=15,
),
),
("test_date", models.DateTimeField()),
("equipment_used", models.CharField(blank=True, max_length=100)),
("lot_number", models.CharField(blank=True, max_length=50)),
("notes", models.TextField(blank=True)),
("verified_at", models.DateTimeField(blank=True, null=True)),
(
"tested_by",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to=settings.AUTH_USER_MODEL,
),
),
(
"verified_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="verified_tests",
to=settings.AUTH_USER_MODEL,
),
),
(
"blood_unit",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="tests",
to="blood_bank.bloodunit",
),
),
],
options={
"ordering": ["-test_date"],
"unique_together": {("blood_unit", "test_type")},
},
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 5.2.4 on 2025-09-04 15:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("blood_bank", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="donor",
name="national_id",
field=models.CharField(default=1129632798, max_length=10, unique=True),
preserve_default=False,
),
migrations.AlterField(
model_name="donor",
name="gender",
field=models.CharField(
choices=[("M", "Male"), ("F", "Female"), ("O", "Other")], max_length=10
),
),
]

View File

525
blood_bank/models.py Normal file
View File

@ -0,0 +1,525 @@
from django.db import models
# from django.contrib.auth.models import User
from django.core.validators import MinValueValidator, MaxValueValidator
from django.utils import timezone
from datetime import timedelta
from core.models import Department
from patients.models import PatientProfile
from accounts.models import User
class BloodGroup(models.Model):
"""Blood group types (A, B, AB, O) with Rh factor"""
ABO_CHOICES = [
('A', 'A'),
('B', 'B'),
('AB', 'AB'),
('O', 'O'),
]
RH_CHOICES = [
('POS', 'Positive'),
('NEG', 'Negative'),
]
abo_type = models.CharField(max_length=2, choices=ABO_CHOICES)
rh_factor = models.CharField(max_length=8, choices=RH_CHOICES)
class Meta:
unique_together = ['abo_type', 'rh_factor']
ordering = ['abo_type', 'rh_factor']
def __str__(self):
return f"{self.abo_type} {self.rh_factor.capitalize()}"
@property
def display_name(self):
rh_symbol = '+' if self.rh_factor == 'POS' else '-'
return f"{self.abo_type}{rh_symbol}"
class Donor(models.Model):
"""Blood donor information and eligibility"""
DONOR_TYPE_CHOICES = [
('voluntary', 'Voluntary'),
('replacement', 'Replacement'),
('autologous', 'Autologous'),
('directed', 'Directed'),
]
STATUS_CHOICES = [
('active', 'Active'),
('deferred', 'Deferred'),
('permanently_deferred', 'Permanently Deferred'),
('inactive', 'Inactive'),
]
donor_id = models.CharField(max_length=20, unique=True)
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
date_of_birth = models.DateField()
gender = models.CharField(max_length=10, choices=[('M', 'Male'), ('F', 'Female'), ('O', 'Other')])
national_id = models.CharField(max_length=10, unique=True)
blood_group = models.ForeignKey(BloodGroup, on_delete=models.PROTECT)
phone = models.CharField(max_length=20)
email = models.EmailField(blank=True)
address = models.TextField()
emergency_contact_name = models.CharField(max_length=100)
emergency_contact_phone = models.CharField(max_length=20)
donor_type = models.CharField(max_length=20, choices=DONOR_TYPE_CHOICES, default='voluntary')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active')
registration_date = models.DateTimeField(auto_now_add=True)
last_donation_date = models.DateTimeField(null=True, blank=True)
total_donations = models.PositiveIntegerField(default=0)
weight = models.FloatField(validators=[MinValueValidator(45.0)]) # Minimum weight for donation
height = models.FloatField(validators=[MinValueValidator(140.0)]) # In cm
notes = models.TextField(blank=True)
created_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='created_donors')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-registration_date']
def __str__(self):
return f"{self.donor_id} - {self.first_name} {self.last_name}"
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"
@property
def age(self):
today = timezone.now().date()
return today.year - self.date_of_birth.year - (
(today.month, today.day) < (self.date_of_birth.month, self.date_of_birth.day))
@property
def is_eligible_for_donation(self):
"""Check if donor is eligible for donation based on last donation date"""
if self.status != 'active':
return False
if not self.last_donation_date:
return True
# Minimum 56 days between whole blood donations
days_since_last = (timezone.now() - self.last_donation_date).days
return days_since_last >= 56
@property
def next_eligible_date(self):
"""Calculate next eligible donation date"""
if not self.last_donation_date:
return timezone.now().date()
return (self.last_donation_date + timedelta(days=56)).date()
class BloodComponent(models.Model):
"""Types of blood components (Whole Blood, RBC, Plasma, Platelets, etc.)"""
COMPONENT_CHOICES = [
('whole_blood', 'Whole Blood'),
('packed_rbc', 'Packed Red Blood Cells'),
('fresh_frozen_plasma', 'Fresh Frozen Plasma'),
('platelets', 'Platelets'),
('cryoprecipitate', 'Cryoprecipitate'),
('granulocytes', 'Granulocytes'),
]
name = models.CharField(max_length=50, choices=COMPONENT_CHOICES, unique=True)
description = models.TextField()
shelf_life_days = models.PositiveIntegerField() # Storage duration in days
storage_temperature = models.CharField(max_length=50) # Storage requirements
volume_ml = models.PositiveIntegerField() # Standard volume in ml
is_active = models.BooleanField(default=True)
class Meta:
ordering = ['name']
def __str__(self):
return self.get_name_display()
class BloodUnit(models.Model):
"""Individual blood unit from donation to disposal"""
STATUS_CHOICES = [
('collected', 'Collected'),
('testing', 'Testing'),
('quarantine', 'Quarantine'),
('available', 'Available'),
('reserved', 'Reserved'),
('issued', 'Issued'),
('transfused', 'Transfused'),
('expired', 'Expired'),
('discarded', 'Discarded'),
]
unit_number = models.CharField(max_length=20, unique=True)
donor = models.ForeignKey(Donor, on_delete=models.PROTECT, related_name='blood_units')
component = models.ForeignKey(BloodComponent, on_delete=models.PROTECT)
blood_group = models.ForeignKey(BloodGroup, on_delete=models.PROTECT)
collection_date = models.DateTimeField()
expiry_date = models.DateTimeField()
volume_ml = models.PositiveIntegerField()
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='collected')
location = models.CharField(max_length=100) # Storage location/refrigerator
bag_type = models.CharField(max_length=50)
anticoagulant = models.CharField(max_length=50, default='CPDA-1')
collection_site = models.CharField(max_length=100)
collected_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='collected_units')
notes = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-collection_date']
def __str__(self):
return f"{self.unit_number} - {self.component} ({self.blood_group})"
@property
def is_expired(self):
return timezone.now() > self.expiry_date
@property
def days_to_expiry(self):
delta = self.expiry_date - timezone.now()
return delta.days if delta.days > 0 else 0
@property
def is_available(self):
return self.status == 'available' and not self.is_expired
class BloodTest(models.Model):
"""Blood testing results for infectious diseases and compatibility"""
TEST_TYPE_CHOICES = [
('abo_rh', 'ABO/Rh Typing'),
('antibody_screen', 'Antibody Screening'),
('hiv', 'HIV'),
('hbv', 'Hepatitis B'),
('hcv', 'Hepatitis C'),
('syphilis', 'Syphilis'),
('htlv', 'HTLV'),
('cmv', 'CMV'),
('malaria', 'Malaria'),
]
RESULT_CHOICES = [
('positive', 'Positive'),
('negative', 'Negative'),
('indeterminate', 'Indeterminate'),
('pending', 'Pending'),
]
blood_unit = models.ForeignKey(BloodUnit, on_delete=models.CASCADE, related_name='tests')
test_type = models.CharField(max_length=20, choices=TEST_TYPE_CHOICES)
result = models.CharField(max_length=15, choices=RESULT_CHOICES, default='pending')
test_date = models.DateTimeField()
tested_by = models.ForeignKey(User, on_delete=models.PROTECT)
equipment_used = models.CharField(max_length=100, blank=True)
lot_number = models.CharField(max_length=50, blank=True)
notes = models.TextField(blank=True)
verified_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='verified_tests', null=True,
blank=True)
verified_at = models.DateTimeField(null=True, blank=True)
class Meta:
unique_together = ['blood_unit', 'test_type']
ordering = ['-test_date']
def __str__(self):
return f"{self.blood_unit.unit_number} - {self.get_test_type_display()}: {self.result}"
class CrossMatch(models.Model):
"""Cross-matching tests between donor blood and recipient"""
COMPATIBILITY_CHOICES = [
('compatible', 'Compatible'),
('incompatible', 'Incompatible'),
('pending', 'Pending'),
]
TEST_TYPE_CHOICES = [
('major', 'Major Crossmatch'),
('minor', 'Minor Crossmatch'),
('immediate_spin', 'Immediate Spin'),
('antiglobulin', 'Antiglobulin Test'),
]
blood_unit = models.ForeignKey(BloodUnit, on_delete=models.CASCADE, related_name='crossmatches')
recipient = models.ForeignKey(PatientProfile, on_delete=models.PROTECT)
test_type = models.CharField(max_length=20, choices=TEST_TYPE_CHOICES)
compatibility = models.CharField(max_length=15, choices=COMPATIBILITY_CHOICES, default='pending')
test_date = models.DateTimeField()
tested_by = models.ForeignKey(User, on_delete=models.PROTECT)
temperature = models.CharField(max_length=20, default='37°C')
incubation_time = models.PositiveIntegerField(default=15) # minutes
notes = models.TextField(blank=True)
verified_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='verified_crossmatches', null=True,
blank=True)
verified_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ['-test_date']
def __str__(self):
return f"{self.blood_unit.unit_number} x {self.recipient} - {self.compatibility}"
class BloodRequest(models.Model):
"""Blood transfusion requests from clinical departments"""
URGENCY_CHOICES = [
('routine', 'Routine'),
('urgent', 'Urgent'),
('emergency', 'Emergency'),
]
STATUS_CHOICES = [
('pending', 'Pending'),
('processing', 'Processing'),
('ready', 'Ready'),
('issued', 'Issued'),
('completed', 'Completed'),
('cancelled', 'Cancelled'),
]
request_number = models.CharField(max_length=20, unique=True)
patient = models.ForeignKey(PatientProfile, on_delete=models.PROTECT, related_name='blood_requests')
requesting_department = models.ForeignKey(Department, on_delete=models.PROTECT)
requesting_physician = models.ForeignKey(User, on_delete=models.PROTECT, related_name='blood_requests')
component_requested = models.ForeignKey(BloodComponent, on_delete=models.PROTECT)
units_requested = models.PositiveIntegerField(validators=[MinValueValidator(1)])
urgency = models.CharField(max_length=10, choices=URGENCY_CHOICES, default='routine')
indication = models.TextField() # Clinical indication for transfusion
special_requirements = models.TextField(blank=True) # CMV negative, irradiated, etc.
patient_blood_group = models.ForeignKey(BloodGroup, on_delete=models.PROTECT)
hemoglobin_level = models.FloatField(null=True, blank=True)
platelet_count = models.IntegerField(null=True, blank=True)
status = models.CharField(max_length=15, choices=STATUS_CHOICES, default='pending')
request_date = models.DateTimeField(auto_now_add=True)
required_by = models.DateTimeField()
processed_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='processed_requests', null=True,
blank=True)
processed_at = models.DateTimeField(null=True, blank=True)
notes = models.TextField(blank=True)
# Cancellation fields
cancellation_reason = models.TextField(blank=True)
cancelled_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='cancelled_requests', null=True,
blank=True)
cancellation_date = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ['-request_date']
def __str__(self):
return f"{self.request_number} - {self.patient} ({self.component_requested})"
@property
def is_overdue(self):
return timezone.now() > self.required_by and self.status not in ['completed', 'cancelled']
class BloodIssue(models.Model):
"""Blood unit issuance to patients"""
blood_request = models.ForeignKey(BloodRequest, on_delete=models.PROTECT, related_name='issues')
blood_unit = models.OneToOneField(BloodUnit, on_delete=models.PROTECT, related_name='issue')
crossmatch = models.ForeignKey(CrossMatch, on_delete=models.PROTECT, null=True, blank=True)
issued_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='issued_units')
issued_to = models.ForeignKey(User, on_delete=models.PROTECT, related_name='received_units') # Nurse/physician
issue_date = models.DateTimeField(auto_now_add=True)
expiry_time = models.DateTimeField() # 4 hours from issue for RBC
returned = models.BooleanField(default=False)
return_date = models.DateTimeField(null=True, blank=True)
return_reason = models.TextField(blank=True)
notes = models.TextField(blank=True)
class Meta:
ordering = ['-issue_date']
def __str__(self):
return f"{self.blood_unit.unit_number} issued to {self.blood_request.patient}"
@property
def is_expired(self):
return timezone.now() > self.expiry_time and not self.returned
class Transfusion(models.Model):
"""Blood transfusion administration records"""
STATUS_CHOICES = [
('started', 'Started'),
('in_progress', 'In Progress'),
('completed', 'Completed'),
('stopped', 'Stopped'),
('adverse_reaction', 'Adverse Reaction'),
]
blood_issue = models.OneToOneField(BloodIssue, on_delete=models.PROTECT, related_name='transfusion')
start_time = models.DateTimeField()
end_time = models.DateTimeField(null=True, blank=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='started')
volume_transfused = models.PositiveIntegerField(null=True, blank=True) # ml
transfusion_rate = models.CharField(max_length=50, blank=True) # ml/hour
administered_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='administered_transfusions')
witnessed_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='witnessed_transfusions', null=True,
blank=True)
pre_transfusion_vitals = models.JSONField(default=dict) # BP, HR, Temp, etc.
post_transfusion_vitals = models.JSONField(default=dict)
vital_signs_history = models.JSONField(default=list) # Array of vital signs during transfusion
current_blood_pressure = models.CharField(max_length=20, blank=True)
current_heart_rate = models.IntegerField(null=True, blank=True)
current_temperature = models.FloatField(null=True, blank=True)
current_respiratory_rate = models.IntegerField(null=True, blank=True)
current_oxygen_saturation = models.IntegerField(null=True, blank=True)
last_vitals_check = models.DateTimeField(null=True, blank=True)
patient_consent = models.BooleanField(default=False)
consent_date = models.DateTimeField(null=True, blank=True)
notes = models.TextField(blank=True)
# Completion/Stop fields
stop_reason = models.TextField(blank=True)
stopped_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='stopped_transfusions', null=True,
blank=True)
completed_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='completed_transfusions', null=True,
blank=True)
completion_notes = models.TextField(blank=True)
class Meta:
ordering = ['-start_time']
def __str__(self):
return f"Transfusion: {self.blood_issue.blood_unit.unit_number} to {self.blood_issue.blood_request.patient}"
@property
def duration_minutes(self):
if self.end_time:
return int((self.end_time - self.start_time).total_seconds() / 60)
return None
class AdverseReaction(models.Model):
"""Adverse transfusion reactions"""
SEVERITY_CHOICES = [
('mild', 'Mild'),
('moderate', 'Moderate'),
('severe', 'Severe'),
('life_threatening', 'Life Threatening'),
]
REACTION_TYPE_CHOICES = [
('febrile', 'Febrile Non-Hemolytic'),
('allergic', 'Allergic'),
('hemolytic_acute', 'Acute Hemolytic'),
('hemolytic_delayed', 'Delayed Hemolytic'),
('anaphylactic', 'Anaphylactic'),
('septic', 'Septic'),
('circulatory_overload', 'Circulatory Overload'),
('lung_injury', 'Transfusion-Related Acute Lung Injury'),
('other', 'Other'),
]
transfusion = models.ForeignKey(Transfusion, on_delete=models.CASCADE, related_name='adverse_reactions')
reaction_type = models.CharField(max_length=30, choices=REACTION_TYPE_CHOICES)
severity = models.CharField(max_length=20, choices=SEVERITY_CHOICES)
onset_time = models.DateTimeField()
symptoms = models.TextField()
treatment_given = models.TextField()
outcome = models.TextField()
reported_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='reported_reactions')
investigated_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='investigated_reactions',
null=True, blank=True)
investigation_notes = models.TextField(blank=True)
regulatory_reported = models.BooleanField(default=False)
report_date = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ['-onset_time']
def __str__(self):
return f"{self.get_reaction_type_display()} - {self.severity} ({self.transfusion.blood_issue.blood_request.patient})"
class InventoryLocation(models.Model):
"""Blood bank storage locations"""
LOCATION_TYPE_CHOICES = [
('refrigerator', 'Refrigerator'),
('freezer', 'Freezer'),
('platelet_agitator', 'Platelet Agitator'),
('quarantine', 'Quarantine'),
('testing', 'Testing Area'),
]
name = models.CharField(max_length=100, unique=True)
location_type = models.CharField(max_length=20, choices=LOCATION_TYPE_CHOICES)
temperature_range = models.CharField(max_length=50)
temperature = models.FloatField(null=True, blank=True) # Current temperature
capacity = models.PositiveIntegerField() # Number of units
current_stock = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True)
notes = models.TextField(blank=True)
class Meta:
ordering = ['name']
def __str__(self):
return f"{self.name} ({self.get_location_type_display()})"
@property
def utilization_percentage(self):
if self.capacity == 0:
return 0
return (self.current_stock / self.capacity) * 100
class QualityControl(models.Model):
"""Quality control tests and monitoring"""
TEST_TYPE_CHOICES = [
('temperature_monitoring', 'Temperature Monitoring'),
('equipment_calibration', 'Equipment Calibration'),
('reagent_testing', 'Reagent Testing'),
('proficiency_testing', 'Proficiency Testing'),
('process_validation', 'Process Validation'),
]
STATUS_CHOICES = [
('pass', 'Pass'),
('fail', 'Fail'),
('pending', 'Pending'),
]
test_type = models.CharField(max_length=30, choices=TEST_TYPE_CHOICES)
test_date = models.DateTimeField()
equipment_tested = models.CharField(max_length=100, blank=True)
parameters_tested = models.TextField()
expected_results = models.TextField()
actual_results = models.TextField()
status = models.CharField(max_length=10, choices=STATUS_CHOICES)
performed_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='qc_tests')
reviewed_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='reviewed_qc_tests', null=True,
blank=True)
review_date = models.DateTimeField(null=True, blank=True)
review_notes = models.TextField(blank=True)
corrective_action = models.TextField(blank=True)
next_test_date = models.DateTimeField(null=True, blank=True)
# CAPA (Corrective and Preventive Action) fields
capa_initiated = models.BooleanField(default=False)
capa_number = models.CharField(max_length=50, blank=True)
capa_priority = models.CharField(max_length=10, choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High')],
blank=True)
capa_initiated_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='initiated_capas', null=True,
blank=True)
capa_date = models.DateTimeField(null=True, blank=True)
capa_assessment = models.TextField(blank=True)
capa_status = models.CharField(max_length=20,
choices=[('open', 'Open'), ('in_progress', 'In Progress'), ('closed', 'Closed')],
blank=True)
class Meta:
ordering = ['-test_date']
def __str__(self):
return f"{self.get_test_type_display()} - {self.test_date.strftime('%Y-%m-%d')} ({self.status})"

3
blood_bank/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

78
blood_bank/urls.py Normal file
View File

@ -0,0 +1,78 @@
from django.urls import path
from . import views
app_name = 'blood_bank'
urlpatterns = [
# Dashboard
path('', views.dashboard, name='dashboard'),
# Donor Management
path('donors/', views.donor_list, name='donor_list'),
path('donors/<int:donor_id>/', views.donor_detail, name='donor_detail'),
path('donors/create/', views.donor_create, name='donor_create'),
path('donors/<int:donor_id>/update/', views.donor_update, name='donor_update'),
path('donors/<int:donor_id>/eligibility/', views.donor_eligibility_check, name='donor_eligibility'),
# Blood Unit Management
path('units/', views.blood_unit_list, name='blood_unit_list'),
path('units/<int:unit_id>/', views.blood_unit_detail, name='blood_unit_detail'),
path('units/create/', views.blood_unit_create, name='blood_unit_create'),
path('units/create/<int:donor_id>/', views.blood_unit_create, name='blood_unit_create_for_donor'),
# Blood Testing
path('units/<int:unit_id>/test/', views.blood_test_create, name='blood_test_create'),
path('units/<int:unit_id>/crossmatch/<int:patient_id>/', views.crossmatch_create, name='crossmatch_create'),
# Blood Requests
path('requests/', views.blood_request_list, name='blood_request_list'),
path('requests/<int:request_id>/', views.blood_request_detail, name='blood_request_detail'),
path('requests/create/', views.blood_request_create, name='blood_request_create'),
# Blood Issue and Transfusion
path('requests/<int:request_id>/issue/', views.blood_issue_create, name='blood_issue_create'),
path('transfusions/', views.transfusion_list, name='transfusion_list'),
path('transfusions/<int:transfusion_id>/', views.transfusion_detail, name='transfusion_detail'),
path('issues/<int:issue_id>/transfusion/', views.transfusion_create, name='transfusion_create'),
# Inventory Management
path('inventory/', views.inventory_overview, name='inventory_overview'),
# Quality Control
path('quality-control/', views.quality_control_list, name='quality_control_list'),
path('quality-control/create/', views.quality_control_create, name='quality_control_create'),
# Reports
path('reports/', views.reports_dashboard, name='reports_dashboard'),
# API Endpoints
path('api/blood-availability/', views.api_blood_availability, name='api_blood_availability'),
path('api/donor-search/', views.api_donor_search, name='api_donor_search'),
# Blood Unit Management APIs
path('api/units/<int:unit_id>/move/', views.api_move_unit, name='api_move_unit'),
path('api/expiry-report/', views.api_expiry_report, name='api_expiry_report'),
# Blood Request Management APIs
path('api/requests/<int:request_id>/cancel/', views.api_cancel_request, name='api_cancel_request'),
path('api/check-availability/', views.api_check_availability, name='api_check_availability'),
path('api/urgency-report/', views.api_urgency_report, name='api_urgency_report'),
# Quality Control APIs
path('api/initiate-capa/', views.api_initiate_capa, name='api_initiate_capa'),
path('api/review-results/', views.api_review_results, name='api_review_results'),
# Transfusion Management APIs
path('api/record-vital-signs/', views.api_record_vital_signs, name='api_record_vital_signs'),
path('api/transfusions/<int:transfusion_id>/stop/', views.api_stop_transfusion, name='api_stop_transfusion'),
path('api/transfusions/<int:transfusion_id>/complete/', views.api_complete_transfusion,
name='api_complete_transfusion'),
# Inventory Management APIs
path('api/inventory-locations/', views.api_inventory_locations, name='api_inventory_locations'),
path('api/locations/<int:location_id>/update/', views.api_update_location, name='api_update_location'),
# Export and Utility APIs
path('api/export-csv/', views.api_export_csv, name='api_export_csv'),
]

1228
blood_bank/views.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,11 @@ from django.db.models import Sum, F
from decimal import Decimal from decimal import Decimal
import operator import operator
from functools import reduce from functools import reduce
from django.utils import timezone
from datetime import datetime, date
from dateutil.relativedelta import relativedelta # pip install python-dateutil
import math
register = template.Library() register = template.Library()
@ -320,3 +325,112 @@ def calculate_stock_value(inventory_items):
return Decimal('0.00') return Decimal('0.00')
def _to_datetime(d):
"""Coerce date or datetime to an aware datetime in current timezone."""
if isinstance(d, datetime):
dt = d
elif isinstance(d, date):
dt = datetime(d.year, d.month, d.day)
else:
return None
if timezone.is_naive(dt):
dt = timezone.make_aware(dt, timezone.get_current_timezone())
return dt
def _age_breakdown(dob, now=None):
"""
Returns (value:int, unit:str) choosing one of hours/days/months/years.
Auto logic:
- < 48 hours -> hours
- < 60 days -> days
- < 24 months -> months
- otherwise -> years
"""
dt_dob = _to_datetime(dob)
if not dt_dob:
return (0, "days")
now = now or timezone.now()
if dt_dob > now: # future DOB guard
return (0, "days")
# Raw time diff
delta = now - dt_dob
total_hours = delta.total_seconds() / 3600.0
total_days = delta.days
# Months/years via relativedelta for calendar accuracy
rd = relativedelta(now, dt_dob)
total_months = rd.years * 12 + rd.months
years = rd.years
if total_hours < 24:
return (int(math.floor(total_hours)), "hours")
if total_days < 30:
return (int(total_days), "days")
if total_months < 12:
return (int(total_months), "months")
return (int(years), "years")
def _age_in_unit(dob, unit, now=None):
"""Force a specific unit: hours/days/months/years."""
dt_dob = _to_datetime(dob)
if not dt_dob:
return 0, unit
now = now or timezone.now()
if dt_dob > now:
return 0, unit
delta = now - dt_dob
if unit == "hours":
val = int(delta.total_seconds() // 3600)
elif unit == "days":
val = delta.days
elif unit == "months":
rd = relativedelta(now, dt_dob)
val = rd.years * 12 + rd.months
elif unit == "years":
rd = relativedelta(now, dt_dob)
val = rd.years
else:
# Fallback to auto if unit invalid
return _age_breakdown(dob, now)
return val, unit
@register.filter
def age_label(dob, mode="auto"):
"""
Returns a human-friendly string like: "12 hours", "17 days", "3 months", "5 years".
Usage:
{{ person.dob|age_label }} -> auto unit
{{ person.dob|age_label:"months" }} -> force months
"""
if mode in ("hours", "days", "months", "years"):
value, unit = _age_in_unit(dob, mode)
else:
value, unit = _age_breakdown(dob)
return f"{value} {unit}"
@register.simple_tag
def age_parts(dob, mode="auto"):
"""
Returns a dict with separate value and unit for flexible rendering.
Usage:
{% age_parts person.dob as a %}
{{ a.value }} {{ a.unit }}
# Or force a unit:
{% age_parts person.dob "months" as a %}
"""
if mode in ("hours", "days", "months", "years"):
value, unit = _age_in_unit(dob, mode)
else:
value, unit = _age_breakdown(dob)
return {"value": value, "unit": unit}

Binary file not shown.

View File

@ -52,6 +52,7 @@ THIRD_PARTY_APPS = [
LOCAL_APPS = [ LOCAL_APPS = [
'core', 'core',
'accounts', 'accounts',
'blood_bank',
'patients', 'patients',
'appointments', 'appointments',
'inpatients', 'inpatients',

View File

@ -37,6 +37,7 @@ urlpatterns += i18n_patterns(
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('', include('core.urls')), path('', include('core.urls')),
path('accounts/', include('accounts.urls')), path('accounts/', include('accounts.urls')),
path('blood-bank/', include('blood_bank.urls')),
path('patients/', include('patients.urls')), path('patients/', include('patients.urls')),
path('appointments/', include('appointments.urls')), path('appointments/', include('appointments.urls')),
path('inpatients/', include('inpatients.urls')), path('inpatients/', include('inpatients.urls')),

View File

@ -0,0 +1,28 @@
# Generated by Django 5.2.4 on 2025-09-03 15:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("inpatients", "0003_alter_bed_bed_position"),
]
operations = [
migrations.AddField(
model_name="bed",
name="is_active",
field=models.BooleanField(default=True, help_text="Active status"),
),
migrations.AddField(
model_name="bed",
name="is_active_out_of_service",
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name="bed",
name="is_operational",
field=models.BooleanField(default=True, help_text="Operational status"),
),
]

View File

@ -340,7 +340,17 @@ class Bed(models.Model):
default='STANDARD', default='STANDARD',
help_text='Type of bed' help_text='Type of bed'
) )
is_operational = models.BooleanField(
default=True,
help_text='Operational status'
)
is_active = models.BooleanField(
default=True,
help_text='Active status'
)
is_active_out_of_service = models.BooleanField(
default=True,
)
room_type = models.CharField( room_type = models.CharField(
max_length=20, max_length=20,
choices=ROOM_TYPE_CHOICES, choices=ROOM_TYPE_CHOICES,

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@ from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils import timezone from django.utils import timezone
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
import json, re
from accounts.models import User from accounts.models import User
from patients.models import PatientProfile from patients.models import PatientProfile
from .models import ( from .models import (
@ -20,92 +20,157 @@ from .models import (
# OPERATING ROOM FORMS # OPERATING ROOM FORMS
# ============================================================================ # ============================================================================
def _list_to_text(value):
if isinstance(value, list):
return "\n".join(str(v) for v in value)
return "" if value in (None, "", []) else str(value)
def _text_to_list(value):
if value is None:
return []
raw = str(value).strip()
if not raw:
return []
# Try JSON list first
try:
parsed = json.loads(raw)
if isinstance(parsed, list):
return [str(x).strip() for x in parsed if str(x).strip()]
except Exception:
pass
# Fallback: newline / comma split
items = []
for line in raw.splitlines():
for piece in line.split(","):
p = piece.strip()
if p:
items.append(p)
return items
class OperatingRoomForm(forms.ModelForm): class OperatingRoomForm(forms.ModelForm):
""" """
Form for creating and updating operating rooms. Full form aligned to the panel template:
- exposes environment, capabilities, scheduling fields
- maps equipment_list & special_features (JSON) <-> textarea
- enforces tenant-scoped uniqueness of room_number
- validates temp/humidity ranges, and simple numeric sanity checks
""" """
class Meta: class Meta:
model = OperatingRoom model = OperatingRoom
fields = [ fields = [
'room_number', 'room_name', 'room_type', 'floor_number', 'building', # Basic
'room_size', 'equipment_list', 'special_features', 'room_number', 'room_name', 'room_type', 'status',
'status', 'is_active', 'wing' # Physical
'floor_number', 'building', 'wing', 'room_size', 'ceiling_height',
# Environment
'temperature_min', 'temperature_max',
'humidity_min', 'humidity_max',
'air_changes_per_hour', 'positive_pressure',
# Capabilities & equipment
'supports_robotic', 'supports_laparoscopic',
'supports_microscopy', 'supports_laser',
'has_c_arm', 'has_ct', 'has_mri', 'has_ultrasound', 'has_neuromonitoring',
'equipment_list', 'special_features',
# Scheduling / staffing
'max_case_duration', 'turnover_time', 'cleaning_time',
'required_nurses', 'required_techs',
'is_active', 'accepts_emergency',
] ]
widgets = { widgets = {
'room_number': forms.TextInput(attrs={ # Basic
'class': 'form-control', 'room_number': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'e.g., OR-01', 'required': True}),
'placeholder': 'e.g., OR-01' 'room_name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'e.g., Main Operating Room 1', 'required': True}),
}), 'room_type': forms.Select(attrs={'class': 'form-control', 'required': True}),
'room_name': forms.TextInput(attrs={ 'status': forms.Select(attrs={'class': 'form-control', 'required': True}),
'class': 'form-control', # Physical
'placeholder': 'e.g., Main Operating Room 1' 'floor_number': forms.NumberInput(attrs={'class': 'form-control', 'min': 0, 'max': 200, 'required': True}),
}), 'building': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'e.g., Main Building'}),
'room_type': forms.Select(attrs={'class': 'form-control'}), 'wing': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'e.g., East Wing'}),
'floor_number': forms.NumberInput(attrs={ 'room_size': forms.NumberInput(attrs={'class': 'form-control', 'min': 1, 'max': 2000}),
'class': 'form-control', 'ceiling_height': forms.NumberInput(attrs={'class': 'form-control', 'min': 1, 'max': 20}),
'min': 1, # Environment
'max': 50 'temperature_min': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1'}),
}), 'temperature_max': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1'}),
'building': forms.TextInput(attrs={ 'humidity_min': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1'}),
'class': 'form-control', 'humidity_max': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1'}),
'placeholder': 'e.g., Main Building' 'air_changes_per_hour':forms.NumberInput(attrs={'class': 'form-control', 'min': 0, 'max': 120}),
}), 'positive_pressure': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'room_size': forms.NumberInput(attrs={ # Capabilities & imaging
'class': 'form-control', 'supports_robotic': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'min': 1, 'supports_laparoscopic':forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'max': 1000 'supports_microscopy': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
}), 'supports_laser': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'equipment_list': forms.Textarea(attrs={ 'has_c_arm': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'class': 'form-control', 'has_ct': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'rows': 4, 'has_mri': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'placeholder': 'List available equipment...' 'has_ultrasound': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
}), 'has_neuromonitoring': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'special_features': forms.Textarea(attrs={ 'equipment_list': forms.Textarea(attrs={'class': 'form-control', 'rows': 4, 'placeholder': 'One per line, or comma-separated…'}),
'class': 'form-control', 'special_features': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Hybrid OR, laminar flow, etc.'}),
'rows': 3, # Scheduling / staffing
'placeholder': 'Special capabilities and features...' 'max_case_duration': forms.NumberInput(attrs={'class': 'form-control', 'min': 30, 'max': 1440}),
}), 'turnover_time': forms.NumberInput(attrs={'class': 'form-control', 'min': 0, 'max': 240}),
'status': forms.Select(attrs={'class': 'form-control'}), 'cleaning_time': forms.NumberInput(attrs={'class': 'form-control', 'min': 0, 'max': 240}),
'required_nurses': forms.NumberInput(attrs={'class': 'form-control', 'min': 0, 'max': 20}),
'required_techs': forms.NumberInput(attrs={'class': 'form-control', 'min': 0, 'max': 20}),
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'wing': forms.TextInput(attrs={ 'accepts_emergency':forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'class': 'form-control',
'placeholder': 'e.g., East Wing'
}),
} }
help_texts = { help_texts = {
'room_number': 'Unique identifier for the operating room', 'room_number': 'Unique identifier (per tenant). Letters, numbers, dashes.',
'room_type': 'Type of procedures this room is designed for', 'room_size': 'Square meters.',
'room_size': 'Size of the room in square meters', 'temperature_min': 'Typical ORs: 1826°C.',
'equipment_list': 'List of permanently installed equipment', 'humidity_min': 'Typical ORs: 3060%.',
'special_features': 'Special features like imaging, robotics, etc.', 'air_changes_per_hour': '20+ is common in OR standards.',
} }
def __init__(self, *args, **kwargs):
self.tenant = kwargs.pop('tenant', None)
super().__init__(*args, **kwargs)
if self.instance and self.instance.pk:
self.fields['equipment_list'].initial = _list_to_text(self.instance.equipment_list)
self.fields['special_features'].initial = _list_to_text(self.instance.special_features)
# JSONField <-> textarea mapping
def clean_equipment_list(self):
return _text_to_list(self.cleaned_data.get('equipment_list'))
def clean_special_features(self):
return _text_to_list(self.cleaned_data.get('special_features'))
def clean_room_number(self): def clean_room_number(self):
room_number = self.cleaned_data['room_number'] room_number = (self.cleaned_data.get('room_number') or '').strip()
if not room_number:
# Check for uniqueness within tenant return room_number
queryset = OperatingRoom.objects.filter(room_number=room_number) if not re.match(r'^[A-Za-z0-9\-]+$', room_number):
raise ValidationError('Room number may contain only letters, numbers, and dashes.')
# tenant-scoped uniqueness
qs = OperatingRoom.objects.all()
tenant = self.tenant or getattr(self.instance, 'tenant', None)
if tenant is not None:
qs = qs.filter(tenant=tenant)
qs = qs.filter(room_number__iexact=room_number)
if self.instance.pk: if self.instance.pk:
queryset = queryset.exclude(pk=self.instance.pk) qs = qs.exclude(pk=self.instance.pk)
if qs.exists():
if queryset.exists(): raise ValidationError('Room number must be unique within the tenant.')
raise ValidationError('Room number must be unique.')
return room_number return room_number
def clean(self): def clean(self):
cleaned_data = super().clean() cleaned = super().clean()
status = cleaned_data.get('status') # Temperature/humidity ranges
is_active = cleaned_data.get('is_active') tmin, tmax = cleaned.get('temperature_min'), cleaned.get('temperature_max')
hmin, hmax = cleaned.get('humidity_min'), cleaned.get('humidity_max')
# Validate status and active state consistency if tmin is not None and tmax is not None and tmin >= tmax:
if not is_active and status not in ['OUT_OF_SERVICE', 'MAINTENANCE']: self.add_error('temperature_max', 'Maximum temperature must be greater than minimum temperature.')
raise ValidationError( if hmin is not None and hmax is not None and hmin >= hmax:
'Inactive rooms must have status "Out of Service" or "Maintenance".' self.add_error('humidity_max', 'Maximum humidity must be greater than minimum humidity.')
) # Simple sanity checks
for field, minv in [('max_case_duration', 1), ('turnover_time', 0), ('cleaning_time', 0)]:
return cleaned_data v = cleaned.get(field)
if v is not None and v < minv:
self.add_error(field, f'{field.replace("_", " ").title()} must be ≥ {minv}.')
return cleaned
# ============================================================================ # ============================================================================

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,36 @@
# Generated by Django 5.2.4 on 2025-09-04 15:07
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("operating_theatre", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name="surgicalnote",
name="surgeon",
field=models.ForeignKey(
help_text="Operating surgeon",
on_delete=django.db.models.deletion.CASCADE,
related_name="surgeon_surgical_notes",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name="surgicalnote",
name="surgical_case",
field=models.OneToOneField(
help_text="Related surgical case",
on_delete=django.db.models.deletion.CASCADE,
related_name="surgical_notes",
to="operating_theatre.surgicalcase",
),
),
]

View File

@ -261,16 +261,25 @@ class OperatingRoom(models.Model):
""" """
return self.status == 'AVAILABLE' and self.is_active return self.status == 'AVAILABLE' and self.is_active
@property
def surgical_cases(self):
"""
All surgical cases scheduled/assigned to this operating room
via its OR blocks.
"""
return SurgicalCase.objects.filter(or_block__operating_room=self)
# (Optional) a clearer alias if you prefer not to shadow the term "surgical_cases"
@property
def cases(self):
return self.surgical_cases
@property @property
def current_case(self): def current_case(self):
""" """
Get current surgical case if room is occupied. Get the in-progress surgical case for this room (if any).
""" """
if self.status == 'OCCUPIED': return self.surgical_cases.filter(status='IN_PROGRESS').order_by('-scheduled_start').first()
return self.surgical_cases.filter(
status='IN_PROGRESS'
).first()
return None
class ORBlock(models.Model): class ORBlock(models.Model):
@ -812,7 +821,7 @@ class SurgicalNote(models.Model):
surgical_case = models.OneToOneField( surgical_case = models.OneToOneField(
SurgicalCase, SurgicalCase,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='surgical_note', related_name='surgical_notes',
help_text='Related surgical case' help_text='Related surgical case'
) )
@ -828,7 +837,7 @@ class SurgicalNote(models.Model):
surgeon = models.ForeignKey( surgeon = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='surgical_notes', related_name='surgeon_surgical_notes',
help_text='Operating surgeon' help_text='Operating surgeon'
) )

View File

@ -21,6 +21,7 @@ urlpatterns = [
path('rooms/<int:pk>/', views.OperatingRoomDetailView.as_view(), name='operating_room_detail'), path('rooms/<int:pk>/', views.OperatingRoomDetailView.as_view(), name='operating_room_detail'),
path('rooms/<int:pk>/update/', views.OperatingRoomUpdateView.as_view(), name='operating_room_update'), path('rooms/<int:pk>/update/', views.OperatingRoomUpdateView.as_view(), name='operating_room_update'),
path('rooms/<int:pk>/delete/', views.OperatingRoomDeleteView.as_view(), name='operating_room_delete'), path('rooms/<int:pk>/delete/', views.OperatingRoomDeleteView.as_view(), name='operating_room_delete'),
path("availability/check/", views.check_room_availability, name="check_room_availability"),
# ============================================================================ # ============================================================================
# SURGICAL NOTE TEMPLATE URLS (FULL CRUD - Master Data) # SURGICAL NOTE TEMPLATE URLS (FULL CRUD - Master Data)
@ -55,6 +56,8 @@ urlpatterns = [
path('notes/', views.SurgicalNoteListView.as_view(), name='surgical_note_list'), path('notes/', views.SurgicalNoteListView.as_view(), name='surgical_note_list'),
path('notes/create/', views.SurgicalNoteCreateView.as_view(), name='surgical_note_create'), path('notes/create/', views.SurgicalNoteCreateView.as_view(), name='surgical_note_create'),
path('notes/<int:pk>/', views.SurgicalNoteDetailView.as_view(), name='surgical_note_detail'), path('notes/<int:pk>/', views.SurgicalNoteDetailView.as_view(), name='surgical_note_detail'),
path('notes/<int:pk>/preview/', views.surgical_note_preview, name='surgical_note_preview'),
# Note: No update/delete views for surgical notes - append-only for clinical records # Note: No update/delete views for surgical notes - append-only for clinical records
# ============================================================================ # ============================================================================
@ -75,8 +78,10 @@ urlpatterns = [
# ============================================================================ # ============================================================================
# ACTION URLS FOR WORKFLOW OPERATIONS # ACTION URLS FOR WORKFLOW OPERATIONS
# ============================================================================ # ============================================================================
path('cases/<int:case_id>/start/', views.start_case, name='start_case'), # path('cases/<int:case_id>/start/', views.start_case, name='start_case'),
path('cases/<int:case_id>/complete/', views.complete_case, name='complete_case'), # path('cases/<int:case_id>/complete/', views.complete_case, name='complete_case'),
path('cases/<int:pk>/start/', views.StartCaseView.as_view(), name='start_case'),
path('cases/<int:pk>/complete/', views.CompleteCaseView.as_view(), name='complete_case'),
path('notes/<int:note_id>/sign/', views.sign_note, name='sign_note'), path('notes/<int:note_id>/sign/', views.sign_note, name='sign_note'),
path('rooms/<int:room_id>/update-status/', views.update_room_status, name='update_room_status'), path('rooms/<int:room_id>/update-status/', views.update_room_status, name='update_room_status'),

File diff suppressed because it is too large Load Diff

2062
or_data.py

File diff suppressed because it is too large Load Diff

BIN
patients/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -4,10 +4,7 @@ Admin configuration for patients app.
from django.contrib import admin from django.contrib import admin
from django.utils.html import format_html from django.utils.html import format_html
from .models import ( from .models import *
PatientProfile, EmergencyContact, InsuranceInfo,
ConsentTemplate, ConsentForm, PatientNote
)
class EmergencyContactInline(admin.TabularInline): class EmergencyContactInline(admin.TabularInline):
@ -425,3 +422,136 @@ class PatientNoteAdmin(admin.ModelAdmin):
def get_queryset(self, request): def get_queryset(self, request):
return super().get_queryset(request).select_related('patient', 'created_by') return super().get_queryset(request).select_related('patient', 'created_by')
class ClaimDocumentInline(admin.TabularInline):
"""Inline admin for claim documents."""
model = ClaimDocument
extra = 0
readonly_fields = ['uploaded_at', 'file_size']
class ClaimStatusHistoryInline(admin.TabularInline):
"""Inline admin for claim status history."""
model = ClaimStatusHistory
extra = 0
readonly_fields = ['changed_at']
ordering = ['-changed_at']
fields = [ 'changed_at']
@admin.register(InsuranceClaim)
class InsuranceClaimAdmin(admin.ModelAdmin):
"""Admin interface for InsuranceClaim."""
list_display = [
'claim_number', 'patient', 'claim_type', 'priority',
'billed_amount', 'approved_amount', 'service_date', 'submitted_date'
]
list_filter = [
'claim_type', 'priority', 'service_date',
'submitted_date', 'processed_date'
]
search_fields = [
'claim_number', 'patient__first_name', 'patient__last_name',
'service_provider', 'facility_name', 'primary_diagnosis_code'
]
ordering = ['-created_at']
fieldsets = (
('Claim Information', {
'fields': ('claim_number', 'patient', 'insurance_info', 'claim_type', 'status', 'priority')
}),
('Service Information', {
'fields': (
'service_date', 'service_provider', 'service_provider_license',
'facility_name', 'facility_license'
)
}),
('Medical Codes', {
'fields': (
'primary_diagnosis_code', 'primary_diagnosis_description',
'secondary_diagnosis_codes', 'procedure_codes'
)
}),
('Financial Information', {
'fields': (
'billed_amount', 'approved_amount', 'paid_amount',
'patient_responsibility', 'discount_amount'
)
}),
('Processing Dates', {
'fields': ('submitted_date', 'processed_date', 'payment_date')
}),
('Saudi-specific Information', {
'fields': ('saudi_id_number', 'insurance_card_number', 'authorization_number')
}),
('Denial/Appeal Information', {
'fields': ('denial_reason', 'denial_code', 'appeal_date', 'appeal_reason'),
'classes': ('collapse',)
}),
('Additional Information', {
'fields': ('notes', 'attachments'),
'classes': ('collapse',)
})
)
readonly_fields = ['created_at', 'updated_at']
inlines = [ClaimDocumentInline, ClaimStatusHistoryInline]
def get_queryset(self, request):
"""Optimize queryset with select_related."""
return super().get_queryset(request).select_related('patient', 'insurance_info')
def formfield_for_foreignkey(self, db_field, request, **kwargs):
"""Optimize foreign key fields."""
if db_field.name == "patient":
kwargs["queryset"] = PatientProfile.objects.select_related('tenant')
elif db_field.name == "insurance_info":
kwargs["queryset"] = InsuranceInfo.objects.select_related('patient')
return super().formfield_for_foreignkey(db_field, request, **kwargs)
@admin.register(ClaimDocument)
class ClaimDocumentAdmin(admin.ModelAdmin):
"""Admin interface for ClaimDocument."""
list_display = [
'title', 'claim', 'document_type', 'file_size_display',
'mime_type', 'uploaded_at', 'uploaded_by'
]
list_filter = ['document_type', 'mime_type', 'uploaded_at']
search_fields = ['title', 'claim__claim_number', 'description']
ordering = ['-uploaded_at']
def file_size_display(self, obj):
"""Display file size in human readable format."""
size = obj.file_size
for unit in ['B', 'KB', 'MB', 'GB']:
if size < 1024.0:
return f"{size:.1f} {unit}"
size /= 1024.0
return f"{size:.1f} TB"
file_size_display.short_description = 'File Size'
@admin.register(ClaimStatusHistory)
class ClaimStatusHistoryAdmin(admin.ModelAdmin):
"""Admin interface for ClaimStatusHistory."""
list_display = [
'claim', 'from_status', 'to_status', 'changed_at', 'changed_by'
]
list_filter = ['from_status', 'to_status', 'changed_at']
search_fields = ['claim__claim_number', 'reason', 'notes']
ordering = ['-changed_at']
def get_queryset(self, request):
"""Optimize queryset with select_related."""
return super().get_queryset(request).select_related('claim', 'changed_by')
# Custom admin site configuration
admin.site.site_header = "Hospital Management System - Patients"
admin.site.site_title = "HMS Patients Admin"
admin.site.index_title = "Patients Administration"

View File

View File

@ -0,0 +1,592 @@
"""
Saudi-influenced Insurance Claims Data Generator
This module generates realistic insurance claims data tailored for the Saudi healthcare system,
including local insurance providers, medical facilities, and healthcare practices.
"""
import random
import uuid
from datetime import datetime, timedelta
from decimal import Decimal
from django.utils import timezone
from django.contrib.auth import get_user_model
User = get_user_model()
class SaudiClaimsDataGenerator:
"""
Generates realistic insurance claims data for Saudi healthcare system.
"""
# Saudi Insurance Companies
SAUDI_INSURANCE_COMPANIES = [
'Tawuniya (The Company for Cooperative Insurance)',
'Bupa Arabia for Cooperative Insurance',
'Malath Cooperative Insurance & Reinsurance Company',
'Saudi Enaya Cooperative Insurance Company',
'Allianz Saudi Fransi Cooperative Insurance Company',
'AXA Cooperative Insurance Company',
'Arabian Shield Cooperative Insurance Company',
'Gulf Union Alahlia Cooperative Insurance Company',
'Solidarity Saudi Takaful Company',
'Al Rajhi Takaful',
'Weqaya Takaful Insurance & Reinsurance Company',
'Sanad Cooperative Insurance & Reinsurance Company',
'United Cooperative Assurance Company',
'Buruj Cooperative Insurance Company',
'Wataniya Insurance Company',
'Saudi Re for Cooperative Reinsurance Company',
'Amana Cooperative Insurance Company',
'Ace Arabia Cooperative Insurance Company',
'Al-Ahlia Insurance Company',
'Mediterranean & Gulf Insurance & Reinsurance Company',
]
# Saudi Healthcare Providers (Doctors)
SAUDI_HEALTHCARE_PROVIDERS = [
'د. أحمد محمد العبدالله', # Dr. Ahmed Mohammed Al-Abdullah
'د. فاطمة علي الزهراني', # Dr. Fatima Ali Al-Zahrani
'د. محمد عبدالرحمن القحطاني', # Dr. Mohammed Abdulrahman Al-Qahtani
'د. نورا سعد الغامدي', # Dr. Nora Saad Al-Ghamdi
'د. خالد يوسف الشهري', # Dr. Khalid Youssef Al-Shahri
'د. عائشة حسن الحربي', # Dr. Aisha Hassan Al-Harbi
'د. عبدالله سليمان المطيري', # Dr. Abdullah Sulaiman Al-Mutairi
'د. مريم أحمد الدوسري', # Dr. Maryam Ahmed Al-Dosari
'د. سعد محمد العتيبي', # Dr. Saad Mohammed Al-Otaibi
'د. هند عبدالعزيز الراشد', # Dr. Hind Abdulaziz Al-Rashid
'د. عمر فهد الخالدي', # Dr. Omar Fahad Al-Khalidi
'د. سارة عبدالرحمن الفيصل', # Dr. Sarah Abdulrahman Al-Faisal
'د. يوسف علي البقمي', # Dr. Youssef Ali Al-Baqami
'د. ليلى محمد الجبير', # Dr. Layla Mohammed Al-Jubair
'د. فيصل عبدالله السديري', # Dr. Faisal Abdullah Al-Sudairi
'د. رنا سعود الأحمد', # Dr. Rana Saud Al-Ahmad
'د. طارق حسام الدين الأنصاري', # Dr. Tariq Hussamuddin Al-Ansari
'د. إيمان عبدالمحسن الشمري', # Dr. Iman Abdulmohsen Al-Shamri
'د. ماجد فواز العسيري', # Dr. Majed Fawaz Al-Asiri
'د. دانا محمد الفهد', # Dr. Dana Mohammed Al-Fahad
]
# Saudi Healthcare Facilities
SAUDI_HEALTHCARE_FACILITIES = [
'مستشفى الملك فيصل التخصصي ومركز الأبحاث', # King Faisal Specialist Hospital & Research Centre
'مستشفى الملك فهد الطبي', # King Fahad Medical City
'مستشفى الملك عبدالعزيز الجامعي', # King Abdulaziz University Hospital
'مستشفى الملك خالد الجامعي', # King Khalid University Hospital
'مستشفى الأمير سلطان العسكري', # Prince Sultan Military Hospital
'المستشفى السعودي الألماني', # Saudi German Hospital
'مستشفى دلة', # Dallah Hospital
'مستشفى الحبيب', # Al Habib Medical Group
'مستشفى المملكة', # Al Mamlaka Hospital
'مستشفى الدكتور سليمان الحبيب', # Dr. Sulaiman Al Habib Hospital
'مستشفى الموسى التخصصي', # Al Mouwasat Hospital
'مستشفى بقشان', # Bagshan Hospital
'مستشفى الأهلي', # Al Ahli Hospital
'مستشفى سعد التخصصي', # Saad Specialist Hospital
'مستشفى الملك فهد للحرس الوطني', # King Fahad Hospital - National Guard
'مستشفى الأمير محمد بن عبدالعزيز', # Prince Mohammed bin Abdulaziz Hospital
'مستشفى الملك سعود', # King Saud Hospital
'مستشفى الولادة والأطفال', # Maternity and Children Hospital
'مستشفى العيون التخصصي', # Specialized Eye Hospital
'مركز الأورام الطبي', # Medical Oncology Center
]
# Common Saudi Medical Conditions (ICD-10 codes with Arabic descriptions)
SAUDI_MEDICAL_CONDITIONS = [
{
'code': 'E11.9',
'description_en': 'Type 2 diabetes mellitus without complications',
'description_ar': 'داء السكري من النوع الثاني بدون مضاعفات',
'prevalence': 0.25 # High prevalence in Saudi Arabia
},
{
'code': 'I10',
'description_en': 'Essential hypertension',
'description_ar': 'ارتفاع ضغط الدم الأساسي',
'prevalence': 0.20
},
{
'code': 'E78.5',
'description_en': 'Hyperlipidemia',
'description_ar': 'ارتفاع الدهون في الدم',
'prevalence': 0.18
},
{
'code': 'M79.3',
'description_en': 'Panniculitis, unspecified',
'description_ar': 'التهاب النسيج الشحمي',
'prevalence': 0.15
},
{
'code': 'J45.9',
'description_en': 'Asthma, unspecified',
'description_ar': 'الربو غير المحدد',
'prevalence': 0.12
},
{
'code': 'K21.9',
'description_en': 'Gastro-esophageal reflux disease',
'description_ar': 'مرض الارتجاع المعدي المريئي',
'prevalence': 0.10
},
{
'code': 'M25.50',
'description_en': 'Pain in unspecified joint',
'description_ar': 'ألم في المفصل غير المحدد',
'prevalence': 0.08
},
{
'code': 'N18.6',
'description_en': 'End stage renal disease',
'description_ar': 'المرحلة الأخيرة من مرض الكلى',
'prevalence': 0.06
},
{
'code': 'F32.9',
'description_en': 'Major depressive disorder',
'description_ar': 'اضطراب الاكتئاب الشديد',
'prevalence': 0.05
},
{
'code': 'Z51.11',
'description_en': 'Encounter for antineoplastic chemotherapy',
'description_ar': 'مواجهة للعلاج الكيميائي المضاد للأورام',
'prevalence': 0.04
},
]
# Common Procedures (CPT codes)
SAUDI_MEDICAL_PROCEDURES = [
{
'code': '99213',
'description': 'Office visit - established patient',
'description_ar': 'زيارة العيادة - مريض منتظم',
'cost_range': (150, 300)
},
{
'code': '99214',
'description': 'Office visit - established patient, moderate complexity',
'description_ar': 'زيارة العيادة - مريض منتظم، تعقيد متوسط',
'cost_range': (200, 400)
},
{
'code': '80053',
'description': 'Comprehensive metabolic panel',
'description_ar': 'فحص الأيض الشامل',
'cost_range': (100, 200)
},
{
'code': '85025',
'description': 'Blood count; complete (CBC)',
'description_ar': 'تعداد الدم الكامل',
'cost_range': (50, 120)
},
{
'code': '71020',
'description': 'Chest X-ray',
'description_ar': 'أشعة سينية للصدر',
'cost_range': (80, 150)
},
{
'code': '93000',
'description': 'Electrocardiogram',
'description_ar': 'تخطيط القلب الكهربائي',
'cost_range': (75, 150)
},
{
'code': '76700',
'description': 'Abdominal ultrasound',
'description_ar': 'الموجات فوق الصوتية للبطن',
'cost_range': (200, 400)
},
{
'code': '45378',
'description': 'Colonoscopy',
'description_ar': 'تنظير القولون',
'cost_range': (800, 1500)
},
{
'code': '47562',
'description': 'Laparoscopic cholecystectomy',
'description_ar': 'استئصال المرارة بالمنظار',
'cost_range': (5000, 8000)
},
{
'code': '66984',
'description': 'Cataract surgery',
'description_ar': 'جراحة الساد',
'cost_range': (3000, 6000)
},
]
# Saudi Names for generating realistic patient data
SAUDI_FIRST_NAMES_MALE = [
'محمد', 'أحمد', 'عبدالله', 'عبدالرحمن', 'علي', 'سعد', 'فهد', 'خالد',
'عبدالعزيز', 'سلطان', 'فيصل', 'عمر', 'يوسف', 'إبراهيم', 'حسن', 'طارق',
'ماجد', 'نواف', 'بندر', 'تركي', 'مشعل', 'وليد', 'صالح', 'عادل'
]
SAUDI_FIRST_NAMES_FEMALE = [
'فاطمة', 'عائشة', 'نورا', 'سارة', 'مريم', 'هند', 'ليلى', 'رنا', 'دانا',
'ريم', 'أمل', 'منى', 'سمر', 'لمى', 'غادة', 'نهى', 'إيمان', 'خديجة',
'زينب', 'رقية', 'جواهر', 'شهد', 'روان', 'لين'
]
SAUDI_FAMILY_NAMES = [
'العبدالله', 'الأحمد', 'المحمد', 'العلي', 'الزهراني', 'الغامدي', 'القحطاني',
'الشهري', 'الحربي', 'المطيري', 'الدوسري', 'العتيبي', 'الراشد', 'الخالدي',
'الفيصل', 'البقمي', 'الجبير', 'السديري', 'الأنصاري', 'الشمري', 'العسيري',
'الفهد', 'السعود', 'آل سعود', 'الملك', 'الأمير', 'الشيخ', 'العثمان',
'الصالح', 'الحسن', 'الحسين', 'الطيار', 'الرشيد', 'الفارس'
]
def __init__(self):
"""Initialize the Saudi claims data generator."""
self.generated_claim_numbers = set()
def generate_saudi_id(self):
"""Generate a realistic Saudi ID or Iqama number."""
# Saudi ID: 1 for Saudi, 2 for resident
prefix = random.choice(['1', '2'])
# Next 9 digits
middle = ''.join([str(random.randint(0, 9)) for _ in range(8)])
# Check digit (simplified)
check_digit = str(random.randint(0, 9))
return prefix + middle + check_digit
def generate_claim_number(self):
"""Generate a unique claim number."""
while True:
year = datetime.now().year
sequence = random.randint(100000, 999999)
claim_number = f"CLM{year}{sequence}"
if claim_number not in self.generated_claim_numbers:
self.generated_claim_numbers.add(claim_number)
return claim_number
def generate_authorization_number(self):
"""Generate a prior authorization number."""
return f"AUTH{random.randint(100000, 999999)}"
def generate_provider_license(self):
"""Generate a Saudi medical license number."""
return f"SML{random.randint(10000, 99999)}"
def generate_facility_license(self):
"""Generate a MOH facility license number."""
return f"MOH{random.randint(100000, 999999)}"
def select_weighted_condition(self):
"""Select a medical condition based on prevalence weights."""
conditions = self.SAUDI_MEDICAL_CONDITIONS.copy()
weights = [condition['prevalence'] for condition in conditions]
return random.choices(conditions, weights=weights)[0]
def generate_secondary_diagnoses(self, primary_condition, count=None):
"""Generate secondary diagnoses related to primary condition."""
if count is None:
count = random.choices([0, 1, 2, 3], weights=[0.4, 0.3, 0.2, 0.1])[0]
secondary = []
available_conditions = [c for c in self.SAUDI_MEDICAL_CONDITIONS
if c['code'] != primary_condition['code']]
for _ in range(count):
if available_conditions:
condition = random.choice(available_conditions)
secondary.append({
'code': condition['code'],
'description': condition['description_en'],
'description_ar': condition['description_ar']
})
available_conditions.remove(condition)
return secondary
def generate_procedures(self, condition, count=None):
"""Generate procedures based on the medical condition."""
if count is None:
count = random.choices([1, 2, 3], weights=[0.6, 0.3, 0.1])[0]
procedures = []
for _ in range(count):
procedure = random.choice(self.SAUDI_MEDICAL_PROCEDURES)
procedures.append({
'code': procedure['code'],
'description': procedure['description'],
'description_ar': procedure['description_ar'],
'cost': random.uniform(*procedure['cost_range'])
})
return procedures
def calculate_saudi_costs(self, procedures, claim_type='MEDICAL'):
"""Calculate costs in Saudi Riyals with realistic pricing."""
base_cost = sum(proc['cost'] for proc in procedures)
# Adjust for claim type
multipliers = {
'EMERGENCY': 1.5,
'INPATIENT': 2.0,
'SURGICAL': 3.0,
'MATERNITY': 2.5,
'DENTAL': 0.8,
'VISION': 0.6,
'PHARMACY': 0.3,
'PREVENTIVE': 0.5,
}
multiplier = multipliers.get(claim_type, 1.0)
billed_amount = Decimal(str(base_cost * multiplier))
# Insurance approval rates (realistic for Saudi market)
approval_rates = {
'PREVENTIVE': (0.95, 1.0),
'MEDICAL': (0.80, 0.95),
'EMERGENCY': (0.90, 1.0),
'INPATIENT': (0.85, 0.95),
'SURGICAL': (0.75, 0.90),
'DENTAL': (0.70, 0.85),
'VISION': (0.60, 0.80),
'PHARMACY': (0.85, 0.95),
'MATERNITY': (0.90, 1.0),
}
min_rate, max_rate = approval_rates.get(claim_type, (0.75, 0.90))
approval_rate = random.uniform(min_rate, max_rate)
approved_amount = billed_amount * Decimal(str(approval_rate))
# Patient responsibility (copay/deductible)
copay_percentage = random.uniform(0.10, 0.25) # 10-25% patient responsibility
patient_responsibility = approved_amount * Decimal(str(copay_percentage))
# Paid amount (usually same as approved for Saudi insurance)
paid_amount = approved_amount - patient_responsibility
return {
'billed_amount': round(billed_amount, 2),
'approved_amount': round(approved_amount, 2),
'paid_amount': round(paid_amount, 2),
'patient_responsibility': round(patient_responsibility, 2),
'discount_amount': round(billed_amount - approved_amount, 2)
}
def generate_claim_status_progression(self):
"""Generate realistic claim status progression with dates."""
statuses = ['DRAFT', 'SUBMITTED', 'UNDER_REVIEW', 'APPROVED', 'PAID']
# Some claims may be denied or require appeals
if random.random() < 0.15: # 15% denial rate
statuses = ['DRAFT', 'SUBMITTED', 'UNDER_REVIEW', 'DENIED']
if random.random() < 0.3: # 30% of denied claims are appealed
statuses.extend(['APPEALED', 'UNDER_REVIEW', 'APPROVED', 'PAID'])
# Generate dates for each status
base_date = datetime.now() - timedelta(days=random.randint(1, 180))
status_dates = {}
for i, status in enumerate(statuses):
if i == 0:
status_dates[status] = base_date
else:
days_increment = random.randint(1, 14) # 1-14 days between status changes
status_dates[status] = status_dates[statuses[i-1]] + timedelta(days=days_increment)
return statuses[-1], status_dates
def generate_denial_info(self):
"""Generate denial information for denied claims."""
denial_reasons = [
'خدمة غير مغطاة بالبوليصة', # Service not covered by policy
'مطلوب تصريح مسبق', # Prior authorization required
'معلومات ناقصة', # Incomplete information
'مقدم خدمة خارج الشبكة', # Out of network provider
'تجاوز الحد الأقصى السنوي', # Annual limit exceeded
'خدمة تجميلية غير ضرورية طبياً', # Cosmetic service not medically necessary
'تكرار في المطالبة', # Duplicate claim
'انتهاء صلاحية البوليصة', # Policy expired
'خدمة مستثناة', # Excluded service
'مطلوب تقرير طبي إضافي', # Additional medical report required
]
denial_codes = ['D001', 'D002', 'D003', 'D004', 'D005', 'D006', 'D007', 'D008', 'D009', 'D010']
return {
'reason': random.choice(denial_reasons),
'code': random.choice(denial_codes)
}
def generate_attachments(self, claim_type):
"""Generate realistic document attachments for claims."""
base_attachments = [
{'type': 'MEDICAL_REPORT', 'name': 'تقرير طبي.pdf'},
{'type': 'INVOICE', 'name': 'فاتورة.pdf'},
{'type': 'INSURANCE_CARD', 'name': 'بطاقة التأمين.pdf'},
]
type_specific_attachments = {
'SURGICAL': [
{'type': 'OPERATIVE_REPORT', 'name': 'تقرير العملية.pdf'},
{'type': 'AUTHORIZATION', 'name': 'تصريح مسبق.pdf'},
],
'EMERGENCY': [
{'type': 'DISCHARGE_SUMMARY', 'name': 'ملخص الخروج.pdf'},
],
'PHARMACY': [
{'type': 'PRESCRIPTION', 'name': 'وصفة طبية.pdf'},
],
'RADIOLOGY': [
{'type': 'RADIOLOGY_REPORT', 'name': 'تقرير الأشعة.pdf'},
],
'DIAGNOSTIC': [
{'type': 'LAB_RESULT', 'name': 'نتائج المختبر.pdf'},
],
}
attachments = base_attachments.copy()
if claim_type in type_specific_attachments:
attachments.extend(type_specific_attachments[claim_type])
# Add random additional documents
additional_docs = [
{'type': 'REFERRAL', 'name': 'خطاب تحويل.pdf'},
{'type': 'ID_COPY', 'name': 'نسخة الهوية.pdf'},
{'type': 'OTHER', 'name': 'مستند إضافي.pdf'},
]
num_additional = random.randint(0, 2)
attachments.extend(random.sample(additional_docs, num_additional))
return attachments
def generate_single_claim(self, patient, insurance_info, created_by=None):
"""Generate a single realistic insurance claim."""
# Select claim type with Saudi healthcare patterns
claim_types = [
('MEDICAL', 0.35),
('OUTPATIENT', 0.25),
('PHARMACY', 0.15),
('DIAGNOSTIC', 0.10),
('EMERGENCY', 0.05),
('INPATIENT', 0.04),
('PREVENTIVE', 0.03),
('DENTAL', 0.02),
('SURGICAL', 0.01),
]
claim_type = random.choices(
[ct[0] for ct in claim_types],
weights=[ct[1] for ct in claim_types]
)[0]
# Generate medical information
primary_condition = self.select_weighted_condition()
secondary_conditions = self.generate_secondary_diagnoses(primary_condition)
procedures = self.generate_procedures(primary_condition)
# Calculate costs
costs = self.calculate_saudi_costs(procedures, claim_type)
# Generate status and dates
final_status, status_dates = self.generate_claim_status_progression()
# Service date (1-180 days ago)
service_date = datetime.now().date() - timedelta(days=random.randint(1, 180))
# Generate claim data
claim_data = {
'claim_number': self.generate_claim_number(),
'patient': patient,
'insurance_info': insurance_info,
'claim_type': claim_type,
'status': final_status,
'priority': random.choices(
['LOW', 'NORMAL', 'HIGH', 'URGENT', 'EMERGENCY'],
weights=[0.1, 0.6, 0.2, 0.08, 0.02]
)[0],
'service_date': service_date,
'service_provider': random.choice(self.SAUDI_HEALTHCARE_PROVIDERS),
'service_provider_license': self.generate_provider_license(),
'facility_name': random.choice(self.SAUDI_HEALTHCARE_FACILITIES),
'facility_license': self.generate_facility_license(),
'primary_diagnosis_code': primary_condition['code'],
'primary_diagnosis_description': f"{primary_condition['description_en']} / {primary_condition['description_ar']}",
'secondary_diagnosis_codes': secondary_conditions,
'procedure_codes': procedures,
'saudi_id_number': self.generate_saudi_id(),
'insurance_card_number': f"IC{random.randint(100000000, 999999999)}",
'authorization_number': self.generate_authorization_number() if random.random() < 0.3 else None,
'notes': f"مطالبة تأمينية لـ {claim_type.lower()} - تم إنشاؤها تلقائياً",
'attachments': self.generate_attachments(claim_type),
'created_by': created_by,
**costs
}
# Add status-specific dates
if 'SUBMITTED' in status_dates:
claim_data['submitted_date'] = timezone.make_aware(
datetime.combine(status_dates['SUBMITTED'].date(), datetime.min.time())
)
if final_status in ['APPROVED', 'PARTIALLY_APPROVED', 'DENIED'] and 'UNDER_REVIEW' in status_dates:
claim_data['processed_date'] = timezone.make_aware(
datetime.combine(status_dates['UNDER_REVIEW'].date(), datetime.min.time())
) + timedelta(days=random.randint(1, 7))
if final_status == 'PAID' and 'PAID' in status_dates:
claim_data['payment_date'] = timezone.make_aware(
datetime.combine(status_dates['PAID'].date(), datetime.min.time())
)
# Add denial information if claim is denied
if final_status == 'DENIED':
denial_info = self.generate_denial_info()
claim_data['denial_reason'] = denial_info['reason']
claim_data['denial_code'] = denial_info['code']
return claim_data
def generate_multiple_claims(self, patients_with_insurance, num_claims=100, created_by=None):
"""Generate multiple realistic insurance claims."""
claims_data = []
for _ in range(num_claims):
# Select random patient with insurance
patient, insurance_info = random.choice(patients_with_insurance)
# Generate claim
claim_data = self.generate_single_claim(patient, insurance_info, created_by)
claims_data.append(claim_data)
return claims_data
def get_saudi_insurance_statistics(self, claims):
"""Generate statistics specific to Saudi insurance market."""
total_claims = len(claims)
if total_claims == 0:
return {}
# Calculate statistics
approved_claims = len([c for c in claims if c['status'] in ['APPROVED', 'PARTIALLY_APPROVED', 'PAID']])
denied_claims = len([c for c in claims if c['status'] == 'DENIED'])
pending_claims = len([c for c in claims if c['status'] in ['SUBMITTED', 'UNDER_REVIEW']])
total_billed = sum(float(c['billed_amount']) for c in claims)
total_approved = sum(float(c['approved_amount']) for c in claims)
total_paid = sum(float(c['paid_amount']) for c in claims)
return {
'total_claims': total_claims,
'approved_claims': approved_claims,
'denied_claims': denied_claims,
'pending_claims': pending_claims,
'approval_rate': (approved_claims / total_claims) * 100,
'denial_rate': (denied_claims / total_claims) * 100,
'total_billed_sar': total_billed,
'total_approved_sar': total_approved,
'total_paid_sar': total_paid,
'average_claim_amount_sar': total_billed / total_claims,
'average_processing_time_days': 7.5, # Typical for Saudi market
}

View File

@ -163,7 +163,7 @@ class ConsentTemplateForm(forms.ModelForm):
fields = [ fields = [
'name', 'description', 'category', 'content', 'version', 'name', 'description', 'category', 'content', 'version',
'is_active', 'requires_signature', 'requires_witness', 'requires_guardian', 'is_active', 'requires_signature', 'requires_witness', 'requires_guardian',
'effective_date', 'expiry_date' 'effective_date', 'expiry_date',
] ]
widgets = { widgets = {
'name': forms.TextInput(attrs={'class': 'form-control'}), 'name': forms.TextInput(attrs={'class': 'form-control'}),

BIN
patients/management/.DS_Store vendored Normal file

Binary file not shown.

View File

View File

View File

@ -0,0 +1,398 @@
"""
Django management command to generate Saudi-influenced insurance claims data.
Usage:
python manage.py generate_saudi_claims --count 100 --tenant-id 1
python manage.py generate_saudi_claims --count 500 --all-tenants
python manage.py generate_saudi_claims --count 50 --patient-id 123
"""
from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth import get_user_model
from django.db import transaction
from django.utils import timezone
from patients.models import PatientProfile, InsuranceInfo, InsuranceClaim, ClaimStatusHistory
from patients.data_generators.saudi_claims_generator import SaudiClaimsDataGenerator
from core.models import Tenant
import random
User = get_user_model()
class Command(BaseCommand):
help = 'Generate Saudi-influenced insurance claims data for testing and development'
def add_arguments(self, parser):
parser.add_argument(
'--count',
type=int,
default=50,
help='Number of claims to generate (default: 50)'
)
parser.add_argument(
'--tenant-id',
type=int,
help='Specific tenant ID to generate claims for'
)
parser.add_argument(
'--all-tenants',
action='store_true',
help='Generate claims for all tenants'
)
parser.add_argument(
'--patient-id',
type=int,
help='Generate claims for a specific patient'
)
parser.add_argument(
'--insurance-id',
type=int,
help='Generate claims for a specific insurance policy'
)
parser.add_argument(
'--claim-type',
choices=['MEDICAL', 'DENTAL', 'VISION', 'PHARMACY', 'EMERGENCY',
'INPATIENT', 'OUTPATIENT', 'PREVENTIVE', 'MATERNITY',
'MENTAL_HEALTH', 'REHABILITATION', 'DIAGNOSTIC', 'SURGICAL', 'CHRONIC_CARE'],
help='Generate claims of specific type only'
)
parser.add_argument(
'--status',
choices=['DRAFT', 'SUBMITTED', 'UNDER_REVIEW', 'APPROVED',
'PARTIALLY_APPROVED', 'DENIED', 'PAID', 'CANCELLED',
'APPEALED', 'RESUBMITTED'],
help='Generate claims with specific status only'
)
parser.add_argument(
'--days-back',
type=int,
default=180,
help='Generate claims from this many days back (default: 180)'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='Show what would be generated without creating records'
)
parser.add_argument(
'--verbose',
action='store_true',
help='Show detailed output'
)
parser.add_argument(
'--clear-existing',
action='store_true',
help='Clear existing claims before generating new ones (USE WITH CAUTION)'
)
def handle(self, *args, **options):
"""Main command handler."""
self.verbosity = options.get('verbosity', 1)
self.verbose = options.get('verbose', False)
try:
# Validate options
self._validate_options(options)
# Get patients with insurance
patients_with_insurance = self._get_patients_with_insurance(options)
if not patients_with_insurance:
raise CommandError("No patients with insurance found. Please create patients and insurance records first.")
# Clear existing claims if requested
if options['clear_existing']:
self._clear_existing_claims(options)
# Generate claims
if options['dry_run']:
self._dry_run(patients_with_insurance, options)
else:
self._generate_claims(patients_with_insurance, options)
except Exception as e:
raise CommandError(f"Error generating claims: {str(e)}")
def _validate_options(self, options):
"""Validate command options."""
if options['tenant_id'] and options['all_tenants']:
raise CommandError("Cannot specify both --tenant-id and --all-tenants")
if options['count'] <= 0:
raise CommandError("Count must be a positive integer")
if options['days_back'] <= 0:
raise CommandError("Days back must be a positive integer")
def _get_patients_with_insurance(self, options):
"""Get patients with insurance based on options."""
patients_with_insurance = []
# Base queryset
patients_query = PatientProfile.objects.select_related('tenant')
insurance_query = InsuranceInfo.objects.select_related('patient', 'patient__tenant')
# Filter by tenant
if options['tenant_id']:
try:
tenant = Tenant.objects.get(id=options['tenant_id'])
patients_query = patients_query.filter(tenant=tenant)
insurance_query = insurance_query.filter(patient__tenant=tenant)
if self.verbose:
self.stdout.write(f"Filtering by tenant: {tenant.name}")
except Tenant.DoesNotExist:
raise CommandError(f"Tenant with ID {options['tenant_id']} not found")
elif not options['all_tenants']:
# Default to first tenant if no specific tenant specified
first_tenant = Tenant.objects.first()
if first_tenant:
patients_query = patients_query.filter(tenant=first_tenant)
insurance_query = insurance_query.filter(patient__tenant=first_tenant)
if self.verbose:
self.stdout.write(f"Using default tenant: {first_tenant.name}")
# Filter by specific patient
if options['patient_id']:
try:
patient = patients_query.get(id=options['patient_id'])
insurance_query = insurance_query.filter(patient=patient)
if self.verbose:
self.stdout.write(f"Filtering by patient: {patient.get_full_name()}")
except PatientProfile.DoesNotExist:
raise CommandError(f"Patient with ID {options['patient_id']} not found")
# Filter by specific insurance
if options['insurance_id']:
try:
insurance = insurance_query.get(id=options['insurance_id'])
patients_with_insurance = [(insurance.patient, insurance)]
if self.verbose:
self.stdout.write(f"Using specific insurance: {insurance.insurance_provider}")
except InsuranceInfo.DoesNotExist:
raise CommandError(f"Insurance with ID {options['insurance_id']} not found")
else:
# Get all patients with insurance
for insurance in insurance_query:
patients_with_insurance.append((insurance.patient, insurance))
return patients_with_insurance
def _clear_existing_claims(self, options):
"""Clear existing claims if requested."""
if not options['clear_existing']:
return
self.stdout.write(
self.style.WARNING("Clearing existing claims...")
)
# Build filter for claims to delete
claims_query = InsuranceClaim.objects.all()
if options['tenant_id']:
claims_query = claims_query.filter(patient__tenant_id=options['tenant_id'])
elif not options['all_tenants']:
first_tenant = Tenant.objects.first()
if first_tenant:
claims_query = claims_query.filter(patient__tenant=first_tenant)
if options['patient_id']:
claims_query = claims_query.filter(patient_id=options['patient_id'])
if options['insurance_id']:
claims_query = claims_query.filter(insurance_info_id=options['insurance_id'])
deleted_count = claims_query.count()
claims_query.delete()
self.stdout.write(
self.style.SUCCESS(f"Cleared {deleted_count} existing claims")
)
def _dry_run(self, patients_with_insurance, options):
"""Show what would be generated without creating records."""
self.stdout.write(
self.style.WARNING("DRY RUN - No records will be created")
)
generator = SaudiClaimsDataGenerator()
# Generate sample claims data
sample_claims = generator.generate_multiple_claims(
patients_with_insurance[:min(5, len(patients_with_insurance))],
min(5, options['count'])
)
self.stdout.write(f"\nWould generate {options['count']} claims")
self.stdout.write(f"Available patients with insurance: {len(patients_with_insurance)}")
if sample_claims:
self.stdout.write(f"\nSample claim data:")
for i, claim in enumerate(sample_claims[:3], 1):
self.stdout.write(f"\nClaim {i}:")
self.stdout.write(f" - Number: {claim['claim_number']}")
self.stdout.write(f" - Patient: {claim['patient'].get_full_name()}")
self.stdout.write(f" - Type: {claim['claim_type']}")
self.stdout.write(f" - Status: {claim['status']}")
self.stdout.write(f" - Amount: {claim['billed_amount']} SAR")
self.stdout.write(f" - Provider: {claim['service_provider']}")
self.stdout.write(f" - Facility: {claim['facility_name']}")
# Show statistics
stats = generator.get_saudi_insurance_statistics(sample_claims)
if stats:
self.stdout.write(f"\nSample statistics:")
self.stdout.write(f" - Total claims: {stats['total_claims']}")
self.stdout.write(f" - Approval rate: {stats['approval_rate']:.1f}%")
self.stdout.write(f" - Average amount: {stats['average_claim_amount_sar']:.2f} SAR")
def _generate_claims(self, patients_with_insurance, options):
"""Generate the actual claims records."""
generator = SaudiClaimsDataGenerator()
created_by = self._get_system_user()
self.stdout.write(f"Generating {options['count']} insurance claims...")
if self.verbose:
self.stdout.write(f"Available patients with insurance: {len(patients_with_insurance)}")
# Generate claims data
claims_data = generator.generate_multiple_claims(
patients_with_insurance,
options['count'],
created_by
)
# Filter by claim type if specified
if options['claim_type']:
claims_data = [c for c in claims_data if c['claim_type'] == options['claim_type']]
if self.verbose:
self.stdout.write(f"Filtered to {len(claims_data)} claims of type {options['claim_type']}")
# Filter by status if specified
if options['status']:
claims_data = [c for c in claims_data if c['status'] == options['status']]
if self.verbose:
self.stdout.write(f"Filtered to {len(claims_data)} claims with status {options['status']}")
if not claims_data:
self.stdout.write(
self.style.WARNING("No claims data generated after filtering")
)
return
# Create claims in database
created_claims = []
created_count = 0
with transaction.atomic():
for claim_data in claims_data:
try:
# Create the claim
claim = InsuranceClaim.objects.create(**claim_data)
created_claims.append(claim)
created_count += 1
# Create status history
self._create_status_history(claim, created_by)
if self.verbose and created_count % 10 == 0:
self.stdout.write(f"Created {created_count} claims...")
except Exception as e:
self.stdout.write(
self.style.ERROR(f"Error creating claim: {str(e)}")
)
continue
# Show results
self.stdout.write(
self.style.SUCCESS(f"Successfully created {created_count} insurance claims")
)
# Show statistics
if created_claims:
stats = generator.get_saudi_insurance_statistics(claims_data)
self.stdout.write(f"\nGenerated claims statistics:")
self.stdout.write(f" - Total claims: {stats['total_claims']}")
self.stdout.write(f" - Approved claims: {stats['approved_claims']}")
self.stdout.write(f" - Denied claims: {stats['denied_claims']}")
self.stdout.write(f" - Pending claims: {stats['pending_claims']}")
self.stdout.write(f" - Approval rate: {stats['approval_rate']:.1f}%")
self.stdout.write(f" - Total billed: {stats['total_billed_sar']:,.2f} SAR")
self.stdout.write(f" - Total approved: {stats['total_approved_sar']:,.2f} SAR")
self.stdout.write(f" - Average claim: {stats['average_claim_amount_sar']:,.2f} SAR")
# Show sample claims
if self.verbose and created_claims:
self.stdout.write(f"\nSample created claims:")
for claim in created_claims[:3]:
self.stdout.write(f" - {claim.claim_number}: {claim.patient.get_full_name()} - {claim.billed_amount} SAR ({claim.status})")
def _create_status_history(self, claim, created_by):
"""Create status history for the claim."""
# Create initial status history entry
ClaimStatusHistory.objects.create(
claim=claim,
from_status=None,
to_status='DRAFT',
reason='Initial claim creation',
changed_by=created_by
)
# If claim has progressed beyond draft, create additional history
if claim.status != 'DRAFT':
statuses = ['DRAFT', 'SUBMITTED', 'UNDER_REVIEW']
if claim.status in ['APPROVED', 'PARTIALLY_APPROVED', 'PAID']:
statuses.append('APPROVED')
if claim.status == 'PAID':
statuses.append('PAID')
elif claim.status == 'DENIED':
statuses.append('DENIED')
# Create history entries for each status transition
for i in range(1, len(statuses)):
ClaimStatusHistory.objects.create(
claim=claim,
from_status=statuses[i-1],
to_status=statuses[i],
reason=f'Automatic status progression to {statuses[i]}',
changed_by=created_by
)
def _get_system_user(self):
"""Get or create a system user for created_by field."""
try:
return User.objects.get(username='system')
except User.DoesNotExist:
# Try to get the first superuser
superuser = User.objects.filter(is_superuser=True).first()
if superuser:
return superuser
# Try to get any user
user = User.objects.first()
if user:
return user
# Create a system user if none exists
return User.objects.create_user(
username='system',
email='system@hospital.local',
first_name='System',
last_name='Generator',
is_active=True
)

View File

@ -0,0 +1,228 @@
"""
Django management command to populate Saudi insurance providers and create sample patients.
Usage:
python manage.py populate_saudi_insurance --tenant-id 1
python manage.py populate_saudi_insurance --create-patients 50
"""
from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth import get_user_model
from django.db import transaction
from django.utils import timezone
from patients.models import PatientProfile, InsuranceInfo
from patients.data_generators.saudi_claims_generator import SaudiClaimsDataGenerator
from core.models import Tenant
import random
from datetime import datetime, timedelta
User = get_user_model()
class Command(BaseCommand):
help = 'Populate Saudi insurance providers and create sample patients with insurance'
def add_arguments(self, parser):
parser.add_argument(
'--tenant-id',
type=int,
required=True,
help='Tenant ID to create data for'
)
parser.add_argument(
'--create-patients',
type=int,
default=20,
help='Number of patients to create (default: 20)'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='Show what would be created without creating records'
)
def handle(self, *args, **options):
"""Main command handler."""
try:
# Get tenant
tenant = Tenant.objects.get(id=options['tenant_id'])
if options['dry_run']:
self._dry_run(tenant, options)
else:
self._create_data(tenant, options)
except Tenant.DoesNotExist:
raise CommandError(f"Tenant with ID {options['tenant_id']} not found")
except Exception as e:
raise CommandError(f"Error: {str(e)}")
def _dry_run(self, tenant, options):
"""Show what would be created."""
self.stdout.write(
self.style.WARNING("DRY RUN - No records will be created")
)
generator = SaudiClaimsDataGenerator()
self.stdout.write(f"\nWould create for tenant: {tenant.name}")
self.stdout.write(f"Number of patients: {options['create_patients']}")
self.stdout.write(f"Insurance companies available: {len(generator.SAUDI_INSURANCE_COMPANIES)}")
# Show sample data
self.stdout.write(f"\nSample insurance companies:")
for company in generator.SAUDI_INSURANCE_COMPANIES[:5]:
self.stdout.write(f" - {company}")
self.stdout.write(f"\nSample patient names:")
for i in range(3):
male_name = f"{random.choice(generator.SAUDI_FIRST_NAMES_MALE)} {random.choice(generator.SAUDI_FAMILY_NAMES)}"
female_name = f"{random.choice(generator.SAUDI_FIRST_NAMES_FEMALE)} {random.choice(generator.SAUDI_FAMILY_NAMES)}"
self.stdout.write(f" - {male_name}")
self.stdout.write(f" - {female_name}")
def _create_data(self, tenant, options):
"""Create the actual data."""
generator = SaudiClaimsDataGenerator()
created_by = self._get_system_user()
self.stdout.write(f"Creating data for tenant: {tenant.name}")
patients_created = 0
insurance_created = 0
with transaction.atomic():
# Create patients with insurance
for i in range(options['create_patients']):
try:
# Generate patient data
is_male = random.choice([True, False])
if is_male:
first_name = random.choice(generator.SAUDI_FIRST_NAMES_MALE)
gender = 'M'
else:
first_name = random.choice(generator.SAUDI_FIRST_NAMES_FEMALE)
gender = 'F'
last_name = random.choice(generator.SAUDI_FAMILY_NAMES)
# Generate birth date (18-80 years old)
birth_date = datetime.now().date() - timedelta(
days=random.randint(18*365, 80*365)
)
# Create patient
patient = PatientProfile.objects.create(
tenant=tenant,
mrn=f"MRN{random.randint(100000, 999999)}",
first_name=first_name,
last_name=last_name,
date_of_birth=birth_date,
gender=gender,
phone_number=f"+966{random.randint(500000000, 599999999)}",
email=f"{first_name.lower()}.{last_name.lower().replace(' ', '')}@example.com",
address=f"الرياض، المملكة العربية السعودية",
city="الرياض",
country="SA",
created_by=created_by
)
patients_created += 1
# Create 1-2 insurance policies per patient
num_insurances = random.choices([1, 2], weights=[0.7, 0.3])[0]
for j in range(num_insurances):
insurance_company = random.choice(generator.SAUDI_INSURANCE_COMPANIES)
# Generate policy dates
effective_date = datetime.now().date() - timedelta(
days=random.randint(30, 365)
)
expiration_date = effective_date + timedelta(days=365)
# Create insurance
insurance = InsuranceInfo.objects.create(
patient=patient,
insurance_type='PRIMARY' if j == 0 else 'SECONDARY',
insurance_company=insurance_company,
plan_name=f"{insurance_company.split()[0]} Premium Plan",
plan_type=random.choice(['HMO', 'PPO', 'EPO']),
policy_number=f"POL{random.randint(100000000, 999999999)}",
member_id=f"MEM{random.randint(100000000, 999999999)}",
group_number=f"GRP{random.randint(10000, 99999)}",
effective_date=effective_date,
expiration_date=expiration_date,
copay=random.choice([50, 75, 100, 150, 200]),
deductible=random.choice([500, 1000, 1500, 2000, 2500]),
subscriber_name=patient.get_full_name(),
subscriber_relationship='SELF',
subscriber_dob=patient.date_of_birth,
created_by=created_by
)
insurance_created += 1
if (i + 1) % 10 == 0:
self.stdout.write(f"Created {i + 1} patients...")
except Exception as e:
self.stdout.write(
self.style.ERROR(f"Error creating patient {i + 1}: {str(e)}")
)
continue
# Show results
self.stdout.write(
self.style.SUCCESS(
f"Successfully created {patients_created} patients and {insurance_created} insurance policies"
)
)
# Show sample data
sample_patients = PatientProfile.objects.filter(tenant=tenant).order_by('-created_at')[:5]
if sample_patients:
self.stdout.write(f"\nSample created patients:")
for patient in sample_patients:
insurance_count = patient.insurance_info.count()
self.stdout.write(
f" - {patient.get_full_name()} (MRN: {patient.mrn}, Insurance policies: {insurance_count})"
)
# Show insurance companies used
used_companies = InsuranceInfo.objects.filter(
patient__tenant=tenant
).values_list('insurance_company', flat=True).distinct()
self.stdout.write(f"\nInsurance companies used ({len(used_companies)}):")
for company in sorted(used_companies)[:10]:
self.stdout.write(f" - {company}")
if len(used_companies) > 10:
self.stdout.write(f" ... and {len(used_companies) - 10} more")
def _get_system_user(self):
"""Get or create a system user for created_by field."""
try:
return User.objects.get(username='system')
except User.DoesNotExist:
# Try to get the first superuser
superuser = User.objects.filter(is_superuser=True).first()
if superuser:
return superuser
# Try to get any user
user = User.objects.first()
if user:
return user
# Create a system user if none exists
return User.objects.create_user(
username='system',
email='system@hospital.local',
first_name='System',
last_name='Generator',
is_active=True
)

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2025-09-03 12:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("patients", "0002_emergencycontact_authorization_number_and_more"),
]
operations = [
migrations.AddField(
model_name="insuranceinfo",
name="is_primary",
field=models.BooleanField(default=False, help_text="Primary insurance"),
),
]

View File

@ -0,0 +1,27 @@
# Generated by Django 5.2.4 on 2025-09-03 13:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("patients", "0003_insuranceinfo_is_primary"),
]
operations = [
migrations.AddField(
model_name="insuranceinfo",
name="status",
field=models.CharField(
choices=[
("PENDING", "Pending"),
("APPROVED", "Approved"),
("DENIED", "Denied"),
],
default="PENDING",
help_text="Consent status",
max_length=20,
),
),
]

View File

@ -0,0 +1,27 @@
# Generated by Django 5.2.4 on 2025-09-03 13:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("patients", "0004_insuranceinfo_status"),
]
operations = [
migrations.AlterField(
model_name="insuranceinfo",
name="status",
field=models.CharField(
choices=[
("PENDING", "Pending"),
("APPROVED", "Approved"),
("DENIED", "Denied"),
],
default="PENDING",
help_text="Insurance status",
max_length=20,
),
),
]

View File

@ -0,0 +1,532 @@
# Generated by Django 5.2.4 on 2025-09-03 14:41
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("patients", "0005_alter_insuranceinfo_status"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="InsuranceClaim",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"claim_number",
models.CharField(
help_text="Unique claim number", max_length=50, unique=True
),
),
(
"claim_type",
models.CharField(
choices=[
("MEDICAL", "Medical"),
("DENTAL", "Dental"),
("VISION", "Vision"),
("PHARMACY", "Pharmacy"),
("EMERGENCY", "Emergency"),
("INPATIENT", "Inpatient"),
("OUTPATIENT", "Outpatient"),
("PREVENTIVE", "Preventive Care"),
("MATERNITY", "Maternity"),
("MENTAL_HEALTH", "Mental Health"),
("REHABILITATION", "Rehabilitation"),
("DIAGNOSTIC", "Diagnostic"),
("SURGICAL", "Surgical"),
("CHRONIC_CARE", "Chronic Care"),
],
default="MEDICAL",
help_text="Type of claim",
max_length=20,
),
),
(
"status",
models.CharField(
choices=[
("DRAFT", "Draft"),
("SUBMITTED", "Submitted"),
("UNDER_REVIEW", "Under Review"),
("APPROVED", "Approved"),
("PARTIALLY_APPROVED", "Partially Approved"),
("DENIED", "Denied"),
("PAID", "Paid"),
("CANCELLED", "Cancelled"),
("APPEALED", "Appealed"),
("RESUBMITTED", "Resubmitted"),
],
default="DRAFT",
help_text="Current claim status",
max_length=20,
),
),
(
"priority",
models.CharField(
choices=[
("LOW", "Low"),
("NORMAL", "Normal"),
("HIGH", "High"),
("URGENT", "Urgent"),
("EMERGENCY", "Emergency"),
],
default="NORMAL",
help_text="Claim priority",
max_length=10,
),
),
(
"service_date",
models.DateField(help_text="Date when service was provided"),
),
(
"service_provider",
models.CharField(
help_text="Healthcare provider who provided the service",
max_length=200,
),
),
(
"service_provider_license",
models.CharField(
blank=True,
help_text="Provider license number (Saudi Medical License)",
max_length=50,
null=True,
),
),
(
"facility_name",
models.CharField(
blank=True,
help_text="Healthcare facility name",
max_length=200,
null=True,
),
),
(
"facility_license",
models.CharField(
blank=True,
help_text="Facility license number (MOH License)",
max_length=50,
null=True,
),
),
(
"primary_diagnosis_code",
models.CharField(
help_text="Primary diagnosis code (ICD-10)", max_length=20
),
),
(
"primary_diagnosis_description",
models.TextField(help_text="Primary diagnosis description"),
),
(
"secondary_diagnosis_codes",
models.JSONField(
blank=True,
default=list,
help_text="Secondary diagnosis codes and descriptions",
),
),
(
"procedure_codes",
models.JSONField(
blank=True,
default=list,
help_text="Procedure codes (CPT/HCPCS) and descriptions",
),
),
(
"billed_amount",
models.DecimalField(
decimal_places=2,
help_text="Total amount billed (SAR)",
max_digits=12,
),
),
(
"approved_amount",
models.DecimalField(
decimal_places=2,
default=0,
help_text="Amount approved by insurance (SAR)",
max_digits=12,
),
),
(
"paid_amount",
models.DecimalField(
decimal_places=2,
default=0,
help_text="Amount actually paid (SAR)",
max_digits=12,
),
),
(
"patient_responsibility",
models.DecimalField(
decimal_places=2,
default=0,
help_text="Patient copay/deductible amount (SAR)",
max_digits=12,
),
),
(
"discount_amount",
models.DecimalField(
decimal_places=2,
default=0,
help_text="Discount applied (SAR)",
max_digits=12,
),
),
(
"submitted_date",
models.DateTimeField(
blank=True,
help_text="Date claim was submitted to insurance",
null=True,
),
),
(
"processed_date",
models.DateTimeField(
blank=True, help_text="Date claim was processed", null=True
),
),
(
"payment_date",
models.DateTimeField(
blank=True, help_text="Date payment was received", null=True
),
),
(
"saudi_id_number",
models.CharField(
blank=True,
help_text="Saudi National ID or Iqama number",
max_length=10,
null=True,
),
),
(
"insurance_card_number",
models.CharField(
blank=True,
help_text="Insurance card number",
max_length=50,
null=True,
),
),
(
"authorization_number",
models.CharField(
blank=True,
help_text="Prior authorization number if required",
max_length=50,
null=True,
),
),
(
"denial_reason",
models.TextField(
blank=True,
help_text="Reason for denial if applicable",
null=True,
),
),
(
"denial_code",
models.CharField(
blank=True,
help_text="Insurance denial code",
max_length=20,
null=True,
),
),
(
"appeal_date",
models.DateTimeField(
blank=True, help_text="Date appeal was filed", null=True
),
),
(
"appeal_reason",
models.TextField(
blank=True, help_text="Reason for appeal", null=True
),
),
(
"notes",
models.TextField(
blank=True,
help_text="Additional notes about the claim",
null=True,
),
),
(
"attachments",
models.JSONField(
blank=True, default=list, help_text="List of attached documents"
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"created_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_claims",
to=settings.AUTH_USER_MODEL,
),
),
(
"insurance_info",
models.ForeignKey(
help_text="Insurance policy used for this claim",
on_delete=django.db.models.deletion.CASCADE,
related_name="claims",
to="patients.insuranceinfo",
),
),
(
"patient",
models.ForeignKey(
help_text="Patient associated with this claim",
on_delete=django.db.models.deletion.CASCADE,
related_name="insurance_claims",
to="patients.patientprofile",
),
),
],
options={
"verbose_name": "Insurance Claim",
"verbose_name_plural": "Insurance Claims",
"db_table": "patients_insurance_claim",
"ordering": ["-created_at"],
},
),
migrations.CreateModel(
name="ClaimStatusHistory",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"from_status",
models.CharField(
blank=True,
choices=[
("DRAFT", "Draft"),
("SUBMITTED", "Submitted"),
("UNDER_REVIEW", "Under Review"),
("APPROVED", "Approved"),
("PARTIALLY_APPROVED", "Partially Approved"),
("DENIED", "Denied"),
("PAID", "Paid"),
("CANCELLED", "Cancelled"),
("APPEALED", "Appealed"),
("RESUBMITTED", "Resubmitted"),
],
help_text="Previous status",
max_length=20,
null=True,
),
),
(
"to_status",
models.CharField(
choices=[
("DRAFT", "Draft"),
("SUBMITTED", "Submitted"),
("UNDER_REVIEW", "Under Review"),
("APPROVED", "Approved"),
("PARTIALLY_APPROVED", "Partially Approved"),
("DENIED", "Denied"),
("PAID", "Paid"),
("CANCELLED", "Cancelled"),
("APPEALED", "Appealed"),
("RESUBMITTED", "Resubmitted"),
],
help_text="New status",
max_length=20,
),
),
(
"reason",
models.TextField(
blank=True, help_text="Reason for status change", null=True
),
),
(
"notes",
models.TextField(
blank=True, help_text="Additional notes", null=True
),
),
("changed_at", models.DateTimeField(auto_now_add=True)),
(
"changed_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
(
"claim",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="status_history",
to="patients.insuranceclaim",
),
),
],
options={
"verbose_name": "Claim Status History",
"verbose_name_plural": "Claim Status Histories",
"db_table": "patients_claim_status_history",
"ordering": ["-changed_at"],
},
),
migrations.CreateModel(
name="ClaimDocument",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"document_type",
models.CharField(
choices=[
("MEDICAL_REPORT", "Medical Report"),
("LAB_RESULT", "Laboratory Result"),
("RADIOLOGY_REPORT", "Radiology Report"),
("PRESCRIPTION", "Prescription"),
("INVOICE", "Invoice"),
("RECEIPT", "Receipt"),
("AUTHORIZATION", "Prior Authorization"),
("REFERRAL", "Referral Letter"),
("DISCHARGE_SUMMARY", "Discharge Summary"),
("OPERATIVE_REPORT", "Operative Report"),
("PATHOLOGY_REPORT", "Pathology Report"),
("INSURANCE_CARD", "Insurance Card Copy"),
("ID_COPY", "ID Copy"),
("OTHER", "Other"),
],
help_text="Type of document",
max_length=20,
),
),
("title", models.CharField(help_text="Document title", max_length=200)),
(
"description",
models.TextField(
blank=True, help_text="Document description", null=True
),
),
(
"file_path",
models.CharField(
help_text="Path to the document file", max_length=500
),
),
(
"file_size",
models.PositiveIntegerField(help_text="File size in bytes"),
),
(
"mime_type",
models.CharField(help_text="MIME type of the file", max_length=100),
),
("uploaded_at", models.DateTimeField(auto_now_add=True)),
(
"uploaded_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
(
"claim",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="documents",
to="patients.insuranceclaim",
),
),
],
options={
"verbose_name": "Claim Document",
"verbose_name_plural": "Claim Documents",
"db_table": "patients_claim_document",
"ordering": ["-uploaded_at"],
},
),
migrations.AddIndex(
model_name="insuranceclaim",
index=models.Index(
fields=["claim_number"], name="patients_in_claim_n_3b3114_idx"
),
),
migrations.AddIndex(
model_name="insuranceclaim",
index=models.Index(
fields=["patient", "service_date"],
name="patients_in_patient_5d8b72_idx",
),
),
migrations.AddIndex(
model_name="insuranceclaim",
index=models.Index(
fields=["status", "priority"], name="patients_in_status_41f139_idx"
),
),
migrations.AddIndex(
model_name="insuranceclaim",
index=models.Index(
fields=["submitted_date"], name="patients_in_submitt_75aa55_idx"
),
),
migrations.AddIndex(
model_name="insuranceclaim",
index=models.Index(
fields=["insurance_info"], name="patients_in_insuran_f48b26_idx"
),
),
]

View File

@ -0,0 +1,33 @@
# Generated by Django 5.2.4 on 2025-09-04 15:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("patients", "0006_insuranceclaim_claimstatushistory_claimdocument_and_more"),
]
operations = [
migrations.AlterField(
model_name="consenttemplate",
name="category",
field=models.CharField(
choices=[
("TREATMENT", "Treatment Consent"),
("PROCEDURE", "Procedure Consent"),
("SURGERY", "Surgical Consent"),
("ANESTHESIA", "Anesthesia Consent"),
("RESEARCH", "Research Consent"),
("PRIVACY", "Privacy Consent"),
("FINANCIAL", "Financial Consent"),
("ADMISSION", "Admission Consent"),
("DISCHARGE", "Discharge Consent"),
("OTHER", "Other"),
],
help_text="Consent category",
max_length=50,
),
),
]

View File

@ -14,6 +14,59 @@ class PatientProfile(models.Model):
""" """
Patient profile with comprehensive demographics and healthcare information. Patient profile with comprehensive demographics and healthcare information.
""" """
GENDER_CHOICES = [
('MALE', 'Male'),
('FEMALE', 'Female'),
('OTHER', 'Other'),
('UNKNOWN', 'Unknown'),
('PREFER_NOT_TO_SAY', 'Prefer not to say'),
]
SEX_ASSIGNED_AT_BIRTH_CHOICES = [
('MALE', 'Male'),
('FEMALE', 'Female'),
('INTERSEX', 'Intersex'),
('UNKNOWN', 'Unknown'),
]
RACE_CHOICES = [
('AMERICAN_INDIAN', 'American Indian or Alaska Native'),
('ASIAN', 'Asian'),
('BLACK', 'Black or African American'),
('PACIFIC_ISLANDER', 'Native Hawaiian or Other Pacific Islander'),
('WHITE', 'White'),
('OTHER', 'Other'),
('UNKNOWN', 'Unknown'),
('DECLINED', 'Patient Declined'),
]
ETHNICITY_CHOICES = [
('HISPANIC', 'Hispanic or Latino'),
('NON_HISPANIC', 'Not Hispanic or Latino'),
('UNKNOWN', 'Unknown'),
('DECLINED', 'Patient Declined'),
]
MARITAL_STATUS_CHOICES = [
('SINGLE', 'Single'),
('MARRIED', 'Married'),
('DIVORCED', 'Divorced'),
('WIDOWED', 'Widowed'),
('SEPARATED', 'Separated'),
('DOMESTIC_PARTNER', 'Domestic Partner'),
('OTHER', 'Other'),
('UNKNOWN', 'Unknown'),
]
COMMUNICATION_PREFERENCE_CHOICES = [
('PHONE', 'Phone'),
('EMAIL', 'Email'),
('SMS', 'SMS'),
('MAIL', 'Mail'),
('PORTAL', 'Patient Portal'),
]
ADVANCE_DIRECTIVE_TYPE_CHOICES = [
('LIVING_WILL', 'Living Will'),
('HEALTHCARE_PROXY', 'Healthcare Proxy'),
('DNR', 'Do Not Resuscitate'),
('POLST', 'POLST'),
('OTHER', 'Other'),
]
# Basic Identifiers # Basic Identifiers
patient_id = models.UUIDField( patient_id = models.UUIDField(
@ -72,23 +125,12 @@ class PatientProfile(models.Model):
) )
gender = models.CharField( gender = models.CharField(
max_length=20, max_length=20,
choices=[ choices=GENDER_CHOICES,
('MALE', 'Male'),
('FEMALE', 'Female'),
('OTHER', 'Other'),
('UNKNOWN', 'Unknown'),
('PREFER_NOT_TO_SAY', 'Prefer not to say'),
],
help_text='Gender' help_text='Gender'
) )
sex_assigned_at_birth = models.CharField( sex_assigned_at_birth = models.CharField(
max_length=20, max_length=20,
choices=[ choices=SEX_ASSIGNED_AT_BIRTH_CHOICES,
('MALE', 'Male'),
('FEMALE', 'Female'),
('INTERSEX', 'Intersex'),
('UNKNOWN', 'Unknown'),
],
blank=True, blank=True,
null=True, null=True,
help_text='Sex assigned at birth' help_text='Sex assigned at birth'
@ -97,28 +139,14 @@ class PatientProfile(models.Model):
# Race and Ethnicity # Race and Ethnicity
race = models.CharField( race = models.CharField(
max_length=50, max_length=50,
choices=[ choices=RACE_CHOICES,
('AMERICAN_INDIAN', 'American Indian or Alaska Native'),
('ASIAN', 'Asian'),
('BLACK', 'Black or African American'),
('PACIFIC_ISLANDER', 'Native Hawaiian or Other Pacific Islander'),
('WHITE', 'White'),
('OTHER', 'Other'),
('UNKNOWN', 'Unknown'),
('DECLINED', 'Patient Declined'),
],
blank=True, blank=True,
null=True, null=True,
help_text='Race' help_text='Race'
) )
ethnicity = models.CharField( ethnicity = models.CharField(
max_length=50, max_length=50,
choices=[ choices=ETHNICITY_CHOICES,
('HISPANIC', 'Hispanic or Latino'),
('NON_HISPANIC', 'Not Hispanic or Latino'),
('UNKNOWN', 'Unknown'),
('DECLINED', 'Patient Declined'),
],
blank=True, blank=True,
null=True, null=True,
help_text='Ethnicity' help_text='Ethnicity'
@ -215,16 +243,7 @@ class PatientProfile(models.Model):
# Marital Status and Family # Marital Status and Family
marital_status = models.CharField( marital_status = models.CharField(
max_length=20, max_length=20,
choices=[ choices=MARITAL_STATUS_CHOICES,
('SINGLE', 'Single'),
('MARRIED', 'Married'),
('DIVORCED', 'Divorced'),
('WIDOWED', 'Widowed'),
('SEPARATED', 'Separated'),
('DOMESTIC_PARTNER', 'Domestic Partner'),
('OTHER', 'Other'),
('UNKNOWN', 'Unknown'),
],
blank=True, blank=True,
null=True, null=True,
help_text='Marital status' help_text='Marital status'
@ -242,13 +261,7 @@ class PatientProfile(models.Model):
) )
communication_preference = models.CharField( communication_preference = models.CharField(
max_length=20, max_length=20,
choices=[ choices=COMMUNICATION_PREFERENCE_CHOICES,
('PHONE', 'Phone'),
('EMAIL', 'Email'),
('SMS', 'SMS'),
('MAIL', 'Mail'),
('PORTAL', 'Patient Portal'),
],
default='PHONE', default='PHONE',
help_text='Preferred communication method' help_text='Preferred communication method'
) )
@ -300,13 +313,7 @@ class PatientProfile(models.Model):
) )
advance_directive_type = models.CharField( advance_directive_type = models.CharField(
max_length=50, max_length=50,
choices=[ choices=ADVANCE_DIRECTIVE_TYPE_CHOICES,
('LIVING_WILL', 'Living Will'),
('HEALTHCARE_PROXY', 'Healthcare Proxy'),
('DNR', 'Do Not Resuscitate'),
('POLST', 'POLST'),
('OTHER', 'Other'),
],
blank=True, blank=True,
null=True, null=True,
help_text='Type of advance directive' help_text='Type of advance directive'
@ -445,7 +452,21 @@ class EmergencyContact(models.Model):
""" """
Emergency contact information for patients. Emergency contact information for patients.
""" """
RELATIONSHIP_CHOICES = [
('SPOUSE', 'Spouse'),
('PARENT', 'Parent'),
('CHILD', 'Child'),
('SIBLING', 'Sibling'),
('GRANDPARENT', 'Grandparent'),
('GRANDCHILD', 'Grandchild'),
('AUNT_UNCLE', 'Aunt/Uncle'),
('COUSIN', 'Cousin'),
('FRIEND', 'Friend'),
('NEIGHBOR', 'Neighbor'),
('CAREGIVER', 'Caregiver'),
('GUARDIAN', 'Guardian'),
('OTHER', 'Other'),
]
# Patient relationship # Patient relationship
patient = models.ForeignKey( patient = models.ForeignKey(
PatientProfile, PatientProfile,
@ -464,21 +485,7 @@ class EmergencyContact(models.Model):
) )
relationship = models.CharField( relationship = models.CharField(
max_length=50, max_length=50,
choices=[ choices=RELATIONSHIP_CHOICES,
('SPOUSE', 'Spouse'),
('PARENT', 'Parent'),
('CHILD', 'Child'),
('SIBLING', 'Sibling'),
('GRANDPARENT', 'Grandparent'),
('GRANDCHILD', 'Grandchild'),
('AUNT_UNCLE', 'Aunt/Uncle'),
('COUSIN', 'Cousin'),
('FRIEND', 'Friend'),
('NEIGHBOR', 'Neighbor'),
('CAREGIVER', 'Caregiver'),
('GUARDIAN', 'Guardian'),
('OTHER', 'Other'),
],
help_text='Relationship to patient' help_text='Relationship to patient'
) )
@ -607,7 +614,36 @@ class InsuranceInfo(models.Model):
""" """
Insurance information for patients. Insurance information for patients.
""" """
INSURANCE_TYPE_CHOICES = [
('PRIMARY', 'Primary'),
('SECONDARY', 'Secondary'),
('TERTIARY', 'Tertiary'),
]
PLAN_TYPE_CHOICES = [
('HMO', 'Health Maintenance Organization'),
('PPO', 'Preferred Provider Organization'),
('EPO', 'Exclusive Provider Organization'),
('POS', 'Point of Service'),
('HDHP', 'High Deductible Health Plan'),
('MEDICARE', 'Medicare'),
('MEDICAID', 'Medicaid'),
('TRICARE', 'TRICARE'),
('WORKERS_COMP', 'Workers Compensation'),
('AUTO', 'Auto Insurance'),
('OTHER', 'Other'),
]
SUBSCRIBER_RELATIONSHIP_CHOICES = [
('SELF', 'Self'),
('SPOUSE', 'Spouse'),
('CHILD', 'Child'),
('PARENT', 'Parent'),
('OTHER', 'Other'),
]
STATUS_CHOICES = [
('PENDING', 'Pending'),
('APPROVED', 'Approved'),
('DENIED', 'Denied'),
]
# Patient relationship # Patient relationship
patient = models.ForeignKey( patient = models.ForeignKey(
PatientProfile, PatientProfile,
@ -618,11 +654,7 @@ class InsuranceInfo(models.Model):
# Insurance Details # Insurance Details
insurance_type = models.CharField( insurance_type = models.CharField(
max_length=20, max_length=20,
choices=[ choices=INSURANCE_TYPE_CHOICES,
('PRIMARY', 'Primary'),
('SECONDARY', 'Secondary'),
('TERTIARY', 'Tertiary'),
],
default='PRIMARY', default='PRIMARY',
help_text='Insurance type' help_text='Insurance type'
) )
@ -640,24 +672,17 @@ class InsuranceInfo(models.Model):
) )
plan_type = models.CharField( plan_type = models.CharField(
max_length=50, max_length=50,
choices=[ choices=PLAN_TYPE_CHOICES,
('HMO', 'Health Maintenance Organization'),
('PPO', 'Preferred Provider Organization'),
('EPO', 'Exclusive Provider Organization'),
('POS', 'Point of Service'),
('HDHP', 'High Deductible Health Plan'),
('MEDICARE', 'Medicare'),
('MEDICAID', 'Medicaid'),
('TRICARE', 'TRICARE'),
('WORKERS_COMP', 'Workers Compensation'),
('AUTO', 'Auto Insurance'),
('OTHER', 'Other'),
],
blank=True, blank=True,
null=True, null=True,
help_text='Plan type' help_text='Plan type'
) )
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='PENDING',
help_text='Insurance status'
)
# Policy Information # Policy Information
policy_number = models.CharField( policy_number = models.CharField(
max_length=100, max_length=100,
@ -677,13 +702,7 @@ class InsuranceInfo(models.Model):
) )
subscriber_relationship = models.CharField( subscriber_relationship = models.CharField(
max_length=20, max_length=20,
choices=[ choices=SUBSCRIBER_RELATIONSHIP_CHOICES,
('SELF', 'Self'),
('SPOUSE', 'Spouse'),
('CHILD', 'Child'),
('PARENT', 'Parent'),
('OTHER', 'Other'),
],
default='SELF', default='SELF',
help_text='Relationship to subscriber' help_text='Relationship to subscriber'
) )
@ -777,6 +796,10 @@ class InsuranceInfo(models.Model):
default=True, default=True,
help_text='Insurance is active' help_text='Insurance is active'
) )
is_primary = models.BooleanField(
default=False,
help_text='Primary insurance'
)
# Notes # Notes
notes = models.TextField( notes = models.TextField(
@ -814,10 +837,497 @@ class InsuranceInfo(models.Model):
return today >= self.effective_date and self.is_active return today >= self.effective_date and self.is_active
class InsuranceClaim(models.Model):
"""
Insurance claims for patient services and treatments.
Designed for Saudi healthcare system with local insurance providers.
"""
# Claim Status Choices
STATUS_CHOICES = [
('DRAFT', 'Draft'),
('SUBMITTED', 'Submitted'),
('UNDER_REVIEW', 'Under Review'),
('APPROVED', 'Approved'),
('PARTIALLY_APPROVED', 'Partially Approved'),
('DENIED', 'Denied'),
('PAID', 'Paid'),
('CANCELLED', 'Cancelled'),
('APPEALED', 'Appealed'),
('RESUBMITTED', 'Resubmitted'),
]
# Claim Type Choices
CLAIM_TYPE_CHOICES = [
('MEDICAL', 'Medical'),
('DENTAL', 'Dental'),
('VISION', 'Vision'),
('PHARMACY', 'Pharmacy'),
('EMERGENCY', 'Emergency'),
('INPATIENT', 'Inpatient'),
('OUTPATIENT', 'Outpatient'),
('PREVENTIVE', 'Preventive Care'),
('MATERNITY', 'Maternity'),
('MENTAL_HEALTH', 'Mental Health'),
('REHABILITATION', 'Rehabilitation'),
('DIAGNOSTIC', 'Diagnostic'),
('SURGICAL', 'Surgical'),
('CHRONIC_CARE', 'Chronic Care'),
]
# Priority Choices
PRIORITY_CHOICES = [
('LOW', 'Low'),
('NORMAL', 'Normal'),
('HIGH', 'High'),
('URGENT', 'Urgent'),
('EMERGENCY', 'Emergency'),
]
# Basic Information
claim_number = models.CharField(
max_length=50,
unique=True,
help_text='Unique claim number'
)
# Relationships
patient = models.ForeignKey(
PatientProfile,
on_delete=models.CASCADE,
related_name='insurance_claims',
help_text='Patient associated with this claim'
)
insurance_info = models.ForeignKey(
InsuranceInfo,
on_delete=models.CASCADE,
related_name='claims',
help_text='Insurance policy used for this claim'
)
# Claim Details
claim_type = models.CharField(
max_length=20,
choices=CLAIM_TYPE_CHOICES,
default='MEDICAL',
help_text='Type of claim'
)
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='DRAFT',
help_text='Current claim status'
)
priority = models.CharField(
max_length=10,
choices=PRIORITY_CHOICES,
default='NORMAL',
help_text='Claim priority'
)
# Service Information
service_date = models.DateField(
help_text='Date when service was provided'
)
service_provider = models.CharField(
max_length=200,
help_text='Healthcare provider who provided the service'
)
service_provider_license = models.CharField(
max_length=50,
blank=True,
null=True,
help_text='Provider license number (Saudi Medical License)'
)
facility_name = models.CharField(
max_length=200,
blank=True,
null=True,
help_text='Healthcare facility name'
)
facility_license = models.CharField(
max_length=50,
blank=True,
null=True,
help_text='Facility license number (MOH License)'
)
# Medical Codes (Saudi/International Standards)
primary_diagnosis_code = models.CharField(
max_length=20,
help_text='Primary diagnosis code (ICD-10)'
)
primary_diagnosis_description = models.TextField(
help_text='Primary diagnosis description'
)
secondary_diagnosis_codes = models.JSONField(
default=list,
blank=True,
help_text='Secondary diagnosis codes and descriptions'
)
procedure_codes = models.JSONField(
default=list,
blank=True,
help_text='Procedure codes (CPT/HCPCS) and descriptions'
)
# Financial Information (Saudi Riyal)
billed_amount = models.DecimalField(
max_digits=12,
decimal_places=2,
help_text='Total amount billed (SAR)'
)
approved_amount = models.DecimalField(
max_digits=12,
decimal_places=2,
default=0,
help_text='Amount approved by insurance (SAR)'
)
paid_amount = models.DecimalField(
max_digits=12,
decimal_places=2,
default=0,
help_text='Amount actually paid (SAR)'
)
patient_responsibility = models.DecimalField(
max_digits=12,
decimal_places=2,
default=0,
help_text='Patient copay/deductible amount (SAR)'
)
discount_amount = models.DecimalField(
max_digits=12,
decimal_places=2,
default=0,
help_text='Discount applied (SAR)'
)
# Claim Processing
submitted_date = models.DateTimeField(
blank=True,
null=True,
help_text='Date claim was submitted to insurance'
)
processed_date = models.DateTimeField(
blank=True,
null=True,
help_text='Date claim was processed'
)
payment_date = models.DateTimeField(
blank=True,
null=True,
help_text='Date payment was received'
)
# Saudi-specific fields
saudi_id_number = models.CharField(
max_length=10,
blank=True,
null=True,
help_text='Saudi National ID or Iqama number'
)
insurance_card_number = models.CharField(
max_length=50,
blank=True,
null=True,
help_text='Insurance card number'
)
authorization_number = models.CharField(
max_length=50,
blank=True,
null=True,
help_text='Prior authorization number if required'
)
# Denial/Appeal Information
denial_reason = models.TextField(
blank=True,
null=True,
help_text='Reason for denial if applicable'
)
denial_code = models.CharField(
max_length=20,
blank=True,
null=True,
help_text='Insurance denial code'
)
appeal_date = models.DateTimeField(
blank=True,
null=True,
help_text='Date appeal was filed'
)
appeal_reason = models.TextField(
blank=True,
null=True,
help_text='Reason for appeal'
)
# Additional Information
notes = models.TextField(
blank=True,
null=True,
help_text='Additional notes about the claim'
)
attachments = models.JSONField(
default=list,
blank=True,
help_text='List of attached documents'
)
# Tracking
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='created_claims'
)
class Meta:
db_table = 'patients_insurance_claim'
verbose_name = 'Insurance Claim'
verbose_name_plural = 'Insurance Claims'
ordering = ['-created_at']
indexes = [
models.Index(fields=['claim_number']),
models.Index(fields=['patient', 'service_date']),
models.Index(fields=['status', 'priority']),
models.Index(fields=['submitted_date']),
models.Index(fields=['insurance_info']),
]
def __str__(self):
return f"Claim {self.claim_number} - {self.patient.get_full_name()}"
@property
def is_approved(self):
"""Check if claim is approved."""
return self.status in ['APPROVED', 'PARTIALLY_APPROVED', 'PAID']
@property
def is_denied(self):
"""Check if claim is denied."""
return self.status == 'DENIED'
@property
def is_paid(self):
"""Check if claim is paid."""
return self.status == 'PAID'
@property
def days_since_submission(self):
"""Calculate days since submission."""
if self.submitted_date:
return (timezone.now() - self.submitted_date).days
return None
@property
def processing_time_days(self):
"""Calculate processing time in days."""
if self.submitted_date and self.processed_date:
return (self.processed_date - self.submitted_date).days
return None
@property
def approval_percentage(self):
"""Calculate approval percentage."""
if self.billed_amount > 0:
return (self.approved_amount / self.billed_amount) * 100
return 0
def save(self, *args, **kwargs):
# Generate claim number if not provided
if not self.claim_number:
import random
from datetime import datetime
year = datetime.now().year
random_num = random.randint(100000, 999999)
self.claim_number = f"CLM{year}{random_num}"
# Auto-set dates based on status changes
if self.status == 'SUBMITTED' and not self.submitted_date:
self.submitted_date = timezone.now()
elif self.status in ['APPROVED', 'PARTIALLY_APPROVED', 'DENIED'] and not self.processed_date:
self.processed_date = timezone.now()
elif self.status == 'PAID' and not self.payment_date:
self.payment_date = timezone.now()
super().save(*args, **kwargs)
class ClaimDocument(models.Model):
"""
Documents attached to insurance claims.
"""
DOCUMENT_TYPE_CHOICES = [
('MEDICAL_REPORT', 'Medical Report'),
('LAB_RESULT', 'Laboratory Result'),
('RADIOLOGY_REPORT', 'Radiology Report'),
('PRESCRIPTION', 'Prescription'),
('INVOICE', 'Invoice'),
('RECEIPT', 'Receipt'),
('AUTHORIZATION', 'Prior Authorization'),
('REFERRAL', 'Referral Letter'),
('DISCHARGE_SUMMARY', 'Discharge Summary'),
('OPERATIVE_REPORT', 'Operative Report'),
('PATHOLOGY_REPORT', 'Pathology Report'),
('INSURANCE_CARD', 'Insurance Card Copy'),
('ID_COPY', 'ID Copy'),
('OTHER', 'Other'),
]
claim = models.ForeignKey(
InsuranceClaim,
on_delete=models.CASCADE,
related_name='documents'
)
document_type = models.CharField(
max_length=20,
choices=DOCUMENT_TYPE_CHOICES,
help_text='Type of document'
)
title = models.CharField(
max_length=200,
help_text='Document title'
)
description = models.TextField(
blank=True,
null=True,
help_text='Document description'
)
file_path = models.CharField(
max_length=500,
help_text='Path to the document file'
)
file_size = models.PositiveIntegerField(
help_text='File size in bytes'
)
mime_type = models.CharField(
max_length=100,
help_text='MIME type of the file'
)
uploaded_at = models.DateTimeField(auto_now_add=True)
uploaded_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True
)
class Meta:
db_table = 'patients_claim_document'
verbose_name = 'Claim Document'
verbose_name_plural = 'Claim Documents'
ordering = ['-uploaded_at']
def __str__(self):
return f"{self.title} - {self.claim.claim_number}"
class ClaimStatusHistory(models.Model):
"""
Track status changes for insurance claims.
"""
claim = models.ForeignKey(
InsuranceClaim,
on_delete=models.CASCADE,
related_name='status_history'
)
from_status = models.CharField(
max_length=20,
choices=InsuranceClaim.STATUS_CHOICES,
blank=True,
null=True,
help_text='Previous status'
)
to_status = models.CharField(
max_length=20,
choices=InsuranceClaim.STATUS_CHOICES,
help_text='New status'
)
reason = models.TextField(
blank=True,
null=True,
help_text='Reason for status change'
)
notes = models.TextField(
blank=True,
null=True,
help_text='Additional notes'
)
changed_at = models.DateTimeField(auto_now_add=True)
changed_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True
)
class Meta:
db_table = 'patients_claim_status_history'
verbose_name = 'Claim Status History'
verbose_name_plural = 'Claim Status Histories'
ordering = ['-changed_at']
def __str__(self):
return f"{self.claim.claim_number}: {self.from_status}{self.to_status}"
class ConsentTemplate(models.Model): class ConsentTemplate(models.Model):
""" """
Templates for consent forms. Templates for consent forms.
""" """
CATEGORY_CHOICES = [
('TREATMENT', 'Treatment Consent'),
('PROCEDURE', 'Procedure Consent'),
('SURGERY', 'Surgical Consent'),
('ANESTHESIA', 'Anesthesia Consent'),
('RESEARCH', 'Research Consent'),
('PRIVACY', 'Privacy Consent'),
('FINANCIAL', 'Financial Consent'),
('ADMISSION', 'Admission Consent'),
('DISCHARGE', 'Discharge Consent'),
('OTHER', 'Other'),
]
# Tenant relationship # Tenant relationship
tenant = models.ForeignKey( tenant = models.ForeignKey(
@ -838,17 +1348,7 @@ class ConsentTemplate(models.Model):
) )
category = models.CharField( category = models.CharField(
max_length=50, max_length=50,
choices=[ choices=CATEGORY_CHOICES,
('TREATMENT', 'Treatment Consent'),
('PROCEDURE', 'Procedure Consent'),
('SURGERY', 'Surgical Consent'),
('ANESTHESIA', 'Anesthesia Consent'),
('RESEARCH', 'Research Consent'),
('PRIVACY', 'Privacy Consent'),
('FINANCIAL', 'Financial Consent'),
('DISCHARGE', 'Discharge Consent'),
('OTHER', 'Other'),
],
help_text='Consent category' help_text='Consent category'
) )
@ -921,7 +1421,13 @@ class ConsentForm(models.Model):
""" """
Patient consent forms. Patient consent forms.
""" """
STATUS_CHOICES = [
('PENDING', 'Pending'),
('SIGNED', 'Signed'),
('DECLINED', 'Declined'),
('EXPIRED', 'Expired'),
('REVOKED', 'Revoked'),
]
# Patient relationship # Patient relationship
patient = models.ForeignKey( patient = models.ForeignKey(
PatientProfile, PatientProfile,
@ -947,13 +1453,7 @@ class ConsentForm(models.Model):
# Status # Status
status = models.CharField( status = models.CharField(
max_length=20, max_length=20,
choices=[ choices=STATUS_CHOICES,
('PENDING', 'Pending'),
('SIGNED', 'Signed'),
('DECLINED', 'Declined'),
('EXPIRED', 'Expired'),
('REVOKED', 'Revoked'),
],
default='PENDING', default='PENDING',
help_text='Consent status' help_text='Consent status'
) )
@ -1136,6 +1636,24 @@ class PatientNote(models.Model):
""" """
General notes and comments about patients. General notes and comments about patients.
""" """
CATEGORY_CHOICES = [
('GENERAL', 'General'),
('ADMINISTRATIVE', 'Administrative'),
('CLINICAL', 'Clinical'),
('BILLING', 'Billing'),
('INSURANCE', 'Insurance'),
('SOCIAL', 'Social'),
('DISCHARGE', 'Discharge Planning'),
('FOLLOW_UP', 'Follow-up'),
('ALERT', 'Alert'),
('OTHER', 'Other'),
]
PRIORITY_CHOICES = [
('LOW', 'Low'),
('NORMAL', 'Normal'),
('HIGH', 'High'),
('URGENT', 'Urgent'),
]
# Patient relationship # Patient relationship
patient = models.ForeignKey( patient = models.ForeignKey(
@ -1164,18 +1682,7 @@ class PatientNote(models.Model):
# Category # Category
category = models.CharField( category = models.CharField(
max_length=50, max_length=50,
choices=[ choices=CATEGORY_CHOICES,
('GENERAL', 'General'),
('ADMINISTRATIVE', 'Administrative'),
('CLINICAL', 'Clinical'),
('BILLING', 'Billing'),
('INSURANCE', 'Insurance'),
('SOCIAL', 'Social'),
('DISCHARGE', 'Discharge Planning'),
('FOLLOW_UP', 'Follow-up'),
('ALERT', 'Alert'),
('OTHER', 'Other'),
],
default='GENERAL', default='GENERAL',
help_text='Note category' help_text='Note category'
) )
@ -1183,12 +1690,7 @@ class PatientNote(models.Model):
# Priority # Priority
priority = models.CharField( priority = models.CharField(
max_length=20, max_length=20,
choices=[ choices=PRIORITY_CHOICES,
('LOW', 'Low'),
('NORMAL', 'Normal'),
('HIGH', 'High'),
('URGENT', 'Urgent'),
],
default='NORMAL', default='NORMAL',
help_text='Note priority' help_text='Note priority'
) )

View File

@ -14,14 +14,14 @@ urlpatterns = [
path('register/', views.PatientCreateView.as_view(), name='patient_registration'), path('register/', views.PatientCreateView.as_view(), name='patient_registration'),
path('update/<int:pk>/', views.PatientUpdateView.as_view(), name='patient_update'), path('update/<int:pk>/', views.PatientUpdateView.as_view(), name='patient_update'),
path('delete/<int:pk>/', views.PatientDeleteView.as_view(), name='patient_delete'), path('delete/<int:pk>/', views.PatientDeleteView.as_view(), name='patient_delete'),
path('consents/', views.ConsentFormListView.as_view(), name='consent_management'), path('consents/', views.ConsentManagementView.as_view(), name='consent_management'),
path('consent/<int:pk>/', views.ConsentFormDetailView.as_view(), name='consent_management_detail'), path('consent/<int:pk>/', views.ConsentFormDetailView.as_view(), name='consent_management_detail'),
path('emergency-contacts/', views.EmergencyContactListView.as_view(), name='emergency_contact_management'), path('emergency-contacts/', views.EmergencyContactListView.as_view(), name='emergency_contact_management'),
path('emergency-contact/<int:pk>/', views.EmergencyContactDetailView.as_view(), name='emergency_contact_management_detail'), path('emergency-contact/<int:pk>/', views.EmergencyContactDetailView.as_view(), name='emergency_contact_management_detail'),
path('emergency-contacts/delete/<int:pk>/', views.EmergencyContactDeleteView.as_view(), name='emergency_contact_delete'), path('emergency-contacts/delete/<int:pk>/', views.EmergencyContactDeleteView.as_view(), name='emergency_contact_delete'),
path('emergency-contacts/update/<int:pk>/', views.EmergencyContactUpdateView.as_view(), name='emergency_contact_update'), path('emergency-contacts/update/<int:pk>/', views.EmergencyContactUpdateView.as_view(), name='emergency_contact_update'),
path('emergency-contacts/create/<int:pk>/', views.EmergencyContactCreateView.as_view(), name='emergency_contact_create'), path('emergency-contacts/create/<int:pk>/', views.EmergencyContactCreateView.as_view(), name='emergency_contact_create'),
path('insurance-info/<int:pk>/', views.InsuranceInfoListView.as_view(), name='insurance_list'), path('insurance-info/', views.InsuranceInfoListView.as_view(), name='insurance_list'),
path('insurance-info/<int:pk>/', views.InsuranceInfoDetailView.as_view(), name='insurance_detail'), path('insurance-info/<int:pk>/', views.InsuranceInfoDetailView.as_view(), name='insurance_detail'),
path('insurance-info/delete/<int:pk>/', views.InsuranceInfoDeleteView.as_view(), name='insurance_delete'), path('insurance-info/delete/<int:pk>/', views.InsuranceInfoDeleteView.as_view(), name='insurance_delete'),
path('insurance-info/update/<int:pk>/', views.InsuranceInfoUpdateView.as_view(), name='insurance_update'), path('insurance-info/update/<int:pk>/', views.InsuranceInfoUpdateView.as_view(), name='insurance_update'),
@ -40,10 +40,33 @@ urlpatterns = [
path('emergency-contacts/<int:patient_id>/', views.emergency_contacts_list, name='emergency_contacts_list'), path('emergency-contacts/<int:patient_id>/', views.emergency_contacts_list, name='emergency_contacts_list'),
path('insurance-info/<int:patient_id>/', views.insurance_info_list, name='insurance_info_list'), path('insurance-info/<int:patient_id>/', views.insurance_info_list, name='insurance_info_list'),
path('consent-forms/<int:patient_id>/', views.consent_forms_list, name='consent_forms_list'), path('consent-forms/<int:patient_id>/', views.consent_forms_list, name='consent_forms_list'),
path('consent-forms/detail/<int:pk>/', views.ConsentFormDetailView.as_view(), name='consent_form_detail'),
path('consent-forms/update/<int:pk>/', views.ConsentFormUpdateView.as_view(), name='consent_form_update'),
path('patient-notes/<int:patient_id>/', views.patient_notes_list, name='patient_notes_list'), path('patient-notes/<int:patient_id>/', views.patient_notes_list, name='patient_notes_list'),
path('add-patient-note/<int:patient_id>/', views.add_patient_note, name='add_patient_note'), path('add-patient-note/<int:patient_id>/', views.add_patient_note, name='add_patient_note'),
path('sign-consent/<int:pk>/', views.sign_consent_form, name='sign_consent_form'), path('sign-consent/<int:pk>/', views.sign_consent_form, name='sign_consent_form'),
path('appointments/<int:patient_id>/', views.patient_appointment_list, name='patient_appointments'), path('appointments/<int:patient_id>/', views.patient_appointment_list, name='patient_appointments'),
path('patient-info/<int:pk>/', views.get_patient_info, name='get_patient_info') path('patient-info/<int:pk>/', views.get_patient_info, name='get_patient_info'),
path('verify-insurance/<int:pk>/', views.verify_insurance, name='verify_insurance'),
path('check-eligibility/<int:pk>/', views.check_eligibility, name='check_eligibility'),
path('renew-insurance/<int:pk>/', views.renew_insurance, name='renew_insurance'),
path('bulk-renew-insurance/', views.bulk_renew_insurance, name='bulk_renew_insurance'),
path('insurance-claims-history/<int:pk>/', views.insurance_claims_history, name='insurance_claims_history'),
path('check-primary-insurance/', views.check_primary_insurance, name='check_primary_insurance'),
path('validate-policy-number/', views.validate_policy_number, name='validate_policy_number'),
path('save-insurance-draft/', views.save_insurance_draft, name='save_insurance_draft'),
path('verify-with-provider/', views.verify_with_provider, name='verify_with_provider'),
# Insurance Claims URLs
path('claims/', views.insurance_claims_list, name='insurance_claims_list'),
path('claims/dashboard/', views.claims_dashboard, name='claims_dashboard'),
path('claims/new/', views.insurance_claim_form, name='insurance_claim_create'),
path('claims/<int:claim_id>/', views.insurance_claim_detail, name='insurance_claim_detail'),
path('claims/<int:claim_id>/edit/', views.insurance_claim_form, name='insurance_claim_edit'),
path('claims/<int:claim_id>/delete/', views.insurance_claim_delete, name='insurance_claim_delete'),
path('claims/<int:claim_id>/update-status/', views.update_claim_status, name='update_claim_status'),
path('claims/bulk-actions/', views.bulk_claim_actions, name='bulk_claim_actions'),
path('patient/<int:patient_id>/insurance/', views.get_patient_insurance, name='get_patient_insurance'),
] ]

File diff suppressed because it is too large Load Diff

BIN
templates/.DS_Store vendored

Binary file not shown.

BIN
templates/blood_bank/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,839 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Crossmatch Test - {{ blood_unit.unit_number }} & {{ patient.full_name }}{% endblock %}
{% block extra_css %}
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
<style>
.form-section {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
border-left: 4px solid #007bff;
}
.form-section h5 {
color: #007bff;
margin-bottom: 15px;
}
.required-field {
color: #dc3545;
}
.patient-info {
background: #d4edda;
border-left: 4px solid #28a745;
}
.unit-info {
background: #d1ecf1;
border-left: 4px solid #17a2b8;
}
.compatibility-info {
background: #fff3cd;
border-left: 4px solid #ffc107;
}
.crossmatch-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 15px;
}
.test-panel {
background: white;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 15px;
}
.test-panel h6 {
color: #495057;
border-bottom: 1px solid #dee2e6;
padding-bottom: 10px;
margin-bottom: 15px;
}
.test-result {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
}
.result-indicator {
width: 20px;
height: 20px;
border-radius: 50%;
display: inline-block;
}
.compatible { background-color: #28a745; }
.incompatible { background-color: #dc3545; }
.pending { background-color: #6c757d; }
.weak { background-color: #ffc107; }
.temperature-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
margin-top: 15px;
}
.temperature-test {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 15px;
text-align: center;
}
.compatibility-summary {
background: #e2e3e5;
border-radius: 8px;
padding: 15px;
margin-top: 15px;
}
.safety-warning {
background: #f8d7da;
border: 2px solid #dc3545;
border-radius: 8px;
padding: 15px;
margin-top: 15px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.8; }
100% { opacity: 1; }
}
</style>
{% endblock %}
{% block content -->
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'blood_bank:dashboard' %}">Blood Bank</a></li>
<li class="breadcrumb-item"><a href="{% url 'blood_bank:blood_unit_list' %}">Blood Units</a></li>
<li class="breadcrumb-item"><a href="{% url 'blood_bank:blood_unit_detail' blood_unit.id %}">{{ blood_unit.unit_number }}</a></li>
<li class="breadcrumb-item active">Crossmatch</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">Crossmatch Test <small>compatibility verification</small></h1>
<!-- END page-header -->
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fa fa-exchange-alt"></i> Crossmatch Compatibility Test
</h4>
<div class="panel-heading-btn">
<span class="badge bg-info">Unit: {{ blood_unit.unit_number }}</span>
<span class="badge bg-success">Patient: {{ patient.patient_id }}</span>
</div>
</div>
<div class="panel-body">
<form method="post" id="crossmatchForm">
{% csrf_token %}
<!-- BEGIN patient information -->
<div class="form-section patient-info">
<h5><i class="fa fa-user-injured"></i> Patient Information</h5>
<div class="row">
<div class="col-md-6">
<table class="table table-borderless mb-0">
<tr>
<td class="fw-bold">Name:</td>
<td>{{ patient.full_name }}</td>
</tr>
<tr>
<td class="fw-bold">Patient ID:</td>
<td>{{ patient.patient_id }}</td>
</tr>
<tr>
<td class="fw-bold">Blood Group:</td>
<td>
<span class="badge bg-primary">{{ patient.blood_group.display_name|default:"Unknown" }}</span>
</td>
</tr>
<tr>
<td class="fw-bold">Age:</td>
<td>{{ patient.age }} years</td>
</tr>
</table>
</div>
<div class="col-md-6">
<table class="table table-borderless mb-0">
<tr>
<td class="fw-bold">Previous Transfusions:</td>
<td>{{ patient.transfusion_history.count }}</td>
</tr>
<tr>
<td class="fw-bold">Pregnancies:</td>
<td>{{ patient.pregnancy_history|default:"N/A" }}</td>
</tr>
<tr>
<td class="fw-bold">Known Antibodies:</td>
<td>{{ patient.known_antibodies|default:"None" }}</td>
</tr>
<tr>
<td class="fw-bold">Last Crossmatch:</td>
<td>{{ patient.last_crossmatch_date|date:"M d, Y"|default:"Never" }}</td>
</tr>
</table>
</div>
</div>
</div>
<!-- END patient information -->
<!-- BEGIN blood unit information -->
<div class="form-section unit-info">
<h5><i class="fa fa-tint"></i> Blood Unit Information</h5>
<div class="row">
<div class="col-md-6">
<table class="table table-borderless mb-0">
<tr>
<td class="fw-bold">Unit Number:</td>
<td>{{ blood_unit.unit_number }}</td>
</tr>
<tr>
<td class="fw-bold">Blood Group:</td>
<td><span class="badge bg-primary">{{ blood_unit.blood_group.display_name }}</span></td>
</tr>
<tr>
<td class="fw-bold">Component:</td>
<td>{{ blood_unit.component.get_name_display }}</td>
</tr>
<tr>
<td class="fw-bold">Volume:</td>
<td>{{ blood_unit.volume_ml }} ml</td>
</tr>
</table>
</div>
<div class="col-md-6">
<table class="table table-borderless mb-0">
<tr>
<td class="fw-bold">Donor:</td>
<td>{{ blood_unit.donor.full_name }}</td>
</tr>
<tr>
<td class="fw-bold">Collection Date:</td>
<td>{{ blood_unit.collection_date|date:"M d, Y" }}</td>
</tr>
<tr>
<td class="fw-bold">Expiry Date:</td>
<td>{{ blood_unit.expiry_date|date:"M d, Y" }}</td>
</tr>
<tr>
<td class="fw-bold">Test Status:</td>
<td>
{% if blood_unit.all_tests_passed %}
<span class="badge bg-success">All Tests Passed</span>
{% else %}
<span class="badge bg-warning">Tests Pending</span>
{% endif %}
</td>
</tr>
</table>
</div>
</div>
</div>
<!-- END blood unit information -->
<!-- BEGIN test information -->
<div class="form-section">
<h5><i class="fa fa-flask"></i> Test Information</h5>
<div class="row">
<div class="col-md-4">
<div class="form-group">
<label for="testDate" class="form-label">
Test Date <span class="required-field">*</span>
</label>
<input type="datetime-local" class="form-control" id="testDate" name="test_date" required>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label for="testedBy" class="form-label">
Tested By <span class="required-field">*</span>
</label>
<select class="form-select" id="testedBy" name="tested_by" required>
<option value="">Select technician...</option>
{% for staff in lab_staff %}
<option value="{{ staff.id }}">{{ staff.get_full_name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label for="testMethod" class="form-label">
Test Method <span class="required-field">*</span>
</label>
<select class="form-select" id="testMethod" name="test_method" required>
<option value="">Select method...</option>
<option value="tube">Tube Method</option>
<option value="gel">Gel Card</option>
<option value="solid_phase">Solid Phase</option>
<option value="automated">Automated System</option>
</select>
</div>
</div>
</div>
</div>
<!-- END test information -->
<!-- BEGIN ABO/Rh compatibility check -->
<div class="form-section compatibility-info">
<h5><i class="fa fa-shield-alt"></i> ABO/Rh Compatibility Check</h5>
<div class="compatibility-summary">
<div class="row">
<div class="col-md-6">
<h6>Patient Blood Group</h6>
<div class="d-flex align-items-center gap-3">
<span class="badge bg-primary fs-5">{{ patient.blood_group.display_name|default:"Unknown" }}</span>
{% if patient.blood_group.display_name == "Unknown" %}
<span class="text-danger">⚠️ Patient blood group must be confirmed</span>
{% endif %}
</div>
</div>
<div class="col-md-6">
<h6>Donor Blood Group</h6>
<div class="d-flex align-items-center gap-3">
<span class="badge bg-info fs-5">{{ blood_unit.blood_group.display_name }}</span>
<div id="aboCompatibility">
<!-- Will be populated by JavaScript -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- END ABO/Rh compatibility check -->
<!-- BEGIN crossmatch testing -->
<div class="form-section">
<h5><i class="fa fa-microscope"></i> Crossmatch Testing</h5>
<div class="crossmatch-grid">
<!-- Major Crossmatch -->
<div class="test-panel">
<h6><i class="fa fa-arrow-right"></i> Major Crossmatch</h6>
<p class="small text-muted">Patient serum + Donor red cells</p>
<div class="temperature-grid">
<div class="temperature-test">
<label class="form-label">Room Temp</label>
<div class="test-result">
<select class="form-select form-select-sm" name="major_room_temp" required>
<option value="">Result</option>
<option value="compatible">Compatible</option>
<option value="incompatible">Incompatible</option>
<option value="weak">Weak Reaction</option>
</select>
<span class="result-indicator pending" id="major_room_temp_indicator"></span>
</div>
</div>
<div class="temperature-test">
<label class="form-label">37°C</label>
<div class="test-result">
<select class="form-select form-select-sm" name="major_37c" required>
<option value="">Result</option>
<option value="compatible">Compatible</option>
<option value="incompatible">Incompatible</option>
<option value="weak">Weak Reaction</option>
</select>
<span class="result-indicator pending" id="major_37c_indicator"></span>
</div>
</div>
<div class="temperature-test">
<label class="form-label">AHG Phase</label>
<div class="test-result">
<select class="form-select form-select-sm" name="major_ahg" required>
<option value="">Result</option>
<option value="compatible">Compatible</option>
<option value="incompatible">Incompatible</option>
<option value="weak">Weak Reaction</option>
</select>
<span class="result-indicator pending" id="major_ahg_indicator"></span>
</div>
</div>
</div>
</div>
<!-- Minor Crossmatch -->
<div class="test-panel">
<h6><i class="fa fa-arrow-left"></i> Minor Crossmatch</h6>
<p class="small text-muted">Donor serum + Patient red cells</p>
<div class="temperature-grid">
<div class="temperature-test">
<label class="form-label">Room Temp</label>
<div class="test-result">
<select class="form-select form-select-sm" name="minor_room_temp">
<option value="">Result</option>
<option value="compatible">Compatible</option>
<option value="incompatible">Incompatible</option>
<option value="weak">Weak Reaction</option>
</select>
<span class="result-indicator pending" id="minor_room_temp_indicator"></span>
</div>
</div>
<div class="temperature-test">
<label class="form-label">37°C</label>
<div class="test-result">
<select class="form-select form-select-sm" name="minor_37c">
<option value="">Result</option>
<option value="compatible">Compatible</option>
<option value="incompatible">Incompatible</option>
<option value="weak">Weak Reaction</option>
</select>
<span class="result-indicator pending" id="minor_37c_indicator"></span>
</div>
</div>
<div class="temperature-test">
<label class="form-label">AHG Phase</label>
<div class="test-result">
<select class="form-select form-select-sm" name="minor_ahg">
<option value="">Result</option>
<option value="compatible">Compatible</option>
<option value="incompatible">Incompatible</option>
<option value="weak">Weak Reaction</option>
</select>
<span class="result-indicator pending" id="minor_ahg_indicator"></span>
</div>
</div>
</div>
</div>
<!-- Control Tests -->
<div class="test-panel">
<h6><i class="fa fa-check-circle"></i> Control Tests</h6>
<p class="small text-muted">Quality control verification</p>
<div class="test-result">
<label class="form-label">Positive Control</label>
<select class="form-select form-select-sm" name="positive_control" required>
<option value="">Result</option>
<option value="positive">Positive</option>
<option value="negative">Negative</option>
<option value="weak">Weak</option>
</select>
<span class="result-indicator pending" id="positive_control_indicator"></span>
</div>
<div class="test-result">
<label class="form-label">Negative Control</label>
<select class="form-select form-select-sm" name="negative_control" required>
<option value="">Result</option>
<option value="positive">Positive</option>
<option value="negative">Negative</option>
<option value="weak">Weak</option>
</select>
<span class="result-indicator pending" id="negative_control_indicator"></span>
</div>
<div class="test-result">
<label class="form-label">Auto Control</label>
<select class="form-select form-select-sm" name="auto_control" required>
<option value="">Result</option>
<option value="positive">Positive</option>
<option value="negative">Negative</option>
<option value="weak">Weak</option>
</select>
<span class="result-indicator pending" id="auto_control_indicator"></span>
</div>
</div>
</div>
</div>
<!-- END crossmatch testing -->
<!-- BEGIN antibody screening -->
<div class="form-section">
<h5><i class="fa fa-search"></i> Antibody Screening</h5>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="antibodyScreen" class="form-label">
Antibody Screen Result <span class="required-field">*</span>
</label>
<select class="form-select" id="antibodyScreen" name="antibody_screen" required>
<option value="">Select result...</option>
<option value="negative">Negative</option>
<option value="positive">Positive</option>
<option value="indeterminate">Indeterminate</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="antibodyIdentification" class="form-label">
Antibody Identification
</label>
<input type="text" class="form-control" id="antibodyIdentification" name="antibody_identification"
placeholder="If positive, specify antibodies identified">
</div>
</div>
</div>
</div>
<!-- END antibody screening -->
<!-- BEGIN test notes -->
<div class="form-section">
<h5><i class="fa fa-clipboard"></i> Test Notes & Observations</h5>
<div class="row">
<div class="col-md-12">
<div class="form-group">
<label for="testNotes" class="form-label">Test Notes</label>
<textarea class="form-control" id="testNotes" name="test_notes" rows="4"
placeholder="Record any observations, reaction strengths, or additional notes..."></textarea>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="reagentLots" class="form-label">Reagent Lot Numbers</label>
<input type="text" class="form-control" id="reagentLots" name="reagent_lots"
placeholder="AHG, enhancement media lot numbers">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="equipmentUsed" class="form-label">Equipment Used</label>
<input type="text" class="form-control" id="equipmentUsed" name="equipment_used"
placeholder="Centrifuge, incubator details">
</div>
</div>
</div>
</div>
<!-- END test notes -->
<!-- BEGIN final result -->
<div class="form-section">
<h5><i class="fa fa-clipboard-check"></i> Final Crossmatch Result</h5>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="finalResult" class="form-label">
Overall Compatibility <span class="required-field">*</span>
</label>
<select class="form-select" id="finalResult" name="final_result" required>
<option value="">Select final result...</option>
<option value="compatible">Compatible</option>
<option value="incompatible">Incompatible</option>
<option value="conditional">Conditional (with restrictions)</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="restrictions" class="form-label">Restrictions/Conditions</label>
<input type="text" class="form-control" id="restrictions" name="restrictions"
placeholder="Any special conditions or restrictions">
</div>
</div>
</div>
<!-- Result summary -->
<div id="resultSummary" class="mt-3">
<!-- Will be populated by JavaScript -->
</div>
</div>
<!-- END final result -->
<!-- BEGIN form actions -->
<div class="d-flex justify-content-between mt-4">
<a href="{% url 'blood_bank:blood_unit_detail' blood_unit.id %}" class="btn btn-secondary">
<i class="fa fa-arrow-left"></i> Cancel
</a>
<div>
<button type="button" class="btn btn-info" onclick="validateCrossmatch()">
<i class="fa fa-check"></i> Validate Results
</button>
<button type="submit" class="btn btn-primary" id="submitBtn" disabled>
<i class="fa fa-save"></i> Save Crossmatch Results
</button>
</div>
</div>
<!-- END form actions -->
</form>
</div>
</div>
<!-- END panel -->
{% endblock %}
{% block extra_js %}
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
<script>
$(document).ready(function() {
// Initialize Select2
$('#testedBy, #testMethod').select2({
theme: 'bootstrap-5'
});
// Set default test date to now
var now = new Date();
$('#testDate').val(now.toISOString().slice(0, 16));
// Check ABO compatibility on load
checkABOCompatibility();
// Update result indicators when test results change
$('select[name$="_temp"], select[name$="_37c"], select[name$="_ahg"], select[name$="_control"]').on('change', function() {
updateResultIndicator(this);
updateResultSummary();
validateCrossmatch();
});
$('#finalResult').on('change', function() {
updateResultSummary();
validateCrossmatch();
});
// Form validation
$('#crossmatchForm').on('submit', function(e) {
if (!validateCrossmatch()) {
e.preventDefault();
}
});
});
function checkABOCompatibility() {
var patientBloodGroup = "{{ patient.blood_group.display_name|default:'Unknown' }}";
var donorBloodGroup = "{{ blood_unit.blood_group.display_name }}";
if (patientBloodGroup === "Unknown") {
$('#aboCompatibility').html('<span class="text-danger">⚠️ Patient blood group unknown</span>');
return;
}
// Simple ABO compatibility check
var compatible = false;
var message = "";
if (patientBloodGroup === donorBloodGroup) {
compatible = true;
message = "✅ Identical blood groups";
} else if (donorBloodGroup === "O-") {
compatible = true;
message = "✅ Universal donor";
} else if (patientBloodGroup === "AB+" && donorBloodGroup.includes("AB")) {
compatible = true;
message = "✅ Compatible";
} else {
compatible = false;
message = "⚠️ Potential incompatibility";
}
var alertClass = compatible ? 'text-success' : 'text-warning';
$('#aboCompatibility').html('<span class="' + alertClass + '">' + message + '</span>');
}
function updateResultIndicator(selectElement) {
var result = $(selectElement).val();
var indicatorId = $(selectElement).attr('name') + '_indicator';
var indicator = $('#' + indicatorId);
indicator.removeClass('compatible incompatible pending weak');
switch(result) {
case 'compatible':
case 'negative':
indicator.addClass('compatible');
break;
case 'incompatible':
case 'positive':
indicator.addClass('incompatible');
break;
case 'weak':
indicator.addClass('weak');
break;
default:
indicator.addClass('pending');
}
}
function updateResultSummary() {
var majorResults = ['major_room_temp', 'major_37c', 'major_ahg'];
var minorResults = ['minor_room_temp', 'minor_37c', 'minor_ahg'];
var controlResults = ['positive_control', 'negative_control', 'auto_control'];
var majorCompatible = majorResults.every(function(name) {
var value = $('select[name="' + name + '"]').val();
return value === 'compatible';
});
var majorIncompatible = majorResults.some(function(name) {
var value = $('select[name="' + name + '"]').val();
return value === 'incompatible';
});
var controlsValid =
$('select[name="positive_control"]').val() === 'positive' &&
$('select[name="negative_control"]').val() === 'negative' &&
$('select[name="auto_control"]').val() === 'negative';
var finalResult = $('#finalResult').val();
var summaryHtml = '<div class="row">';
// Major crossmatch summary
if (majorCompatible) {
summaryHtml += '<div class="col-md-4"><div class="alert alert-success"><strong>Major Crossmatch:</strong> Compatible</div></div>';
} else if (majorIncompatible) {
summaryHtml += '<div class="col-md-4"><div class="alert alert-danger"><strong>Major Crossmatch:</strong> Incompatible</div></div>';
} else {
summaryHtml += '<div class="col-md-4"><div class="alert alert-info"><strong>Major Crossmatch:</strong> Pending</div></div>';
}
// Controls summary
if (controlsValid) {
summaryHtml += '<div class="col-md-4"><div class="alert alert-success"><strong>Controls:</strong> Valid</div></div>';
} else {
summaryHtml += '<div class="col-md-4"><div class="alert alert-warning"><strong>Controls:</strong> Check Required</div></div>';
}
// Final result
if (finalResult === 'compatible') {
summaryHtml += '<div class="col-md-4"><div class="alert alert-success"><strong>Final Result:</strong> Compatible</div></div>';
} else if (finalResult === 'incompatible') {
summaryHtml += '<div class="col-md-4"><div class="alert alert-danger"><strong>Final Result:</strong> Incompatible</div></div>';
} else if (finalResult === 'conditional') {
summaryHtml += '<div class="col-md-4"><div class="alert alert-warning"><strong>Final Result:</strong> Conditional</div></div>';
} else {
summaryHtml += '<div class="col-md-4"><div class="alert alert-info"><strong>Final Result:</strong> Pending</div></div>';
}
summaryHtml += '</div>';
// Add safety warnings
if (majorIncompatible || finalResult === 'incompatible') {
summaryHtml += '<div class="safety-warning">';
summaryHtml += '<h6><i class="fa fa-exclamation-triangle"></i> INCOMPATIBLE CROSSMATCH DETECTED</h6>';
summaryHtml += '<p><strong>This blood unit MUST NOT be transfused to this patient.</strong></p>';
summaryHtml += '<p>Immediate investigation and alternative blood unit selection required.</p>';
summaryHtml += '</div>';
}
$('#resultSummary').html(summaryHtml);
}
function validateCrossmatch() {
var errors = [];
// Check required fields
if (!$('#testDate').val()) {
errors.push('Please enter test date');
}
if (!$('#testedBy').val()) {
errors.push('Please select who performed the test');
}
if (!$('#testMethod').val()) {
errors.push('Please select test method');
}
// Check major crossmatch results
var majorResults = ['major_room_temp', 'major_37c', 'major_ahg'];
var missingMajor = majorResults.filter(function(name) {
return !$('select[name="' + name + '"]').val();
});
if (missingMajor.length > 0) {
errors.push('Please complete all major crossmatch results');
}
// Check control results
var controlResults = ['positive_control', 'negative_control', 'auto_control'];
var missingControls = controlResults.filter(function(name) {
return !$('select[name="' + name + '"]').val();
});
if (missingControls.length > 0) {
errors.push('Please complete all control test results');
}
// Check antibody screen
if (!$('#antibodyScreen').val()) {
errors.push('Please enter antibody screen result');
}
// Check final result
if (!$('#finalResult').val()) {
errors.push('Please select final compatibility result');
}
// Validate control results
var positiveControl = $('select[name="positive_control"]').val();
var negativeControl = $('select[name="negative_control"]').val();
var autoControl = $('select[name="auto_control"]').val();
if (positiveControl === 'negative') {
errors.push('Positive control should be positive - check reagents');
}
if (negativeControl === 'positive') {
errors.push('Negative control should be negative - check for contamination');
}
if (autoControl === 'positive') {
errors.push('Auto control positive - patient may have autoantibodies');
}
// Enable/disable submit button
$('#submitBtn').prop('disabled', errors.length > 0);
if (errors.length > 0) {
if (errors.length < 8) { // Only show errors if validation was explicitly requested
Swal.fire({
icon: 'error',
title: 'Validation Errors',
html: '<ul class="text-start"><li>' + errors.join('</li><li>') + '</li></ul>',
confirmButtonText: 'OK'
});
}
return false;
}
// Check for incompatibility
var finalResult = $('#finalResult').val();
if (finalResult === 'incompatible') {
Swal.fire({
icon: 'error',
title: 'Incompatible Crossmatch',
html: '<strong>This crossmatch shows incompatibility.</strong><br><br>This blood unit must not be used for this patient. Please select an alternative unit.',
confirmButtonText: 'I Understand'
});
} else if (finalResult === 'compatible') {
Swal.fire({
icon: 'success',
title: 'Compatible Crossmatch',
text: 'Crossmatch is compatible. Blood unit cleared for transfusion.',
timer: 1500,
showConfirmButton: false
});
}
return true;
}
</script>
{% endblock %}

View File

@ -0,0 +1,364 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Blood Bank Dashboard{% endblock %}
{% block extra_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" />
{% endblock %}
{% block content %}
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item active">Blood Bank Dashboard</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">Blood Bank Dashboard <small>overview of blood bank operations</small></h1>
<!-- END page-header -->
<!-- BEGIN row -->
<div class="row">
<!-- BEGIN col-3 -->
<div class="col-xl-3 col-md-6">
<div class="widget widget-stats bg-blue">
<div class="stats-icon"><i class="fa fa-users"></i></div>
<div class="stats-info">
<h4>ACTIVE DONORS</h4>
<p>{{ total_donors }}</p>
</div>
<div class="stats-link">
<a href="{% url 'blood_bank:donor_list' %}">View Detail <i class="fa fa-arrow-alt-circle-right"></i></a>
</div>
</div>
</div>
<!-- END col-3 -->
<!-- BEGIN col-3 -->
<div class="col-xl-3 col-md-6">
<div class="widget widget-stats bg-info">
<div class="stats-icon"><i class="fa fa-tint"></i></div>
<div class="stats-info">
<h4>AVAILABLE UNITS</h4>
<p>{{ total_units }}</p>
</div>
<div class="stats-link">
<a href="{% url 'blood_bank:blood_unit_list' %}">View Detail <i class="fa fa-arrow-alt-circle-right"></i></a>
</div>
</div>
</div>
<!-- END col-3 -->
<!-- BEGIN col-3 -->
<div class="col-xl-3 col-md-6">
<div class="widget widget-stats bg-orange">
<div class="stats-icon"><i class="fa fa-clipboard-list"></i></div>
<div class="stats-info">
<h4>PENDING REQUESTS</h4>
<p>{{ pending_requests }}</p>
</div>
<div class="stats-link">
<a href="{% url 'blood_bank:blood_request_list' %}">View Detail <i class="fa fa-arrow-alt-circle-right"></i></a>
</div>
</div>
</div>
<!-- END col-3 -->
<!-- BEGIN col-3 -->
<div class="col-xl-3 col-md-6">
<div class="widget widget-stats bg-red">
<div class="stats-icon"><i class="fa fa-exclamation-triangle"></i></div>
<div class="stats-info">
<h4>EXPIRING SOON</h4>
<p>{{ expiring_soon }}</p>
</div>
<div class="stats-link">
<a href="{% url 'blood_bank:inventory_overview' %}">View Detail <i class="fa fa-arrow-alt-circle-right"></i></a>
</div>
</div>
</div>
<!-- END col-3 -->
</div>
<!-- END row -->
<!-- BEGIN row -->
<div class="row">
<!-- BEGIN col-8 -->
<div class="col-xl-8">
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">Blood Group Distribution</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
</div>
</div>
<div class="panel-body">
<div class="table-responsive">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>Blood Group</th>
<th>Available Units</th>
<th>Percentage</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for stat in blood_group_stats %}
<tr>
<td>
<span class="badge bg-primary">
{{ stat.blood_group__abo_type }}{% if stat.blood_group__rh_factor == 'positive' %}+{% else %}-{% endif %}
</span>
</td>
<td>{{ stat.count }}</td>
<td>
{% widthratio stat.count total_units 100 %}%
</td>
<td>
{% if stat.count >= 10 %}
<span class="badge bg-success">Adequate</span>
{% elif stat.count >= 5 %}
<span class="badge bg-warning">Low</span>
{% else %}
<span class="badge bg-danger">Critical</span>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center">No blood units available</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- END panel -->
</div>
<!-- END col-8 -->
<!-- BEGIN col-4 -->
<div class="col-xl-4">
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">Quick Stats</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
</div>
</div>
<div class="panel-body">
<div class="list-group">
<div class="list-group-item d-flex justify-content-between align-items-center">
<div>
<i class="fa fa-tint text-info me-2"></i>
Recent Donations (7 days)
</div>
<span class="badge bg-info rounded-pill">{{ recent_donations }}</span>
</div>
<div class="list-group-item d-flex justify-content-between align-items-center">
<div>
<i class="fa fa-heartbeat text-success me-2"></i>
Active Transfusions
</div>
<span class="badge bg-success rounded-pill">{{ active_transfusions }}</span>
</div>
<div class="list-group-item d-flex justify-content-between align-items-center">
<div>
<i class="fa fa-clock text-warning me-2"></i>
Expiring This Week
</div>
<span class="badge bg-warning rounded-pill">{{ expiring_soon }}</span>
</div>
</div>
</div>
</div>
<!-- END panel -->
</div>
<!-- END col-4 -->
</div>
<!-- END row -->
<!-- BEGIN row -->
<div class="row">
<!-- BEGIN col-6 -->
<div class="col-xl-6">
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">Recent Blood Units</h4>
<div class="panel-heading-btn">
<a href="{% url 'blood_bank:blood_unit_list' %}" class="btn btn-xs btn-primary">View All</a>
</div>
</div>
<div class="panel-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Unit Number</th>
<th>Donor</th>
<th>Component</th>
<th>Blood Group</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for unit in recent_units %}
<tr>
<td>
<a href="{% url 'blood_bank:blood_unit_detail' unit.id %}">
{{ unit.unit_number }}
</a>
</td>
<td>{{ unit.donor.full_name }}</td>
<td>{{ unit.component.get_name_display }}</td>
<td>
<span class="badge bg-primary">{{ unit.blood_group.display_name }}</span>
</td>
<td>
{% if unit.status == 'available' %}
<span class="badge bg-success">{{ unit.get_status_display }}</span>
{% elif unit.status == 'expired' %}
<span class="badge bg-danger">{{ unit.get_status_display }}</span>
{% else %}
<span class="badge bg-info">{{ unit.get_status_display }}</span>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center">No recent blood units</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- END panel -->
</div>
<!-- END col-6 -->
<!-- BEGIN col-6 -->
<div class="col-xl-6">
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">Urgent Blood Requests</h4>
<div class="panel-heading-btn">
<a href="{% url 'blood_bank:blood_request_list' %}" class="btn btn-xs btn-primary">View All</a>
</div>
</div>
<div class="panel-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Request #</th>
<th>Patient</th>
<th>Component</th>
<th>Units</th>
<th>Urgency</th>
</tr>
</thead>
<tbody>
{% for request in urgent_requests %}
<tr>
<td>
<a href="{% url 'blood_bank:blood_request_detail' request.id %}">
{{ request.request_number }}
</a>
</td>
<td>{{ request.patient.full_name }}</td>
<td>{{ request.component_requested.get_name_display }}</td>
<td>{{ request.units_requested }}</td>
<td>
<span class="badge bg-danger">{{ request.get_urgency_display }}</span>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center">No urgent requests</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- END panel -->
</div>
<!-- END col-6 -->
</div>
<!-- END row -->
<!-- BEGIN row -->
<div class="row">
<div class="col-xl-12">
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">Quick Actions</h4>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-3 mb-3">
<a href="{% url 'blood_bank:donor_create' %}" class="btn btn-primary btn-lg w-100">
<i class="fa fa-user-plus fa-2x d-block mb-2"></i>
Register Donor
</a>
</div>
<div class="col-md-3 mb-3">
<a href="{% url 'blood_bank:blood_unit_create' %}" class="btn btn-info btn-lg w-100">
<i class="fa fa-tint fa-2x d-block mb-2"></i>
Register Blood Unit
</a>
</div>
<div class="col-md-3 mb-3">
<a href="{% url 'blood_bank:blood_request_create' %}" class="btn btn-warning btn-lg w-100">
<i class="fa fa-clipboard-list fa-2x d-block mb-2"></i>
Create Request
</a>
</div>
<div class="col-md-3 mb-3">
<a href="{% url 'blood_bank:inventory_overview' %}" class="btn btn-success btn-lg w-100">
<i class="fa fa-boxes fa-2x d-block mb-2"></i>
View Inventory
</a>
</div>
</div>
</div>
</div>
<!-- END panel -->
</div>
</div>
<!-- END row -->
{% 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() {
// Auto-refresh dashboard every 5 minutes
setInterval(function() {
location.reload();
}, 300000);
});
</script>
{% endblock %}

View File

@ -0,0 +1,279 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Delete Donor - {{ donor.full_name }}{% endblock %}
{% block content %}
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'blood_bank:dashboard' %}">Blood Bank</a></li>
<li class="breadcrumb-item"><a href="{% url 'blood_bank:donor_list' %}">Donors</a></li>
<li class="breadcrumb-item"><a href="{% url 'blood_bank:donor_detail' donor.id %}">{{ donor.donor_id }}</a></li>
<li class="breadcrumb-item active">Delete</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">Delete Donor <small>{{ donor.full_name }}</small></h1>
<!-- END page-header -->
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fa fa-exclamation-triangle text-danger"></i> Confirm Donor Deletion
</h4>
</div>
<div class="panel-body">
<div class="alert alert-danger">
<h5><i class="fa fa-exclamation-triangle"></i> Warning</h5>
<p>You are about to permanently delete this donor record. This action cannot be undone.</p>
</div>
<!-- BEGIN donor info -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Donor Information</h5>
</div>
<div class="card-body">
<table class="table table-borderless">
<tr>
<td class="fw-bold">Donor ID:</td>
<td>{{ donor.donor_id }}</td>
</tr>
<tr>
<td class="fw-bold">Name:</td>
<td>{{ donor.full_name }}</td>
</tr>
<tr>
<td class="fw-bold">Blood Group:</td>
<td>
<span class="badge bg-primary">{{ donor.blood_group.display_name }}</span>
</td>
</tr>
<tr>
<td class="fw-bold">Phone:</td>
<td>{{ donor.phone }}</td>
</tr>
<tr>
<td class="fw-bold">Registration Date:</td>
<td>{{ donor.registration_date|date:"M d, Y" }}</td>
</tr>
<tr>
<td class="fw-bold">Status:</td>
<td>
<span class="badge bg-{% if donor.status == 'active' %}success{% else %}secondary{% endif %}">
{{ donor.get_status_display }}
</span>
</td>
</tr>
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Impact Assessment</h5>
</div>
<div class="card-body">
<div class="alert alert-info">
<h6><i class="fa fa-info-circle"></i> Related Records</h6>
<ul class="mb-0">
<li><strong>{{ donor.total_donations }}</strong> blood unit(s) collected</li>
<li><strong>{{ donor.blood_units.count }}</strong> blood unit record(s)</li>
{% if donor.blood_units.filter(status='available').exists %}
<li class="text-warning">
<i class="fa fa-exclamation-triangle"></i>
Has active blood units in inventory
</li>
{% endif %}
</ul>
</div>
{% if donor.blood_units.filter(status='available').exists %}
<div class="alert alert-warning">
<h6><i class="fa fa-exclamation-triangle"></i> Active Blood Units</h6>
<p class="mb-0">This donor has blood units currently available in inventory.
Deleting this donor will affect inventory tracking.</p>
</div>
{% endif %}
{% if donor.total_donations > 0 %}
<div class="alert alert-info">
<h6><i class="fa fa-history"></i> Donation History</h6>
<p class="mb-0">This donor has a donation history. Consider marking as
"inactive" instead of deleting to preserve historical data.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- END donor info -->
<!-- BEGIN deletion options -->
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">Deletion Options</h5>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label class="form-label">Reason for Deletion <span class="text-danger">*</span></label>
<select class="form-select" name="deletion_reason" required>
<option value="">Select reason...</option>
<option value="duplicate_record">Duplicate Record</option>
<option value="data_error">Data Entry Error</option>
<option value="donor_request">Donor Request</option>
<option value="privacy_compliance">Privacy Compliance</option>
<option value="other">Other</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Additional Notes</label>
<textarea class="form-control" name="deletion_notes" rows="3"
placeholder="Provide additional details about the deletion..."></textarea>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="archive_data" id="archiveData" checked>
<label class="form-check-label" for="archiveData">
Archive donation history before deletion
</label>
<div class="form-text">Keep anonymized donation records for statistical purposes</div>
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="confirm_deletion" id="confirmDeletion" required>
<label class="form-check-label" for="confirmDeletion">
<strong>I understand that this action cannot be undone</strong>
</label>
</div>
</div>
<!-- BEGIN form actions -->
<div class="d-flex justify-content-between">
<div>
<a href="{% url 'blood_bank:donor_detail' donor.id %}" class="btn btn-secondary">
<i class="fa fa-arrow-left"></i> Cancel
</a>
<a href="{% url 'blood_bank:donor_update' donor.id %}" class="btn btn-warning">
<i class="fa fa-pause"></i> Mark as Inactive Instead
</a>
</div>
<button type="submit" class="btn btn-danger" id="deleteBtn" disabled>
<i class="fa fa-trash"></i> Delete Donor Permanently
</button>
</div>
<!-- END form actions -->
</form>
</div>
</div>
<!-- END deletion options -->
<!-- BEGIN alternative actions -->
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Alternative Actions</h5>
</div>
<div class="card-body">
<p class="text-muted">Consider these alternatives to permanent deletion:</p>
<div class="row">
<div class="col-md-6">
<div class="d-grid">
<a href="{% url 'blood_bank:donor_update' donor.id %}" class="btn btn-outline-warning">
<i class="fa fa-pause"></i> Mark as Inactive
</a>
</div>
<small class="text-muted">Preserves historical data while preventing new donations</small>
</div>
<div class="col-md-6">
<div class="d-grid">
<a href="{% url 'blood_bank:donor_update' donor.id %}" class="btn btn-outline-info">
<i class="fa fa-edit"></i> Update Information
</a>
</div>
<small class="text-muted">Correct any data errors without deletion</small>
</div>
</div>
</div>
</div>
<!-- END alternative actions -->
</div>
</div>
<!-- END panel -->
{% endblock %}
{% block extra_js %}
<script>
$(document).ready(function() {
// Enable delete button only when confirmation is checked
$('#confirmDeletion').on('change', function() {
$('#deleteBtn').prop('disabled', !this.checked);
});
// Form submission confirmation
$('form').on('submit', function(e) {
var reason = $('select[name="deletion_reason"]').val();
if (!reason) {
e.preventDefault();
Swal.fire({
icon: 'error',
title: 'Missing Information',
text: 'Please select a reason for deletion.',
confirmButtonText: 'OK'
});
return;
}
e.preventDefault();
Swal.fire({
title: 'Final Confirmation',
html: `
<div class="text-start">
<p><strong>You are about to permanently delete:</strong></p>
<ul>
<li>Donor: {{ donor.full_name }} ({{ donor.donor_id }})</li>
<li>Reason: ${$('select[name="deletion_reason"] option:selected').text()}</li>
</ul>
<p class="text-danger"><strong>This action cannot be undone!</strong></p>
</div>
`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Yes, Delete Permanently',
cancelButtonText: 'Cancel'
}).then((result) => {
if (result.isConfirmed) {
// Show loading
Swal.fire({
title: 'Deleting Donor...',
text: 'Please wait while we process the deletion.',
allowOutsideClick: false,
didOpen: () => {
Swal.showLoading();
}
});
// Submit the form
this.submit();
}
});
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,330 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Donor Details - {{ donor.full_name }}{% endblock %}
{% block extra_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" />
{% endblock %}
{% block content %}
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'blood_bank:dashboard' %}">Blood Bank</a></li>
<li class="breadcrumb-item"><a href="{% url 'blood_bank:donor_list' %}">Donors</a></li>
<li class="breadcrumb-item active">{{ donor.donor_id }}</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">
Donor Details
<small>{{ donor.full_name }} ({{ donor.donor_id }})</small>
</h1>
<!-- END page-header -->
<!-- BEGIN row -->
<div class="row">
<!-- BEGIN col-4 -->
<div class="col-xl-4">
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">Donor Information</h4>
<div class="panel-heading-btn">
<a href="{% url 'blood_bank:donor_update' donor.id %}" class="btn btn-warning btn-sm">
<i class="fa fa-edit"></i> Edit
</a>
</div>
</div>
<div class="panel-body">
<div class="row mb-3">
<div class="col-12 text-center">
<div class="bg-light rounded p-4 mb-3">
<i class="fa fa-user fa-4x text-muted mb-2"></i>
<h4>{{ donor.full_name }}</h4>
<p class="text-muted mb-0">{{ donor.donor_id }}</p>
</div>
</div>
</div>
<table class="table table-borderless">
<tr>
<td class="fw-bold">Blood Group:</td>
<td>
<span class="badge bg-primary fs-6">{{ donor.blood_group.display_name }}</span>
</td>
</tr>
<tr>
<td class="fw-bold">Age:</td>
<td>{{ donor.age }} years</td>
</tr>
<tr>
<td class="fw-bold">Gender:</td>
<td>{{ donor.get_gender_display }}</td>
</tr>
<tr>
<td class="fw-bold">Weight:</td>
<td>{{ donor.weight }} kg</td>
</tr>
<tr>
<td class="fw-bold">Height:</td>
<td>{{ donor.height }} cm</td>
</tr>
<tr>
<td class="fw-bold">Status:</td>
<td>
{% if donor.status == 'active' %}
<span class="badge bg-success">{{ donor.get_status_display }}</span>
{% elif donor.status == 'deferred' %}
<span class="badge bg-warning">{{ donor.get_status_display }}</span>
{% elif donor.status == 'permanently_deferred' %}
<span class="badge bg-danger">{{ donor.get_status_display }}</span>
{% else %}
<span class="badge bg-secondary">{{ donor.get_status_display }}</span>
{% endif %}
</td>
</tr>
<tr>
<td class="fw-bold">Donor Type:</td>
<td>{{ donor.get_donor_type_display }}</td>
</tr>
</table>
<hr>
<h6 class="fw-bold">Contact Information</h6>
<table class="table table-borderless">
<tr>
<td class="fw-bold">Phone:</td>
<td>{{ donor.phone }}</td>
</tr>
<tr>
<td class="fw-bold">Email:</td>
<td>{{ donor.email|default:"Not provided" }}</td>
</tr>
<tr>
<td class="fw-bold">Address:</td>
<td>{{ donor.address }}</td>
</tr>
</table>
<hr>
<h6 class="fw-bold">Emergency Contact</h6>
<table class="table table-borderless">
<tr>
<td class="fw-bold">Name:</td>
<td>{{ donor.emergency_contact_name }}</td>
</tr>
<tr>
<td class="fw-bold">Phone:</td>
<td>{{ donor.emergency_contact_phone }}</td>
</tr>
</table>
{% if donor.notes %}
<hr>
<h6 class="fw-bold">Notes</h6>
<p class="text-muted">{{ donor.notes }}</p>
{% endif %}
</div>
</div>
<!-- END panel -->
</div>
<!-- END col-4 -->
<!-- BEGIN col-8 -->
<div class="col-xl-8">
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">Donation Statistics</h4>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-3">
<div class="widget widget-stats bg-blue">
<div class="stats-icon"><i class="fa fa-tint"></i></div>
<div class="stats-info">
<h4>TOTAL DONATIONS</h4>
<p>{{ donor.total_donations }}</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="widget widget-stats bg-info">
<div class="stats-icon"><i class="fa fa-calendar"></i></div>
<div class="stats-info">
<h4>LAST DONATION</h4>
<p>
{% if donor.last_donation_date %}
{{ donor.last_donation_date|date:"M d, Y" }}
{% else %}
Never
{% endif %}
</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="widget widget-stats bg-orange">
<div class="stats-icon"><i class="fa fa-clock"></i></div>
<div class="stats-info">
<h4>NEXT ELIGIBLE</h4>
<p>{{ donor.next_eligible_date|date:"M d, Y" }}</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="widget widget-stats {% if donor.is_eligible_for_donation %}bg-green{% else %}bg-red{% endif %}">
<div class="stats-icon">
<i class="fa {% if donor.is_eligible_for_donation %}fa-check{% else %}fa-times{% endif %}"></i>
</div>
<div class="stats-info">
<h4>ELIGIBILITY</h4>
<p>{% if donor.is_eligible_for_donation %}Eligible{% else %}Not Eligible{% endif %}</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- END panel -->
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">Donation History</h4>
<div class="panel-heading-btn">
{% if donor.is_eligible_for_donation %}
<a href="{% url 'blood_bank:donor_eligibility' donor.id %}" class="btn btn-success btn-sm">
<i class="fa fa-tint"></i> New Donation
</a>
{% endif %}
</div>
</div>
<div class="panel-body">
{% if blood_units %}
<div class="table-responsive">
<table id="donationTable" class="table table-striped table-bordered">
<thead>
<tr>
<th>Unit Number</th>
<th>Component</th>
<th>Collection Date</th>
<th>Volume (ml)</th>
<th>Status</th>
<th>Expiry Date</th>
<th>Location</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for unit in blood_units %}
<tr>
<td>
<a href="{% url 'blood_bank:blood_unit_detail' unit.id %}">
{{ unit.unit_number }}
</a>
</td>
<td>{{ unit.component.get_name_display }}</td>
<td>{{ unit.collection_date|date:"M d, Y H:i" }}</td>
<td>{{ unit.volume_ml }}</td>
<td>
{% if unit.status == 'available' %}
<span class="badge bg-success">{{ unit.get_status_display }}</span>
{% elif unit.status == 'expired' %}
<span class="badge bg-danger">{{ unit.get_status_display }}</span>
{% elif unit.status == 'transfused' %}
<span class="badge bg-info">{{ unit.get_status_display }}</span>
{% else %}
<span class="badge bg-warning">{{ unit.get_status_display }}</span>
{% endif %}
</td>
<td>
{{ unit.expiry_date|date:"M d, Y" }}
{% if unit.days_to_expiry <= 3 and unit.status == 'available' %}
<span class="badge bg-danger ms-1">Expiring Soon</span>
{% endif %}
</td>
<td>{{ unit.location }}</td>
<td>
<a href="{% url 'blood_bank:blood_unit_detail' unit.id %}"
class="btn btn-outline-primary btn-sm" title="View Details">
<i class="fa fa-eye"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="fa fa-tint fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No Donation History</h5>
<p class="text-muted">This donor has not made any donations yet.</p>
{% if donor.is_eligible_for_donation %}
<a href="{% url 'blood_bank:donor_eligibility' donor.id %}" class="btn btn-primary">
<i class="fa fa-tint"></i> Start First Donation
</a>
{% endif %}
</div>
{% endif %}
</div>
</div>
<!-- END panel -->
</div>
<!-- END col-8 -->
</div>
<!-- END row -->
<!-- BEGIN action buttons -->
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between">
<a href="{% url 'blood_bank:donor_list' %}" class="btn btn-default">
<i class="fa fa-arrow-left"></i> Back to Donors
</a>
<div>
{% if donor.is_eligible_for_donation %}
<a href="{% url 'blood_bank:donor_eligibility' donor.id %}" class="btn btn-success">
<i class="fa fa-tint"></i> Check Eligibility
</a>
{% endif %}
<a href="{% url 'blood_bank:donor_update' donor.id %}" class="btn btn-warning">
<i class="fa fa-edit"></i> Edit Donor
</a>
<button type="button" class="btn btn-info" onclick="window.print()">
<i class="fa fa-print"></i> Print
</button>
</div>
</div>
</div>
</div>
<!-- END action buttons -->
{% 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() {
$('#donationTable').DataTable({
responsive: true,
pageLength: 10,
order: [[2, 'desc']], // Sort by collection date
columnDefs: [
{ orderable: false, targets: [7] } // Actions column
]
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,449 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Donor Eligibility Check - {{ donor.full_name }}{% endblock %}
{% block extra_css %}
<style>
.eligibility-card {
border: 2px solid #e9ecef;
border-radius: 10px;
transition: all 0.3s ease;
}
.eligibility-card.eligible {
border-color: #28a745;
background-color: #f8fff9;
}
.eligibility-card.not-eligible {
border-color: #dc3545;
background-color: #fff8f8;
}
.question-card {
border: 1px solid #e9ecef;
border-radius: 8px;
margin-bottom: 1rem;
transition: all 0.3s ease;
}
.question-card:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.question-card.answered-yes {
border-color: #28a745;
background-color: #f8fff9;
}
.question-card.answered-no {
border-color: #dc3545;
background-color: #fff8f8;
}
</style>
{% endblock %}
{% block content %}
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'blood_bank:dashboard' %}">Blood Bank</a></li>
<li class="breadcrumb-item"><a href="{% url 'blood_bank:donor_list' %}">Donors</a></li>
<li class="breadcrumb-item"><a href="{% url 'blood_bank:donor_detail' donor.id %}">{{ donor.donor_id }}</a></li>
<li class="breadcrumb-item active">Eligibility Check</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">
Donor Eligibility Check
<small>{{ donor.full_name }} ({{ donor.donor_id }})</small>
</h1>
<!-- END page-header -->
<!-- BEGIN row -->
<div class="row">
<!-- BEGIN col-4 -->
<div class="col-xl-4">
<!-- BEGIN donor info panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">Donor Information</h4>
</div>
<div class="panel-body">
<div class="text-center mb-3">
<i class="fa fa-user fa-3x text-muted mb-2"></i>
<h5>{{ donor.full_name }}</h5>
<p class="text-muted">{{ donor.donor_id }}</p>
</div>
<table class="table table-borderless">
<tr>
<td class="fw-bold">Blood Group:</td>
<td>
<span class="badge bg-primary">{{ donor.blood_group.display_name }}</span>
</td>
</tr>
<tr>
<td class="fw-bold">Age:</td>
<td>{{ donor.age }} years</td>
</tr>
<tr>
<td class="fw-bold">Weight:</td>
<td>{{ donor.weight }} kg</td>
</tr>
<tr>
<td class="fw-bold">Total Donations:</td>
<td>{{ donor.total_donations }}</td>
</tr>
<tr>
<td class="fw-bold">Last Donation:</td>
<td>
{% if donor.last_donation_date %}
{{ donor.last_donation_date|date:"M d, Y" }}
{% else %}
<span class="text-muted">Never</span>
{% endif %}
</td>
</tr>
</table>
</div>
</div>
<!-- END donor info panel -->
<!-- BEGIN eligibility status -->
<div class="eligibility-card p-4 {% if is_eligible %}eligible{% else %}not-eligible{% endif %}">
<div class="text-center">
<i class="fa {% if is_eligible %}fa-check-circle text-success{% else %}fa-times-circle text-danger{% endif %} fa-3x mb-3"></i>
<h4>
{% if is_eligible %}
Eligible for Donation
{% else %}
Not Eligible for Donation
{% endif %}
</h4>
<p class="mb-0">
{% if is_eligible %}
This donor meets the basic eligibility criteria.
{% else %}
Next eligible date: {{ next_eligible_date|date:"M d, Y" }}
{% endif %}
</p>
</div>
</div>
<!-- END eligibility status -->
</div>
<!-- END col-4 -->
<!-- BEGIN col-8 -->
<div class="col-xl-8">
<!-- BEGIN eligibility screening panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">Eligibility Screening Questionnaire</h4>
<div class="panel-heading-btn">
<span class="badge bg-info">Required before donation</span>
</div>
</div>
<div class="panel-body">
<form method="post" id="eligibilityForm">
{% csrf_token %}
<!-- BEGIN basic health questions -->
<div class="mb-4">
<h5 class="text-primary mb-3">
<i class="fa fa-heartbeat"></i> Basic Health Questions
</h5>
<div class="question-card p-3" data-question="feeling_well">
<div class="form-check">
{{ form.feeling_well }}
<label class="form-check-label fw-bold" for="{{ form.feeling_well.id_for_label }}">
{{ form.feeling_well.label }}
</label>
</div>
{% if form.feeling_well.errors %}
<div class="text-danger small mt-1">{{ form.feeling_well.errors.0 }}</div>
{% endif %}
</div>
<div class="question-card p-3" data-question="adequate_sleep">
<div class="form-check">
{{ form.adequate_sleep }}
<label class="form-check-label fw-bold" for="{{ form.adequate_sleep.id_for_label }}">
{{ form.adequate_sleep.label }}
</label>
</div>
{% if form.adequate_sleep.errors %}
<div class="text-danger small mt-1">{{ form.adequate_sleep.errors.0 }}</div>
{% endif %}
</div>
<div class="question-card p-3" data-question="eaten_today">
<div class="form-check">
{{ form.eaten_today }}
<label class="form-check-label fw-bold" for="{{ form.eaten_today.id_for_label }}">
{{ form.eaten_today.label }}
</label>
</div>
{% if form.eaten_today.errors %}
<div class="text-danger small mt-1">{{ form.eaten_today.errors.0 }}</div>
{% endif %}
</div>
</div>
<!-- END basic health questions -->
<!-- BEGIN medical history -->
<div class="mb-4">
<h5 class="text-primary mb-3">
<i class="fa fa-stethoscope"></i> Medical History
</h5>
<div class="question-card p-3" data-question="recent_illness">
<div class="form-check">
{{ form.recent_illness }}
<label class="form-check-label fw-bold" for="{{ form.recent_illness.id_for_label }}">
{{ form.recent_illness.label }}
</label>
</div>
<small class="text-muted">Including cold, flu, fever, or any infection</small>
</div>
<div class="question-card p-3" data-question="medications">
<div class="form-check">
{{ form.medications }}
<label class="form-check-label fw-bold" for="{{ form.medications.id_for_label }}">
{{ form.medications.label }}
</label>
</div>
<small class="text-muted">Including prescription and over-the-counter medications</small>
</div>
<div class="question-card p-3" data-question="recent_travel">
<div class="form-check">
{{ form.recent_travel }}
<label class="form-check-label fw-bold" for="{{ form.recent_travel.id_for_label }}">
{{ form.recent_travel.label }}
</label>
</div>
<small class="text-muted">Travel to malaria-endemic areas may require deferral</small>
</div>
</div>
<!-- END medical history -->
<!-- BEGIN risk factors -->
<div class="mb-4">
<h5 class="text-primary mb-3">
<i class="fa fa-exclamation-triangle"></i> Risk Factors
</h5>
<div class="question-card p-3" data-question="recent_tattoo">
<div class="form-check">
{{ form.recent_tattoo }}
<label class="form-check-label fw-bold" for="{{ form.recent_tattoo.id_for_label }}">
{{ form.recent_tattoo.label }}
</label>
</div>
<small class="text-muted">Including permanent makeup and body piercing</small>
</div>
<div class="question-card p-3" data-question="recent_surgery">
<div class="form-check">
{{ form.recent_surgery }}
<label class="form-check-label fw-bold" for="{{ form.recent_surgery.id_for_label }}">
{{ form.recent_surgery.label }}
</label>
</div>
<small class="text-muted">Any surgical procedure or dental work</small>
</div>
</div>
<!-- END risk factors -->
<!-- BEGIN additional notes -->
<div class="mb-4">
<h5 class="text-primary mb-3">
<i class="fa fa-notes-medical"></i> Additional Information
</h5>
<div class="form-group">
<label class="form-label" for="{{ form.notes.id_for_label }}">
{{ form.notes.label }}
</label>
{{ form.notes }}
{% if form.notes.errors %}
<div class="text-danger small">{{ form.notes.errors.0 }}</div>
{% endif %}
</div>
</div>
<!-- END additional notes -->
<!-- BEGIN form actions -->
<div class="d-flex justify-content-between">
<a href="{% url 'blood_bank:donor_detail' donor.id %}" class="btn btn-default">
<i class="fa fa-arrow-left"></i> Back to Donor
</a>
<div>
<button type="button" class="btn btn-info" onclick="reviewAnswers()">
<i class="fa fa-eye"></i> Review Answers
</button>
<button type="submit" class="btn btn-success" id="proceedBtn" disabled>
<i class="fa fa-arrow-right"></i> Proceed to Donation
</button>
</div>
</div>
<!-- END form actions -->
</form>
</div>
</div>
<!-- END eligibility screening panel -->
</div>
<!-- END col-8 -->
</div>
<!-- END row -->
{% endblock %}
{% block extra_js %}
<script>
$(document).ready(function() {
// Check form validity on load and when inputs change
checkFormValidity();
$('input[type="checkbox"]').on('change', function() {
updateQuestionCard(this);
checkFormValidity();
});
// Initialize question cards based on current values
$('input[type="checkbox"]').each(function() {
updateQuestionCard(this);
});
});
function updateQuestionCard(checkbox) {
var $card = $(checkbox).closest('.question-card');
var questionName = $card.data('question');
$card.removeClass('answered-yes answered-no');
if ($(checkbox).is(':checked')) {
if (['feeling_well', 'adequate_sleep', 'eaten_today'].includes(questionName)) {
$card.addClass('answered-yes');
} else {
$card.addClass('answered-no');
}
} else {
if (['feeling_well', 'adequate_sleep', 'eaten_today'].includes(questionName)) {
$card.addClass('answered-no');
} else {
$card.addClass('answered-yes');
}
}
}
function checkFormValidity() {
var isValid = true;
var requiredQuestions = ['feeling_well', 'adequate_sleep', 'eaten_today'];
// Check required questions
requiredQuestions.forEach(function(question) {
var checkbox = $('input[data-question="' + question + '"]');
if (!checkbox.is(':checked')) {
isValid = false;
}
});
// Check if any disqualifying answers
var disqualifyingQuestions = ['recent_illness', 'recent_tattoo', 'recent_surgery'];
disqualifyingQuestions.forEach(function(question) {
var checkbox = $('input[name="' + question + '"]');
if (checkbox.is(':checked')) {
isValid = false;
}
});
// Enable/disable proceed button
$('#proceedBtn').prop('disabled', !isValid);
if (isValid) {
$('#proceedBtn').removeClass('btn-secondary').addClass('btn-success');
} else {
$('#proceedBtn').removeClass('btn-success').addClass('btn-secondary');
}
}
function reviewAnswers() {
var answers = [];
$('input[type="checkbox"]').each(function() {
var label = $(this).next('label').text();
var answer = $(this).is(':checked') ? 'Yes' : 'No';
answers.push('<tr><td>' + label + '</td><td><strong>' + answer + '</strong></td></tr>');
});
var notesValue = $('textarea[name="notes"]').val();
if (notesValue) {
answers.push('<tr><td>Additional Notes</td><td>' + notesValue + '</td></tr>');
}
Swal.fire({
title: 'Review Screening Answers',
html: '<table class="table table-sm">' + answers.join('') + '</table>',
width: '600px',
confirmButtonText: 'Looks Good',
showCancelButton: true,
cancelButtonText: 'Make Changes'
});
}
// Form submission handling
$('#eligibilityForm').on('submit', function(e) {
var requiredAnswered = $('#{{ form.feeling_well.id_for_label }}').is(':checked') &&
$('#{{ form.adequate_sleep.id_for_label }}').is(':checked') &&
$('#{{ form.eaten_today.id_for_label }}').is(':checked');
var hasDisqualifyingFactors = $('#{{ form.recent_illness.id_for_label }}').is(':checked') ||
$('#{{ form.recent_tattoo.id_for_label }}').is(':checked') ||
$('#{{ form.recent_surgery.id_for_label }}').is(':checked');
if (!requiredAnswered) {
e.preventDefault();
Swal.fire({
icon: 'error',
title: 'Incomplete Screening',
text: 'Please answer all required health questions.',
confirmButtonText: 'OK'
});
return;
}
if (hasDisqualifyingFactors) {
e.preventDefault();
Swal.fire({
icon: 'warning',
title: 'Temporary Deferral',
text: 'Based on the answers provided, this donor should be temporarily deferred.',
confirmButtonText: 'Understood'
});
return;
}
// Show confirmation before proceeding
e.preventDefault();
Swal.fire({
title: 'Proceed to Donation?',
text: 'The donor has passed the eligibility screening. Proceed to blood collection?',
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Yes, Proceed',
cancelButtonText: 'Cancel'
}).then((result) => {
if (result.isConfirmed) {
// Submit the form
$('#eligibilityForm')[0].submit();
}
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,433 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block extra_css %}
<link href="{% static 'assets/plugins/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
{% endblock %}
{% block content %}
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'blood_bank:dashboard' %}">Blood Bank</a></li>
<li class="breadcrumb-item"><a href="{% url 'blood_bank:donor_list' %}">Donors</a></li>
<li class="breadcrumb-item active">{{ title }}</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">{{ title }} <small>donor information management</small></h1>
<!-- END page-header -->
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">{{ title }}</h4>
<div class="panel-heading-btn">
<a href="{% url 'blood_bank:donor_list' %}" class="btn btn-default btn-sm">
<i class="fa fa-arrow-left"></i> Back to List
</a>
</div>
</div>
<div class="panel-body">
<form method="post" class="form-horizontal" id="donorForm">
{% csrf_token %}
<!-- BEGIN personal information -->
<fieldset>
<legend class="mb-3">
<i class="fa fa-user text-primary"></i> Personal Information
</legend>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label" for="{{ form.donor_id.id_for_label }}">
Donor ID <span class="text-danger">*</span>
</label>
{{ form.donor_id }}
{% if form.donor_id.errors %}
<div class="text-danger small">{{ form.donor_id.errors.0 }}</div>
{% endif %}
<div class="form-text">Unique identifier for the donor</div>
</div>
<div class="col-md-6">
<label class="form-label" for="{{ form.blood_group.id_for_label }}">
Blood Group <span class="text-danger">*</span>
</label>
{{ form.blood_group }}
{% if form.blood_group.errors %}
<div class="text-danger small">{{ form.blood_group.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label" for="{{ form.first_name.id_for_label }}">
First Name <span class="text-danger">*</span>
</label>
{{ form.first_name }}
{% if form.first_name.errors %}
<div class="text-danger small">{{ form.first_name.errors.0 }}</div>
{% endif %}
</div>
<div class="col-md-6">
<label class="form-label" for="{{ form.last_name.id_for_label }}">
Last Name <span class="text-danger">*</span>
</label>
{{ form.last_name }}
{% if form.last_name.errors %}
<div class="text-danger small">{{ form.last_name.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-md-4">
<label class="form-label" for="{{ form.date_of_birth.id_for_label }}">
Date of Birth <span class="text-danger">*</span>
</label>
{{ form.date_of_birth }}
{% if form.date_of_birth.errors %}
<div class="text-danger small">{{ form.date_of_birth.errors.0 }}</div>
{% endif %}
<div class="form-text">Must be 18-65 years old</div>
</div>
<div class="col-md-4">
<label class="form-label" for="{{ form.gender.id_for_label }}">
Gender <span class="text-danger">*</span>
</label>
{{ form.gender }}
{% if form.gender.errors %}
<div class="text-danger small">{{ form.gender.errors.0 }}</div>
{% endif %}
</div>
<div class="col-md-4">
<div id="ageDisplay" class="mt-4">
<span class="badge bg-info">Age will be calculated</span>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label" for="{{ form.weight.id_for_label }}">
Weight (kg) <span class="text-danger">*</span>
</label>
{{ form.weight }}
{% if form.weight.errors %}
<div class="text-danger small">{{ form.weight.errors.0 }}</div>
{% endif %}
<div class="form-text">Minimum 45 kg required for donation</div>
</div>
<div class="col-md-6">
<label class="form-label" for="{{ form.height.id_for_label }}">
Height (cm) <span class="text-danger">*</span>
</label>
{{ form.height }}
{% if form.height.errors %}
<div class="text-danger small">{{ form.height.errors.0 }}</div>
{% endif %}
</div>
</div>
</fieldset>
<!-- END personal information -->
<!-- BEGIN contact information -->
<fieldset>
<legend class="mb-3">
<i class="fa fa-phone text-primary"></i> Contact Information
</legend>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label" for="{{ form.phone.id_for_label }}">
Phone Number <span class="text-danger">*</span>
</label>
{{ form.phone }}
{% if form.phone.errors %}
<div class="text-danger small">{{ form.phone.errors.0 }}</div>
{% endif %}
</div>
<div class="col-md-6">
<label class="form-label" for="{{ form.email.id_for_label }}">
Email Address
</label>
{{ form.email }}
{% if form.email.errors %}
<div class="text-danger small">{{ form.email.errors.0 }}</div>
{% endif %}
<div class="form-text">Optional but recommended</div>
</div>
</div>
<div class="row mb-3">
<div class="col-12">
<label class="form-label" for="{{ form.address.id_for_label }}">
Address <span class="text-danger">*</span>
</label>
{{ form.address }}
{% if form.address.errors %}
<div class="text-danger small">{{ form.address.errors.0 }}</div>
{% endif %}
</div>
</div>
</fieldset>
<!-- END contact information -->
<!-- BEGIN emergency contact -->
<fieldset>
<legend class="mb-3">
<i class="fa fa-exclamation-triangle text-primary"></i> Emergency Contact
</legend>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label" for="{{ form.emergency_contact_name.id_for_label }}">
Emergency Contact Name <span class="text-danger">*</span>
</label>
{{ form.emergency_contact_name }}
{% if form.emergency_contact_name.errors %}
<div class="text-danger small">{{ form.emergency_contact_name.errors.0 }}</div>
{% endif %}
</div>
<div class="col-md-6">
<label class="form-label" for="{{ form.emergency_contact_phone.id_for_label }}">
Emergency Contact Phone <span class="text-danger">*</span>
</label>
{{ form.emergency_contact_phone }}
{% if form.emergency_contact_phone.errors %}
<div class="text-danger small">{{ form.emergency_contact_phone.errors.0 }}</div>
{% endif %}
</div>
</div>
</fieldset>
<!-- END emergency contact -->
<!-- BEGIN donor status -->
<fieldset>
<legend class="mb-3">
<i class="fa fa-cog text-primary"></i> Donor Status
</legend>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label" for="{{ form.donor_type.id_for_label }}">
Donor Type <span class="text-danger">*</span>
</label>
{{ form.donor_type }}
{% if form.donor_type.errors %}
<div class="text-danger small">{{ form.donor_type.errors.0 }}</div>
{% endif %}
</div>
<div class="col-md-6">
<label class="form-label" for="{{ form.status.id_for_label }}">
Status <span class="text-danger">*</span>
</label>
{{ form.status }}
{% if form.status.errors %}
<div class="text-danger small">{{ form.status.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-12">
<label class="form-label" for="{{ form.notes.id_for_label }}">
Notes
</label>
{{ form.notes }}
{% if form.notes.errors %}
<div class="text-danger small">{{ form.notes.errors.0 }}</div>
{% endif %}
<div class="form-text">Any additional information about the donor</div>
</div>
</div>
</fieldset>
<!-- END donor status -->
<!-- BEGIN form actions -->
<div class="row">
<div class="col-12">
<hr>
<div class="d-flex justify-content-between">
<a href="{% url 'blood_bank:donor_list' %}" class="btn btn-default">
<i class="fa fa-times"></i> Cancel
</a>
<div>
<button type="button" class="btn btn-info" onclick="validateForm()">
<i class="fa fa-check-circle"></i> Validate
</button>
<button type="submit" class="btn btn-primary">
<i class="fa fa-save"></i> Save Donor
</button>
</div>
</div>
</div>
</div>
<!-- END form actions -->
</form>
</div>
</div>
<!-- END panel -->
{% endblock %}
{% block extra_js %}
<script src="{% static 'assets/plugins/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js' %}"></script>
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
<script src="{% static 'assets/plugins/jquery-mask/dist/jquery.mask.min.js' %}"></script>
<script>
$(document).ready(function() {
// Initialize Select2 for dropdowns
$('.form-select').select2({
theme: 'bootstrap-5',
width: '100%'
});
// Initialize date picker
$('#{{ form.date_of_birth.id_for_label }}').datepicker({
format: 'yyyy-mm-dd',
autoclose: true,
todayHighlight: true,
endDate: '-18y',
startDate: '-65y'
});
// Phone number masking
$('#{{ form.phone.id_for_label }}').mask('(000) 000-0000');
$('#{{ form.emergency_contact_phone.id_for_label }}').mask('(000) 000-0000');
// Calculate age when date of birth changes
$('#{{ form.date_of_birth.id_for_label }}').on('change', function() {
var dob = new Date($(this).val());
var today = new Date();
var age = Math.floor((today - dob) / (365.25 * 24 * 60 * 60 * 1000));
if (age >= 18 && age <= 65) {
$('#ageDisplay').html('<span class="badge bg-success">Age: ' + age + ' years (Eligible)</span>');
} else if (age < 18) {
$('#ageDisplay').html('<span class="badge bg-danger">Age: ' + age + ' years (Too Young)</span>');
} else {
$('#ageDisplay').html('<span class="badge bg-danger">Age: ' + age + ' years (Too Old)</span>');
}
});
// Weight validation
$('#{{ form.weight.id_for_label }}').on('input', function() {
var weight = parseFloat($(this).val());
var feedback = $(this).siblings('.form-text');
if (weight < 45) {
feedback.html('<span class="text-danger">Weight too low for donation (minimum 45 kg)</span>');
} else {
feedback.html('<span class="text-success">Weight acceptable for donation</span>');
}
});
// Auto-generate donor ID if empty
$('#{{ form.first_name.id_for_label }}, #{{ form.last_name.id_for_label }}').on('blur', function() {
var donorId = $('#{{ form.donor_id.id_for_label }}').val();
if (!donorId) {
var firstName = $('#{{ form.first_name.id_for_label }}').val();
var lastName = $('#{{ form.last_name.id_for_label }}').val();
if (firstName && lastName) {
var timestamp = new Date().getTime().toString().slice(-6);
var generatedId = 'D' + firstName.charAt(0).toUpperCase() +
lastName.charAt(0).toUpperCase() + timestamp;
$('#{{ form.donor_id.id_for_label }}').val(generatedId);
}
}
});
});
function validateForm() {
var isValid = true;
var errors = [];
// Check required fields
var requiredFields = [
'{{ form.donor_id.id_for_label }}',
'{{ form.first_name.id_for_label }}',
'{{ form.last_name.id_for_label }}',
'{{ form.date_of_birth.id_for_label }}',
'{{ form.gender.id_for_label }}',
'{{ form.blood_group.id_for_label }}',
'{{ form.weight.id_for_label }}',
'{{ form.height.id_for_label }}',
'{{ form.phone.id_for_label }}',
'{{ form.address.id_for_label }}',
'{{ form.emergency_contact_name.id_for_label }}',
'{{ form.emergency_contact_phone.id_for_label }}'
];
requiredFields.forEach(function(fieldId) {
var field = $('#' + fieldId);
if (!field.val()) {
isValid = false;
errors.push(field.prev('label').text().replace(' *', '') + ' is required');
field.addClass('is-invalid');
} else {
field.removeClass('is-invalid');
}
});
// Validate age
var dob = new Date($('#{{ form.date_of_birth.id_for_label }}').val());
var today = new Date();
var age = Math.floor((today - dob) / (365.25 * 24 * 60 * 60 * 1000));
if (age < 18 || age > 65) {
isValid = false;
errors.push('Donor must be between 18 and 65 years old');
}
// Validate weight
var weight = parseFloat($('#{{ form.weight.id_for_label }}').val());
if (weight < 45) {
isValid = false;
errors.push('Minimum weight for donation is 45 kg');
}
// Show validation results
if (isValid) {
Swal.fire({
icon: 'success',
title: 'Validation Successful',
text: 'All fields are valid. You can now save the donor.',
confirmButtonText: 'OK'
});
} else {
Swal.fire({
icon: 'error',
title: 'Validation Failed',
html: '<ul class="text-start"><li>' + errors.join('</li><li>') + '</li></ul>',
confirmButtonText: 'Fix Errors'
});
}
}
// Form submission validation
$('#donorForm').on('submit', function(e) {
var weight = parseFloat($('#{{ form.weight.id_for_label }}').val());
var dob = new Date($('#{{ form.date_of_birth.id_for_label }}').val());
var today = new Date();
var age = Math.floor((today - dob) / (365.25 * 24 * 60 * 60 * 1000));
if (weight < 45 || age < 18 || age > 65) {
e.preventDefault();
Swal.fire({
icon: 'warning',
title: 'Invalid Data',
text: 'Please check the donor eligibility criteria before saving.',
confirmButtonText: 'OK'
});
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,298 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Donor Management{% endblock %}
{% block extra_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" />
{% endblock %}
{% block content %}
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'blood_bank:dashboard' %}">Blood Bank</a></li>
<li class="breadcrumb-item active">Donors</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">Donor Management <small>manage blood donors</small></h1>
<!-- END page-header -->
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">Blood Donors</h4>
<div class="panel-heading-btn">
<a href="{% url 'blood_bank:donor_create' %}" class="btn btn-primary btn-sm">
<i class="fa fa-plus"></i> Add New Donor
</a>
</div>
</div>
<div class="panel-body">
<!-- BEGIN search and filter form -->
<form method="get" class="mb-3">
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label for="search">Search</label>
<input type="text" class="form-control" id="search" name="search"
value="{{ search_query }}" placeholder="Donor ID, Name, Phone">
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label for="status">Status</label>
<select class="form-select" id="status" name="status">
<option value="">All Statuses</option>
{% for value, label in status_choices %}
<option value="{{ value }}" {% if status_filter == value %}selected{% endif %}>
{{ label }}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label for="blood_group">Blood Group</label>
<select class="form-select" id="blood_group" name="blood_group">
<option value="">All Blood Groups</option>
{% for group in blood_groups %}
<option value="{{ group.id }}" {% if blood_group_filter == group.id|stringformat:"s" %}selected{% endif %}>
{{ group.display_name }}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>&nbsp;</label>
<div class="d-grid">
<button type="submit" class="btn btn-primary">
<i class="fa fa-search"></i> Search
</button>
</div>
</div>
</div>
</div>
</form>
<!-- END search and filter form -->
<!-- BEGIN table -->
<div class="table-responsive">
<table id="donorTable" class="table table-striped table-bordered align-middle">
<thead>
<tr>
<th>Donor ID</th>
<th>Name</th>
<th>Blood Group</th>
<th>Age</th>
<th>Phone</th>
<th>Status</th>
<th>Total Donations</th>
<th>Last Donation</th>
<th>Eligible</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for donor in page_obj %}
<tr>
<td>
<a href="{% url 'blood_bank:donor_detail' donor.id %}" class="text-decoration-none">
<strong>{{ donor.donor_id }}</strong>
</a>
</td>
<td>{{ donor.full_name }}</td>
<td>
<span class="badge bg-primary">{{ donor.blood_group.display_name }}</span>
</td>
<td>{{ donor.age }}</td>
<td>{{ donor.phone }}</td>
<td>
{% if donor.status == 'active' %}
<span class="badge bg-success">{{ donor.get_status_display }}</span>
{% elif donor.status == 'deferred' %}
<span class="badge bg-warning">{{ donor.get_status_display }}</span>
{% elif donor.status == 'permanently_deferred' %}
<span class="badge bg-danger">{{ donor.get_status_display }}</span>
{% else %}
<span class="badge bg-secondary">{{ donor.get_status_display }}</span>
{% endif %}
</td>
<td>{{ donor.total_donations }}</td>
<td>
{% if donor.last_donation_date %}
{{ donor.last_donation_date|date:"M d, Y" }}
{% else %}
<span class="text-muted">Never</span>
{% endif %}
</td>
<td>
{% if donor.is_eligible_for_donation %}
<span class="badge bg-success">
<i class="fa fa-check"></i> Eligible
</span>
{% else %}
<span class="badge bg-danger">
<i class="fa fa-times"></i> Not Eligible
</span>
{% endif %}
</td>
<td>
<div class="btn-group" role="group">
<a href="{% url 'blood_bank:donor_detail' donor.id %}"
class="btn btn-outline-primary btn-sm" title="View Details">
<i class="fa fa-eye"></i>
</a>
<a href="{% url 'blood_bank:donor_update' donor.id %}"
class="btn btn-outline-warning btn-sm" title="Edit">
<i class="fa fa-edit"></i>
</a>
{% if donor.is_eligible_for_donation %}
<a href="{% url 'blood_bank:donor_eligibility' donor.id %}"
class="btn btn-outline-success btn-sm" title="Donation Check">
<i class="fa fa-tint"></i>
</a>
{% endif %}
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="10" class="text-center">
<div class="py-4">
<i class="fa fa-users fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No donors found</h5>
<p class="text-muted">Try adjusting your search criteria or add a new donor.</p>
<a href="{% url 'blood_bank:donor_create' %}" class="btn btn-primary">
<i class="fa fa-plus"></i> Add New Donor
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- END table -->
<!-- BEGIN pagination -->
{% if page_obj.has_other_pages %}
<nav aria-label="Donor pagination">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1{% if search_query %}&search={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if blood_group_filter %}&blood_group={{ blood_group_filter }}{% endif %}">
<i class="fa fa-angle-double-left"></i>
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if blood_group_filter %}&blood_group={{ blood_group_filter }}{% endif %}">
<i class="fa fa-angle-left"></i>
</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 search_query %}&search={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if blood_group_filter %}&blood_group={{ blood_group_filter }}{% 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 search_query %}&search={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if blood_group_filter %}&blood_group={{ blood_group_filter }}{% endif %}">
<i class="fa fa-angle-right"></i>
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if search_query %}&search={{ search_query }}{% endif %}{% if status_filter %}&status={{ status_filter }}{% endif %}{% if blood_group_filter %}&blood_group={{ blood_group_filter }}{% endif %}">
<i class="fa fa-angle-double-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
<!-- END pagination -->
<!-- BEGIN summary -->
<div class="row mt-3">
<div class="col-md-6">
<p class="text-muted">
Showing {{ page_obj.start_index }} to {{ page_obj.end_index }} of {{ page_obj.paginator.count }} donors
</p>
</div>
<div class="col-md-6 text-end">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="window.print()">
<i class="fa fa-print"></i> Print
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="exportToCSV()">
<i class="fa fa-download"></i> Export
</button>
</div>
</div>
</div>
<!-- END summary -->
</div>
</div>
<!-- END panel -->
{% 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 for enhanced functionality
$('#donorTable').DataTable({
responsive: true,
pageLength: 25,
order: [[0, 'desc']],
columnDefs: [
{ orderable: false, targets: [9] } // Actions column
]
});
});
function exportToCSV() {
// Simple CSV export functionality
var csv = [];
var rows = document.querySelectorAll("#donorTable tr");
for (var i = 0; i < rows.length; i++) {
var row = [], cols = rows[i].querySelectorAll("td, th");
for (var j = 0; j < cols.length - 1; j++) { // Exclude actions column
row.push(cols[j].innerText);
}
csv.push(row.join(","));
}
var csvFile = new Blob([csv.join("\n")], {type: "text/csv"});
var downloadLink = document.createElement("a");
downloadLink.download = "donors.csv";
downloadLink.href = window.URL.createObjectURL(csvFile);
downloadLink.style.display = "none";
document.body.appendChild(downloadLink);
downloadLink.click();
}
</script>
{% endblock %}

View File

@ -0,0 +1,604 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Blood Bank Inventory Dashboard{% endblock %}
{% block extra_css %}
<link href="{% static 'assets/plugins/chart.js/dist/Chart.min.css' %}" rel="stylesheet" />
<style>
.inventory-card {
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
transition: transform 0.3s ease;
}
.inventory-card:hover {
transform: translateY(-5px);
}
.blood-group-card {
border-left: 4px solid;
margin-bottom: 15px;
}
.blood-group-o { border-left-color: #dc3545; }
.blood-group-a { border-left-color: #007bff; }
.blood-group-b { border-left-color: #28a745; }
.blood-group-ab { border-left-color: #ffc107; }
.expiry-alert {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.7; }
100% { opacity: 1; }
}
.temperature-normal { color: #28a745; }
.temperature-warning { color: #ffc107; }
.temperature-critical { color: #dc3545; }
.chart-container {
position: relative;
height: 300px;
}
</style>
{% endblock %}
{% block content %}
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'blood_bank:dashboard' %}">Blood Bank</a></li>
<li class="breadcrumb-item active">Inventory Dashboard</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">Blood Bank Inventory <small>real-time inventory monitoring</small></h1>
<!-- END page-header -->
<!-- BEGIN summary cards -->
<div class="row mb-4">
<div class="col-xl-3 col-md-6">
<div class="card inventory-card bg-primary text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h6 class="card-title">Total Units</h6>
<h2 class="mb-0">{{ total_units }}</h2>
<small>Available for transfusion</small>
</div>
<div class="align-self-center">
<i class="fa fa-tint fa-3x opacity-75"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card inventory-card bg-success text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h6 class="card-title">Fresh Units</h6>
<h2 class="mb-0">{{ fresh_units }}</h2>
<small>Less than 7 days old</small>
</div>
<div class="align-self-center">
<i class="fa fa-check-circle fa-3x opacity-75"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card inventory-card bg-warning text-white expiry-alert">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h6 class="card-title">Expiring Soon</h6>
<h2 class="mb-0">{{ expiring_units }}</h2>
<small>Within 3 days</small>
</div>
<div class="align-self-center">
<i class="fa fa-exclamation-triangle fa-3x opacity-75"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card inventory-card bg-danger text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h6 class="card-title">Critical Low</h6>
<h2 class="mb-0">{{ critical_low_count }}</h2>
<small>Below minimum levels</small>
</div>
<div class="align-self-center">
<i class="fa fa-exclamation-circle fa-3x opacity-75"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- END summary cards -->
<!-- BEGIN row -->
<div class="row">
<!-- BEGIN col-8 -->
<div class="col-xl-8">
<!-- BEGIN blood group inventory -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">Blood Group Inventory</h4>
<div class="panel-heading-btn">
<button type="button" class="btn btn-info btn-sm" onclick="refreshInventory()">
<i class="fa fa-refresh"></i> Refresh
</button>
</div>
</div>
<div class="panel-body">
<div class="row">
{% for group in blood_groups %}
<div class="col-md-6">
<div class="blood-group-card card blood-group-{{ group.abo_type|lower }}">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="card-title mb-1">{{ group.display_name }}</h5>
<div class="row">
<div class="col-6">
<small class="text-muted">Whole Blood</small>
<h6 class="mb-0">{{ group.whole_blood_count }}</h6>
</div>
<div class="col-6">
<small class="text-muted">RBC</small>
<h6 class="mb-0">{{ group.rbc_count }}</h6>
</div>
</div>
<div class="row">
<div class="col-6">
<small class="text-muted">Plasma</small>
<h6 class="mb-0">{{ group.plasma_count }}</h6>
</div>
<div class="col-6">
<small class="text-muted">Platelets</small>
<h6 class="mb-0">{{ group.platelet_count }}</h6>
</div>
</div>
</div>
<div class="text-end">
<h3 class="mb-0">{{ group.total_units }}</h3>
<small class="text-muted">Total Units</small>
{% if group.is_critical_low %}
<br><span class="badge bg-danger">Critical</span>
{% elif group.is_low %}
<br><span class="badge bg-warning">Low</span>
{% else %}
<br><span class="badge bg-success">Good</span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- END blood group inventory -->
<!-- BEGIN inventory trends -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">Inventory Trends</h4>
<div class="panel-heading-btn">
<select class="form-select form-select-sm" id="trendPeriod" onchange="updateTrendChart()">
<option value="7">Last 7 Days</option>
<option value="30">Last 30 Days</option>
<option value="90">Last 3 Months</option>
</select>
</div>
</div>
<div class="panel-body">
<div class="chart-container">
<canvas id="inventoryTrendChart"></canvas>
</div>
</div>
</div>
<!-- END inventory trends -->
<!-- BEGIN component distribution -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">Component Distribution</h4>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-6">
<div class="chart-container">
<canvas id="componentChart"></canvas>
</div>
</div>
<div class="col-md-6">
<div class="chart-container">
<canvas id="bloodGroupChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<!-- END component distribution -->
</div>
<!-- END col-8 -->
<!-- BEGIN col-4 -->
<div class="col-xl-4">
<!-- BEGIN storage locations -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">Storage Locations</h4>
</div>
<div class="panel-body">
{% for location in storage_locations %}
<div class="card mb-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="card-title mb-1">{{ location.name }}</h6>
<small class="text-muted">{{ location.location_type }}</small>
</div>
<div class="text-end">
<h5 class="mb-0">{{ location.unit_count }}</h5>
<small class="text-muted">Units</small>
</div>
</div>
<div class="mt-2">
<div class="d-flex justify-content-between">
<small>Temperature:</small>
<small class="{% if location.temperature_status == 'normal' %}temperature-normal{% elif location.temperature_status == 'warning' %}temperature-warning{% else %}temperature-critical{% endif %}">
{{ location.current_temperature }}°C
<i class="fa fa-thermometer-half"></i>
</small>
</div>
<div class="progress mt-1" style="height: 5px;">
<div class="progress-bar {% if location.capacity_percentage > 90 %}bg-danger{% elif location.capacity_percentage > 75 %}bg-warning{% else %}bg-success{% endif %}"
style="width: {{ location.capacity_percentage }}%"></div>
</div>
<small class="text-muted">{{ location.capacity_percentage }}% capacity</small>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- END storage locations -->
<!-- BEGIN expiry alerts -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">Expiry Alerts</h4>
<div class="panel-heading-btn">
<span class="badge bg-warning">{{ expiring_units }}</span>
</div>
</div>
<div class="panel-body">
{% for unit in expiring_units_list %}
<div class="alert alert-warning d-flex justify-content-between align-items-center">
<div>
<strong>{{ unit.unit_number }}</strong><br>
<small>{{ unit.blood_group.display_name }} - {{ unit.component.get_name_display }}</small>
</div>
<div class="text-end">
<span class="badge bg-danger">{{ unit.days_to_expiry }} days</span><br>
<small class="text-muted">{{ unit.expiry_date|date:"M d" }}</small>
</div>
</div>
{% empty %}
<div class="text-center py-3">
<i class="fa fa-check-circle fa-2x text-success mb-2"></i>
<p class="text-muted mb-0">No units expiring soon</p>
</div>
{% endfor %}
</div>
</div>
<!-- END expiry alerts -->
<!-- BEGIN recent activity -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">Recent Activity</h4>
</div>
<div class="panel-body">
<div class="timeline">
{% for activity in recent_activities %}
<div class="timeline-item">
<div class="timeline-time">{{ activity.timestamp|date:"H:i" }}</div>
<div class="timeline-body">
<div class="timeline-content">
<i class="fa {{ activity.icon }} text-{{ activity.color }}"></i>
{{ activity.description }}
</div>
</div>
</div>
{% empty %}
<div class="text-center py-3">
<i class="fa fa-clock fa-2x text-muted mb-2"></i>
<p class="text-muted mb-0">No recent activity</p>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- END recent activity -->
<!-- BEGIN quick actions -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">Quick Actions</h4>
</div>
<div class="panel-body">
<div class="d-grid gap-2">
<a href="{% url 'blood_bank:blood_unit_create' %}" class="btn btn-primary">
<i class="fa fa-plus"></i> Register Blood Unit
</a>
<a href="{% url 'blood_bank:blood_request_list' %}" class="btn btn-info">
<i class="fa fa-clipboard-list"></i> View Requests
</a>
<a href="{% url 'blood_bank:donor_list' %}" class="btn btn-success">
<i class="fa fa-users"></i> Manage Donors
</a>
<button type="button" class="btn btn-warning" onclick="generateInventoryReport()">
<i class="fa fa-file-pdf"></i> Generate Report
</button>
</div>
</div>
</div>
<!-- END quick actions -->
</div>
<!-- END col-4 -->
</div>
<!-- END row -->
{% endblock %}
{% block extra_js %}
<script src="{% static 'assets/plugins/chart.js/dist/Chart.min.js' %}"></script>
<script>
$(document).ready(function() {
initializeCharts();
// Auto-refresh every 5 minutes
setInterval(function() {
if (document.visibilityState === 'visible') {
refreshInventory();
}
}, 300000);
});
function initializeCharts() {
// Inventory Trend Chart
var trendCtx = document.getElementById('inventoryTrendChart').getContext('2d');
var trendChart = new Chart(trendCtx, {
type: 'line',
data: {
labels: {{ trend_labels|safe }},
datasets: [{
label: 'Total Units',
data: {{ trend_data|safe }},
borderColor: '#007bff',
backgroundColor: 'rgba(0, 123, 255, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
// Component Distribution Chart
var componentCtx = document.getElementById('componentChart').getContext('2d');
var componentChart = new Chart(componentCtx, {
type: 'doughnut',
data: {
labels: {{ component_labels|safe }},
datasets: [{
data: {{ component_data|safe }},
backgroundColor: [
'#dc3545',
'#007bff',
'#28a745',
'#ffc107',
'#6f42c1'
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'By Component'
}
}
}
});
// Blood Group Distribution Chart
var bloodGroupCtx = document.getElementById('bloodGroupChart').getContext('2d');
var bloodGroupChart = new Chart(bloodGroupCtx, {
type: 'doughnut',
data: {
labels: {{ blood_group_labels|safe }},
datasets: [{
data: {{ blood_group_data|safe }},
backgroundColor: [
'#dc3545',
'#007bff',
'#28a745',
'#ffc107'
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'By Blood Group'
}
}
}
});
}
function refreshInventory() {
// Show loading indicator
Swal.fire({
title: 'Refreshing Inventory...',
allowOutsideClick: false,
didOpen: () => {
Swal.showLoading();
}
});
// Simulate refresh (in real implementation, this would be an AJAX call)
setTimeout(function() {
Swal.close();
location.reload();
}, 2000);
}
function updateTrendChart() {
var period = document.getElementById('trendPeriod').value;
// Here you would make an AJAX call to get new data
// For now, we'll just show a message
Swal.fire({
icon: 'info',
title: 'Updating Chart',
text: `Loading data for last ${period} days...`,
timer: 1500,
showConfirmButton: false
});
}
function generateInventoryReport() {
Swal.fire({
title: 'Generate Inventory Report',
html: `
<div class="text-start">
<div class="mb-3">
<label class="form-label">Report Type</label>
<select class="form-select" id="reportType">
<option value="summary">Inventory Summary</option>
<option value="detailed">Detailed Inventory</option>
<option value="expiry">Expiry Report</option>
<option value="usage">Usage Analysis</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Date Range</label>
<select class="form-select" id="dateRange">
<option value="current">Current Inventory</option>
<option value="week">Last Week</option>
<option value="month">Last Month</option>
<option value="quarter">Last Quarter</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Format</label>
<select class="form-select" id="reportFormat">
<option value="pdf">PDF</option>
<option value="excel">Excel</option>
<option value="csv">CSV</option>
</select>
</div>
</div>
`,
showCancelButton: true,
confirmButtonText: 'Generate Report',
cancelButtonText: 'Cancel',
preConfirm: () => {
const reportType = document.getElementById('reportType').value;
const dateRange = document.getElementById('dateRange').value;
const format = document.getElementById('reportFormat').value;
return { reportType, dateRange, format };
}
}).then((result) => {
if (result.isConfirmed) {
Swal.fire({
icon: 'success',
title: 'Report Generated',
text: `${result.value.reportType} report in ${result.value.format} format is being prepared.`,
confirmButtonText: 'OK'
});
}
});
}
// Timeline styles
document.addEventListener('DOMContentLoaded', function() {
var style = document.createElement('style');
style.textContent = `
.timeline {
position: relative;
padding-left: 20px;
}
.timeline::before {
content: '';
position: absolute;
left: 10px;
top: 0;
bottom: 0;
width: 2px;
background: #e9ecef;
}
.timeline-item {
position: relative;
margin-bottom: 15px;
}
.timeline-item::before {
content: '';
position: absolute;
left: -15px;
top: 5px;
width: 8px;
height: 8px;
border-radius: 50%;
background: #007bff;
}
.timeline-time {
font-size: 0.8em;
color: #6c757d;
margin-bottom: 2px;
}
.timeline-content {
font-size: 0.9em;
}
`;
document.head.appendChild(style);
});
</script>
{% endblock %}

View File

@ -0,0 +1,743 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Issue Blood Unit - Request #{{ blood_request.request_number }}{% endblock %}
{% block extra_css %}
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
<style>
.form-section {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
border-left: 4px solid #007bff;
}
.form-section h5 {
color: #007bff;
margin-bottom: 15px;
}
.required-field {
color: #dc3545;
}
.request-info {
background: #d4edda;
border-left: 4px solid #28a745;
}
.unit-selection {
background: #d1ecf1;
border-left: 4px solid #17a2b8;
}
.safety-checks {
background: #fff3cd;
border-left: 4px solid #ffc107;
}
.emergency-section {
background: #f8d7da;
border-left: 4px solid #dc3545;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.8; }
100% { opacity: 1; }
}
.unit-card {
background: white;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
cursor: pointer;
transition: all 0.3s ease;
}
.unit-card:hover {
border-color: #007bff;
box-shadow: 0 2px 8px rgba(0,123,255,0.2);
}
.unit-card.selected {
border-color: #28a745;
background-color: #d4edda;
}
.unit-card.incompatible {
border-color: #dc3545;
background-color: #f8d7da;
cursor: not-allowed;
}
.compatibility-badge {
position: absolute;
top: 10px;
right: 10px;
}
.issue-preview {
background: #e2e3e5;
border-radius: 8px;
padding: 15px;
margin-top: 15px;
}
.verification-checklist {
background: #f8f9fa;
border: 2px solid #28a745;
border-radius: 8px;
padding: 15px;
margin-top: 15px;
}
.transport-info {
background: #e9ecef;
border-radius: 8px;
padding: 15px;
margin-top: 15px;
}
</style>
{% endblock %}
{% block content %}
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'blood_bank:dashboard' %}">Blood Bank</a></li>
<li class="breadcrumb-item"><a href="{% url 'blood_bank:blood_request_list' %}">Blood Requests</a></li>
<li class="breadcrumb-item"><a href="{% url 'blood_bank:blood_request_detail' blood_request.id %}">{{ blood_request.request_number }}</a></li>
<li class="breadcrumb-item active">Issue Blood</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">
Issue Blood Unit
<small>Request #{{ blood_request.request_number }}</small>
{% if blood_request.urgency == 'emergency' %}
<span class="badge bg-danger ms-2">EMERGENCY</span>
{% endif %}
</h1>
<!-- END page-header -->
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fa fa-shipping-fast"></i> Blood Unit Issue
</h4>
<div class="panel-heading-btn">
<span class="badge bg-info">Request: {{ blood_request.request_number }}</span>
</div>
</div>
<div class="panel-body">
<form method="post" id="bloodIssueForm">
{% csrf_token %}
<!-- BEGIN request information -->
<div class="form-section {% if blood_request.urgency == 'emergency' %}emergency-section{% else %}request-info{% endif %}">
<h5><i class="fa fa-file-medical"></i> Blood Request Information</h5>
<div class="row">
<div class="col-md-6">
<table class="table table-borderless mb-0">
<tr>
<td class="fw-bold">Request Number:</td>
<td>{{ blood_request.request_number }}</td>
</tr>
<tr>
<td class="fw-bold">Patient:</td>
<td>{{ blood_request.patient.full_name }} ({{ blood_request.patient.patient_id }})</td>
</tr>
<tr>
<td class="fw-bold">Blood Group:</td>
<td><span class="badge bg-primary">{{ blood_request.patient.blood_group.display_name }}</span></td>
</tr>
<tr>
<td class="fw-bold">Component Requested:</td>
<td>{{ blood_request.component.get_name_display }}</td>
</tr>
</table>
</div>
<div class="col-md-6">
<table class="table table-borderless mb-0">
<tr>
<td class="fw-bold">Quantity:</td>
<td>{{ blood_request.quantity_requested }} units</td>
</tr>
<tr>
<td class="fw-bold">Urgency:</td>
<td>
<span class="badge bg-{% if blood_request.urgency == 'emergency' %}danger{% elif blood_request.urgency == 'urgent' %}warning{% else %}info{% endif %}">
{{ blood_request.get_urgency_display }}
</span>
</td>
</tr>
<tr>
<td class="fw-bold">Requested By:</td>
<td>{{ blood_request.requested_by.get_full_name }}</td>
</tr>
<tr>
<td class="fw-bold">Department:</td>
<td>{{ blood_request.department.name }}</td>
</tr>
</table>
</div>
</div>
{% if blood_request.special_requirements %}
<div class="mt-3">
<h6>Special Requirements:</h6>
<div class="alert alert-info">{{ blood_request.special_requirements }}</div>
</div>
{% endif %}
</div>
<!-- END request information -->
<!-- BEGIN unit selection -->
<div class="form-section unit-selection">
<h5><i class="fa fa-tint"></i> Available Blood Units</h5>
<div class="row">
<div class="col-md-12">
<p class="text-muted">Select compatible blood units for this request. Units are filtered by blood group compatibility and availability.</p>
<div id="availableUnits">
{% for unit in available_units %}
<div class="unit-card position-relative" data-unit-id="{{ unit.id }}" onclick="selectUnit(this)">
<div class="row">
<div class="col-md-8">
<h6>{{ unit.unit_number }}</h6>
<div class="row">
<div class="col-md-6">
<small class="text-muted">Blood Group:</small><br>
<span class="badge bg-primary">{{ unit.blood_group.display_name }}</span>
</div>
<div class="col-md-6">
<small class="text-muted">Component:</small><br>
{{ unit.component.get_name_display }}
</div>
</div>
<div class="row mt-2">
<div class="col-md-6">
<small class="text-muted">Volume:</small><br>
{{ unit.volume_ml }} ml
</div>
<div class="col-md-6">
<small class="text-muted">Expiry:</small><br>
{{ unit.expiry_date|date:"M d, Y" }}
{% if unit.days_to_expiry <= 3 %}
<span class="badge bg-warning">Expires Soon</span>
{% endif %}
</div>
</div>
</div>
<div class="col-md-4">
<div class="text-end">
<small class="text-muted">Donor:</small><br>
{{ unit.donor.full_name }}<br>
<small class="text-muted">Collection:</small><br>
{{ unit.collection_date|date:"M d, Y" }}<br>
<small class="text-muted">Location:</small><br>
{{ unit.location.name }}
</div>
</div>
</div>
<!-- Compatibility badge -->
<div class="compatibility-badge">
{% if unit.is_compatible %}
<span class="badge bg-success">Compatible</span>
{% else %}
<span class="badge bg-danger">Check Required</span>
{% endif %}
</div>
<!-- Crossmatch status -->
{% if unit.crossmatch_status %}
<div class="mt-2">
<small class="text-muted">Crossmatch:</small>
<span class="badge bg-{% if unit.crossmatch_status == 'compatible' %}success{% elif unit.crossmatch_status == 'incompatible' %}danger{% else %}warning{% endif %}">
{{ unit.crossmatch_status|title }}
</span>
</div>
{% endif %}
</div>
{% empty %}
<div class="alert alert-warning">
<i class="fa fa-exclamation-triangle"></i> No compatible blood units available for this request.
<br>Please check inventory or contact blood bank supervisor.
</div>
{% endfor %}
</div>
<input type="hidden" name="selected_units" id="selectedUnits" value="">
</div>
</div>
</div>
<!-- END unit selection -->
<!-- BEGIN issue details -->
<div class="form-section">
<h5><i class="fa fa-info-circle"></i> Issue Details</h5>
<div class="row">
<div class="col-md-4">
<div class="form-group">
<label for="issueDate" class="form-label">
Issue Date & Time <span class="required-field">*</span>
</label>
<input type="datetime-local" class="form-control" id="issueDate" name="issue_date" required>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label for="issuedBy" class="form-label">
Issued By <span class="required-field">*</span>
</label>
<select class="form-select" id="issuedBy" name="issued_by" required>
<option value="">Select staff member...</option>
{% for staff in blood_bank_staff %}
<option value="{{ staff.id }}">{{ staff.get_full_name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label for="receivedBy" class="form-label">
Received By <span class="required-field">*</span>
</label>
<select class="form-select" id="receivedBy" name="received_by" required>
<option value="">Select receiving staff...</option>
{% for staff in clinical_staff %}
<option value="{{ staff.id }}">{{ staff.get_full_name }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
<!-- END issue details -->
<!-- BEGIN transport information -->
<div class="form-section">
<h5><i class="fa fa-truck"></i> Transport Information</h5>
<div class="transport-info">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="transportMethod" class="form-label">
Transport Method <span class="required-field">*</span>
</label>
<select class="form-select" id="transportMethod" name="transport_method" required>
<option value="">Select method...</option>
<option value="hand_carry">Hand Carry</option>
<option value="pneumatic_tube">Pneumatic Tube</option>
<option value="courier">Courier Service</option>
<option value="emergency_transport">Emergency Transport</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="transportContainer" class="form-label">
Transport Container <span class="required-field">*</span>
</label>
<select class="form-select" id="transportContainer" name="transport_container" required>
<option value="">Select container...</option>
<option value="insulated_bag">Insulated Transport Bag</option>
<option value="cooler_box">Cooler Box</option>
<option value="temperature_controlled">Temperature Controlled Container</option>
<option value="emergency_kit">Emergency Transport Kit</option>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="transportTemperature" class="form-label">
Transport Temperature (°C)
</label>
<input type="number" step="0.1" class="form-control" id="transportTemperature"
name="transport_temperature" placeholder="2-6°C" min="1" max="10">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="estimatedDelivery" class="form-label">
Estimated Delivery Time
</label>
<input type="datetime-local" class="form-control" id="estimatedDelivery" name="estimated_delivery">
</div>
</div>
</div>
</div>
</div>
<!-- END transport information -->
<!-- BEGIN verification checklist -->
<div class="form-section safety-checks">
<h5><i class="fa fa-shield-alt"></i> Pre-Issue Verification Checklist</h5>
<div class="verification-checklist">
<div class="row">
<div class="col-md-6">
<h6>Unit Verification</h6>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="unitNumberVerified" required>
<label class="form-check-label" for="unitNumberVerified">
Blood unit number verified against request
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="bloodGroupVerified" required>
<label class="form-check-label" for="bloodGroupVerified">
Blood group compatibility verified
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="expiryDateChecked" required>
<label class="form-check-label" for="expiryDateChecked">
Expiry date checked and valid
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="visualInspection" required>
<label class="form-check-label" for="visualInspection">
Visual inspection completed (no clots, discoloration)
</label>
</div>
</div>
<div class="col-md-6">
<h6>Documentation & Testing</h6>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="crossmatchCompleted" required>
<label class="form-check-label" for="crossmatchCompleted">
Crossmatch completed and compatible
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="testingCompleted" required>
<label class="form-check-label" for="testingCompleted">
All required testing completed and negative
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="patientIdentityVerified" required>
<label class="form-check-label" for="patientIdentityVerified">
Patient identity verified (2 identifiers)
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="requestAuthorized" required>
<label class="form-check-label" for="requestAuthorized">
Request properly authorized by physician
</label>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-12">
<h6>Emergency Procedures (if applicable)</h6>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="emergencyProtocol">
<label class="form-check-label" for="emergencyProtocol">
Emergency release protocol followed (if applicable)
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="emergencyConsent">
<label class="form-check-label" for="emergencyConsent">
Emergency consent documented (if applicable)
</label>
</div>
</div>
</div>
</div>
</div>
<!-- END verification checklist -->
<!-- BEGIN special instructions -->
<div class="form-section">
<h5><i class="fa fa-clipboard-list"></i> Special Instructions & Notes</h5>
<div class="row">
<div class="col-md-12">
<div class="form-group">
<label for="issueNotes" class="form-label">Issue Notes</label>
<textarea class="form-control" id="issueNotes" name="issue_notes" rows="3"
placeholder="Any special handling instructions, observations, or notes..."></textarea>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="handlingInstructions" class="form-label">Handling Instructions</label>
<select class="form-select" id="handlingInstructions" name="handling_instructions">
<option value="">Select if applicable...</option>
<option value="keep_refrigerated">Keep Refrigerated (2-6°C)</option>
<option value="room_temperature">Room Temperature (20-24°C)</option>
<option value="immediate_use">For Immediate Use</option>
<option value="irradiated">Irradiated Product</option>
<option value="cmv_negative">CMV Negative</option>
<option value="leukoreduced">Leukoreduced</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="returnTime" class="form-label">Return Time Limit</label>
<input type="datetime-local" class="form-control" id="returnTime" name="return_time_limit"
placeholder="If not used, return by...">
</div>
</div>
</div>
</div>
<!-- END special instructions -->
<!-- BEGIN issue preview -->
<div class="issue-preview">
<h6><i class="fa fa-eye"></i> Issue Summary</h6>
<div id="issueSummary">
<p class="text-muted">Select blood units and complete the form to preview issue details</p>
</div>
</div>
<!-- END issue preview -->
<!-- BEGIN form actions -->
<div class="d-flex justify-content-between mt-4">
<a href="{% url 'blood_bank:blood_request_detail' blood_request.id %}" class="btn btn-secondary">
<i class="fa fa-arrow-left"></i> Cancel
</a>
<div>
<button type="button" class="btn btn-info" onclick="validateIssue()">
<i class="fa fa-check"></i> Validate Issue
</button>
<button type="submit" class="btn btn-primary" id="submitBtn" disabled>
<i class="fa fa-shipping-fast"></i> Issue Blood Units
</button>
</div>
</div>
<!-- END form actions -->
</form>
</div>
</div>
<!-- END panel -->
{% endblock %}
{% block extra_js %}
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
<script>
var selectedUnits = [];
$(document).ready(function() {
// Initialize Select2
$('#issuedBy, #receivedBy, #transportMethod, #transportContainer, #handlingInstructions').select2({
theme: 'bootstrap-5'
});
// Set default issue date to now
var now = new Date();
$('#issueDate').val(now.toISOString().slice(0, 16));
// Set estimated delivery time (30 minutes from now)
var deliveryTime = new Date(now.getTime() + 30 * 60000);
$('#estimatedDelivery').val(deliveryTime.toISOString().slice(0, 16));
// Set return time limit (4 hours from now for RBC, 6 hours for platelets)
var returnTime = new Date(now.getTime() + 4 * 60 * 60000);
$('#returnTime').val(returnTime.toISOString().slice(0, 16));
// Update displays when form changes
updateIssueSummary();
// Event listeners
$('.verification-checklist input[type="checkbox"]').on('change', function() {
validateVerificationChecklist();
});
$('#transportMethod, #transportContainer').on('change', function() {
updateIssueSummary();
});
// Form validation
$('#bloodIssueForm').on('submit', function(e) {
if (!validateIssue()) {
e.preventDefault();
}
});
});
function selectUnit(unitCard) {
var unitId = $(unitCard).data('unit-id');
if ($(unitCard).hasClass('incompatible')) {
Swal.fire({
icon: 'warning',
title: 'Incompatible Unit',
text: 'This unit may not be compatible. Please verify crossmatch results.',
confirmButtonText: 'OK'
});
return;
}
if ($(unitCard).hasClass('selected')) {
// Deselect unit
$(unitCard).removeClass('selected');
selectedUnits = selectedUnits.filter(id => id !== unitId);
} else {
// Select unit
$(unitCard).addClass('selected');
selectedUnits.push(unitId);
}
$('#selectedUnits').val(selectedUnits.join(','));
updateIssueSummary();
validateIssue();
}
function updateIssueSummary() {
if (selectedUnits.length === 0) {
$('#issueSummary').html('<p class="text-muted">Select blood units and complete the form to preview issue details</p>');
return;
}
var summaryHtml = '<div class="row">';
summaryHtml += '<div class="col-md-6">';
summaryHtml += '<h6>Selected Units (' + selectedUnits.length + ')</h6>';
selectedUnits.forEach(function(unitId) {
var unitCard = $('.unit-card[data-unit-id="' + unitId + '"]');
var unitNumber = unitCard.find('h6').text();
summaryHtml += '<div class="badge bg-success me-1 mb-1">' + unitNumber + '</div>';
});
summaryHtml += '</div>';
summaryHtml += '<div class="col-md-6">';
summaryHtml += '<h6>Issue Details</h6>';
var issuedBy = $('#issuedBy option:selected').text();
var receivedBy = $('#receivedBy option:selected').text();
var transportMethod = $('#transportMethod option:selected').text();
var issueDate = $('#issueDate').val();
if (issuedBy && issuedBy !== 'Select staff member...') {
summaryHtml += '<p><strong>Issued By:</strong> ' + issuedBy + '</p>';
}
if (receivedBy && receivedBy !== 'Select receiving staff...') {
summaryHtml += '<p><strong>Received By:</strong> ' + receivedBy + '</p>';
}
if (transportMethod && transportMethod !== 'Select method...') {
summaryHtml += '<p><strong>Transport:</strong> ' + transportMethod + '</p>';
}
if (issueDate) {
summaryHtml += '<p><strong>Issue Time:</strong> ' + new Date(issueDate).toLocaleString() + '</p>';
}
summaryHtml += '</div>';
summaryHtml += '</div>';
// Add emergency warning if applicable
{% if blood_request.urgency == 'emergency' %}
summaryHtml += '<div class="alert alert-danger mt-2">';
summaryHtml += '<strong>⚠️ EMERGENCY ISSUE:</strong> Expedited processing and transport required.';
summaryHtml += '</div>';
{% endif %}
$('#issueSummary').html(summaryHtml);
}
function validateVerificationChecklist() {
var checkboxes = $('.verification-checklist input[type="checkbox"]:not(#emergencyProtocol):not(#emergencyConsent)');
var checkedCount = checkboxes.filter(':checked').length;
var totalCount = checkboxes.length;
$('#submitBtn').prop('disabled', checkedCount < totalCount || selectedUnits.length === 0);
if (checkedCount === totalCount) {
$('.verification-checklist').removeClass('border-warning').addClass('border-success');
} else {
$('.verification-checklist').removeClass('border-success').addClass('border-warning');
}
}
function validateIssue() {
var errors = [];
// Check selected units
if (selectedUnits.length === 0) {
errors.push('Please select at least one blood unit');
}
// Check required fields
if (!$('#issueDate').val()) {
errors.push('Please enter issue date and time');
}
if (!$('#issuedBy').val()) {
errors.push('Please select who is issuing the blood');
}
if (!$('#receivedBy').val()) {
errors.push('Please select who is receiving the blood');
}
if (!$('#transportMethod').val()) {
errors.push('Please select transport method');
}
if (!$('#transportContainer').val()) {
errors.push('Please select transport container');
}
// Check verification checklist
var requiredCheckboxes = $('.verification-checklist input[type="checkbox"]:not(#emergencyProtocol):not(#emergencyConsent)');
var checkedCount = requiredCheckboxes.filter(':checked').length;
if (checkedCount < requiredCheckboxes.length) {
errors.push('Please complete all verification checklist items');
}
// Check quantity match
var requestedQuantity = {{ blood_request.quantity_requested }};
if (selectedUnits.length !== requestedQuantity) {
errors.push('Selected units (' + selectedUnits.length + ') do not match requested quantity (' + requestedQuantity + ')');
}
// Enable/disable submit button
$('#submitBtn').prop('disabled', errors.length > 0);
if (errors.length > 0) {
if (errors.length < 8) { // Only show errors if validation was explicitly requested
Swal.fire({
icon: 'error',
title: 'Validation Errors',
html: '<ul class="text-start"><li>' + errors.join('</li><li>') + '</li></ul>',
confirmButtonText: 'OK'
});
}
return false;
}
Swal.fire({
icon: 'success',
title: 'Validation Passed',
text: 'All requirements met. Ready to issue blood units.',
timer: 1500,
showConfirmButton: false
});
return true;
}
</script>
{% endblock %}

View File

@ -0,0 +1,542 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Delete QC Record - {{ qc_record.test_name }}{% endblock %}
{% block extra_css %}
<style>
.delete-warning {
background: #f8d7da;
border: 2px solid #dc3545;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.8; }
100% { opacity: 1; }
}
.qc-info {
background: #d1ecf1;
border-left: 4px solid #17a2b8;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.impact-assessment {
background: #fff3cd;
border-left: 4px solid #ffc107;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.compliance-warning {
background: #f8d7da;
border: 2px solid #dc3545;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.deletion-options {
background: #d4edda;
border-left: 4px solid #28a745;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.required-field {
color: #dc3545;
}
.regulatory-alert {
background: #dc3545;
color: white;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
text-align: center;
font-weight: bold;
}
</style>
{% endblock %}
{% block content %}
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'blood_bank:dashboard' %}">Blood Bank</a></li>
<li class="breadcrumb-item"><a href="{% url 'blood_bank:quality_control_list' %}">Quality Control</a></li>
<li class="breadcrumb-item"><a href="{% url 'blood_bank:quality_control_detail' qc_record.id %}">{{ qc_record.test_name }}</a></li>
<li class="breadcrumb-item active">Delete Record</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">
Delete QC Record
<small>{{ qc_record.test_name }}</small>
{% if qc_record.result == 'failed' %}
<span class="badge bg-danger ms-2">FAILED TEST</span>
{% endif %}
</h1>
<!-- END page-header -->
<!-- BEGIN regulatory alert -->
<div class="regulatory-alert">
<i class="fa fa-exclamation-triangle fa-2x"></i><br>
WARNING: Deleting QC records may violate regulatory compliance requirements.<br>
Consider archiving instead of permanent deletion.
</div>
<!-- END regulatory alert -->
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fa fa-trash"></i> Delete Quality Control Record
</h4>
<div class="panel-heading-btn">
<span class="badge bg-danger">CRITICAL ACTION</span>
</div>
</div>
<div class="panel-body">
<!-- BEGIN delete warning -->
<div class="delete-warning">
<h5><i class="fa fa-exclamation-triangle"></i> CRITICAL ACTION REQUIRED</h5>
<p class="mb-2"><strong>You are about to permanently delete a quality control record.</strong></p>
<p class="mb-0">This action will:</p>
<ul class="mb-0">
<li>Permanently remove the QC test record from the system</li>
<li>Delete all associated test results and data</li>
<li>Remove compliance documentation</li>
<li>Impact regulatory audit trails</li>
<li>Potentially violate FDA, AABB, and CAP requirements</li>
{% if qc_record.result == 'failed' %}
<li><strong>Delete critical failure documentation</strong></li>
{% endif %}
{% if qc_record.capa_initiated %}
<li><strong>Affect active CAPA investigations</strong></li>
{% endif %}
</ul>
</div>
<!-- END delete warning -->
<!-- BEGIN QC record information -->
<div class="qc-info">
<h5><i class="fa fa-clipboard-check"></i> QC Record Information</h5>
<div class="row">
<div class="col-md-6">
<table class="table table-borderless mb-0">
<tr>
<td class="fw-bold">Test Name:</td>
<td>{{ qc_record.test_name }}</td>
</tr>
<tr>
<td class="fw-bold">Test Type:</td>
<td>{{ qc_record.get_test_type_display }}</td>
</tr>
<tr>
<td class="fw-bold">Test Date:</td>
<td>{{ qc_record.test_date|date:"M d, Y H:i" }}</td>
</tr>
<tr>
<td class="fw-bold">Tested By:</td>
<td>{{ qc_record.tested_by.get_full_name }}</td>
</tr>
</table>
</div>
<div class="col-md-6">
<table class="table table-borderless mb-0">
<tr>
<td class="fw-bold">Result:</td>
<td>
<span class="badge bg-{% if qc_record.result == 'passed' %}success{% elif qc_record.result == 'failed' %}danger{% else %}warning{% endif %}">
{{ qc_record.get_result_display }}
</span>
</td>
</tr>
<tr>
<td class="fw-bold">Sample ID:</td>
<td>{{ qc_record.sample_id|default:"Not specified" }}</td>
</tr>
<tr>
<td class="fw-bold">Equipment:</td>
<td>{{ qc_record.equipment_used|default:"Not specified" }}</td>
</tr>
<tr>
<td class="fw-bold">Reviewed:</td>
<td>
{% if qc_record.reviewed_by %}
<span class="text-success">Yes - {{ qc_record.reviewed_by.get_full_name }}</span>
{% else %}
<span class="text-warning">Pending Review</span>
{% endif %}
</td>
</tr>
</table>
</div>
</div>
{% if qc_record.test_notes %}
<div class="mt-3">
<h6>Test Notes:</h6>
<div class="alert alert-info">{{ qc_record.test_notes }}</div>
</div>
{% endif %}
</div>
<!-- END QC record information -->
<!-- BEGIN compliance warning -->
<div class="compliance-warning">
<h5><i class="fa fa-shield-alt"></i> Regulatory Compliance Impact</h5>
<div class="row">
<div class="col-md-6">
<h6>Affected Standards</h6>
<ul>
<li><strong>FDA 21 CFR Part 606:</strong> QC record retention requirements</li>
<li><strong>AABB Standards:</strong> Quality documentation requirements</li>
<li><strong>ISO 15189:</strong> Quality management system records</li>
<li><strong>CAP Requirements:</strong> Laboratory quality assurance</li>
</ul>
</div>
<div class="col-md-6">
<h6>Compliance Risks</h6>
<ul>
<li>Loss of audit trail integrity</li>
<li>Regulatory inspection findings</li>
<li>Accreditation issues</li>
<li>Legal liability concerns</li>
{% if qc_record.result == 'failed' %}
<li><strong>Loss of failure investigation records</strong></li>
{% endif %}
</ul>
</div>
</div>
{% if qc_record.result == 'failed' %}
<div class="alert alert-danger mt-3">
<strong><i class="fa fa-exclamation-triangle"></i> CRITICAL:</strong>
This is a failed QC test. Deleting this record may violate regulatory requirements for failure investigation documentation.
</div>
{% endif %}
{% if qc_record.capa_initiated %}
<div class="alert alert-warning mt-3">
<strong><i class="fa fa-exclamation-circle"></i> ACTIVE CAPA:</strong>
This record is associated with an active CAPA ({{ qc_record.capa_number }}). Deletion may impact the investigation.
</div>
{% endif %}
</div>
<!-- END compliance warning -->
<!-- BEGIN impact assessment -->
<div class="impact-assessment">
<h5><i class="fa fa-exclamation-circle"></i> Impact Assessment</h5>
<div class="row">
<div class="col-md-6">
<h6>Related Records</h6>
<ul>
{% if qc_record.blood_unit %}
<li><strong>Blood Unit:</strong> {{ qc_record.blood_unit.unit_number }}</li>
{% endif %}
{% if qc_record.equipment_used %}
<li><strong>Equipment:</strong> {{ qc_record.equipment_used }}</li>
{% endif %}
<li><strong>Test Results:</strong> All parameter data will be lost</li>
<li><strong>Trend Data:</strong> Historical trending will be affected</li>
</ul>
</div>
<div class="col-md-6">
<h6>System Impact</h6>
<ul>
<li>QC statistics and reports will be affected</li>
<li>Trend analysis data will be incomplete</li>
<li>Audit trail will show deletion event</li>
<li>Compliance reports may be impacted</li>
{% if qc_record.result == 'failed' %}
<li><strong>Failure rate calculations will change</strong></li>
{% endif %}
</ul>
</div>
</div>
</div>
<!-- END impact assessment -->
<!-- BEGIN deletion form -->
<form method="post" id="deletionForm">
{% csrf_token %}
<div class="deletion-options">
<h5><i class="fa fa-clipboard-list"></i> Deletion Authorization</h5>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="deletionReason" class="form-label">
Deletion Reason <span class="required-field">*</span>
</label>
<select class="form-select" id="deletionReason" name="deletion_reason" required>
<option value="">Select reason...</option>
<option value="data_entry_error">Data entry error</option>
<option value="duplicate_record">Duplicate record</option>
<option value="test_invalidated">Test invalidated</option>
<option value="equipment_malfunction">Equipment malfunction</option>
<option value="sample_contamination">Sample contamination</option>
<option value="procedural_error">Procedural error</option>
<option value="regulatory_approval">Regulatory approval obtained</option>
<option value="other">Other (specify below)</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="authorizedBy" class="form-label">
Authorized By <span class="required-field">*</span>
</label>
<select class="form-select" id="authorizedBy" name="authorized_by" required>
<option value="">Select authorizing person...</option>
{% for supervisor in supervisors %}
<option value="{{ supervisor.id }}">{{ supervisor.get_full_name }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="authorizationCode" class="form-label">
Authorization Code <span class="required-field">*</span>
</label>
<input type="text" class="form-control" id="authorizationCode" name="authorization_code"
placeholder="Enter supervisor authorization code" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="regulatoryApproval" class="form-label">
Regulatory Approval
</label>
<input type="text" class="form-control" id="regulatoryApproval" name="regulatory_approval"
placeholder="Reference number (if applicable)">
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="form-group">
<label for="deletionJustification" class="form-label">
Detailed Justification <span class="required-field">*</span>
</label>
<textarea class="form-control" id="deletionJustification" name="deletion_justification"
rows="4" required placeholder="Provide detailed justification for deletion, including regulatory compliance considerations..."></textarea>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="alternativeAction" class="form-label">Alternative Action Considered</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="archiveConsidered" name="archive_considered">
<label class="form-check-label" for="archiveConsidered">
Archive record instead of deletion
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="correctionConsidered" name="correction_considered">
<label class="form-check-label" for="correctionConsidered">
Correct data instead of deletion
</label>
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="mitigationMeasures" class="form-label">Mitigation Measures</label>
<textarea class="form-control" id="mitigationMeasures" name="mitigation_measures"
rows="3" placeholder="Describe measures to prevent similar issues..."></textarea>
</div>
</div>
</div>
</div>
<!-- BEGIN confirmation checklist -->
<div class="alert alert-danger">
<h6><i class="fa fa-check-square"></i> Critical Confirmation Checklist</h6>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="confirmRegulatory" required>
<label class="form-check-label" for="confirmRegulatory">
I confirm that this deletion complies with all regulatory requirements
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="confirmAuditTrail" required>
<label class="form-check-label" for="confirmAuditTrail">
I understand that this action will be permanently recorded in the audit trail
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="confirmImpact" required>
<label class="form-check-label" for="confirmImpact">
I acknowledge the impact on compliance and quality documentation
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="confirmAuthorization" required>
<label class="form-check-label" for="confirmAuthorization">
I have proper authorization to perform this critical action
</label>
</div>
{% if qc_record.result == 'failed' %}
<div class="form-check">
<input class="form-check-input" type="checkbox" id="confirmFailureRecord" required>
<label class="form-check-label" for="confirmFailureRecord">
I understand that deleting a failed QC record may violate regulatory requirements
</label>
</div>
{% endif %}
{% if qc_record.capa_initiated %}
<div class="form-check">
<input class="form-check-input" type="checkbox" id="confirmCapa" required>
<label class="form-check-label" for="confirmCapa">
I acknowledge that this record is associated with an active CAPA investigation
</label>
</div>
{% endif %}
</div>
<!-- END confirmation checklist -->
<!-- BEGIN form actions -->
<div class="d-flex justify-content-between mt-4">
<a href="{% url 'blood_bank:quality_control_detail' qc_record.id %}" class="btn btn-secondary">
<i class="fa fa-arrow-left"></i> Cancel Deletion
</a>
<div>
<button type="button" class="btn btn-warning" onclick="suggestArchive()">
<i class="fa fa-archive"></i> Archive Instead
</button>
<button type="button" class="btn btn-info" onclick="validateDeletion()">
<i class="fa fa-check"></i> Validate Deletion
</button>
<button type="submit" class="btn btn-danger" id="submitBtn" disabled>
<i class="fa fa-trash"></i> Permanently Delete Record
</button>
</div>
</div>
<!-- END form actions -->
</form>
<!-- END deletion form -->
</div>
</div>
<!-- END panel -->
{% endblock %}
{% block extra_js %}
<script>
$(document).ready(function() {
// Form validation
$('.form-check-input, #deletionReason, #authorizedBy, #authorizationCode, #deletionJustification').on('change input', function() {
validateDeletion();
});
$('#deletionForm').on('submit', function(e) {
if (!validateDeletion()) {
e.preventDefault();
} else {
// Final confirmation
if (!confirm('Are you absolutely certain you want to permanently delete this QC record? This action cannot be undone and may violate regulatory requirements.')) {
e.preventDefault();
}
}
});
});
function validateDeletion() {
var errors = [];
// Check required fields
if (!$('#deletionReason').val()) {
errors.push('Please select a deletion reason');
}
if (!$('#authorizedBy').val()) {
errors.push('Please select the authorizing person');
}
if (!$('#authorizationCode').val().trim()) {
errors.push('Please enter the authorization code');
}
if (!$('#deletionJustification').val().trim()) {
errors.push('Please provide detailed justification');
}
// Check confirmation checkboxes
var requiredCheckboxes = ['confirmRegulatory', 'confirmAuditTrail', 'confirmImpact', 'confirmAuthorization'];
{% if qc_record.result == 'failed' %}
requiredCheckboxes.push('confirmFailureRecord');
{% endif %}
{% if qc_record.capa_initiated %}
requiredCheckboxes.push('confirmCapa');
{% endif %}
var uncheckedBoxes = requiredCheckboxes.filter(function(id) {
return !document.getElementById(id).checked;
});
if (uncheckedBoxes.length > 0) {
errors.push('Please confirm all checklist items');
}
// Enable/disable submit button
$('#submitBtn').prop('disabled', errors.length > 0);
if (errors.length > 0 && errors.length < 6) {
// Only show errors if there are just a few (user is actively trying to submit)
return false;
}
return true;
}
function suggestArchive() {
Swal.fire({
title: 'Consider Archiving',
html: `
<div class="text-start">
<p><strong>Archiving is often a better alternative to deletion:</strong></p>
<ul>
<li>Maintains regulatory compliance</li>
<li>Preserves audit trail integrity</li>
<li>Allows future reference if needed</li>
<li>Meets retention requirements</li>
</ul>
<p>Would you like to archive this record instead?</p>
</div>
`,
icon: 'info',
showCancelButton: true,
confirmButtonText: 'Yes, Archive Instead',
cancelButtonText: 'Continue with Deletion'
}).then((result) => {
if (result.isConfirmed) {
// In real implementation, this would redirect to archive function
window.location.href = '{% url "blood_bank:quality_control_detail" qc_record.id %}';
}
});
}
</script>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More