diff --git a/apps/journeys/models.py b/apps/journeys/models.py index af052ce..6e4a5b7 100644 --- a/apps/journeys/models.py +++ b/apps/journeys/models.py @@ -31,7 +31,7 @@ class StageStatus(BaseChoices): class PatientJourneyTemplate(UUIDModel, TimeStampedModel): """ Journey template defines the stages for a patient journey type. - + Example: OPD journey with stages: 1. MD Consultation (trigger: OPD_VISIT_COMPLETED) 2. Lab (trigger: LAB_ORDER_COMPLETED) @@ -46,7 +46,7 @@ class PatientJourneyTemplate(UUIDModel, TimeStampedModel): db_index=True ) description = models.TextField(blank=True) - + # Configuration hospital = models.ForeignKey( 'organizations.Hospital', @@ -59,14 +59,14 @@ class PatientJourneyTemplate(UUIDModel, TimeStampedModel): default=False, help_text="Default template for this journey type in this hospital" ) - + class Meta: ordering = ['hospital', 'journey_type', 'name'] unique_together = [['hospital', 'journey_type', 'name']] indexes = [ models.Index(fields=['hospital', 'journey_type', 'is_active']), ] - + def __str__(self): return f"{self.hospital.name} - {self.get_journey_type_display()} - {self.name}" @@ -74,12 +74,12 @@ class PatientJourneyTemplate(UUIDModel, TimeStampedModel): class PatientJourneyStageTemplate(UUIDModel, TimeStampedModel): """ Stage template defines a stage within a journey. - + Each stage: - Has a trigger_event_code that maps to integration events - Can optionally send a survey when completed - Has an order for sequencing - + Example: "MD Consultation" stage - trigger_event_code: "OPD_VISIT_COMPLETED" - survey_template: MD Consultation Survey @@ -90,7 +90,7 @@ class PatientJourneyStageTemplate(UUIDModel, TimeStampedModel): on_delete=models.CASCADE, related_name='stages' ) - + # Stage identification name = models.CharField(max_length=200) name_ar = models.CharField(max_length=200, blank=True) @@ -102,14 +102,14 @@ class PatientJourneyStageTemplate(UUIDModel, TimeStampedModel): default=0, help_text="Order of this stage in the journey" ) - + # Event trigger configuration trigger_event_code = models.CharField( max_length=100, help_text="Event code that triggers completion of this stage (e.g., OPD_VISIT_COMPLETED)", db_index=True ) - + # Survey configuration survey_template = models.ForeignKey( 'surveys.SurveyTemplate', @@ -127,7 +127,7 @@ class PatientJourneyStageTemplate(UUIDModel, TimeStampedModel): default=0, help_text="Hours to wait before sending survey (0 = immediate)" ) - + # Requirements requires_physician = models.BooleanField( default=False, @@ -137,16 +137,16 @@ class PatientJourneyStageTemplate(UUIDModel, TimeStampedModel): default=False, help_text="Does this stage require department information?" ) - + # Configuration is_optional = models.BooleanField( default=False, help_text="Can this stage be skipped?" ) is_active = models.BooleanField(default=True) - + description = models.TextField(blank=True) - + class Meta: ordering = ['journey_template', 'order'] unique_together = [['journey_template', 'code']] @@ -154,7 +154,7 @@ class PatientJourneyStageTemplate(UUIDModel, TimeStampedModel): models.Index(fields=['journey_template', 'order']), models.Index(fields=['trigger_event_code']), ] - + def __str__(self): return f"{self.journey_template.name} - {self.name} (Order: {self.order})" @@ -162,7 +162,7 @@ class PatientJourneyStageTemplate(UUIDModel, TimeStampedModel): class PatientJourneyInstance(UUIDModel, TimeStampedModel): """ Journey instance tracks an actual patient's journey through a pathway. - + Linked to: - Journey template (defines the stages) - Patient (who is going through the journey) @@ -173,7 +173,7 @@ class PatientJourneyInstance(UUIDModel, TimeStampedModel): on_delete=models.PROTECT, related_name='instances' ) - + # Patient and encounter information patient = models.ForeignKey( 'organizations.Patient', @@ -186,7 +186,7 @@ class PatientJourneyInstance(UUIDModel, TimeStampedModel): db_index=True, help_text="Unique encounter ID from HIS system" ) - + # Journey metadata hospital = models.ForeignKey( 'organizations.Hospital', @@ -200,7 +200,7 @@ class PatientJourneyInstance(UUIDModel, TimeStampedModel): blank=True, related_name='journey_instances' ) - + # Status tracking status = models.CharField( max_length=20, @@ -208,18 +208,18 @@ class PatientJourneyInstance(UUIDModel, TimeStampedModel): default=StatusChoices.ACTIVE, db_index=True ) - + # Timestamps started_at = models.DateTimeField(auto_now_add=True) completed_at = models.DateTimeField(null=True, blank=True) - + # Metadata metadata = models.JSONField( default=dict, blank=True, help_text="Additional metadata from HIS system" ) - + class Meta: ordering = ['-started_at'] indexes = [ @@ -227,10 +227,10 @@ class PatientJourneyInstance(UUIDModel, TimeStampedModel): models.Index(fields=['patient', '-started_at']), models.Index(fields=['hospital', 'status', '-started_at']), ] - + def __str__(self): return f"{self.patient.get_full_name()} - {self.journey_template.name} ({self.encounter_id})" - + def get_completion_percentage(self): """Calculate journey completion percentage""" total_stages = self.stage_instances.count() @@ -238,7 +238,7 @@ class PatientJourneyInstance(UUIDModel, TimeStampedModel): return 0 completed_stages = self.stage_instances.filter(status=StageStatus.COMPLETED).count() return int((completed_stages / total_stages) * 100) - + def is_complete(self): """Check if all required stages are completed""" required_stages = self.stage_instances.filter( @@ -250,14 +250,14 @@ class PatientJourneyInstance(UUIDModel, TimeStampedModel): class PatientJourneyStageInstance(UUIDModel, TimeStampedModel): """ Stage instance tracks completion of a specific stage in a patient's journey. - + Status flow: 1. PENDING - Stage created but not started 2. IN_PROGRESS - Event received, processing 3. COMPLETED - Stage completed successfully 4. SKIPPED - Stage skipped (optional stages only) 5. CANCELLED - Journey cancelled - + When completed: - If auto_send_survey=True, create SurveyInstance - Record completion timestamp @@ -273,7 +273,7 @@ class PatientJourneyStageInstance(UUIDModel, TimeStampedModel): on_delete=models.PROTECT, related_name='instances' ) - + # Status status = models.CharField( max_length=20, @@ -281,7 +281,7 @@ class PatientJourneyStageInstance(UUIDModel, TimeStampedModel): default=StageStatus.PENDING, db_index=True ) - + # Completion details completed_at = models.DateTimeField(null=True, blank=True, db_index=True) completed_by_event = models.ForeignKey( @@ -292,7 +292,7 @@ class PatientJourneyStageInstance(UUIDModel, TimeStampedModel): related_name='completed_stages', help_text="Integration event that completed this stage" ) - + # Context from event physician = models.ForeignKey( 'organizations.Physician', @@ -310,7 +310,7 @@ class PatientJourneyStageInstance(UUIDModel, TimeStampedModel): related_name='journey_stages', help_text="Department where this stage occurred" ) - + # Survey tracking survey_instance = models.ForeignKey( 'surveys.SurveyInstance', @@ -321,14 +321,14 @@ class PatientJourneyStageInstance(UUIDModel, TimeStampedModel): help_text="Survey instance created for this stage" ) survey_sent_at = models.DateTimeField(null=True, blank=True) - + # Metadata metadata = models.JSONField( default=dict, blank=True, help_text="Additional data from integration event" ) - + class Meta: ordering = ['journey_instance', 'stage_template__order'] unique_together = [['journey_instance', 'stage_template']] @@ -336,18 +336,18 @@ class PatientJourneyStageInstance(UUIDModel, TimeStampedModel): models.Index(fields=['journey_instance', 'status']), models.Index(fields=['status', 'completed_at']), ] - + def __str__(self): return f"{self.journey_instance.encounter_id} - {self.stage_template.name} ({self.status})" - + def can_complete(self): """Check if this stage can be completed""" return self.status in [StageStatus.PENDING, StageStatus.IN_PROGRESS] - + def complete(self, event=None, physician=None, department=None, metadata=None): """ Mark stage as completed. - + This method should be called by the event processing task. It will: 1. Update status to COMPLETED @@ -356,27 +356,27 @@ class PatientJourneyStageInstance(UUIDModel, TimeStampedModel): 4. Trigger survey creation if configured """ from django.utils import timezone - + if not self.can_complete(): return False - + self.status = StageStatus.COMPLETED self.completed_at = timezone.now() self.completed_by_event = event - + if physician: self.physician = physician if department: self.department = department if metadata: self.metadata.update(metadata) - + self.save() - + # Check if journey is complete if self.journey_instance.is_complete(): self.journey_instance.status = StatusChoices.COMPLETED self.journey_instance.completed_at = timezone.now() self.journey_instance.save() - + return True diff --git a/apps/organizations/models.py b/apps/organizations/models.py index c8b33b9..04c4f41 100644 --- a/apps/organizations/models.py +++ b/apps/organizations/models.py @@ -11,13 +11,13 @@ class Hospital(UUIDModel, TimeStampedModel): name = models.CharField(max_length=200) name_ar = models.CharField(max_length=200, blank=True, verbose_name="Name (Arabic)") code = models.CharField(max_length=50, unique=True, db_index=True) - + # Contact information address = models.TextField(blank=True) city = models.CharField(max_length=100, blank=True) phone = models.CharField(max_length=20, blank=True) email = models.EmailField(blank=True) - + # Status status = models.CharField( max_length=20, @@ -25,28 +25,30 @@ class Hospital(UUIDModel, TimeStampedModel): default=StatusChoices.ACTIVE, db_index=True ) - + # Metadata license_number = models.CharField(max_length=100, blank=True) capacity = models.IntegerField(null=True, blank=True, help_text="Bed capacity") + metadata = models.JSONField(default=dict, blank=True) - + class Meta: ordering = ['name'] verbose_name_plural = 'Hospitals' - + def __str__(self): return self.name +# TODO: Add branch class Department(UUIDModel, TimeStampedModel): """Department within a hospital""" hospital = models.ForeignKey(Hospital, on_delete=models.CASCADE, related_name='departments') - + name = models.CharField(max_length=200) name_ar = models.CharField(max_length=200, blank=True, verbose_name="Name (Arabic)") code = models.CharField(max_length=50, db_index=True) - + # Hierarchy parent = models.ForeignKey( 'self', @@ -55,7 +57,7 @@ class Department(UUIDModel, TimeStampedModel): blank=True, related_name='sub_departments' ) - + # Manager manager = models.ForeignKey( 'accounts.User', @@ -64,12 +66,12 @@ class Department(UUIDModel, TimeStampedModel): blank=True, related_name='managed_departments' ) - + # Contact phone = models.CharField(max_length=20, blank=True) email = models.EmailField(blank=True) location = models.CharField(max_length=200, blank=True, help_text="Building/Floor/Room") - + # Status status = models.CharField( max_length=20, @@ -77,11 +79,11 @@ class Department(UUIDModel, TimeStampedModel): default=StatusChoices.ACTIVE, db_index=True ) - + class Meta: ordering = ['hospital', 'name'] unique_together = [['hospital', 'code']] - + def __str__(self): return f"{self.hospital.name} - {self.name}" @@ -96,17 +98,17 @@ class Physician(UUIDModel, TimeStampedModel): blank=True, related_name='physician_profile' ) - + # Basic information first_name = models.CharField(max_length=100) last_name = models.CharField(max_length=100) first_name_ar = models.CharField(max_length=100, blank=True) last_name_ar = models.CharField(max_length=100, blank=True) - + # Professional information license_number = models.CharField(max_length=100, unique=True, db_index=True) specialization = models.CharField(max_length=200) - + # Organization hospital = models.ForeignKey(Hospital, on_delete=models.CASCADE, related_name='physicians') department = models.ForeignKey( @@ -116,11 +118,11 @@ class Physician(UUIDModel, TimeStampedModel): blank=True, related_name='physicians' ) - + # Contact phone = models.CharField(max_length=20, blank=True) email = models.EmailField(blank=True) - + # Status status = models.CharField( max_length=20, @@ -128,13 +130,13 @@ class Physician(UUIDModel, TimeStampedModel): default=StatusChoices.ACTIVE, db_index=True ) - + class Meta: ordering = ['last_name', 'first_name'] - + def __str__(self): return f"Dr. {self.first_name} {self.last_name}" - + def get_full_name(self): return f"{self.first_name} {self.last_name}" @@ -146,7 +148,7 @@ class Employee(UUIDModel, TimeStampedModel): on_delete=models.CASCADE, related_name='employee_profile' ) - + # Organization hospital = models.ForeignKey(Hospital, on_delete=models.CASCADE, related_name='employees') department = models.ForeignKey( @@ -156,12 +158,12 @@ class Employee(UUIDModel, TimeStampedModel): blank=True, related_name='employees' ) - + # Job information employee_id = models.CharField(max_length=50, unique=True, db_index=True) job_title = models.CharField(max_length=200) hire_date = models.DateField(null=True, blank=True) - + # Status status = models.CharField( max_length=20, @@ -169,10 +171,10 @@ class Employee(UUIDModel, TimeStampedModel): default=StatusChoices.ACTIVE, db_index=True ) - + class Meta: ordering = ['user__last_name', 'user__first_name'] - + def __str__(self): return f"{self.user.get_full_name()} - {self.job_title}" @@ -182,12 +184,12 @@ class Patient(UUIDModel, TimeStampedModel): # Basic information mrn = models.CharField(max_length=50, unique=True, db_index=True, verbose_name="Medical Record Number") national_id = models.CharField(max_length=50, blank=True, db_index=True) - + first_name = models.CharField(max_length=100) last_name = models.CharField(max_length=100) first_name_ar = models.CharField(max_length=100, blank=True) last_name_ar = models.CharField(max_length=100, blank=True) - + # Demographics date_of_birth = models.DateField(null=True, blank=True) gender = models.CharField( @@ -195,13 +197,13 @@ class Patient(UUIDModel, TimeStampedModel): choices=[('male', 'Male'), ('female', 'Female'), ('other', 'Other')], blank=True ) - + # Contact phone = models.CharField(max_length=20, blank=True) email = models.EmailField(blank=True) address = models.TextField(blank=True) city = models.CharField(max_length=100, blank=True) - + # Primary hospital primary_hospital = models.ForeignKey( Hospital, @@ -210,7 +212,7 @@ class Patient(UUIDModel, TimeStampedModel): blank=True, related_name='patients' ) - + # Status status = models.CharField( max_length=20, @@ -218,12 +220,12 @@ class Patient(UUIDModel, TimeStampedModel): default=StatusChoices.ACTIVE, db_index=True ) - + class Meta: ordering = ['last_name', 'first_name'] - + def __str__(self): return f"{self.first_name} {self.last_name} (MRN: {self.mrn})" - + def get_full_name(self): return f"{self.first_name} {self.last_name}" diff --git a/config/settings/dev.py b/config/settings/dev.py index c40b2e7..f9c28c8 100644 --- a/config/settings/dev.py +++ b/config/settings/dev.py @@ -43,4 +43,4 @@ LOGGING['loggers']['apps']['level'] = 'DEBUG' # noqa # Disable some security features for development SECURE_SSL_REDIRECT = False SESSION_COOKIE_SECURE = False -CSRF_COOKIE_SECURE = False +CSRF_COOKIE_SECURE = False \ No newline at end of file