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 hr.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})"