""" Occupational Therapy (OT) models for the Tenhal Multidisciplinary Healthcare Platform. This module handles OT consultations and session notes based on OT-F-1 and OT-F-3 forms. Enhanced with comprehensive field-level data capture and scoring system. """ 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 OTConsult(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin, ClinicallySignableMixin): """ OT consultation form (OT-F-1). Initial assessment and evaluation with comprehensive data capture. """ class ReferralReason(models.TextChoices): DIAGNOSIS = 'Diagnosis', _('Multi-disciplinary Team Diagnosis') CONSULTATION = 'Consultation', _('Consultation') ASSESSMENT = 'Assessment', _('Assessment') INTERVENTION = 'Intervention', _('Intervention') PARENT_TRAINING = 'ParentTraining', _('Parent Training') class Recommendation(models.TextChoices): CONTINUE = 'CONTINUE', _('Continue Treatment') DISCHARGE = 'DISCHARGE', _('Discharge') REFER_TO_OTHER = 'REFER_TO_OTHER', _('Refer to Other Service') # Core Relationships patient = models.ForeignKey( 'core.Patient', on_delete=models.CASCADE, related_name='ot_consults', verbose_name=_("Patient") ) appointment = models.ForeignKey( 'appointments.Appointment', on_delete=models.SET_NULL, null=True, blank=True, related_name='ot_consults', 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='ot_consults_provided', verbose_name=_("Provider") ) # Section 2: Reasons for Referral referral_reason = models.CharField( max_length=50, choices=ReferralReason.choices, blank=True, verbose_name=_("Referral Reason") ) # Section 4: Developmental History - Motor Learning & Regression motor_learning_difficulty = models.BooleanField( null=True, blank=True, verbose_name=_("Difficulty Learning New Motor Skills") ) motor_learning_details = models.TextField( blank=True, verbose_name=_("Motor Learning Details") ) motor_skill_regression = models.BooleanField( null=True, blank=True, verbose_name=_("Lost Previously Gained Motor Skills") ) regression_details = models.TextField( blank=True, verbose_name=_("Regression Details") ) # Section 6: Eating/Feeding eats_healthy_variety = models.BooleanField( null=True, blank=True, verbose_name=_("Eats Healthy Variety of Food") ) eats_variety_textures = models.BooleanField( null=True, blank=True, verbose_name=_("Eats Variety of Textures and Flavors") ) participates_family_meals = models.BooleanField( null=True, blank=True, verbose_name=_("Participates in Family Meals") ) eating_comments = models.TextField( blank=True, verbose_name=_("Eating Comments") ) # Section 7: Behavior Comments infant_behavior_comments = models.TextField( blank=True, verbose_name=_("Infant Behavior Comments") ) current_behavior_comments = models.TextField( blank=True, verbose_name=_("Current Behavior Comments") ) # Section 8: Recommendation recommendation = models.CharField( max_length=20, choices=Recommendation.choices, blank=True, verbose_name=_("Recommendation") ) recommendation_notes = models.TextField( blank=True, verbose_name=_("Recommendation Notes") ) # Section 10: Clinician Signature clinician_name = models.CharField( max_length=200, blank=True, verbose_name=_("Clinician Name") ) clinician_signature = models.CharField( max_length=200, blank=True, verbose_name=_("Clinician Signature") ) # Scoring fields (calculated) self_help_score = models.PositiveIntegerField( default=0, verbose_name=_("Self-Help Score") ) behavior_score = models.PositiveIntegerField( default=0, verbose_name=_("Behavior Score") ) developmental_score = models.PositiveIntegerField( default=0, verbose_name=_("Developmental Score") ) eating_score = models.PositiveIntegerField( default=0, verbose_name=_("Eating Score") ) total_score = models.PositiveIntegerField( default=0, verbose_name=_("Total Score") ) score_interpretation = models.CharField( max_length=100, blank=True, verbose_name=_("Score Interpretation") ) history = HistoricalRecords() class Meta: verbose_name = _("OT Consultation") verbose_name_plural = _("OT 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"OT Consultation - {self.patient} - {self.consultation_date}" def calculate_scores(self): """Calculate all scores based on related data.""" # Self-Help Score (max 24) self_help_score = 0 for skill in self.self_help_skills.all(): if skill.response == 'yes': self_help_score += 2 # Behavior Score (max 48) behavior_score = 0 for behavior in self.infant_behaviors.all(): if behavior.response == 'yes': behavior_score += 2 elif behavior.response == 'sometimes': behavior_score += 1 for behavior in self.current_behaviors.all(): if behavior.response == 'yes': behavior_score += 2 elif behavior.response == 'sometimes': behavior_score += 1 # Developmental Score (max 6) developmental_score = 0 required_milestones = self.milestones.filter(is_required=True) for milestone in required_milestones: if milestone.age_achieved: developmental_score += 2 # Eating Score (max 6) eating_score = 0 if self.eats_healthy_variety: eating_score += 2 if self.eats_variety_textures: eating_score += 2 if self.participates_family_meals: eating_score += 2 # Total Score total_score = self_help_score + behavior_score + developmental_score + eating_score # Interpretation if total_score <= 30: interpretation = "⚠️ Needs Immediate Attention" elif total_score <= 60: interpretation = "⚠ Moderate Difficulty - Follow-Up Needed" else: interpretation = "✅ Age-Appropriate Skills" # Update fields self.self_help_score = self_help_score self.behavior_score = behavior_score self.developmental_score = developmental_score self.eating_score = eating_score self.total_score = total_score self.score_interpretation = interpretation return { 'self_help': self_help_score, 'behavior': behavior_score, 'developmental': developmental_score, 'eating': eating_score, 'total': total_score, 'interpretation': interpretation } class OTDifficultyArea(UUIDPrimaryKeyMixin): """ Section 3: Areas of Difficulty (max 3 selections). """ AREA_CHOICES = [ ('sensory', _('Sensory skills')), ('fineMotor', _('Fine motor skills')), ('grossMotor', _('Gross motor skills')), ('oralMotor', _('Oral motor / Feeding')), ('adl', _('ADL Activities')), ('handwriting', _('Handwriting')), ('play', _('Play')), ('social', _('Social skills')), ('selfInjury', _('Self-injurious behavior')), ('disorganized', _('Disorganized behaviors')), ('homeRec', _('Home recommendations')), ('parentEd', _('Parental education')), ] consult = models.ForeignKey( OTConsult, on_delete=models.CASCADE, related_name='difficulty_areas', verbose_name=_("Consultation") ) area = models.CharField( max_length=50, choices=AREA_CHOICES, verbose_name=_("Difficulty Area") ) details = models.TextField( blank=True, verbose_name=_("Details") ) order = models.PositiveSmallIntegerField( default=0, verbose_name=_("Order") ) class Meta: verbose_name = _("OT Difficulty Area") verbose_name_plural = _("OT Difficulty Areas") ordering = ['consult', 'order'] unique_together = [['consult', 'area']] def __str__(self): return f"{self.get_area_display()} - {self.consult}" class OTMilestone(UUIDPrimaryKeyMixin): """ Section 4: Developmental History - Motor Milestones. """ MILESTONE_CHOICES = [ ('headControl', _('Controlling head')), ('reachObject', _('Reaching for object')), ('rollOver', _('Rolling over both ways')), ('fingerFeed', _('Finger feeding')), ('sitting', _('Sitting alone')), ('pullStand', _('Pulling to stand')), ('crawling', _('Creeping on all fours')), ('drawCircle', _('Drawing a circle')), ('spoon', _('Eating with spoon')), ('cutScissors', _('Cutting with scissors')), ('walking', _('Walking')), ('drinkCup', _('Drinking from a cup')), ('jump', _('Jumping')), ('hop', _('Hopping')), ('hopOneFoot', _('Hopping on one foot')), ('bike', _('Riding a bike')), ] consult = models.ForeignKey( OTConsult, on_delete=models.CASCADE, related_name='milestones', verbose_name=_("Consultation") ) milestone = models.CharField( max_length=50, choices=MILESTONE_CHOICES, verbose_name=_("Milestone") ) age_achieved = models.CharField( max_length=50, blank=True, verbose_name=_("Age Achieved") ) notes = models.TextField( blank=True, verbose_name=_("Notes") ) is_required = models.BooleanField( default=False, verbose_name=_("Required Field") ) class Meta: verbose_name = _("OT Milestone") verbose_name_plural = _("OT Milestones") ordering = ['consult', 'id'] unique_together = [['consult', 'milestone']] def __str__(self): return f"{self.get_milestone_display()} - {self.age_achieved}" class OTSelfHelpSkill(UUIDPrimaryKeyMixin): """ Section 5: Self-Help Skills by age range. """ AGE_RANGE_CHOICES = [ ('8-9', _('8-9 months')), ('12-18', _('12-18 months')), ('18-24', _('18-24 months')), ('2-3', _('2-3 years')), ('3-4', _('3-4 years')), ('5-6', _('5-6 years')), ] RESPONSE_CHOICES = [ ('yes', _('Yes')), ('no', _('No')), ] consult = models.ForeignKey( OTConsult, on_delete=models.CASCADE, related_name='self_help_skills', verbose_name=_("Consultation") ) age_range = models.CharField( max_length=10, choices=AGE_RANGE_CHOICES, verbose_name=_("Age Range") ) skill_name = models.CharField( max_length=200, verbose_name=_("Skill") ) response = models.CharField( max_length=10, choices=RESPONSE_CHOICES, blank=True, verbose_name=_("Response") ) comments = models.TextField( blank=True, verbose_name=_("Comments") ) class Meta: verbose_name = _("OT Self-Help Skill") verbose_name_plural = _("OT Self-Help Skills") ordering = ['consult', 'age_range', 'id'] def __str__(self): return f"{self.age_range}: {self.skill_name}" class OTInfantBehavior(UUIDPrimaryKeyMixin): """ Section 7: Infant Behavior (First 12 Months). """ BEHAVIOR_CHOICES = [ ('cried', _('Cried a lot, fussy, irritable')), ('good', _('Was good, non-demanding')), ('alert', _('Was alert')), ('quiet', _('Was quiet')), ('passive', _('Was passive')), ('active', _('Was active')), ('likedHeld', _('Liked being held')), ('resistedHeld', _('Resisted being held')), ('floppy', _('Was floppy when held')), ('tense', _('Was tense when held')), ('sleepGood', _('Had good sleep patterns')), ('sleepIrregular', _('Had irregular sleep patterns')), ] RESPONSE_CHOICES = [ ('yes', _('Yes')), ('no', _('No')), ('sometimes', _('Sometimes')), ] consult = models.ForeignKey( OTConsult, on_delete=models.CASCADE, related_name='infant_behaviors', verbose_name=_("Consultation") ) behavior = models.CharField( max_length=50, choices=BEHAVIOR_CHOICES, verbose_name=_("Behavior") ) response = models.CharField( max_length=20, choices=RESPONSE_CHOICES, blank=True, verbose_name=_("Response") ) class Meta: verbose_name = _("OT Infant Behavior") verbose_name_plural = _("OT Infant Behaviors") ordering = ['consult', 'id'] unique_together = [['consult', 'behavior']] def __str__(self): return f"{self.get_behavior_display()}: {self.response}" class OTCurrentBehavior(UUIDPrimaryKeyMixin): """ Section 7: Current Behavior. """ BEHAVIOR_CHOICES = [ ('quiet', _('Is mostly quiet')), ('active', _('Is overly active')), ('tires', _('Tires easily')), ('talks', _('Talks constantly')), ('impulsive', _('Is impulsive')), ('restless', _('Is restless')), ('stubborn', _('Is stubborn')), ('resistant', _('Is resistant to change')), ('fights', _('Fights frequently')), ('tantrums', _('Exhibits frequent temper tantrums')), ('clumsy', _('Is clumsy')), ('frustrated', _('Is frustrated easily')), ] RESPONSE_CHOICES = [ ('yes', _('Yes')), ('no', _('No')), ('sometimes', _('Sometimes')), ] consult = models.ForeignKey( OTConsult, on_delete=models.CASCADE, related_name='current_behaviors', verbose_name=_("Consultation") ) behavior = models.CharField( max_length=50, choices=BEHAVIOR_CHOICES, verbose_name=_("Behavior") ) response = models.CharField( max_length=20, choices=RESPONSE_CHOICES, blank=True, verbose_name=_("Response") ) class Meta: verbose_name = _("OT Current Behavior") verbose_name_plural = _("OT Current Behaviors") ordering = ['consult', 'id'] unique_together = [['consult', 'behavior']] def __str__(self): return f"{self.get_behavior_display()}: {self.response}" # ============================================================================ # OT Session Models (OT-F-3) # ============================================================================ class OTSession(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin, ClinicallySignableMixin): """ OT session notes form (OT-F-3). Progress tracking and session documentation. """ class SessionType(models.TextChoices): CONSULT = 'CONSULT', _('Consultation') INDIVIDUAL = 'INDIVIDUAL', _('Individual Session') GROUP = 'GROUP', _('Group Session') PARENT_TRAINING = 'PARENT_TRAINING', _('Parent Training') # Core Relationships patient = models.ForeignKey( 'core.Patient', on_delete=models.CASCADE, related_name='ot_sessions', verbose_name=_("Patient") ) appointment = models.ForeignKey( 'appointments.Appointment', on_delete=models.SET_NULL, null=True, blank=True, related_name='ot_sessions', verbose_name=_("Appointment") ) # NEW: Link to centralized session (for scheduling/capacity management) session = models.ForeignKey( 'appointments.Session', on_delete=models.CASCADE, null=True, blank=True, related_name='ot_notes', verbose_name=_("Session"), help_text=_("Link to centralized session for scheduling") ) # NEW: Link to specific participant (for group sessions) session_participant = models.ForeignKey( 'appointments.SessionParticipant', on_delete=models.CASCADE, null=True, blank=True, related_name='ot_notes', verbose_name=_("Session Participant"), help_text=_("For group sessions: which participant these notes are for") ) session_date = models.DateField( verbose_name=_("Session Date") ) provider = models.ForeignKey( 'core.User', on_delete=models.SET_NULL, null=True, related_name='ot_sessions_provided', verbose_name=_("Provider") ) # Session Details session_type = models.CharField( max_length=20, choices=SessionType.choices, verbose_name=_("Session Type") ) cooperative_level = models.PositiveSmallIntegerField( null=True, blank=True, help_text=_("Cooperative level (1-4 scale)"), verbose_name=_("Cooperative Level (1-4)") ) distraction_tolerance = models.PositiveSmallIntegerField( null=True, blank=True, help_text=_("Distraction tolerance (1-4 scale)"), verbose_name=_("Distraction Tolerance (1-4)") ) # Activities Checklist (TextField: "Today we work on...") activities_checklist = models.TextField( blank=True, help_text=_("List of activities worked on during session"), verbose_name=_("Activities Checklist") ) # Session Notes observations = models.TextField( blank=True, verbose_name=_("Observations") ) activities_performed = models.TextField( blank=True, verbose_name=_("Activities Performed") ) recommendations = models.TextField( blank=True, verbose_name=_("Recommendations") ) history = HistoricalRecords() class Meta: verbose_name = _("OT Session") verbose_name_plural = _("OT Sessions") ordering = ['-session_date', '-created_at'] indexes = [ models.Index(fields=['patient', 'session_date']), models.Index(fields=['provider', 'session_date']), models.Index(fields=['tenant', 'session_date']), ] def __str__(self): return f"OT Session - {self.patient} - {self.session_date}" @property def cooperative_level_display(self): """Return cooperative level with description.""" levels = { 1: _("Poor"), 2: _("Fair"), 3: _("Good"), 4: _("Excellent") } return levels.get(self.cooperative_level, "") @property def distraction_tolerance_display(self): """Return distraction tolerance with description.""" levels = { 1: _("Low"), 2: _("Moderate"), 3: _("Good"), 4: _("High") } return levels.get(self.distraction_tolerance, "") class OTTargetSkill(UUIDPrimaryKeyMixin): """ Target skills with 0-10 scoring for OT sessions. Tracks progress on specific therapeutic goals. """ session = models.ForeignKey( OTSession, on_delete=models.CASCADE, related_name='target_skills', verbose_name=_("Session") ) skill_name = models.CharField( max_length=200, verbose_name=_("Skill Name") ) score = models.PositiveSmallIntegerField( help_text=_("Score from 0 (not achieved) to 10 (fully achieved)"), verbose_name=_("Score (0-10)") ) notes = models.TextField( blank=True, verbose_name=_("Notes") ) order = models.PositiveIntegerField( default=0, help_text=_("Order of skills in the list"), verbose_name=_("Order") ) class Meta: verbose_name = _("OT Target Skill") verbose_name_plural = _("OT Target Skills") ordering = ['session', 'order'] def __str__(self): return f"{self.skill_name} - Score: {self.score}/10" @property def score_percentage(self): """Return score as percentage.""" return (self.score / 10) * 100 @property def achievement_level(self): """Return achievement level description.""" if self.score == 0: return _("Not Achieved") elif self.score <= 3: return _("Emerging") elif self.score <= 6: return _("Developing") elif self.score <= 9: return _("Proficient") else: return _("Mastered") class OTProgressReport(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin, ClinicallySignableMixin): """ OT progress report summarizing treatment outcomes. """ patient = models.ForeignKey( 'core.Patient', on_delete=models.CASCADE, related_name='ot_progress_reports', verbose_name=_("Patient") ) report_date = models.DateField( verbose_name=_("Report Date") ) provider = models.ForeignKey( 'core.User', on_delete=models.SET_NULL, null=True, related_name='ot_progress_reports_provided', verbose_name=_("Provider") ) # Session Summary sessions_scheduled = models.PositiveIntegerField( default=0, verbose_name=_("Sessions Scheduled") ) sessions_attended = models.PositiveIntegerField( default=0, verbose_name=_("Sessions Attended") ) # Progress Summary goals_progress = models.TextField( blank=True, help_text=_("Progress on each goal"), verbose_name=_("Goals Progress") ) overall_progress = models.TextField( blank=True, verbose_name=_("Overall Progress") ) # Recommendations recommendations = models.TextField( blank=True, verbose_name=_("Recommendations") ) continue_treatment = models.BooleanField( default=True, verbose_name=_("Continue Treatment") ) history = HistoricalRecords() class Meta: verbose_name = _("OT Progress Report") verbose_name_plural = _("OT Progress Reports") ordering = ['-report_date', '-created_at'] indexes = [ models.Index(fields=['patient', 'report_date']), models.Index(fields=['tenant', 'report_date']), ] def __str__(self): return f"OT Progress Report - {self.patient} - {self.report_date}" @property def attendance_rate(self): """Calculate attendance rate percentage.""" if self.sessions_scheduled > 0: return round((self.sessions_attended / self.sessions_scheduled) * 100, 2) return 0 # ============================================================================ # Scoring Configuration Model # ============================================================================ class OTScoringConfig(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin): """ Dynamic scoring configuration for OT consultations. Allows customization of scoring thresholds and interpretations. """ name = models.CharField( max_length=200, verbose_name=_("Configuration Name") ) is_active = models.BooleanField( default=True, verbose_name=_("Is Active") ) # Maximum scores for each domain self_help_max = models.PositiveIntegerField( default=24, verbose_name=_("Self-Help Max Score") ) behavior_max = models.PositiveIntegerField( default=48, verbose_name=_("Behavior Max Score") ) developmental_max = models.PositiveIntegerField( default=6, verbose_name=_("Developmental Max Score") ) eating_max = models.PositiveIntegerField( default=6, verbose_name=_("Eating Max Score") ) # Interpretation thresholds immediate_attention_threshold = models.PositiveIntegerField( default=30, help_text=_("Scores at or below this require immediate attention"), verbose_name=_("Immediate Attention Threshold") ) moderate_difficulty_threshold = models.PositiveIntegerField( default=60, help_text=_("Scores at or below this indicate moderate difficulty"), verbose_name=_("Moderate Difficulty Threshold") ) # Interpretation labels immediate_attention_label = models.CharField( max_length=200, default="⚠️ Needs Immediate Attention", verbose_name=_("Immediate Attention Label") ) moderate_difficulty_label = models.CharField( max_length=200, default="⚠ Moderate Difficulty - Follow-Up Needed", verbose_name=_("Moderate Difficulty Label") ) age_appropriate_label = models.CharField( max_length=200, default="✅ Age-Appropriate Skills", verbose_name=_("Age-Appropriate Label") ) # Recommendation templates immediate_attention_recommendation = models.TextField( default="The child presents significant delays or difficulties across multiple developmental domains. Immediate referral to Occupational Therapy and interdisciplinary evaluation is recommended.", verbose_name=_("Immediate Attention Recommendation") ) moderate_difficulty_recommendation = models.TextField( default="The child shows moderate concerns that warrant intervention. Recommend starting OT sessions and monitoring progress within 2–4 months.", verbose_name=_("Moderate Difficulty Recommendation") ) age_appropriate_recommendation = models.TextField( default="Child demonstrates age-appropriate functioning in assessed areas. Recommend regular developmental screening as part of preventive care.", verbose_name=_("Age-Appropriate Recommendation") ) class Meta: verbose_name = _("OT Scoring Configuration") verbose_name_plural = _("OT Scoring Configurations") ordering = ['-is_active', '-created_at'] def __str__(self): return f"{self.name} {'(Active)' if self.is_active else '(Inactive)'}" @property def total_max_score(self): """Calculate total maximum possible score.""" return self.self_help_max + self.behavior_max + self.developmental_max + self.eating_max