388 lines
12 KiB
Python
388 lines
12 KiB
Python
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"
|
|
|