1
This commit is contained in:
parent
fef9abdd4f
commit
7d56370811
@ -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
|
||||
|
||||
@ -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}"
|
||||
|
||||
@ -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
|
||||
Loading…
x
Reference in New Issue
Block a user