Kulcare Docs
DeveloperCare plansCare planArchitecture

Multi-Practitioner Encounter Documentation Architecture

Status: Planning Created: 2025-12-25 Last Updated: 2025-12-25 Related: Clinical Data Hierarchy

Objective

Extend the existing encounter system to support practitioner-specific documentation beyond the current doctor-centric Prescription model.

Target Practitioners:

  • Doctor: Prescriptions, medical certificates (existing - Prescription model)
  • Therapist/Psychologist: Session notes, mood assessments, safety plans (NEW)
  • Physiotherapist: Session notes, ROM measurements, exercise prescriptions (NEW)
  • Dietitian: Consultation notes, meal plans, nutritional assessments (NEW)

Existing Infrastructure (ALREADY BUILT)

1. Encounter System (100% Complete)

Location: backend/parts/clinic/models/encounter.rb, frontend/apps/cms/src/features/encounter/

Already supports:

  • Multiple practitioner types: doctor | physiotherapist | psychologist | therapist | nurse | dietitian | other
  • Polymorphic practitioner association
  • Visit types: in_person | teleconsultation | follow_up | home_visit
  • Full CRUD API and frontend hooks
  • Navigates to PrescriptionPage for documentation

Gap: All encounters use the same Prescription model, which is doctor-centric.

2. Longitudinal Health Record (100% Complete)

Location: frontend/apps/cms/src/pages/care-plan/LongitudinalHealthRecord.tsx

Already built:

  • PatientSummaryDashboard - Key metrics and recent activity
  • VitalsTrendChart - BP, weight, heart rate trends over time
  • ProblemListManager - Persistent problem tracking with severity
  • MedicationTimeline - Medication history visualization
  • EncounterHistory - All patient encounters
  • Route: /careplan/patient/:patientId

Relationship to EpisodeOfCare: See Clinical Data Hierarchy for the complete relationship between:

  • Longitudinal Health Record (LHR) - Patient-centric aggregated view across ALL encounters
  • Episode of Care - Program-specific container for a course of treatment (e.g., 12-session physio program)
  • Encounter - Single visit/session (atomic clinical event)

These three concepts form a clinical data hierarchy and are complementary, not conflicting. LHR will be updated to display active Episodes and group encounters by Episode.

3. Prescription Model (Existing)

  • Medications with dosage, frequency
  • Investigations (lab tests)
  • Clinical observations
  • Advice and follow-up

Gap: Works great for doctors but inappropriate for therapists, physiotherapists, dietitians.


What This Plan Adds

ComponentStatusPurpose
EncounterDocumentNEWPolymorphic documentation (1:N per encounter)
EncounterParticipantNEWMulti-provider encounters
EpisodeOfCareNEWLongitudinal treatment programs
ClinicalMeasurementNEWUnified vitals/ROM/anthropometrics
AssessmentInstrumentNEWData-driven assessments (PHQ-9, GAD-7)
Safety alertsNEWRisk monitoring for therapy

Key Architecture Decisions

Based on architectural review, the design addresses these critical concerns:

  1. Flexible 1:N documents per encounter - Not rigid 1:1 mapping
  2. Multi-practitioner support - EncounterParticipant for co-managed encounters
  3. Episode vs Encounter separation - Treatment programs vs per-visit sessions
  4. Document lifecycle - draft/signed/locked/amended workflow
  5. Granular visibility - Not just "is_private" boolean
  6. Data-driven assessments - Instruments defined in data, not code
  7. Generalized measurements - ClinicalMeasurement instead of just "vitals"
  8. Backend-driven registry - Single source of truth via API
  9. Clinical safety hooks - Risk alerts for therapy

Core Data Model

Patient
   |
   +-- has_many :episodes_of_care (longitudinal)
   |       |
   |       +-- has_many :encounters
   |       +-- has_many :treatment_goals
   |       +-- has_many :progress_notes
   |
   +-- has_many :encounters (can exist without episode)

Encounter (hub)
   |
   +-- has_many :encounter_participants (providers)
   +-- has_many :encounter_documents (1..N documents)
   |       |
   |       +-- doc_type: prescription | therapy_session | physio_session | nutrition_consultation
   |       +-- status: draft | signed | locked | amended
   |       +-- data: JSONB (structured content)
   |       +-- has_many :child records (ROM, exercises, meal plans)
   |
   +-- has_many :clinical_measurements (generalized vitals/ROM/anthropometrics)
   +-- has_many :assessment_responses (PHQ-9, GAD-7, etc.)
   +-- has_many :encounter_attachments (files/media)
   +-- has_many :encounter_notes (shared notes with visibility)

Phase 1: Core Infrastructure Tables

1.1 EpisodeOfCare (Longitudinal Care)

File: backend/parts/clinic/models/episode_of_care.rb

# Represents a course of treatment spanning multiple encounters
create_table :episodes_of_care do |t|
  t.references :patient, null: false, foreign_key: true
  t.references :clinic, null: false, foreign_key: true
  t.references :primary_provider, foreign_key: { to_table: :users }

  t.string :episode_type  # therapy | physio | nutrition | chronic_care | etc
  t.string :status        # active | on_hold | completed | discharged
  t.string :reason        # chief complaint / referral reason
  t.date :start_date
  t.date :end_date
  t.date :target_end_date

  t.integer :total_sessions_planned
  t.integer :sessions_completed, default: 0
  t.string :frequency     # weekly | biweekly | monthly | as_needed

  t.text :long_term_goals
  t.text :discharge_summary
  t.jsonb :metadata, default: {}

  t.boolean :deleted, default: false
  t.datetime :deleted_at
  t.timestamps
end

add_index :episodes_of_care, [:patient_id, :status]
add_index :episodes_of_care, [:clinic_id, :episode_type]

1.2 EncounterParticipant (Multi-Provider Support)

File: backend/parts/clinic/models/encounter_participant.rb

# Enables multi-practitioner encounters and proper attribution
create_table :encounter_participants do |t|
  t.references :encounter, null: false, foreign_key: true
  t.references :user, null: false, foreign_key: true  # provider

  t.string :role, null: false  # primary | assistant | supervisor | scribe | consultant
  t.string :practitioner_type  # doctor | psychologist | physiotherapist | dietitian

  t.boolean :can_view_private_notes, default: false
  t.boolean :can_sign, default: false
  t.boolean :can_edit, default: true

  t.datetime :joined_at
  t.datetime :left_at

  t.timestamps
end

add_index :encounter_participants, [:encounter_id, :user_id], unique: true
add_index :encounter_participants, [:encounter_id, :role]

1.3 EncounterDocument (Polymorphic Documentation)

File: backend/parts/clinic/models/encounter_document.rb

# Generic document container - 1:N documents per encounter
create_table :encounter_documents do |t|
  t.references :encounter, null: false, foreign_key: true
  t.references :episode_of_care, foreign_key: true  # optional link to episode
  t.references :author, null: false, foreign_key: { to_table: :users }

  t.string :doc_type, null: false
  # Types: prescription | therapy_session | therapy_assessment | therapy_safety_plan |
  #        physio_session | physio_evaluation | nutrition_consultation |
  #        nutrition_followup | general_note | medical_certificate

  t.string :status, null: false, default: 'draft'
  # Status: draft | pending_signature | signed | locked | amended | void

  t.string :title
  t.jsonb :data, null: false, default: {}  # structured content

  # Signature workflow
  t.references :signed_by, foreign_key: { to_table: :users }
  t.datetime :signed_at
  t.references :co_signer, foreign_key: { to_table: :users }
  t.datetime :co_signed_at

  # Amendment tracking
  t.integer :version, default: 1
  t.references :amends_document, foreign_key: { to_table: :encounter_documents }
  t.text :amendment_reason

  # Visibility
  t.string :visibility, default: 'care_team'
  # Visibility: private_to_author | care_team | clinic | patient_visible
  t.boolean :is_psychotherapy_note, default: false  # stricter access per 42 CFR Part 2
  t.jsonb :allowed_user_ids, default: []

  t.boolean :deleted, default: false
  t.datetime :deleted_at
  t.timestamps
end

add_index :encounter_documents, [:encounter_id, :doc_type]
add_index :encounter_documents, [:author_id, :status]
add_index :encounter_documents, :episode_of_care_id
add_index :encounter_documents, :status
add_index :encounter_documents, :data, using: :gin  # for JSONB queries

1.4 ClinicalMeasurement (Generalized Vitals/ROM/Anthropometrics)

File: backend/parts/clinic/models/clinical_measurement.rb

# Unified measurement system for all clinical data points
create_table :clinical_measurements do |t|
  t.references :encounter, null: false, foreign_key: true
  t.references :encounter_document, foreign_key: true  # optional link
  t.references :recorded_by, null: false, foreign_key: { to_table: :users }

  t.string :measurement_type, null: false
  # Types: bp_systolic | bp_diastolic | heart_rate | temperature | spo2 | weight | height |
  #        bmi | waist_circumference | body_fat_pct | pain_scale |
  #        rom_[joint]_[movement] | grip_strength | etc

  t.decimal :value, precision: 10, scale: 2
  t.string :value_text  # for non-numeric (e.g., "regular" for pulse rhythm)
  t.string :unit        # mmHg | bpm | F | C | kg | lb | cm | in | degrees | etc

  t.string :method      # measured | self_reported | calculated | device_recorded
  t.string :body_site   # left | right | bilateral | n/a
  t.string :position    # sitting | standing | supine | prone

  t.jsonb :metadata, default: {}  # device info, reference range, etc
  t.text :notes

  t.datetime :measured_at
  t.timestamps
end

add_index :clinical_measurements, [:encounter_id, :measurement_type]
add_index :clinical_measurements, :measurement_type

1.5 AssessmentInstrument & AssessmentResponse (Data-Driven Assessments)

File: backend/parts/clinic/models/assessment_instrument.rb

# Definition of assessment tools (PHQ-9, GAD-7, ROM scales, etc.)
create_table :assessment_instruments do |t|
  t.string :code, null: false  # phq9 | gad7 | bdi2 | oswestry | vas_pain | etc
  t.string :name, null: false
  t.string :version
  t.string :category  # mental_health | pain | functional | nutrition | etc

  t.jsonb :questions, null: false, default: []  # array of question definitions
  t.jsonb :scoring_rules, null: false, default: {}  # how to calculate scores
  t.jsonb :severity_bands, default: []  # score ranges and interpretations

  t.integer :min_score
  t.integer :max_score
  t.boolean :higher_is_worse, default: true

  t.text :description
  t.text :instructions
  t.string :license_info

  t.boolean :active, default: true
  t.timestamps
end

add_index :assessment_instruments, :code, unique: true
add_index :assessment_instruments, :category

File: backend/parts/clinic/models/assessment_response.rb

# Completed assessment with answers and computed score
create_table :assessment_responses do |t|
  t.references :encounter, null: false, foreign_key: true
  t.references :encounter_document, foreign_key: true
  t.references :assessment_instrument, null: false, foreign_key: true
  t.references :administered_by, null: false, foreign_key: { to_table: :users }

  t.jsonb :answers, null: false, default: {}  # question_id => answer mapping
  t.decimal :total_score, precision: 6, scale: 2
  t.string :severity_level  # computed from scoring_rules
  t.text :clinical_interpretation

  t.datetime :administered_at
  t.integer :time_to_complete_seconds

  t.boolean :requires_follow_up, default: false
  t.boolean :safety_alert_triggered, default: false  # for suicidal ideation questions
  t.text :follow_up_notes

  t.timestamps
end

add_index :assessment_responses, [:encounter_id, :assessment_instrument_id]
add_index :assessment_responses, :safety_alert_triggered

1.6 EncounterNote (Shared Notes with Visibility)

File: backend/parts/clinic/models/encounter_note.rb

create_table :encounter_notes do |t|
  t.references :encounter, null: false, foreign_key: true
  t.references :encounter_document, foreign_key: true  # optional link
  t.references :author, null: false, foreign_key: { to_table: :users }

  t.string :note_type, null: false
  # Types: clinical_observation | progress | assessment | plan | general | addendum

  t.text :content, null: false
  t.jsonb :structured_data, default: {}

  t.string :visibility, null: false, default: 'care_team'
  # Visibility: private_to_author | care_team | clinic | patient_visible
  t.boolean :is_psychotherapy_note, default: false
  t.jsonb :allowed_user_ids, default: []

  t.boolean :deleted, default: false
  t.datetime :deleted_at
  t.timestamps
end

add_index :encounter_notes, [:encounter_id, :note_type]
add_index :encounter_notes, :visibility

1.7 EncounterAttachment (Files/Media)

File: backend/parts/clinic/models/encounter_attachment.rb

create_table :encounter_attachments do |t|
  t.references :encounter, null: false, foreign_key: true
  t.references :encounter_document, foreign_key: true
  t.references :uploaded_by, null: false, foreign_key: { to_table: :users }

  t.string :file_type  # image | video | pdf | document | audio
  t.string :category   # photo | exercise_video | worksheet | consent_form | etc
  t.string :title
  t.text :description

  # CarrierWave/ActiveStorage fields
  t.string :file
  t.string :content_type
  t.integer :file_size

  t.boolean :patient_visible, default: false

  t.boolean :deleted, default: false
  t.datetime :deleted_at
  t.timestamps
end

add_index :encounter_attachments, :encounter_id
add_index :encounter_attachments, :encounter_document_id

Phase 2: Specialized Child Tables (for relational data)

2.1 Therapy-Specific

# TreatmentGoal - shared across therapy/physio/nutrition episodes
create_table :treatment_goals do |t|
  t.references :episode_of_care, null: false, foreign_key: true
  t.references :created_by, null: false, foreign_key: { to_table: :users }

  t.string :goal_type  # short_term | long_term | maintenance
  t.text :description, null: false
  t.string :status     # active | achieved | modified | discontinued
  t.date :target_date
  t.date :achieved_date

  t.integer :progress_percent, default: 0
  t.text :progress_notes
  t.jsonb :milestones, default: []

  t.timestamps
end

# SafetyPlan - for therapy when risk is elevated
create_table :safety_plans do |t|
  t.references :encounter_document, null: false, foreign_key: true
  t.references :patient, null: false, foreign_key: true

  t.jsonb :warning_signs, default: []
  t.jsonb :coping_strategies, default: []
  t.jsonb :support_contacts, default: []  # name, phone, relationship
  t.jsonb :professional_contacts, default: []
  t.text :environment_safety_steps
  t.text :reasons_for_living

  t.datetime :reviewed_at
  t.references :reviewed_by, foreign_key: { to_table: :users }

  t.timestamps
end

2.2 Physio-Specific

# ROMMeasurement - linked to encounter_document
create_table :rom_measurements do |t|
  t.references :encounter_document, null: false, foreign_key: true

  t.string :joint, null: false      # shoulder | elbow | wrist | hip | knee | ankle | spine
  t.string :movement, null: false   # flexion | extension | abduction | adduction | rotation
  t.string :side                    # left | right | bilateral

  t.decimal :active_rom, precision: 5, scale: 2
  t.decimal :passive_rom, precision: 5, scale: 2
  t.string :unit, default: 'degrees'

  t.integer :pain_during, default: 0  # 0-10
  t.text :notes

  t.timestamps
end

add_index :rom_measurements, [:encounter_document_id, :joint, :movement]

# ExercisePrescription - linked to encounter_document
create_table :exercise_prescriptions do |t|
  t.references :encounter_document, null: false, foreign_key: true
  t.references :exercise_library_item, foreign_key: true  # optional link to library

  t.string :exercise_name, null: false
  t.string :category        # strengthening | stretching | balance | cardio | functional
  t.text :description
  t.text :instructions

  t.integer :sets
  t.integer :reps
  t.integer :hold_seconds
  t.string :frequency       # daily | 2x_daily | 3x_week | etc
  t.string :intensity       # light | moderate | vigorous

  t.text :precautions
  t.text :progression_criteria
  t.integer :order_index

  t.string :video_url
  t.string :image_url

  t.timestamps
end

2.3 Nutrition-Specific

# MealPlan - linked to encounter_document
create_table :meal_plans do |t|
  t.references :encounter_document, null: false, foreign_key: true

  t.string :plan_type       # daily | weekly | template
  t.date :effective_date

  t.jsonb :meals, null: false, default: []
  # Structure: [{ meal_type: 'breakfast', time: '08:00', foods: [...], portions: [...], calories: N }]

  t.integer :target_calories
  t.integer :target_protein_g
  t.integer :target_carbs_g
  t.integer :target_fat_g
  t.integer :target_fiber_g

  t.jsonb :restrictions, default: []  # gluten_free | dairy_free | low_sodium | etc
  t.text :special_instructions

  t.timestamps
end

# NutritionalAssessment - detailed intake analysis
create_table :nutritional_assessments do |t|
  t.references :encounter_document, null: false, foreign_key: true

  t.decimal :height_cm, precision: 5, scale: 2
  t.decimal :weight_kg, precision: 5, scale: 2
  t.decimal :bmi, precision: 4, scale: 2
  t.decimal :waist_cm, precision: 5, scale: 2
  t.decimal :body_fat_pct, precision: 4, scale: 2

  t.string :weight_goal      # lose | gain | maintain
  t.decimal :target_weight_kg, precision: 5, scale: 2

  t.text :diet_history
  t.jsonb :food_allergies, default: []
  t.jsonb :dietary_restrictions, default: []
  t.jsonb :supplements, default: []
  t.jsonb :medications_affecting_nutrition, default: []

  t.integer :meals_per_day
  t.integer :snacks_per_day
  t.text :eating_schedule
  t.text :hydration_notes

  t.timestamps
end

Phase 3: Encounter Type Registry (Backend-Driven)

3.1 EncounterTypeConfig Model

File: backend/parts/clinic/models/encounter_type_config.rb

# Database-driven configuration per encounter type
create_table :encounter_type_configs do |t|
  t.string :practitioner_type, null: false  # doctor | psychologist | physiotherapist | dietitian
  t.references :clinic, foreign_key: true   # null = system default, clinic_id = override

  t.jsonb :allowed_doc_types, null: false, default: []
  t.jsonb :tabs, null: false, default: []
  t.string :default_tab

  t.boolean :has_prescriptions, default: false
  t.jsonb :required_measurements, default: []
  t.jsonb :enabled_assessments, default: []  # codes from AssessmentInstrument

  t.jsonb :print_templates, default: []
  t.jsonb :ai_scribe_config, default: {}

  t.boolean :active, default: true
  t.timestamps
end

add_index :encounter_type_configs, [:practitioner_type, :clinic_id], unique: true

3.2 API Endpoint for Registry

Controller: encounter_type_configs_controller.rb

# GET /api/v1/encounter_type_configs
# Returns configuration for current clinic/practitioner type
# Frontend fetches and caches this on app load

def index
  configs = EncounterTypeConfig.for_clinic(current_clinic)
                               .includes(:assessment_instruments)
  render json: EncounterTypeConfigSerializer.new(configs)
end

3.3 Default Configurations

# Seeded system defaults
DEFAULTS = {
  doctor: {
    allowed_doc_types: ['prescription', 'medical_certificate', 'general_note'],
    tabs: ['vitals', 'observations', 'medications', 'investigations', 'advice', 'next_visit'],
    default_tab: 'vitals',
    has_prescriptions: true,
    required_measurements: ['bp_systolic', 'bp_diastolic', 'heart_rate', 'temperature'],
    enabled_assessments: []
  },
  psychologist: {
    allowed_doc_types: ['therapy_session', 'therapy_assessment', 'therapy_safety_plan', 'general_note'],
    tabs: ['measurements', 'assessments', 'session_notes', 'treatment_plan', 'homework', 'next_visit'],
    default_tab: 'session_notes',
    has_prescriptions: false,
    required_measurements: [],
    enabled_assessments: ['phq9', 'gad7', 'bdi2', 'pcl5']
  },
  physiotherapist: {
    allowed_doc_types: ['physio_session', 'physio_evaluation', 'general_note'],
    tabs: ['measurements', 'assessment', 'rom', 'treatment', 'exercises', 'progress', 'next_visit'],
    default_tab: 'assessment',
    has_prescriptions: false,
    required_measurements: ['pain_scale'],
    enabled_assessments: ['oswestry', 'dash', 'lefs']
  },
  dietitian: {
    allowed_doc_types: ['nutrition_consultation', 'nutrition_followup', 'general_note'],
    tabs: ['measurements', 'nutritional_assessment', 'meal_plan', 'goals', 'progress', 'next_visit'],
    default_tab: 'nutritional_assessment',
    has_prescriptions: false,
    required_measurements: ['weight', 'height', 'bmi', 'waist_circumference'],
    enabled_assessments: []
  }
}

Phase 4: Encounter Model Updates

4.1 Updated Encounter Associations

File: backend/parts/clinic/models/encounter.rb

class Encounter < ApplicationRecord
  # Existing associations...

  # New associations
  belongs_to :episode_of_care, optional: true
  has_many :encounter_participants, dependent: :destroy
  has_many :providers, through: :encounter_participants, source: :user

  has_many :encounter_documents, dependent: :destroy
  has_many :clinical_measurements, dependent: :destroy
  has_many :assessment_responses, dependent: :destroy
  has_many :encounter_notes, dependent: :destroy
  has_many :encounter_attachments, dependent: :destroy

  # Primary provider convenience
  def primary_provider
    encounter_participants.find_by(role: 'primary')&.user
  end

  def primary_practitioner_type
    encounter_participants.find_by(role: 'primary')&.practitioner_type
  end

  # Get applicable configuration
  def type_config
    EncounterTypeConfig.for_practitioner_type(primary_practitioner_type, clinic)
  end

  # Get all documents of specific type
  def documents_of_type(doc_type)
    encounter_documents.where(doc_type: doc_type)
  end

  # Get primary document (most common use case)
  def primary_document
    encounter_documents.where.not(status: ['void', 'amended']).order(:created_at).first
  end
end

4.2 Document Audit Log

File: backend/parts/clinic/models/document_audit_log.rb

# Immutable audit trail for medico-legal defensibility
create_table :document_audit_logs do |t|
  t.references :encounter_document, null: false, foreign_key: true
  t.references :user, null: false, foreign_key: true

  t.string :action, null: false  # created | viewed | edited | signed | co_signed | amended | voided
  t.jsonb :changes, default: {}  # diff of what changed
  t.jsonb :previous_data, default: {}  # snapshot before change

  t.string :ip_address
  t.string :user_agent

  t.datetime :performed_at, null: false
  t.timestamps
end

add_index :document_audit_logs, [:encounter_document_id, :performed_at]
add_index :document_audit_logs, :action

Phase 5: Clinical Safety System

5.1 Safety Alert Configuration

# SafetyAlertRule - configurable thresholds
create_table :safety_alert_rules do |t|
  t.references :clinic, foreign_key: true  # null = system default

  t.string :rule_type, null: false
  # Types: assessment_threshold | measurement_threshold | keyword_detection

  t.string :trigger_source  # assessment_code or measurement_type
  t.string :operator        # >= | > | <= | < | == | contains
  t.decimal :threshold_value, precision: 6, scale: 2
  t.string :threshold_text  # for keyword matching

  t.string :severity, null: false  # info | warning | critical
  t.text :alert_message
  t.jsonb :required_actions, default: []  # safety_plan | supervisor_review | etc
  t.jsonb :notify_roles, default: []

  t.boolean :active, default: true
  t.timestamps
end

# SafetyAlert - triggered alerts
create_table :safety_alerts do |t|
  t.references :encounter, null: false, foreign_key: true
  t.references :patient, null: false, foreign_key: true
  t.references :safety_alert_rule, null: false, foreign_key: true
  t.references :triggered_by_response, foreign_key: { to_table: :assessment_responses }

  t.string :severity, null: false
  t.text :alert_message
  t.jsonb :trigger_data, default: {}

  t.string :status, default: 'open'  # open | acknowledged | resolved
  t.references :acknowledged_by, foreign_key: { to_table: :users }
  t.datetime :acknowledged_at
  t.text :resolution_notes
  t.references :resolved_by, foreign_key: { to_table: :users }
  t.datetime :resolved_at

  t.timestamps
end

add_index :safety_alerts, [:patient_id, :status]
add_index :safety_alerts, [:encounter_id, :severity]

5.2 Safety Alert Service

class SafetyAlertService
  def self.check_assessment_response(response)
    rules = SafetyAlertRule.for_assessment(response.assessment_instrument.code)

    rules.each do |rule|
      if rule.triggered_by?(response)
        create_alert(response.encounter, response, rule)
      end
    end
  end

  def self.create_alert(encounter, trigger, rule)
    alert = SafetyAlert.create!(
      encounter: encounter,
      patient: encounter.patient,
      safety_alert_rule: rule,
      triggered_by_response: trigger,
      severity: rule.severity,
      alert_message: rule.alert_message,
      trigger_data: { score: trigger.total_score, threshold: rule.threshold_value }
    )

    # Notify relevant parties
    SafetyAlertNotificationJob.perform_later(alert.id)

    # Require safety plan if configured
    if rule.required_actions.include?('safety_plan')
      # Flag that safety plan is required
    end
  end
end

Phase 6: Frontend Architecture

6.1 Backend-Driven Tab Registry

// Frontend fetches configuration from backend on app load
// GET /api/v1/encounter_type_configs

interface EncounterTypeConfig {
  practitioner_type: string;
  allowed_doc_types: string[];
  tabs: TabConfig[];
  default_tab: string;
  has_prescriptions: boolean;
  required_measurements: string[];
  enabled_assessments: AssessmentInstrument[];
}

interface TabConfig {
  id: string;
  label: string;
  icon?: string;
  component: string;  // Component name to render
  required?: boolean;
}

// Tab components are mapped client-side
const TAB_COMPONENTS: Record<string, React.LazyExoticComponent<any>> = {
  'vitals': lazy(() => import('./tabs/VitalsTab')),
  'measurements': lazy(() => import('./tabs/MeasurementsTab')),
  'observations': lazy(() => import('./tabs/ObservationsTab')),
  'session_notes': lazy(() => import('./tabs/SessionNotesTab')),
  'assessments': lazy(() => import('./tabs/AssessmentsTab')),
  'rom': lazy(() => import('./tabs/ROMTab')),
  'exercises': lazy(() => import('./tabs/ExercisesTab')),
  'meal_plan': lazy(() => import('./tabs/MealPlanTab')),
  'nutritional_assessment': lazy(() => import('./tabs/NutritionalAssessmentTab')),
  'treatment_plan': lazy(() => import('./tabs/TreatmentPlanTab')),
  'homework': lazy(() => import('./tabs/HomeworkTab')),
  'progress': lazy(() => import('./tabs/ProgressTab')),
  'next_visit': lazy(() => import('./tabs/NextVisitTab')),
  // ... etc
};

6.2 Dynamic Encounter Page

// apps/cms/src/pages/encounter/EncounterPage.tsx

export function EncounterPage() {
  const { encounterId } = useParams();
  const { data: encounter } = useEncounter(encounterId);
  const { data: typeConfig } = useEncounterTypeConfig(encounter?.primary_practitioner_type);

  const [activeTab, setActiveTab] = useState<string>();

  useEffect(() => {
    if (typeConfig && !activeTab) {
      setActiveTab(typeConfig.default_tab);
    }
  }, [typeConfig]);

  if (!typeConfig) return <Loading />;

  const TabComponent = TAB_COMPONENTS[activeTab];

  return (
    <div className="encounter-page">
      <EncounterHeader encounter={encounter} />

      <TabNavigation
        tabs={typeConfig.tabs}
        activeTab={activeTab}
        onTabChange={setActiveTab}
      />

      <Suspense fallback={<TabLoading />}>
        <TabComponent
          encounter={encounter}
          config={typeConfig}
        />
      </Suspense>
    </div>
  );
}

6.3 Component Directory Structure

frontend/apps/cms/src/components/
├── encounter/
│   ├── tabs/
│   │   ├── VitalsTab.tsx           # Traditional vitals (BP, HR, temp)
│   │   ├── MeasurementsTab.tsx     # Generic measurements (vitals + ROM + anthropometrics)
│   │   ├── ObservationsTab.tsx     # Clinical observations
│   │   ├── SessionNotesTab.tsx     # Therapy/session documentation
│   │   ├── AssessmentsTab.tsx      # PHQ-9, GAD-7, etc. (data-driven)
│   │   ├── ROMTab.tsx              # Range of motion for physio
│   │   ├── ExercisesTab.tsx        # Exercise prescription
│   │   ├── MealPlanTab.tsx         # Nutrition meal planning
│   │   ├── NutritionalAssessmentTab.tsx
│   │   ├── TreatmentPlanTab.tsx    # Goals and treatment planning
│   │   ├── HomeworkTab.tsx         # Therapy homework assignments
│   │   ├── ProgressTab.tsx         # Episode progress tracking
│   │   ├── NextVisitTab.tsx        # Follow-up scheduling
│   │   └── MedicationsTab.tsx      # Prescriptions (doctor only)
│   │
│   ├── documents/
│   │   ├── DocumentList.tsx        # List of encounter documents
│   │   ├── DocumentEditor.tsx      # Generic document editor
│   │   ├── DocumentSignature.tsx   # Signature workflow
│   │   └── DocumentViewer.tsx      # Read-only view with audit trail
│   │
│   ├── assessments/
│   │   ├── AssessmentRenderer.tsx  # Data-driven assessment form
│   │   ├── AssessmentResults.tsx   # Score display with severity
│   │   └── AssessmentHistory.tsx   # Prior assessment comparison
│   │
│   ├── safety/
│   │   ├── SafetyAlertBanner.tsx   # Alert display
│   │   ├── SafetyPlanEditor.tsx    # Safety plan form
│   │   └── RiskAssessment.tsx      # Risk level indicator
│   │
│   └── shared/
│       ├── MeasurementInput.tsx    # Generic measurement entry
│       ├── VisibilitySelector.tsx  # Note visibility control
│       └── ParticipantList.tsx     # Multi-provider display

Implementation Sprints

Sprint 1: Core Infrastructure

  • Create migrations: episode_of_care, encounter_participant, encounter_document
  • Create migrations: clinical_measurement, assessment_instrument, assessment_response
  • Create migrations: encounter_note, encounter_attachment, document_audit_log
  • Create base models with associations and validations
  • Seed assessment instruments (PHQ-9, GAD-7, etc.)

Sprint 2: Encounter Type System

  • Create encounter_type_config model and migration
  • Create seeder for default configurations
  • Add GET /encounter_type_configs API endpoint
  • Update Encounter model with new associations
  • Create serializers for new models

Sprint 3: Document Workflow

  • Implement document status state machine
  • Add signature workflow
  • Implement amendment tracking
  • Add document audit logging
  • Create document visibility scopes

Sprint 4: Safety System

  • Create safety_alert_rule and safety_alert models
  • Implement SafetyAlertService
  • Add safety plan model and workflow
  • Create notification system for alerts
  • Add safety dashboard for supervisors

Sprint 5: Frontend - Infrastructure

  • Create useEncounterTypeConfig hook
  • Refactor EncounterPage to use dynamic tabs
  • Create TabNavigation component
  • Create MeasurementsTab (unified vitals/ROM/anthropometrics)
  • Create AssessmentsTab with data-driven rendering

Sprint 6: Frontend - Practitioner Tabs

  • SessionNotesTab for therapy
  • ROMTab for physio
  • ExercisesTab for physio
  • MealPlanTab for nutrition
  • NutritionalAssessmentTab for nutrition

Sprint 7: Frontend - Document System

  • DocumentList component
  • DocumentEditor with visibility controls
  • DocumentSignature workflow
  • ParticipantList for multi-provider
  • SafetyAlertBanner and SafetyPlanEditor

Sprint 8: Integration & Polish

  • Print templates per document type
  • AI Scribe context adaptation per practitioner type
  • Episode of care management UI
  • Progress tracking across episodes
  • Testing and documentation

Database Constraints & Indexes

# Key constraints to add
add_foreign_key :encounter_documents, :encounter_documents, column: :amends_document_id
add_foreign_key :episodes_of_care, :users, column: :primary_provider_id

# Partial unique indexes
add_index :encounter_participants, [:encounter_id, :role],
          where: "role = 'primary'",
          unique: true,
          name: 'idx_one_primary_per_encounter'

# GIN indexes for JSONB querying
add_index :encounter_documents, :data, using: :gin
add_index :clinical_measurements, :metadata, using: :gin
add_index :assessment_responses, :answers, using: :gin

# Composite indexes for common queries
add_index :encounter_documents, [:encounter_id, :status, :doc_type]
add_index :clinical_measurements, [:encounter_id, :measurement_type, :measured_at]

Migration from Existing Prescription Model

The existing Prescription model will continue to work. The migration path:

  1. Prescription remains as-is for doctor encounters
  2. New encounter types use EncounterDocument
  3. Optionally, migrate Prescription data to EncounterDocument over time
  4. Frontend detects practitioner type and renders appropriate UI
# Encounter model supports both patterns during transition
class Encounter
  has_one :prescription  # Legacy
  has_many :encounter_documents  # New

  def documentation
    # Returns prescription for doctors, encounter_documents for others
    if primary_practitioner_type == 'doctor'
      prescription
    else
      encounter_documents
    end
  end
end

Feature Flags

# Backend feature flags
ENABLE_MULTI_PRACTITIONER_ENCOUNTERS: false
ENABLE_EPISODE_OF_CARE: false
ENABLE_SAFETY_ALERTS: false
ENABLE_DOCUMENT_SIGNATURES: false
// Frontend feature flags
ENABLE_THERAPY_ENCOUNTERS: false
ENABLE_PHYSIO_ENCOUNTERS: false
ENABLE_NUTRITION_ENCOUNTERS: false
ENABLE_DYNAMIC_TABS: false

Enable incrementally as each component is ready.


On this page

ObjectiveExisting Infrastructure (ALREADY BUILT)1. Encounter System (100% Complete)2. Longitudinal Health Record (100% Complete)3. Prescription Model (Existing)What This Plan AddsKey Architecture DecisionsCore Data ModelPhase 1: Core Infrastructure Tables1.1 EpisodeOfCare (Longitudinal Care)1.2 EncounterParticipant (Multi-Provider Support)1.3 EncounterDocument (Polymorphic Documentation)1.4 ClinicalMeasurement (Generalized Vitals/ROM/Anthropometrics)1.5 AssessmentInstrument & AssessmentResponse (Data-Driven Assessments)1.6 EncounterNote (Shared Notes with Visibility)1.7 EncounterAttachment (Files/Media)Phase 2: Specialized Child Tables (for relational data)2.1 Therapy-Specific2.2 Physio-Specific2.3 Nutrition-SpecificPhase 3: Encounter Type Registry (Backend-Driven)3.1 EncounterTypeConfig Model3.2 API Endpoint for Registry3.3 Default ConfigurationsPhase 4: Encounter Model Updates4.1 Updated Encounter Associations4.2 Document Audit LogPhase 5: Clinical Safety System5.1 Safety Alert Configuration5.2 Safety Alert ServicePhase 6: Frontend Architecture6.1 Backend-Driven Tab Registry6.2 Dynamic Encounter Page6.3 Component Directory StructureImplementation SprintsSprint 1: Core InfrastructureSprint 2: Encounter Type SystemSprint 3: Document WorkflowSprint 4: Safety SystemSprint 5: Frontend - InfrastructureSprint 6: Frontend - Practitioner TabsSprint 7: Frontend - Document SystemSprint 8: Integration & PolishDatabase Constraints & IndexesMigration from Existing Prescription ModelFeature FlagsRelated Documentation