543 lines
16 KiB
Python
543 lines
16 KiB
Python
"""
|
|
Medical models for the Tenhal Multidisciplinary Healthcare Platform.
|
|
|
|
This module handles medical consultations and follow-ups based on
|
|
MD-F-1 and MD-F-2 forms.
|
|
"""
|
|
|
|
from django.db import models
|
|
from django.utils.translation import gettext_lazy as _
|
|
from simple_history.models import HistoricalRecords
|
|
|
|
from core.models import (
|
|
UUIDPrimaryKeyMixin,
|
|
TimeStampedMixin,
|
|
TenantOwnedMixin,
|
|
ClinicallySignableMixin,
|
|
)
|
|
|
|
|
|
class MedicalConsultation(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin, ClinicallySignableMixin):
|
|
"""
|
|
Medical consultation form (MD-F-1).
|
|
Comprehensive medical history and assessment.
|
|
"""
|
|
|
|
# Core Relationships
|
|
patient = models.ForeignKey(
|
|
'core.Patient',
|
|
on_delete=models.CASCADE,
|
|
related_name='medical_consultations',
|
|
verbose_name=_("Patient")
|
|
)
|
|
appointment = models.ForeignKey(
|
|
'appointments.Appointment',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='medical_consultations',
|
|
verbose_name=_("Appointment")
|
|
)
|
|
consultation_date = models.DateField(
|
|
verbose_name=_("Consultation Date")
|
|
)
|
|
provider = models.ForeignKey(
|
|
'core.User',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
related_name='medical_consultations_provided',
|
|
verbose_name=_("Provider")
|
|
)
|
|
|
|
# History Sections
|
|
chief_complaint = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Chief Complaint")
|
|
)
|
|
present_illness_history = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("History of Present Illness")
|
|
)
|
|
past_medical_history = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Past Medical History")
|
|
)
|
|
vaccination_status = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Vaccination Status")
|
|
)
|
|
family_history = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Family History")
|
|
)
|
|
social_history = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Social History")
|
|
)
|
|
pregnancy_history = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Pregnancy History")
|
|
)
|
|
neonatal_history = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Neonatal History")
|
|
)
|
|
|
|
# Developmental Milestones
|
|
developmental_motor_milestones = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Developmental Motor Milestones")
|
|
)
|
|
developmental_language_milestones = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Developmental Language Milestones")
|
|
)
|
|
developmental_social_milestones = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Developmental Social Milestones")
|
|
)
|
|
developmental_cognitive_milestones = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Developmental Cognitive Milestones")
|
|
)
|
|
|
|
# Behavioral Symptoms (JSONField - checklist)
|
|
behavioral_symptoms = models.JSONField(
|
|
default=dict,
|
|
blank=True,
|
|
help_text=_("Behavioral symptoms checklist with yes/no/notes"),
|
|
verbose_name=_("Behavioral Symptoms")
|
|
)
|
|
|
|
# Physical Exam (JSONField - structured)
|
|
physical_exam = models.JSONField(
|
|
default=dict,
|
|
blank=True,
|
|
help_text=_("Structured physical examination findings"),
|
|
verbose_name=_("Physical Exam")
|
|
)
|
|
|
|
# Assessment & Plan
|
|
clinical_summary = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Clinical Summary")
|
|
)
|
|
recommendations = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Recommendations")
|
|
)
|
|
referrals_needed = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Referrals Needed")
|
|
)
|
|
|
|
# Medications (JSONField - like target skills)
|
|
medications = models.JSONField(
|
|
default=list,
|
|
blank=True,
|
|
help_text=_("List of current medications with compliance and effectiveness"),
|
|
verbose_name=_("Medications")
|
|
)
|
|
|
|
# Lab & Radiology Orders
|
|
lab_orders = models.JSONField(
|
|
default=list,
|
|
blank=True,
|
|
help_text=_("List of lab tests ordered"),
|
|
verbose_name=_("Lab Orders")
|
|
)
|
|
radiology_orders = models.JSONField(
|
|
default=list,
|
|
blank=True,
|
|
help_text=_("List of radiology studies ordered"),
|
|
verbose_name=_("Radiology Orders")
|
|
)
|
|
|
|
history = HistoricalRecords()
|
|
|
|
@property
|
|
def medications_json(self):
|
|
"""Return medications as JSON string for form field."""
|
|
import json
|
|
return json.dumps(self.medications) if self.medications else '[]'
|
|
|
|
class Meta:
|
|
verbose_name = _("Medical Consultation")
|
|
verbose_name_plural = _("Medical Consultations")
|
|
ordering = ['-consultation_date', '-created_at']
|
|
indexes = [
|
|
models.Index(fields=['patient', 'consultation_date']),
|
|
models.Index(fields=['provider', 'consultation_date']),
|
|
models.Index(fields=['tenant', 'consultation_date']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"Medical Consultation - {self.patient} - {self.consultation_date}"
|
|
|
|
|
|
class MedicationPlan(UUIDPrimaryKeyMixin):
|
|
"""
|
|
Medication details for a medical consultation.
|
|
Tracks current medications, compliance, and effectiveness.
|
|
"""
|
|
|
|
class Frequency(models.TextChoices):
|
|
DAILY = 'DAILY', _('Daily')
|
|
BID = 'BID', _('BID (Twice daily)')
|
|
TID = 'TID', _('TID (Three times daily)')
|
|
QID = 'QID', _('QID (Four times daily)')
|
|
PRN = 'PRN', _('PRN (As needed)')
|
|
OTHER = 'OTHER', _('Other')
|
|
|
|
class Compliance(models.TextChoices):
|
|
GOOD = 'GOOD', _('Good')
|
|
PARTIAL = 'PARTIAL', _('Partial')
|
|
BAD = 'BAD', _('Bad')
|
|
|
|
consultation = models.ForeignKey(
|
|
MedicalConsultation,
|
|
on_delete=models.CASCADE,
|
|
related_name='medication_plans_old',
|
|
verbose_name=_("Consultation")
|
|
)
|
|
drug_name = models.CharField(
|
|
max_length=200,
|
|
verbose_name=_("Drug Name")
|
|
)
|
|
dose = models.CharField(
|
|
max_length=100,
|
|
verbose_name=_("Dose")
|
|
)
|
|
frequency = models.CharField(
|
|
max_length=20,
|
|
choices=Frequency.choices,
|
|
verbose_name=_("Frequency")
|
|
)
|
|
frequency_other = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
verbose_name=_("Frequency (Other)")
|
|
)
|
|
compliance = models.CharField(
|
|
max_length=20,
|
|
choices=Compliance.choices,
|
|
blank=True,
|
|
verbose_name=_("Compliance")
|
|
)
|
|
gains = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Gains/Benefits")
|
|
)
|
|
side_effects = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Side Effects")
|
|
)
|
|
target_behavior = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Target Behavior")
|
|
)
|
|
improved = models.BooleanField(
|
|
default=False,
|
|
verbose_name=_("Improved")
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = _("Medication Plan")
|
|
verbose_name_plural = _("Medication Plans")
|
|
ordering = ['consultation', 'drug_name']
|
|
|
|
def __str__(self):
|
|
return f"{self.drug_name} - {self.dose} {self.get_frequency_display()}"
|
|
|
|
|
|
class ConsultationResponse(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin):
|
|
"""
|
|
Response to medical consultation from other disciplines.
|
|
Allows OT, SLP, ABA, etc. to respond to medical consultations.
|
|
"""
|
|
|
|
class ResponseType(models.TextChoices):
|
|
OT = 'OT', _('Occupational Therapy')
|
|
SLP = 'SLP', _('Speech-Language Pathology')
|
|
ABA = 'ABA', _('ABA Therapy')
|
|
NURSING = 'NURSING', _('Nursing')
|
|
OTHER = 'OTHER', _('Other')
|
|
|
|
consultation = models.ForeignKey(
|
|
MedicalConsultation,
|
|
on_delete=models.CASCADE,
|
|
related_name='responses',
|
|
verbose_name=_("Consultation")
|
|
)
|
|
response_type = models.CharField(
|
|
max_length=20,
|
|
choices=ResponseType.choices,
|
|
verbose_name=_("Response Type")
|
|
)
|
|
responder = models.ForeignKey(
|
|
'core.User',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
related_name='consultation_responses',
|
|
verbose_name=_("Responder")
|
|
)
|
|
response_date = models.DateField(
|
|
verbose_name=_("Response Date")
|
|
)
|
|
assessment = models.TextField(
|
|
verbose_name=_("Assessment")
|
|
)
|
|
recommendations = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Recommendations")
|
|
)
|
|
follow_up_needed = models.BooleanField(
|
|
default=False,
|
|
verbose_name=_("Follow-up Needed")
|
|
)
|
|
notes = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Additional Notes")
|
|
)
|
|
|
|
history = HistoricalRecords()
|
|
|
|
class Meta:
|
|
verbose_name = _("Consultation Response")
|
|
verbose_name_plural = _("Consultation Responses")
|
|
ordering = ['-response_date', '-created_at']
|
|
indexes = [
|
|
models.Index(fields=['consultation', 'response_date']),
|
|
models.Index(fields=['responder', 'response_date']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.get_response_type_display()} Response to {self.consultation}"
|
|
|
|
|
|
class ConsultationFeedback(UUIDPrimaryKeyMixin, TimeStampedMixin):
|
|
"""
|
|
Feedback on medical consultation.
|
|
Can be from family, caregivers, or interdisciplinary team.
|
|
"""
|
|
|
|
class FeedbackType(models.TextChoices):
|
|
FAMILY = 'FAMILY', _('Family/Caregiver')
|
|
TEAM = 'TEAM', _('Interdisciplinary Team')
|
|
PEER = 'PEER', _('Peer Review')
|
|
SUPERVISOR = 'SUPERVISOR', _('Supervisor')
|
|
|
|
class SatisfactionRating(models.IntegerChoices):
|
|
VERY_DISSATISFIED = 1, _('Very Dissatisfied')
|
|
DISSATISFIED = 2, _('Dissatisfied')
|
|
NEUTRAL = 3, _('Neutral')
|
|
SATISFIED = 4, _('Satisfied')
|
|
VERY_SATISFIED = 5, _('Very Satisfied')
|
|
|
|
consultation = models.ForeignKey(
|
|
MedicalConsultation,
|
|
on_delete=models.CASCADE,
|
|
related_name='feedback',
|
|
verbose_name=_("Consultation")
|
|
)
|
|
feedback_type = models.CharField(
|
|
max_length=20,
|
|
choices=FeedbackType.choices,
|
|
verbose_name=_("Feedback Type")
|
|
)
|
|
submitted_by = models.ForeignKey(
|
|
'core.User',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='consultation_feedback_submitted',
|
|
verbose_name=_("Submitted By")
|
|
)
|
|
submitted_by_name = models.CharField(
|
|
max_length=200,
|
|
blank=True,
|
|
verbose_name=_("Submitted By Name"),
|
|
help_text=_("For family/caregiver feedback")
|
|
)
|
|
feedback_date = models.DateField(
|
|
verbose_name=_("Feedback Date")
|
|
)
|
|
satisfaction_rating = models.IntegerField(
|
|
choices=SatisfactionRating.choices,
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("Satisfaction Rating")
|
|
)
|
|
communication_rating = models.IntegerField(
|
|
choices=SatisfactionRating.choices,
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("Communication Rating")
|
|
)
|
|
care_quality_rating = models.IntegerField(
|
|
choices=SatisfactionRating.choices,
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("Care Quality Rating")
|
|
)
|
|
comments = models.TextField(
|
|
verbose_name=_("Comments")
|
|
)
|
|
concerns = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Concerns/Issues")
|
|
)
|
|
suggestions = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Suggestions for Improvement")
|
|
)
|
|
|
|
history = HistoricalRecords()
|
|
|
|
class Meta:
|
|
verbose_name = _("Consultation Feedback")
|
|
verbose_name_plural = _("Consultation Feedback")
|
|
ordering = ['-feedback_date', '-created_at']
|
|
indexes = [
|
|
models.Index(fields=['consultation', 'feedback_date']),
|
|
models.Index(fields=['feedback_type']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.get_feedback_type_display()} Feedback for {self.consultation}"
|
|
|
|
@property
|
|
def average_rating(self):
|
|
"""Calculate average rating across all rating fields."""
|
|
ratings = [
|
|
r for r in [
|
|
self.satisfaction_rating,
|
|
self.communication_rating,
|
|
self.care_quality_rating
|
|
] if r is not None
|
|
]
|
|
return sum(ratings) / len(ratings) if ratings else None
|
|
|
|
|
|
class MedicalFollowUp(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin, ClinicallySignableMixin):
|
|
"""
|
|
Medical follow-up form (MD-F-2).
|
|
Tracks progress since previous consultation.
|
|
"""
|
|
|
|
class FamilySatisfaction(models.TextChoices):
|
|
ZERO = '0', _('0%')
|
|
FIFTY = '50', _('50%')
|
|
HUNDRED = '100', _('100%')
|
|
|
|
# Core Relationships
|
|
patient = models.ForeignKey(
|
|
'core.Patient',
|
|
on_delete=models.CASCADE,
|
|
related_name='medical_followups',
|
|
verbose_name=_("Patient")
|
|
)
|
|
appointment = models.ForeignKey(
|
|
'appointments.Appointment',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='medical_followups',
|
|
verbose_name=_("Appointment")
|
|
)
|
|
previous_consultation = models.ForeignKey(
|
|
MedicalConsultation,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='followups',
|
|
verbose_name=_("Previous Consultation")
|
|
)
|
|
followup_date = models.DateField(
|
|
verbose_name=_("Follow-up Date")
|
|
)
|
|
provider = models.ForeignKey(
|
|
'core.User',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
related_name='medical_followups_provided',
|
|
verbose_name=_("Provider")
|
|
)
|
|
|
|
# Previous Complaints Status
|
|
previous_complaints_status = models.JSONField(
|
|
default=dict,
|
|
blank=True,
|
|
help_text=_("Status of previous complaints: complaint → RESOLVED/STATIC/WORSE"),
|
|
verbose_name=_("Previous Complaints Status")
|
|
)
|
|
new_complaints = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("New Complaints")
|
|
)
|
|
|
|
# Link to Nursing Vitals
|
|
nursing_vitals = models.ForeignKey(
|
|
'nursing.NursingEncounter',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='medical_followups',
|
|
verbose_name=_("Nursing Vitals")
|
|
)
|
|
|
|
# Assessment
|
|
assessment = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Assessment")
|
|
)
|
|
recommendations = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Recommendations")
|
|
)
|
|
family_satisfaction = models.CharField(
|
|
max_length=10,
|
|
choices=FamilySatisfaction.choices,
|
|
blank=True,
|
|
verbose_name=_("Family Satisfaction")
|
|
)
|
|
|
|
# Medication Snapshot
|
|
medication_snapshot = models.JSONField(
|
|
default=list,
|
|
blank=True,
|
|
help_text=_("Snapshot of current medications at time of follow-up"),
|
|
verbose_name=_("Medication Snapshot")
|
|
)
|
|
|
|
history = HistoricalRecords()
|
|
|
|
class Meta:
|
|
verbose_name = _("Medical Follow-up")
|
|
verbose_name_plural = _("Medical Follow-ups")
|
|
ordering = ['-followup_date', '-created_at']
|
|
indexes = [
|
|
models.Index(fields=['patient', 'followup_date']),
|
|
models.Index(fields=['previous_consultation']),
|
|
models.Index(fields=['provider', 'followup_date']),
|
|
models.Index(fields=['tenant', 'followup_date']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"Medical Follow-up - {self.patient} - {self.followup_date}"
|
|
|
|
def save(self, *args, **kwargs):
|
|
# Auto-populate medication snapshot from previous consultation if not set
|
|
if self.previous_consultation and not self.medication_snapshot:
|
|
self.medication_snapshot = [
|
|
{
|
|
'drug_name': med.drug_name,
|
|
'dose': med.dose,
|
|
'frequency': med.get_frequency_display(),
|
|
'compliance': med.get_compliance_display() if med.compliance else '',
|
|
'improved': med.improved,
|
|
}
|
|
for med in self.previous_consultation.medication_plans_old.all()
|
|
]
|
|
super().save(*args, **kwargs)
|