Skip to content

Resident Evaluation Platform - Feature Specification

Feature ID: resident-evaluationsStatus: Planned Priority: High Target Release: Q1 2026 Document Version: 1.0 Last Updated: October 23, 2025


Executive Summary

What is this feature?

The Resident Evaluation Platform replaces Elentra, providing a comprehensive digital solution for evaluating medical residents throughout their training. This platform enables:

  • Institutional administrators to create customizable evaluation forms
  • Preceptors and senior residents to evaluate junior residents
  • Residents to complete self-assessments and submit them for preceptor approval
  • Automated email reminders to ensure timely completion of evaluations
  • Secure data export when residents complete their training

Why do we need this?

Medical residency programs require systematic evaluation of residents across multiple competencies (CanMEDS framework in Canada, ACGME milestones in the US). Current solutions like Elentra are expensive, inflexible, and don't integrate with clinical documentation workflows. By building this into Noumaris, we provide a unified platform for both clinical documentation and resident evaluation.

Key Benefits

BenefitDescription
Cost SavingsEliminates $10,000-50,000/year licensing fees for Elentra
CustomizationInstitution-specific forms tailored to program requirements
IntegrationSeamless connection with clinical scribe and documentation features
ComplianceBuilt-in audit trails, data retention, and HIPAA compliance
User ExperienceModern, mobile-responsive interface with real-time notifications

User Personas

1. Dr. Sarah Chen - Program Director (Institutional Admin)

Role: Creates and manages evaluation forms for the Family Medicine residency program Goals:

  • Ensure all residents receive timely, comprehensive evaluations
  • Track competency development across all CanMEDS roles
  • Generate reports for accreditation bodies (CFPC, ACGME)

Pain Points with Current System:

  • Elentra forms are rigid and don't match program-specific needs
  • No integration with clinical workflows
  • Difficult to track completion rates

2. Dr. Michael Rodriguez - Staff Physician (Preceptor)

Role: Evaluates residents during clinical rotations Goals:

  • Quickly complete evaluation forms between patients
  • Provide meaningful feedback tied to specific clinical encounters
  • Track resident progress over time

Pain Points with Current System:

  • Separate login for evaluation system
  • Can't reference clinical notes from the same encounter
  • Email reminders are ineffective

3. Dr. Aisha Patel - PGY-3 Resident (Senior Resident)

Role: Evaluates junior residents, completes self-assessments Goals:

  • Document her own learning and competency development
  • Provide constructive feedback to junior residents
  • Access her evaluation history for career portfolio

Pain Points with Current System:

  • Self-assessments are disconnected from actual clinical work
  • Can't see feedback patterns over time
  • No mobile access

4. Dr. James Thompson - PGY-1 Resident (Junior Resident)

Role: Receives evaluations, completes self-assessments Goals:

  • Understand his strengths and areas for improvement
  • Complete required self-assessments efficiently
  • Track progress toward competency milestones

Pain Points with Current System:

  • Delayed feedback (evaluations arrive weeks later)
  • Hard to correlate feedback with specific rotations
  • No visibility into evaluation status

User Workflows

Workflow 1: Institutional Admin Creates Evaluation Form

┌─────────────────────────────────────────────────────────────────┐
│ 1. Dr. Chen (Admin) logs into Noumaris                         │
│    → Navigates to "Evaluation Forms" dashboard                 │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 2. Clicks "Create New Form Template"                           │
│    → Enters form details:                                       │
│      - Name: "Monthly Clinical Competency Assessment"          │
│      - Description: "For Family Medicine residents"            │
│      - Target audience: PGY-1 to PGY-3                         │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 3. Uses drag-and-drop form builder to add fields:             │
│    → Text: "Clinical encounter summary"                        │
│    → Rating scale: "Medical Expert competency" (1-5)           │
│    → CanMEDS tags: Select "Medical Expert", "Communicator"    │
│    → Checkbox: "Resident demonstrated EPA 1.1" (mandatory)     │
│    → Signature field: "Preceptor signature" (mandatory)        │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 4. Previews form → Saves as "Draft"                           │
│    → Reviews with program committee                            │
│    → Returns and clicks "Publish Form"                         │
│    → System assigns version number: v1.0                       │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 5. Form is now available for use                              │
│    → Appears in preceptor/senior resident form selection       │
│    → Cannot be edited (must create new version)                │
└─────────────────────────────────────────────────────────────────┘

Non-Technical Explanation: Dr. Chen builds evaluation forms like creating a survey in Google Forms, but with medical education-specific features (CanMEDS competencies, EPA tracking). Once published, the form is locked to preserve historical data integrity.


Workflow 2: Preceptor Evaluates Resident

┌─────────────────────────────────────────────────────────────────┐
│ 1. Dr. Rodriguez (Preceptor) finishes clinic with resident     │
│    → Logs into Noumaris → Sees notification:                   │
│      "Evaluate Dr. Thompson (PGY-1) - Family Medicine Clinic"  │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 2. Clicks notification → Opens evaluation form                 │
│    → Form auto-populates:                                       │
│      - Resident name: Dr. James Thompson                       │
│      - Date: October 23, 2025                                  │
│      - Clinic session: Family Medicine AM Clinic               │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 3. Completes form fields:                                      │
│    → "Clinical encounter summary": "Managed 3 complex cases"   │
│    → "Medical Expert": 4/5 stars                               │
│    → "Communicator": 5/5 stars                                 │
│    → Checks "EPA 1.1 demonstrated"                             │
│    → Adds comments: "Excellent diagnostic reasoning"           │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 4. Signs form digitally → Clicks "Submit Evaluation"          │
│    → System validates all mandatory fields                     │
│    → Saves evaluation with timestamp and signature             │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 5. Dr. Thompson (resident) receives email notification:       │
│    → "New evaluation completed by Dr. Rodriguez"               │
│    → Evaluation appears in "My Evaluations" dashboard          │
└─────────────────────────────────────────────────────────────────┘

Non-Technical Explanation: Preceptors fill out evaluation forms directly in Noumaris after working with residents. The process is streamlined with auto-filled information and takes 2-3 minutes. Residents are immediately notified.


Workflow 3: Resident Self-Assessment with Preceptor Sign-Off

┌─────────────────────────────────────────────────────────────────┐
│ 1. Dr. Patel (PGY-3) completes a procedure independently       │
│    → Wants to document competency achievement                  │
│    → Opens Noumaris → "Self-Assessment" → "New Assessment"     │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 2. Selects form: "Procedure Competency - Central Line"        │
│    → Completes self-assessment:                                │
│      - "I successfully placed central line independently"      │
│      - Self-rates Medical Expert: 4/5                          │
│      - Uploads photo documentation of procedure                │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 3. Clicks "Submit for Sign-Off"                               │
│    → Selects preceptor: Dr. Rodriguez                          │
│    → System sends email to Dr. Rodriguez:                      │
│      "Dr. Patel has submitted self-assessment for your review" │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 4. Dr. Rodriguez reviews self-assessment                       │
│    → Can edit Dr. Patel's responses if needed                  │
│    → Adjusts Medical Expert rating: 4/5 → 5/5                  │
│    → Adds comment: "Excellent technique, independent practice" │
│    → Signs digitally → Clicks "Approve"                        │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 5. Dr. Patel receives notification:                           │
│    → "Self-assessment approved by Dr. Rodriguez"               │
│    → Evaluation now appears in official record                 │
│    → Counts toward EPA completion requirements                 │
└─────────────────────────────────────────────────────────────────┘

Non-Technical Explanation: Residents can document their own achievements and submit them to preceptors for verification. Preceptors can review, adjust ratings if needed, and sign off. This creates a collaborative evaluation process.


Workflow 4: Senior Resident Evaluates Junior Resident

┌─────────────────────────────────────────────────────────────────┐
│ 1. Dr. Patel (PGY-3, Senior) supervises Dr. Thompson (PGY-1)  │
│    → After shift, initiates evaluation                         │
│    → System checks: Is Dr. Patel senior enough to evaluate?    │
│      ✓ PGY-3 ≥ Institution threshold (PGY-3) → Allowed        │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 2. Selects form: "Peer Teaching Assessment"                   │
│    → Evaluates Dr. Thompson's clinical skills                  │
│    → Focuses on teamwork and communication                     │
│    → Signs and submits                                         │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 3. Dr. Thompson receives notification                          │
│    → Views evaluation from senior peer                         │
│    → Evaluation marked as "Peer Evaluation" (vs Preceptor)    │
└─────────────────────────────────────────────────────────────────┘

Non-Technical Explanation: Senior residents (typically PGY-3+) can evaluate junior residents. The system automatically determines who qualifies as "senior" based on training year.


Workflow 5: External Preceptor (Non-Noumaris User) Completes Evaluation

┌─────────────────────────────────────────────────────────────────┐
│ 1. Dr. Chen (Admin) assigns evaluation to external preceptor   │
│    → Enters email: [email protected]
│    → Resident: Dr. Thompson                                    │
│    → Form: "Rural Rotation Assessment"                         │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 2. External preceptor receives email:                          │
│    "You've been asked to evaluate Dr. James Thompson"         │
│    → Email contains:                                           │
│      - Secure magic link (expires in 48 hours)                 │
│      - Resident name and rotation details                      │
│      - Estimated completion time: 5 minutes                    │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 3. Preceptor clicks link → No login required                  │
│    → Opens evaluation form directly                            │
│    → Completes evaluation                                      │
│    → Provides digital signature                                │
│    → Clicks "Submit"                                           │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 4. System saves evaluation                                     │
│    → Links to Dr. Thompson's record                            │
│    → Magic link expires (single-use)                           │
│    → Dr. Thompson receives notification                        │
└─────────────────────────────────────────────────────────────────┘

Non-Technical Explanation: External preceptors (who don't have Noumaris accounts) can complete evaluations via secure email links without creating accounts. This reduces friction while maintaining security.


Workflow 6: Automated Email Reminders

┌─────────────────────────────────────────────────────────────────┐
│ Day 0: Evaluation assigned to Dr. Rodriguez                    │
│        → Immediate email: "New evaluation request"             │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ Day 3: Still incomplete                                        │
│        → Reminder email: "Gentle reminder: evaluation pending" │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ Day 7: Still incomplete                                        │
│        → Reminder email: "Action needed: evaluation overdue"   │
│        → CC to program director (Dr. Chen)                     │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ Day 14: Still incomplete                                       │
│        → Urgent reminder: "Final reminder: evaluation overdue" │
│        → Dr. Chen receives dashboard alert                     │
└─────────────────────────────────────────────────────────────────┘

Non-Technical Explanation: The system automatically sends reminder emails at intervals (3, 7, 14 days) to ensure evaluations are completed. Program directors are notified of overdue evaluations.


Workflow 7: Resident Graduation - Data Export and Archival

┌─────────────────────────────────────────────────────────────────┐
│ 1. Dr. Thompson (PGY-3) completes residency                    │
│    → Dr. Chen initiates "Graduate Resident" workflow           │
│    → Selects Dr. Thompson from resident list                   │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 2. System generates comprehensive export package:              │
│    → PDF: All evaluations with signatures (200+ pages)         │
│    → CSV: Structured data for portfolio analysis               │
│    → Competency progression charts                             │
│    → EPA completion certificates                               │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 3. Export package sent via secure email:                      │
│    → To: Dr. Thompson's personal email                         │
│    → CC: Dr. Chen (program director)                           │
│    → Password-protected ZIP file                               │
│    → Download link expires in 30 days                          │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 4. Data retention policy executed:                            │
│    → Day 0-90: Data remains in system (grace period)           │
│    → Day 90: Soft-delete from active database                 │
│    → Day 365: Hard-delete (GDPR/HIPAA compliance)              │
│    → Audit log retained for 7 years (accreditation)            │
└─────────────────────────────────────────────────────────────────┘

Non-Technical Explanation: When residents graduate, the system automatically packages all their evaluations for their career portfolio, then securely deletes personal data after a retention period. This complies with privacy regulations while preserving accreditation records.


Technical Architecture

System Components

┌─────────────────────────────────────────────────────────────────────┐
│                         FRONTEND (React)                            │
│  ┌──────────────────┐  ┌──────────────────┐  ┌─────────────────┐  │
│  │  Form Builder    │  │  Evaluation      │  │  Admin          │  │
│  │  Component       │  │  Dashboard       │  │  Dashboard      │  │
│  └──────────────────┘  └──────────────────┘  └─────────────────┘  │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │  Dynamic Form Renderer (JSONB → UI Components)              │  │
│  └──────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────┘
                                  ↓ HTTPS/JWT
┌─────────────────────────────────────────────────────────────────────┐
│                      BACKEND API (FastAPI)                          │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │  API Endpoints                                               │  │
│  │  • /api/evaluations/templates (CRUD)                        │  │
│  │  • /api/evaluations (workflow management)                   │  │
│  │  • /api/evaluations/{id}/sign (approval workflow)           │  │
│  │  • /api/preceptors (external preceptor management)          │  │
│  └──────────────────────────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │  Business Logic Layer                                        │  │
│  │  • Permission validation (role-based access)                │  │
│  │  • Form versioning engine                                   │  │
│  │  • Email notification service                               │  │
│  │  • Magic link token generator                               │  │
│  └──────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│                      DATABASE (PostgreSQL)                          │
│  ┌────────────────────┐  ┌────────────────────┐                    │
│  │ evaluation_form_   │  │ evaluations        │                    │
│  │ templates          │  │ (with JSONB)       │                    │
│  └────────────────────┘  └────────────────────┘                    │
│  ┌────────────────────┐  ┌────────────────────┐                    │
│  │ evaluation_        │  │ preceptors         │                    │
│  │ signoffs           │  │ (external)         │                    │
│  └────────────────────┘  └────────────────────┘                    │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│                 BACKGROUND SERVICES                                 │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │  Email Service (SendGrid/AWS SES)                           │  │
│  │  • Evaluation notifications                                 │  │
│  │  • Reminder scheduling (3, 7, 14 day intervals)            │  │
│  │  • Magic link delivery                                      │  │
│  └──────────────────────────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │  Cron Jobs (FastAPI BackgroundTasks)                        │  │
│  │  • Reminder checker (runs daily)                            │  │
│  │  • Data archival (runs weekly)                              │  │
│  │  • Magic link expiry cleanup                                │  │
│  └──────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────┘

Database Schema Design

Core Tables

1. evaluation_form_templates

Stores evaluation form templates with versioning.

ColumnTypeDescription
idINTEGER (PK)Unique identifier
institution_idINTEGER (FK)Institution owning this template
nameVARCHAR(255)Template name (e.g., "Monthly Clinical Assessment")
descriptionTEXTPurpose and usage instructions
version_numberINTEGERVersion tracking (1, 2, 3...)
parent_template_idINTEGER (FK, nullable)Links to previous version
statusENUMdraft, published, retired, deleted
form_schemaJSONBComplete form structure (fields, validation, layout)
created_byVARCHARUser ID of creator (admin)
published_atTIMESTAMPWhen template was published
retired_atTIMESTAMPWhen template was retired
created_atTIMESTAMPCreation timestamp
updated_atTIMESTAMPLast modification timestamp

Index Strategy:

  • idx_institution_status on (institution_id, status) - Fast filtering by institution and status
  • idx_parent_version on (parent_template_id, version_number) - Version lineage queries

Form Schema Structure (JSONB):

json
{
  "version": "1.0",
  "title": "Monthly Clinical Competency Assessment",
  "sections": [
    {
      "id": "section_1",
      "title": "Clinical Encounter Details",
      "fields": [
        {
          "id": "field_1",
          "type": "textarea",
          "label": "Describe the clinical encounter",
          "required": true,
          "max_length": 500
        },
        {
          "id": "field_2",
          "type": "rating_scale",
          "label": "Medical Expert Competency",
          "required": true,
          "scale": {
            "min": 1,
            "max": 5,
            "labels": {
              "1": "Needs significant improvement",
              "3": "Meets expectations",
              "5": "Exceptional"
            }
          }
        },
        {
          "id": "field_3",
          "type": "canmeds_tags",
          "label": "CanMEDS Roles Demonstrated",
          "required": true,
          "options": [
            "Medical Expert",
            "Communicator",
            "Collaborator",
            "Leader",
            "Health Advocate",
            "Scholar",
            "Professional"
          ]
        },
        {
          "id": "field_4",
          "type": "signature",
          "label": "Preceptor Signature",
          "required": true
        }
      ]
    }
  ],
  "metadata": {
    "target_audience": ["PGY-1", "PGY-2", "PGY-3"],
    "specialty": "Family Medicine",
    "completion_time_minutes": 10
  }
}

2. evaluations

Individual evaluation instances linked to residents.

ColumnTypeDescription
idINTEGER (PK)Unique identifier
form_template_idINTEGER (FK)Reference to template used
form_snapshotJSONBFrozen copy of form schema at creation time
evaluated_resident_idVARCHAR (FK)User ID of resident being evaluated
evaluator_idVARCHAR (FK)User ID of person completing evaluation
evaluator_typeENUMpreceptor, senior_resident, self, external_preceptor
evaluator_emailVARCHARFor external preceptors
statusENUMdraft, submitted, pending_signoff, approved, rejected
responsesJSONBUser's answers to form fields
submitted_atTIMESTAMPWhen evaluation was submitted
approved_atTIMESTAMPWhen preceptor approved
approved_byVARCHARUser ID of approver
visibilityENUMvisible_after_approval, immediately_visible
encounter_dateDATEDate of clinical encounter
rotationVARCHARRotation name (e.g., "Family Medicine Clinic")
created_atTIMESTAMPCreation timestamp
updated_atTIMESTAMPLast modification timestamp

Index Strategy:

  • idx_resident_status on (evaluated_resident_id, status) - Resident's evaluation dashboard
  • idx_evaluator_status on (evaluator_id, status) - Evaluator's pending tasks
  • idx_encounter_date on (encounter_date DESC) - Timeline views

Responses Structure (JSONB):

json
{
  "field_1": {
    "value": "Patient presented with chest pain. Resident performed thorough history and physical...",
    "edited_by": null,
    "edited_at": null
  },
  "field_2": {
    "value": 4,
    "edited_by": "preceptor_user_id_123",
    "edited_at": "2025-10-23T14:30:00Z"
  },
  "field_3": {
    "value": ["Medical Expert", "Communicator", "Professional"],
    "edited_by": null,
    "edited_at": null
  },
  "field_4": {
    "value": "data:image/png;base64,iVBORw0KGgoAAAANS...",
    "signature_timestamp": "2025-10-23T14:35:00Z",
    "signer_name": "Dr. Michael Rodriguez"
  }
}

3. evaluation_signoffs

Tracks approval workflow for evaluations.

ColumnTypeDescription
idINTEGER (PK)Unique identifier
evaluation_idINTEGER (FK)Evaluation being signed off
signer_idVARCHAR (FK)User ID of person signing
signer_roleENUMpreceptor, program_director, chief_resident
actionENUMapproved, rejected, requested_changes
commentsTEXTFeedback or reasons for rejection
signature_dataTEXTBase64-encoded signature image
changes_madeJSONBAudit trail of edits before sign-off
signed_atTIMESTAMPWhen action was taken
ip_addressVARCHARIP address for audit trail

Index Strategy:

  • idx_evaluation_id on (evaluation_id, signed_at DESC) - Sign-off history

Changes Made Audit (JSONB):

json
{
  "field_2": {
    "original_value": 3,
    "new_value": 4,
    "reason": "Underestimated competency initially"
  }
}

4. preceptors

External preceptors who don't have Noumaris accounts.

ColumnTypeDescription
idINTEGER (PK)Unique identifier
institution_idINTEGER (FK)Institution they're associated with
emailVARCHARContact email (unique per institution)
first_nameVARCHARFirst name
last_nameVARCHARLast name
specialtyVARCHARMedical specialty
credentialsVARCHARMD, DO, MBBS, etc.
phoneVARCHARPhone number (optional)
is_verifiedBOOLEANEmail verification status
magic_link_tokenVARCHARCurrent active magic link token
token_expires_atTIMESTAMPMagic link expiry
last_active_atTIMESTAMPLast time they completed an evaluation
created_byVARCHARAdmin who added them
created_atTIMESTAMPCreation timestamp
updated_atTIMESTAMPLast modification timestamp

Index Strategy:

  • idx_institution_email on (institution_id, email) - Unique constraint
  • idx_magic_token on (magic_link_token) - Fast token lookup

5. evaluation_reminders

Scheduled email reminders for pending evaluations.

ColumnTypeDescription
idINTEGER (PK)Unique identifier
evaluation_idINTEGER (FK)Evaluation to remind about
recipient_emailVARCHAREmail to send reminder to
reminder_typeENUMinitial, 3_day, 7_day, 14_day, urgent
scheduled_forTIMESTAMPWhen to send reminder
sent_atTIMESTAMPWhen reminder was actually sent
statusENUMpending, sent, failed, cancelled
email_template_idVARCHARReference to email template used
created_atTIMESTAMPCreation timestamp

Index Strategy:

  • idx_scheduled_status on (scheduled_for, status) - Cron job queries

6. evaluation_exports

Tracks data exports for graduated/departed residents.

ColumnTypeDescription
idINTEGER (PK)Unique identifier
resident_idVARCHAR (FK)User ID of resident
institution_idINTEGER (FK)Institution
exported_byVARCHAR (FK)Admin who initiated export
export_formatENUMpdf, csv, combined
file_pathVARCHARCloud storage path (GCS bucket)
file_size_bytesBIGINTFile size
download_linkVARCHARTemporary download URL
link_expires_atTIMESTAMPDownload link expiry (30 days)
retention_period_endTIMESTAMPWhen to hard-delete (90 days)
exported_atTIMESTAMPExport timestamp
downloaded_atTIMESTAMPWhen resident downloaded
statusENUMgenerated, downloaded, expired, deleted

Index Strategy:

  • idx_retention_status on (retention_period_end, status) - Cleanup job

7. evaluation_field_types (Reference Data)

Predefined field types for form builder.

ColumnTypeDescription
idINTEGER (PK)Unique identifier
field_typeVARCHARtext_input, textarea, rating_scale, etc.
display_nameVARCHAR"Text Input", "Rating Scale (1-5)"
descriptionTEXTUsage instructions
default_configJSONBDefault settings for this field type
iconVARCHARIcon identifier for UI
categoryVARCHAR"Basic", "Advanced", "Clinical-Specific"
is_activeBOOLEANEnabled/disabled

Seed Data:

sql
INSERT INTO evaluation_field_types (field_type, display_name, category, default_config) VALUES
  ('text_input', 'Short Text', 'Basic', '{"max_length": 255}'),
  ('textarea', 'Long Text', 'Basic', '{"max_length": 2000, "rows": 5}'),
  ('rating_scale', 'Rating Scale', 'Advanced', '{"min": 1, "max": 5}'),
  ('canmeds_tags', 'CanMEDS Roles', 'Clinical-Specific', '{"multi_select": true}'),
  ('epa_assessment', 'EPA Achievement', 'Clinical-Specific', '{"epa_list": []}'),
  ('signature', 'Digital Signature', 'Advanced', '{"required": true}');

Database Relationships

institutions (1) ──────< (many) evaluation_form_templates
                 ──────< (many) preceptors
                 ──────< (many) resident_profiles

evaluation_form_templates (1) ──────< (many) evaluations
evaluation_form_templates (self-referencing) parent_template_id → version lineage

users (as resident) (1) ──────< (many) evaluations [evaluated_resident_id]
users (as evaluator) (1) ──────< (many) evaluations [evaluator_id]

evaluations (1) ──────< (many) evaluation_signoffs
evaluations (1) ──────< (many) evaluation_reminders

users (as resident) (1) ──────< (many) evaluation_exports

PostgreSQL-Specific Optimizations

1. JSONB Indexing for Fast Queries

sql
-- Index on form_schema for fast field searches
CREATE INDEX idx_form_schema_gin ON evaluation_form_templates USING GIN (form_schema);

-- Index on responses for searching evaluation content
CREATE INDEX idx_responses_gin ON evaluations USING GIN (responses);

-- Query example: Find all evaluations where Medical Expert rating >= 4
SELECT * FROM evaluations
WHERE responses @> '{"field_2": {"value": 4}}';

2. Partial Indexes for Active Records

sql
-- Only index active templates (exclude deleted/retired)
CREATE INDEX idx_active_templates
ON evaluation_form_templates (institution_id, created_at DESC)
WHERE status IN ('draft', 'published');

-- Only index pending evaluations (most common queries)
CREATE INDEX idx_pending_evaluations
ON evaluations (evaluator_id, created_at DESC)
WHERE status IN ('draft', 'pending_signoff');

3. Materialized View for Analytics

sql
-- Pre-computed resident competency dashboard
CREATE MATERIALIZED VIEW resident_competency_summary AS
SELECT
  evaluated_resident_id,
  form_template_id,
  COUNT(*) as total_evaluations,
  AVG((responses->'field_2'->>'value')::numeric) as avg_medical_expert_rating,
  jsonb_agg(DISTINCT responses->'field_3'->'value') as canmeds_roles_covered,
  MAX(encounter_date) as last_evaluation_date
FROM evaluations
WHERE status = 'approved'
GROUP BY evaluated_resident_id, form_template_id;

-- Refresh nightly via cron job
REFRESH MATERIALIZED VIEW CONCURRENTLY resident_competency_summary;

Backend API Design

RESTful Endpoint Specification

Form Template Management

POST /api/evaluations/templates

Purpose: Create new evaluation form template (draft) Permission: Institutional admin only Request Body:

json
{
  "name": "Monthly Clinical Competency Assessment",
  "description": "Standard evaluation for all residents",
  "form_schema": {
    "sections": [...],
    "metadata": {...}
  }
}

Response: 201 Created

json
{
  "id": 123,
  "status": "draft",
  "version_number": 1,
  "created_at": "2025-10-23T10:00:00Z"
}

PUT /api/evaluations/templates/{id}

Purpose: Update draft template Permission: Institutional admin, same institution Validation: Only drafts can be updated; published templates are immutable Request Body: Partial or full form_schema update Response: 200 OK with updated template


POST /api/evaluations/templates/{id}/publish

Purpose: Publish draft template, making it available for use Permission: Institutional admin with can_manage_documents permission Business Logic:

  1. Validate all mandatory fields are present in form_schema
  2. Set status = 'published'
  3. Set published_at = NOW()
  4. If previous version exists, link via parent_template_id
  5. Increment version_number
  6. Create audit log entry

Response: 200 OK

json
{
  "id": 123,
  "status": "published",
  "version_number": 1,
  "published_at": "2025-10-23T14:00:00Z",
  "message": "Template published successfully. Now available for evaluations."
}

POST /api/evaluations/templates/{id}/retire

Purpose: Retire published template (hide from new assignments) Permission: Institutional admin Business Logic:

  • Set status = 'retired', retired_at = NOW()
  • Existing evaluations using this template remain unaffected
  • Template no longer appears in form selection dropdowns

GET /api/evaluations/templates

Purpose: List templates with filtering Permission: All authenticated users (filtered by institution) Query Parameters:

  • status: draft, published, retired (default: published)
  • institution_id: Filter by institution (admins only)
  • page, limit: Pagination

Response: 200 OK

json
{
  "templates": [
    {
      "id": 123,
      "name": "Monthly Clinical Assessment",
      "version_number": 2,
      "status": "published",
      "usage_count": 47
    }
  ],
  "total": 15,
  "page": 1
}

Evaluation Workflows

POST /api/evaluations

Purpose: Create new evaluation Permission: Preceptors, senior residents (PGY ≥ 3) Request Body:

json
{
  "form_template_id": 123,
  "evaluated_resident_id": "resident_user_id_456",
  "encounter_date": "2025-10-23",
  "rotation": "Family Medicine Clinic"
}

Response: 201 Created

json
{
  "id": 789,
  "status": "draft",
  "form_snapshot": {...},  // Full form schema copied
  "evaluator_type": "preceptor"
}

POST /api/evaluations/self

Purpose: Resident creates self-assessment Permission: Resident users only Request Body: Same as above, but evaluated_resident_id auto-set to current user Business Logic:

  • Set evaluator_type = 'self'
  • Set status = 'draft'
  • Evaluation NOT visible to resident until approved by preceptor

PUT /api/evaluations/{id}/responses

Purpose: Update evaluation responses Permission: Evaluation owner OR preceptor during sign-off review Request Body:

json
{
  "responses": {
    "field_1": {"value": "Excellent clinical encounter..."},
    "field_2": {"value": 5}
  }
}

Business Logic:

  • If edited by preceptor during sign-off, track changes in audit log
  • Validate against form_snapshot schema
  • Auto-save every 30 seconds (frontend)

POST /api/evaluations/{id}/submit

Purpose: Submit evaluation (makes it final) Permission: Evaluation creator Business Logic:

  1. Validate all required fields completed
  2. If evaluator_type = 'self', set status = 'pending_signoff' and trigger email to preceptor
  3. If evaluator_type = 'preceptor' or 'senior_resident', set status = 'submitted' and notify resident
  4. Create evaluation_reminders record if pending sign-off

Response: 200 OK

json
{
  "status": "pending_signoff",
  "message": "Evaluation submitted to Dr. Rodriguez for sign-off"
}

POST /api/evaluations/{id}/sign

Purpose: Preceptor approves/rejects evaluation Permission: Designated preceptor OR program director Request Body:

json
{
  "action": "approved",  // or "rejected", "requested_changes"
  "comments": "Excellent self-assessment. Agreed with all points.",
  "signature_data": "data:image/png;base64,...",
  "changes_made": {
    "field_2": {"original_value": 4, "new_value": 5}
  }
}

Business Logic:

  1. Create record in evaluation_signoffs
  2. Update evaluations.status:
    • approvedstatus = 'approved', approved_at = NOW()
    • rejectedstatus = 'rejected'
    • requested_changesstatus = 'draft', send notification
  3. If approved, make evaluation visible to resident
  4. Cancel pending reminders

GET /api/evaluations

Purpose: List evaluations (role-based filtering) Permission: All authenticated users Query Parameters:

  • evaluated_resident_id: Filter by resident
  • evaluator_id: Filter by evaluator
  • status: Filter by status
  • date_from, date_to: Date range
  • form_template_id: Filter by form type

Business Logic (automatic filtering):

  • Residents: Only see approved evaluations where evaluated_resident_id = current_user.id
  • Preceptors: See all evaluations where evaluator_id = current_user.id OR assigned for sign-off
  • Admins: See all evaluations in their institution

External Preceptor Management

POST /api/preceptors

Purpose: Add external preceptor to institution Permission: Institutional admin Request Body:

json
{
  "email": "[email protected]",
  "first_name": "Jane",
  "last_name": "Smith",
  "specialty": "Emergency Medicine",
  "credentials": "MD, FRCPC"
}

Response: 201 Created


POST /api/evaluations/assign-external

Purpose: Assign evaluation to external preceptor via email Permission: Institutional admin Request Body:

json
{
  "preceptor_email": "[email protected]",
  "form_template_id": 123,
  "evaluated_resident_id": "resident_user_id_456",
  "encounter_date": "2025-10-23"
}

Business Logic:

  1. Create evaluation with evaluator_type = 'external_preceptor'
  2. Generate magic link token (JWT, 48-hour expiry)
  3. Send email with link: https://app.noumaris.com/evaluations/guest/{token}
  4. Store token in preceptors.magic_link_token

GET /api/evaluations/guest/{token}

Purpose: External preceptor accesses evaluation via magic link Permission: Public (token-based authentication) Business Logic:

  1. Verify token signature and expiry
  2. Decode token to get evaluation_id
  3. Return evaluation form + prefilled data
  4. Allow form completion without login

POST /api/evaluations/guest/{token}/submit

Purpose: External preceptor submits evaluation Permission: Valid magic link token Business Logic:

  1. Validate token
  2. Save responses to evaluation
  3. Set status = 'submitted'
  4. Invalidate token (single-use)
  5. Notify resident and admin

Data Export & Archival

POST /api/evaluations/export/{resident_id}

Purpose: Export all evaluations for a resident Permission: Institutional admin OR resident (self-export) Query Parameters:

  • format: pdf, csv, combined (default: combined)
  • include_drafts: true/false (default: false)

Business Logic:

  1. Query all approved evaluations for resident
  2. Generate PDF (formatted reports) using ReportLab/WeasyPrint
  3. Generate CSV (structured data)
  4. Zip files, upload to GCS bucket
  5. Create temporary signed URL (30-day expiry)
  6. Create record in evaluation_exports
  7. Send email with download link

Response: 202 Accepted

json
{
  "export_id": 456,
  "status": "processing",
  "message": "Export started. You'll receive an email with download link shortly."
}

POST /api/evaluations/archive/{resident_id}

Purpose: Archive resident data after graduation Permission: Institutional admin with can_manage_residentsBusiness Logic:

  1. Trigger export workflow first
  2. Set resident_profiles.status = 'archived'
  3. Anonymize PII in evaluations (replace with "Archived Resident [ID]")
  4. Schedule hard-delete job for 90 days later
  5. Create audit log entry

Permission Matrix

EndpointSuperadminInst. AdminPreceptorSenior ResidentResident
POST /evaluations/templates
POST /evaluations/templates/{id}/publish
POST /evaluations✓ (if PGY ≥ 3)
POST /evaluations/self
POST /evaluations/{id}/sign✓ (if assigned)
GET /evaluations (all)
GET /evaluations (own)
POST /evaluations/export/{resident_id}✓ (self only)
POST /evaluations/archive/{resident_id}

Error Handling Standards

Standard Error Response Format

json
{
  "error": {
    "code": "EVALUATION_001",
    "message": "Cannot edit published template",
    "details": "Template ID 123 is published. Create a new version to make changes.",
    "timestamp": "2025-10-23T14:30:00Z"
  }
}

Error Codes

  • EVAL_001: Cannot edit published/retired template
  • EVAL_002: Evaluation not found or access denied
  • EVAL_003: Invalid form schema (validation failed)
  • EVAL_004: Missing required fields in evaluation
  • EVAL_005: Invalid sign-off permission
  • EVAL_006: Magic link expired or invalid
  • EVAL_007: Senior resident insufficient PGY level
  • EVAL_008: Export already in progress

Frontend Implementation

Component Architecture

src/components/evaluations/
├── admin/
│   ├── FormBuilder.jsx              # Drag-drop form creation UI
│   ├── FormTemplateList.jsx         # Template library (tabs: Draft/Published/Retired)
│   ├── FormPreview.jsx              # Live preview panel
│   ├── FieldPalette.jsx             # Draggable field types
│   └── VersionHistory.jsx           # Template version lineage
├── forms/
│   ├── DynamicFormRenderer.jsx      # Renders JSONB → React components
│   ├── FormFieldFactory.jsx         # Maps field types to components
│   ├── fields/
│   │   ├── TextInputField.jsx
│   │   ├── RatingScaleField.jsx
│   │   ├── CanMEDSTagSelector.jsx   # Multi-select CanMEDS competencies
│   │   ├── SignatureField.jsx       # Digital signature capture
│   │   ├── EPAAssessmentField.jsx   # EPA achievement checkboxes
│   │   └── FileUploadField.jsx
│   └── FormValidation.js            # Client-side validation logic
├── dashboards/
│   ├── ResidentEvaluationDashboard.jsx    # Resident view
│   ├── PreceptorEvaluationDashboard.jsx   # Preceptor view
│   ├── AdminEvaluationDashboard.jsx       # Admin analytics
│   └── EvaluationTimeline.jsx             # Workflow progress tracker
├── workflows/
│   ├── EvaluationCreate.jsx         # Create new evaluation
│   ├── EvaluationEdit.jsx           # Edit draft evaluation
│   ├── SelfAssessment.jsx           # Resident self-assessment flow
│   ├── SignOffModal.jsx             # Approve/reject/request changes
│   └── ExternalPreceptorView.jsx    # Magic link guest view
└── shared/
    ├── EvaluationCard.jsx           # List item component
    ├── StatusBadge.jsx              # Status indicators
    └── ReminderScheduler.jsx        # Admin reminder configuration

Key Frontend Features

1. Form Builder (Admin)

Technology: React DnD (Drag and Drop)

Features:

  • Drag field types from palette to form canvas
  • Click field to configure (label, required, validation)
  • Reorder fields via drag
  • Delete fields
  • Live preview panel
  • Save as draft / Publish workflow

State Management:

javascript
const [formSchema, setFormSchema] = useState({
  sections: [
    {
      id: 'section_1',
      title: 'Clinical Encounter',
      fields: []
    }
  ]
});

const handleAddField = (fieldType, sectionId) => {
  const newField = {
    id: `field_${Date.now()}`,
    type: fieldType,
    label: '',
    required: false,
    config: DEFAULT_CONFIGS[fieldType]
  };
  // Add to section...
};

2. Dynamic Form Renderer

Purpose: Render evaluation forms from JSONB schema

Implementation:

javascript
// DynamicFormRenderer.jsx
import FormFieldFactory from './FormFieldFactory';

const DynamicFormRenderer = ({ formSchema, responses, onResponseChange, readOnly }) => {
  return (
    <form>
      {formSchema.sections.map(section => (
        <section key={section.id}>
          <h3>{section.title}</h3>
          {section.fields.map(field => (
            <FormFieldFactory
              key={field.id}
              field={field}
              value={responses[field.id]?.value}
              onChange={(value) => onResponseChange(field.id, value)}
              readOnly={readOnly}
            />
          ))}
        </section>
      ))}
    </form>
  );
};

// FormFieldFactory.jsx
const FormFieldFactory = ({ field, value, onChange, readOnly }) => {
  switch (field.type) {
    case 'text_input':
      return <TextInputField {...field} value={value} onChange={onChange} readOnly={readOnly} />;
    case 'rating_scale':
      return <RatingScaleField {...field} value={value} onChange={onChange} readOnly={readOnly} />;
    case 'canmeds_tags':
      return <CanMEDSTagSelector {...field} value={value} onChange={onChange} readOnly={readOnly} />;
    case 'signature':
      return <SignatureField {...field} value={value} onChange={onChange} readOnly={readOnly} />;
    // ... other field types
    default:
      return <div>Unknown field type: {field.type}</div>;
  }
};

3. CanMEDS Tag Selector

Component: Multi-select dropdown with color-coded tags

UI Design:

┌─────────────────────────────────────────────────────────────┐
│ CanMEDS Roles Demonstrated *                               │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ [Medical Expert ×] [Communicator ×] [Professional ×]   │ │
│ │ ▼ Select more roles...                                  │ │
│ └─────────────────────────────────────────────────────────┘ │
│   Available roles:                                          │
│   □ Collaborator    □ Leader    □ Health Advocate          │
│   □ Scholar                                                 │
└─────────────────────────────────────────────────────────────┘

Implementation:

javascript
const CanMEDSTagSelector = ({ value = [], onChange, required }) => {
  const CANMEDS_ROLES = [
    { id: 'medical_expert', label: 'Medical Expert', color: 'blue' },
    { id: 'communicator', label: 'Communicator', color: 'green' },
    { id: 'collaborator', label: 'Collaborator', color: 'purple' },
    { id: 'leader', label: 'Leader', color: 'orange' },
    { id: 'health_advocate', label: 'Health Advocate', color: 'red' },
    { id: 'scholar', label: 'Scholar', color: 'indigo' },
    { id: 'professional', label: 'Professional', color: 'gray' }
  ];

  const handleToggle = (roleId) => {
    if (value.includes(roleId)) {
      onChange(value.filter(id => id !== roleId));
    } else {
      onChange([...value, roleId]);
    }
  };

  return (
    <div className="canmeds-selector">
      <label>CanMEDS Roles Demonstrated {required && '*'}</label>
      <div className="selected-tags">
        {value.map(roleId => {
          const role = CANMEDS_ROLES.find(r => r.id === roleId);
          return (
            <span key={roleId} className={`tag tag-${role.color}`}>
              {role.label}
              <button onClick={() => handleToggle(roleId)}>×</button>
            </span>
          );
        })}
      </div>
      <div className="available-roles">
        {CANMEDS_ROLES.filter(role => !value.includes(role.id)).map(role => (
          <label key={role.id}>
            <input type="checkbox" onChange={() => handleToggle(role.id)} />
            {role.label}
          </label>
        ))}
      </div>
    </div>
  );
};

4. Signature Capture

Library: react-signature-canvas

Features:

  • Touchscreen/mouse drawing
  • Clear and redraw
  • Save as base64 PNG
  • Display saved signatures

Implementation:

javascript
import SignatureCanvas from 'react-signature-canvas';

const SignatureField = ({ value, onChange, readOnly, label }) => {
  const sigCanvas = useRef(null);

  const handleSave = () => {
    const dataUrl = sigCanvas.current.toDataURL('image/png');
    onChange({
      value: dataUrl,
      signature_timestamp: new Date().toISOString(),
      signer_name: currentUser.full_name
    });
  };

  const handleClear = () => {
    sigCanvas.current.clear();
  };

  if (readOnly && value) {
    return (
      <div>
        <label>{label}</label>
        <img src={value.value} alt="Signature" />
        <p className="text-sm text-gray-500">
          Signed by {value.signer_name} on {new Date(value.signature_timestamp).toLocaleString()}
        </p>
      </div>
    );
  }

  return (
    <div className="signature-field">
      <label>{label}</label>
      <div className="signature-canvas-container">
        <SignatureCanvas
          ref={sigCanvas}
          canvasProps={{ width: 500, height: 200, className: 'signature-canvas' }}
        />
      </div>
      <div className="signature-actions">
        <button type="button" onClick={handleClear}>Clear</button>
        <button type="button" onClick={handleSave}>Save Signature</button>
      </div>
    </div>
  );
};

5. Evaluation Timeline (Workflow Progress)

Component: Visual stepper showing evaluation status

UI Design:

┌─────────────────────────────────────────────────────────────────┐
│ Evaluation Timeline                                             │
│                                                                 │
│  ✓ Created                 ✓ Submitted              ⏳ Pending  │
│  Oct 20, 2025              Oct 20, 2025             Sign-off    │
│  by Dr. Thompson           by Dr. Thompson                      │
│       │                         │                        │       │
│       └─────────────────────────┴────────────────────────┘       │
│                                                                 │
│  Waiting for: Dr. Rodriguez to approve                         │
│  Reminder sent: Oct 23, 2025                                   │
└─────────────────────────────────────────────────────────────────┘

6. Auto-Save Functionality

Implementation: Debounced auto-save every 30 seconds

javascript
import { useDebounce } from 'use-debounce';

const EvaluationEdit = ({ evaluationId }) => {
  const [responses, setResponses] = useState({});
  const [debouncedResponses] = useDebounce(responses, 30000); // 30 seconds

  useEffect(() => {
    if (Object.keys(debouncedResponses).length > 0) {
      api.put(`/api/evaluations/${evaluationId}/responses`, {
        responses: debouncedResponses
      });
      showToast('Draft saved', 'success');
    }
  }, [debouncedResponses]);

  return (
    <DynamicFormRenderer
      formSchema={formSchema}
      responses={responses}
      onResponseChange={(fieldId, value) => {
        setResponses(prev => ({
          ...prev,
          [fieldId]: { value }
        }));
      }}
    />
  );
};

Routing Structure

javascript
// App.jsx routes
<Routes>
  {/* Admin routes */}
  <Route path="/evaluations/admin" element={<ProtectedRoute roles={['institution_admin']} />}>
    <Route index element={<AdminEvaluationDashboard />} />
    <Route path="templates" element={<FormTemplateList />} />
    <Route path="templates/new" element={<FormBuilder />} />
    <Route path="templates/:id/edit" element={<FormBuilder />} />
    <Route path="analytics" element={<EvaluationAnalytics />} />
  </Route>

  {/* Preceptor routes */}
  <Route path="/evaluations/preceptor" element={<ProtectedRoute roles={['preceptor', 'institution_admin']} />}>
    <Route index element={<PreceptorEvaluationDashboard />} />
    <Route path="new" element={<EvaluationCreate />} />
    <Route path=":id/edit" element={<EvaluationEdit />} />
    <Route path=":id/signoff" element={<SignOffModal />} />
  </Route>

  {/* Resident routes */}
  <Route path="/evaluations/resident" element={<ProtectedRoute roles={['resident']} />}>
    <Route index element={<ResidentEvaluationDashboard />} />
    <Route path="self-assessment" element={<SelfAssessment />} />
    <Route path=":id/view" element={<EvaluationView readOnly />} />
    <Route path="export" element={<ExportData />} />
  </Route>

  {/* Public routes (external preceptors) */}
  <Route path="/evaluations/guest/:token" element={<ExternalPreceptorView />} />
</Routes>

Email Notification System

Email Templates

1. Evaluation Assigned (to Preceptor)

Subject: Evaluate Dr. [Resident Name] - [Form Name]

Hi Dr. [Preceptor Name],

You've been assigned to evaluate Dr. [Resident Name] using the "[Form Name]" evaluation form.

Encounter Details:
- Date: [Encounter Date]
- Rotation: [Rotation Name]
- Estimated completion time: 5-10 minutes

[Complete Evaluation Button - Links to: /evaluations/preceptor/[id]/edit]

Questions? Contact [Program Director Email]

This evaluation was assigned by [Admin Name] from [Institution Name].

2. Self-Assessment Submitted for Sign-Off (to Preceptor)

Subject: Sign-off requested: Dr. [Resident Name]'s self-assessment

Hi Dr. [Preceptor Name],

Dr. [Resident Name] has completed a self-assessment and requested your sign-off.

Self-Assessment: [Form Name]
Encounter Date: [Date]
Submitted: [Timestamp]

Please review, make any necessary edits, and approve or request changes.

[Review & Sign Off Button - Links to: /evaluations/preceptor/[id]/signoff]

Reminder: You can edit the resident's responses before signing if needed.

3. Evaluation Approved (to Resident)

Subject: Evaluation completed by Dr. [Preceptor Name]

Hi Dr. [Resident Name],

Dr. [Preceptor Name] has completed your evaluation.

Evaluation: [Form Name]
Encounter Date: [Date]
Approved: [Timestamp]

[View Evaluation Button - Links to: /evaluations/resident/[id]/view]

This evaluation is now part of your permanent training record.

4. Reminder - Pending Evaluation (3-day)

Subject: Reminder: Evaluation for Dr. [Resident Name]

Hi Dr. [Preceptor Name],

Friendly reminder: You have a pending evaluation for Dr. [Resident Name].

Assigned: 3 days ago
Encounter Date: [Date]
Form: [Form Name]

[Complete Now Button]

We appreciate your timely feedback to help residents track their progress.

5. Reminder - Overdue Evaluation (7-day, 14-day)

Subject: Action needed: Overdue evaluation

Hi Dr. [Preceptor Name],

This evaluation for Dr. [Resident Name] is now overdue.

Assigned: 7 days ago
Encounter: [Date] - [Rotation]

[Complete Evaluation Button]

If you have questions or need to reassign this evaluation, please contact [Program Director].

---
CC: [Program Director Email]

Email Service Integration

Recommended Service: SendGrid or AWS SES

Implementation:

python
# backend/src/noumaris_backend/services/email_service.py

from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail
import os

class EmailService:
    def __init__(self):
        self.sg = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY'))
        self.from_email = "[email protected]"

    def send_evaluation_assigned(self, preceptor_email, resident_name, form_name, evaluation_id):
        """Send email when evaluation is assigned to preceptor"""
        message = Mail(
            from_email=self.from_email,
            to_emails=preceptor_email,
            subject=f"Evaluate Dr. {resident_name} - {form_name}",
            html_content=self._render_template('evaluation_assigned.html', {
                'resident_name': resident_name,
                'form_name': form_name,
                'evaluation_url': f"https://app.noumaris.com/evaluations/preceptor/{evaluation_id}/edit"
            })
        )

        try:
            response = self.sg.send(message)
            return response.status_code == 202
        except Exception as e:
            logger.error(f"Email send failed: {e}")
            return False

    def send_signoff_request(self, preceptor_email, resident_name, form_name, evaluation_id):
        """Send email when resident submits self-assessment for sign-off"""
        # Similar implementation...

    def send_reminder(self, preceptor_email, resident_name, days_overdue, evaluation_id):
        """Send reminder email for pending evaluation"""
        # Similar implementation...

    def _render_template(self, template_name, context):
        """Render HTML email template with Jinja2"""
        from jinja2 import Environment, FileSystemLoader
        env = Environment(loader=FileSystemLoader('templates/emails'))
        template = env.get_template(template_name)
        return template.render(**context)

Reminder Scheduling (Background Job)

Implementation: FastAPI BackgroundTasks + APScheduler

python
# backend/src/noumaris_backend/services/reminder_scheduler.py

from apscheduler.schedulers.asyncio import AsyncIOScheduler
from datetime import datetime, timedelta
from .email_service import EmailService

class ReminderScheduler:
    def __init__(self):
        self.scheduler = AsyncIOScheduler()
        self.email_service = EmailService()

    def start(self):
        """Start scheduler (runs on app startup)"""
        # Check for pending reminders every day at 9 AM
        self.scheduler.add_job(
            self.process_reminders,
            'cron',
            hour=9,
            minute=0
        )
        self.scheduler.start()

    async def process_reminders(self):
        """Check all pending evaluations and send reminders"""
        with db_manager.create_session() as session:
            # Get evaluations in draft/pending_signoff status
            pending_evals = session.query(Evaluation).filter(
                Evaluation.status.in_(['draft', 'pending_signoff']),
                Evaluation.submitted_at == None
            ).all()

            for eval in pending_evals:
                days_since_assigned = (datetime.utcnow() - eval.created_at).days

                # Send reminders at 3, 7, 14 day intervals
                if days_since_assigned in [3, 7, 14]:
                    # Check if reminder already sent
                    existing_reminder = session.query(EvaluationReminder).filter(
                        EvaluationReminder.evaluation_id == eval.id,
                        EvaluationReminder.reminder_type == f'{days_since_assigned}_day',
                        EvaluationReminder.status == 'sent'
                    ).first()

                    if not existing_reminder:
                        self._send_reminder(eval, days_since_assigned)
                        self._create_reminder_record(session, eval.id, f'{days_since_assigned}_day')

    def _send_reminder(self, evaluation, days_overdue):
        """Send reminder email"""
        preceptor = self._get_preceptor(evaluation.evaluator_id)
        resident = self._get_resident(evaluation.evaluated_resident_id)

        self.email_service.send_reminder(
            preceptor.email,
            resident.full_name,
            days_overdue,
            evaluation.id
        )

        # CC program director on 7+ day reminders
        if days_overdue >= 7:
            institution = self._get_institution(evaluation.institution_id)
            self.email_service.send_reminder(
                institution.primary_contact_email,
                f"[Overdue] {resident.full_name}",
                days_overdue,
                evaluation.id
            )

Security & Compliance

HIPAA Compliance Measures

1. Data Encryption

  • At Rest: PostgreSQL transparent data encryption (TDE) via GCP Cloud SQL
  • In Transit: TLS 1.3 for all API requests
  • Backups: Encrypted backups with 7-year retention (accreditation requirement)

2. Access Controls

  • Authentication: Keycloak JWT with short-lived tokens (15-minute expiry)
  • Authorization: Role-based access control (RBAC) enforced at API layer
  • Audit Logging: All evaluation access logged to audit_log table
python
# Audit log middleware
@app.middleware("http")
async def audit_middleware(request: Request, call_next):
    if request.url.path.startswith("/api/evaluations"):
        user_id = get_current_user_id(request)
        log_entry = {
            "user_id": user_id,
            "action": f"{request.method} {request.url.path}",
            "ip_address": request.client.host,
            "timestamp": datetime.utcnow()
        }
        # Write to audit_log table
        db.insert_audit_log(log_entry)

    response = await call_next(request)
    return response

3. Data Minimization

  • Only collect necessary evaluation data
  • Anonymize/pseudonymize in exports
  • Hard-delete after retention period (90 days post-graduation)

4. Breach Notification

  • Automated alerts for suspicious access patterns (e.g., >100 evaluation views/hour)
  • Incident response plan documented in /docs/compliance/incident-response.md

Token Structure: JWT with short expiry

python
import jwt
from datetime import datetime, timedelta

def generate_magic_link_token(evaluation_id, preceptor_email):
    payload = {
        'evaluation_id': evaluation_id,
        'preceptor_email': preceptor_email,
        'exp': datetime.utcnow() + timedelta(hours=48),  # 48-hour expiry
        'iat': datetime.utcnow(),
        'jti': str(uuid.uuid4())  # Unique token ID (prevents replay)
    }

    token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')

    # Store token hash in database for single-use validation
    db.insert_magic_token(evaluation_id, hash_token(token))

    return token

def validate_magic_link_token(token):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])

        # Check if token already used
        token_hash = hash_token(token)
        if db.is_token_used(token_hash):
            raise Exception("Token already used")

        # Mark as used
        db.mark_token_used(token_hash)

        return payload
    except jwt.ExpiredSignatureError:
        raise Exception("Token expired")
    except jwt.InvalidTokenError:
        raise Exception("Invalid token")

Rate Limiting

Protect against abuse:

  • Magic link endpoint: 5 requests/minute per IP
  • Evaluation creation: 20 evaluations/hour per user
  • Export endpoint: 3 exports/day per resident
python
from slowapi import Limiter
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)

@app.post("/api/evaluations/guest/{token}")
@limiter.limit("5/minute")
async def guest_evaluation(token: str, request: Request):
    # Validate token and return evaluation form
    ...

Implementation Roadmap

Phase 1: Database & Core API (Weeks 1-2)

Week 1: Database Foundation

  • [ ] Create Alembic migration for all new tables
  • [ ] Seed evaluation_field_types with 15+ field types
  • [ ] Write SQLAlchemy models for all tables
  • [ ] Create database indexes (GIN, partial, composite)
  • [ ] Write unit tests for models

Week 2: Core CRUD Endpoints

  • [ ] Implement /api/evaluations/templates (CRUD)
  • [ ] Implement form versioning logic (draft → published → retired)
  • [ ] Implement /api/evaluations (create, list, get)
  • [ ] Add permission validation middleware
  • [ ] Write API integration tests (pytest)

Deliverables:

  • Migration script ready for production
  • 10+ API endpoints functional
  • 80% test coverage on backend

Phase 2: Form Builder UI (Weeks 3-4)

Week 3: Admin Form Builder

  • [ ] Create drag-drop form builder component (React DnD)
  • [ ] Build field palette with 15+ field types
  • [ ] Implement field configuration modal
  • [ ] Add live preview panel
  • [ ] Implement save-as-draft / publish workflow

Week 4: Form Renderer

  • [ ] Create DynamicFormRenderer component
  • [ ] Build FormFieldFactory with all field types
  • [ ] Implement CanMEDSTagSelector component
  • [ ] Implement SignatureField component
  • [ ] Add form validation (client-side)

Deliverables:

  • Admins can create/publish forms
  • Forms render correctly from JSONB
  • All field types functional

Phase 3: Evaluation Workflows (Weeks 5-6)

Week 5: Preceptor Workflow

  • [ ] Build PreceptorEvaluationDashboard
  • [ ] Implement evaluation creation flow
  • [ ] Add auto-save functionality (30-second debounce)
  • [ ] Implement submit evaluation flow
  • [ ] Create evaluation list/filter components

Week 6: Resident Self-Assessment

  • [ ] Build SelfAssessment component
  • [ ] Implement submit-for-signoff flow
  • [ ] Create SignOffModal component (approve/reject/edit)
  • [ ] Build EvaluationTimeline component
  • [ ] Implement visibility controls (approved-only for residents)

Deliverables:

  • Preceptors can complete evaluations end-to-end
  • Residents can self-assess and get sign-off
  • Full workflow testing complete

Phase 4: Email Notifications (Week 7)

Week 7: Email System

  • [ ] Integrate SendGrid/AWS SES
  • [ ] Create 5 HTML email templates (Jinja2)
  • [ ] Implement EmailService class
  • [ ] Build ReminderScheduler with APScheduler
  • [ ] Test email delivery (staging environment)

Deliverables:

  • All 5 email templates functional
  • Automated reminders working (3, 7, 14-day)
  • Email logs in database

Phase 5: External Preceptors (Week 8)

Week 8: Magic Link System

  • [ ] Build /api/preceptors CRUD endpoints
  • [ ] Implement magic link token generation (JWT)
  • [ ] Create ExternalPreceptorView component (public)
  • [ ] Add single-use token validation
  • [ ] Test guest evaluation workflow

Deliverables:

  • External preceptors can complete evals via email link
  • Token security tested (expiry, single-use, replay prevention)

Phase 6: Export & Archival (Week 9)

Week 9: Data Export

  • [ ] Implement PDF export (ReportLab/WeasyPrint)
  • [ ] Implement CSV export
  • [ ] Create GCS bucket upload logic
  • [ ] Build resident graduation workflow
  • [ ] Implement data retention policy (90-day cleanup job)

Deliverables:

  • Residents can export all evaluations
  • Auto-archival on graduation
  • Data deletion after retention period

Phase 7: Analytics & Dashboards (Week 10)

Week 10: Admin Analytics

  • [ ] Build AdminEvaluationDashboard
  • [ ] Create competency progression charts (Chart.js)
  • [ ] Implement completion rate tracking
  • [ ] Build EPA milestone tracker
  • [ ] Create export reports for accreditation

Deliverables:

  • Admins see institution-wide analytics
  • Competency progression visualizations
  • Exportable reports for CFPC/ACGME

Phase 8: Testing & Documentation (Weeks 11-12)

Week 11: Comprehensive Testing

  • [ ] End-to-end tests (Playwright)
  • [ ] Security audit (OWASP Top 10 checklist)
  • [ ] Load testing (100 concurrent evaluations)
  • [ ] Accessibility audit (WCAG 2.1 AA)
  • [ ] Cross-browser testing (Chrome, Safari, Firefox)

Week 12: Documentation & Launch Prep

  • [ ] Update /docs/features/resident-evaluation-platform.md (this file) with implementation details
  • [ ] Create /docs/guides/evaluation-workflows.md (user guide)
  • [ ] Record demo video for institutional admins
  • [ ] Write changelog and release notes
  • [ ] Prepare beta launch announcement

Deliverables:

  • All tests passing (>90% coverage)
  • Documentation complete
  • Beta launch ready

Performance Considerations

Database Query Optimization

1. Eager Loading for Evaluations

python
# Avoid N+1 queries when loading evaluation lists
evaluations = session.query(Evaluation)\
    .options(
        joinedload(Evaluation.evaluated_resident),
        joinedload(Evaluation.evaluator),
        joinedload(Evaluation.form_template)
    )\
    .filter(Evaluation.status == 'approved')\
    .all()

2. JSONB Indexing for Fast Searches

sql
-- Find all evaluations with Medical Expert rating >= 4
SELECT * FROM evaluations
WHERE responses @> '{"field_2": {"value": 4}}';

-- This query uses the GIN index: idx_responses_gin

3. Materialized Views for Dashboards

sql
-- Pre-compute resident progress summary (refreshed nightly)
CREATE MATERIALIZED VIEW resident_progress_summary AS
SELECT
  rp.user_id,
  rp.pgy_level,
  COUNT(DISTINCT e.id) as total_evaluations,
  AVG((e.responses->'medical_expert_field'->>'value')::numeric) as avg_rating,
  COUNT(DISTINCT e.encounter_date) as total_encounters,
  jsonb_object_agg(
    eft.name,
    COUNT(e.id)
  ) as evaluations_by_form_type
FROM resident_profiles rp
LEFT JOIN evaluations e ON e.evaluated_resident_id = rp.user_id
LEFT JOIN evaluation_form_templates eft ON eft.id = e.form_template_id
WHERE e.status = 'approved'
GROUP BY rp.user_id, rp.pgy_level;

-- Query is instant (no joins needed)
SELECT * FROM resident_progress_summary WHERE user_id = 'resident_123';

Frontend Performance

1. Virtual Scrolling for Long Lists

Use react-window for evaluation lists with 100+ items:

javascript
import { FixedSizeList } from 'react-window';

const EvaluationList = ({ evaluations }) => {
  const Row = ({ index, style }) => (
    <div style={style}>
      <EvaluationCard evaluation={evaluations[index]} />
    </div>
  );

  return (
    <FixedSizeList
      height={600}
      itemCount={evaluations.length}
      itemSize={120}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
};

2. Code Splitting

javascript
// Lazy load form builder (large dependency: React DnD)
const FormBuilder = lazy(() => import('./components/FormBuilder'));

<Suspense fallback={<LoadingSpinner />}>
  <FormBuilder />
</Suspense>

3. React Query for Caching

javascript
import { useQuery } from '@tanstack/react-query';

const useEvaluations = (filters) => {
  return useQuery({
    queryKey: ['evaluations', filters],
    queryFn: () => api.get('/api/evaluations', { params: filters }),
    staleTime: 5 * 60 * 1000, // Cache for 5 minutes
  });
};

Risks & Mitigation

RiskLikelihoodImpactMitigation Strategy
Complexity Creep: Form builder becomes too complex for adminsMediumHighProvide 10+ pre-built templates; offer "clone and modify" workflow instead of building from scratch
Low Preceptor Adoption: Preceptors ignore email remindersHighHighIntegrate with existing clinical workflow (link from scribe session); escalate to program director after 7 days
Data Migration: Existing Elentra data needs importMediumMediumBuild one-time CSV import tool; provide manual entry option; prioritize new evaluations over historical data
Performance: JSONB queries slow at scale (10,000+ evaluations)LowMediumUse materialized views for dashboards; implement pagination (50 items/page); consider read replicas
External Preceptor Security: Magic links shared/leakedMediumHighSingle-use tokens; 48-hour expiry; rate limiting; audit log for suspicious access
Form Versioning Confusion: Admins accidentally retire active formsMediumMediumRequire confirmation modal; show "X active evaluations" warning; allow un-retire within 24 hours
Email Deliverability: Reminders go to spamMediumHighUse reputable service (SendGrid); SPF/DKIM/DMARC setup; allow users to whitelist sender; in-app notifications as backup

Success Metrics

Phase 1 (3 months post-launch):

  • ✅ 50+ institutional admins create custom forms
  • ✅ 500+ evaluations completed
  • ✅ 80% preceptor completion rate within 7 days
  • ✅ <5% support tickets related to bugs

Phase 2 (6 months post-launch):

  • ✅ 2,000+ evaluations completed
  • ✅ 90% preceptor completion rate
  • ✅ 10+ institutions cancel Elentra subscriptions
  • ✅ Average form completion time <8 minutes

Phase 3 (12 months post-launch):

  • ✅ 10,000+ evaluations completed
  • ✅ Feature parity with Elentra
  • ✅ 95% user satisfaction (NPS score >50)
  • ✅ Revenue: $200k+ from institutions adopting evaluation feature

Open Questions & Future Enhancements

Open Questions

  1. Competency Frameworks: Should we support ACGME milestones in addition to CanMEDS?
  2. Multi-Evaluator Forms: Should we support forms requiring sign-off from multiple preceptors?
  3. Integration with AFMC (Canadian): Should we integrate with AFMC's Passport for resident portfolios?
  4. Mobile App: Do preceptors need a native mobile app, or is responsive web sufficient?

Future Enhancements (Post-MVP)

  • AI-Powered Insights: Analyze evaluation text to identify competency gaps
  • Peer Comparison: Anonymous benchmarking (resident performance vs. cohort average)
  • Gamification: Badges/achievements for residents (e.g., "10 Excellent Communicator ratings")
  • Video Uploads: Allow video documentation of procedures
  • Voice-to-Form: Use Deepgram transcription to auto-populate evaluation fields from voice dictation
  • Integration with ERAS/CaRMS: Export evaluations for residency applications

Appendix

A. CanMEDS Framework Reference

CanMEDS Roles (College of Family Physicians of Canada):

  1. Medical Expert (Central Role)

    • Apply medical knowledge
    • Perform clinical assessments
    • Develop management plans
  2. Communicator

    • Develop rapport with patients
    • Effectively gather and share information
    • Work with patients to develop care plans
  3. Collaborator

    • Work effectively within a healthcare team
    • Participate in productive conflict resolution
  4. Leader

    • Contribute to healthcare system improvement
    • Manage resources wisely
    • Demonstrate leadership skills
  5. Health Advocate

    • Respond to individual patient health needs
    • Identify determinants of health
    • Promote health equity
  6. Scholar

    • Engage in lifelong learning
    • Teach patients, families, and colleagues
    • Contribute to scholarship
  7. Professional

    • Demonstrate commitment to patients
    • Practice ethically and with integrity
    • Manage conflicts of interest

B. EPA (Entrustable Professional Activities) Examples

Family Medicine EPAs (CFPC):

  • EPA 1.1: Performing a complete and appropriate assessment
  • EPA 1.2: Prioritizing issues and developing a management plan
  • EPA 2.1: Assessing and managing patients with acute emergent conditions
  • EPA 3.1: Providing care for patients with chronic disease
  • EPA 4.1: Providing prenatal, intrapartum, and postpartum care
  • EPA 5.1: Providing care for children
  • EPA 6.1: Providing palliative and end-of-life care

Each EPA has progression levels: "Observation only" → "Direct supervision" → "Indirect supervision" → "Autonomous practice"


C. Accreditation Requirements

CFPC (Canada):

  • Residents must complete minimum number of evaluations per rotation
  • CanMEDS competencies must be assessed regularly
  • Portfolio must include self-assessments
  • 360-degree evaluations required annually

ACGME (United States):

  • Milestone assessments every 6 months
  • Clinical Competency Committee (CCC) review required
  • Evaluation data retained for 7 years post-graduation

D. Glossary

TermDefinition
CanMEDSCompetency framework for physician training (Canada)
EPAEntrustable Professional Activity - tasks residents can perform independently
PGYPost-Graduate Year (PGY-1 = first year resident, PGY-3 = third year)
PreceptorStaff physician who supervises and evaluates residents
Chief ResidentSenior resident with administrative responsibilities
CFPCCollege of Family Physicians of Canada
ACGMEAccreditation Council for Graduate Medical Education (US)
Magic LinkSecure, single-use URL for external preceptor access
Form SnapshotFrozen copy of form structure stored with evaluation

Document Metadata

Authors: Claude Code (AI Systems Architect), Noumaris Engineering Team Reviewers: [To be assigned] Approval: [Pending] Related Documents:

  • /docs/architecture/backend.md - Backend architecture
  • /docs/decisions/index.md - Architectural decision records
  • /docs/api/endpoints.md - API documentation
  • /docs/compliance/hipaa.md - HIPAA compliance guide

Change Log:

  • 2025-10-23: Initial draft (v1.0) - Comprehensive feature specification

Questions or feedback? Contact the engineering team or open an issue in the repository.

Internal documentation for Noumaris platform