This commit is contained in:
ismail 2025-12-31 13:32:00 +03:00
parent fef9abdd4f
commit 7d56370811
3 changed files with 78 additions and 76 deletions

View File

@ -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

View File

@ -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}"

View File

@ -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