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
| Benefit | Description |
|---|---|
| Cost Savings | Eliminates $10,000-50,000/year licensing fees for Elentra |
| Customization | Institution-specific forms tailored to program requirements |
| Integration | Seamless connection with clinical scribe and documentation features |
| Compliance | Built-in audit trails, data retention, and HIPAA compliance |
| User Experience | Modern, 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.
| Column | Type | Description |
|---|---|---|
id | INTEGER (PK) | Unique identifier |
institution_id | INTEGER (FK) | Institution owning this template |
name | VARCHAR(255) | Template name (e.g., "Monthly Clinical Assessment") |
description | TEXT | Purpose and usage instructions |
version_number | INTEGER | Version tracking (1, 2, 3...) |
parent_template_id | INTEGER (FK, nullable) | Links to previous version |
status | ENUM | draft, published, retired, deleted |
form_schema | JSONB | Complete form structure (fields, validation, layout) |
created_by | VARCHAR | User ID of creator (admin) |
published_at | TIMESTAMP | When template was published |
retired_at | TIMESTAMP | When template was retired |
created_at | TIMESTAMP | Creation timestamp |
updated_at | TIMESTAMP | Last modification timestamp |
Index Strategy:
idx_institution_statuson(institution_id, status)- Fast filtering by institution and statusidx_parent_versionon(parent_template_id, version_number)- Version lineage queries
Form Schema Structure (JSONB):
{
"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.
| Column | Type | Description |
|---|---|---|
id | INTEGER (PK) | Unique identifier |
form_template_id | INTEGER (FK) | Reference to template used |
form_snapshot | JSONB | Frozen copy of form schema at creation time |
evaluated_resident_id | VARCHAR (FK) | User ID of resident being evaluated |
evaluator_id | VARCHAR (FK) | User ID of person completing evaluation |
evaluator_type | ENUM | preceptor, senior_resident, self, external_preceptor |
evaluator_email | VARCHAR | For external preceptors |
status | ENUM | draft, submitted, pending_signoff, approved, rejected |
responses | JSONB | User's answers to form fields |
submitted_at | TIMESTAMP | When evaluation was submitted |
approved_at | TIMESTAMP | When preceptor approved |
approved_by | VARCHAR | User ID of approver |
visibility | ENUM | visible_after_approval, immediately_visible |
encounter_date | DATE | Date of clinical encounter |
rotation | VARCHAR | Rotation name (e.g., "Family Medicine Clinic") |
created_at | TIMESTAMP | Creation timestamp |
updated_at | TIMESTAMP | Last modification timestamp |
Index Strategy:
idx_resident_statuson(evaluated_resident_id, status)- Resident's evaluation dashboardidx_evaluator_statuson(evaluator_id, status)- Evaluator's pending tasksidx_encounter_dateon(encounter_date DESC)- Timeline views
Responses Structure (JSONB):
{
"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.
| Column | Type | Description |
|---|---|---|
id | INTEGER (PK) | Unique identifier |
evaluation_id | INTEGER (FK) | Evaluation being signed off |
signer_id | VARCHAR (FK) | User ID of person signing |
signer_role | ENUM | preceptor, program_director, chief_resident |
action | ENUM | approved, rejected, requested_changes |
comments | TEXT | Feedback or reasons for rejection |
signature_data | TEXT | Base64-encoded signature image |
changes_made | JSONB | Audit trail of edits before sign-off |
signed_at | TIMESTAMP | When action was taken |
ip_address | VARCHAR | IP address for audit trail |
Index Strategy:
idx_evaluation_idon(evaluation_id, signed_at DESC)- Sign-off history
Changes Made Audit (JSONB):
{
"field_2": {
"original_value": 3,
"new_value": 4,
"reason": "Underestimated competency initially"
}
}4. preceptors
External preceptors who don't have Noumaris accounts.
| Column | Type | Description |
|---|---|---|
id | INTEGER (PK) | Unique identifier |
institution_id | INTEGER (FK) | Institution they're associated with |
email | VARCHAR | Contact email (unique per institution) |
first_name | VARCHAR | First name |
last_name | VARCHAR | Last name |
specialty | VARCHAR | Medical specialty |
credentials | VARCHAR | MD, DO, MBBS, etc. |
phone | VARCHAR | Phone number (optional) |
is_verified | BOOLEAN | Email verification status |
magic_link_token | VARCHAR | Current active magic link token |
token_expires_at | TIMESTAMP | Magic link expiry |
last_active_at | TIMESTAMP | Last time they completed an evaluation |
created_by | VARCHAR | Admin who added them |
created_at | TIMESTAMP | Creation timestamp |
updated_at | TIMESTAMP | Last modification timestamp |
Index Strategy:
idx_institution_emailon(institution_id, email)- Unique constraintidx_magic_tokenon(magic_link_token)- Fast token lookup
5. evaluation_reminders
Scheduled email reminders for pending evaluations.
| Column | Type | Description |
|---|---|---|
id | INTEGER (PK) | Unique identifier |
evaluation_id | INTEGER (FK) | Evaluation to remind about |
recipient_email | VARCHAR | Email to send reminder to |
reminder_type | ENUM | initial, 3_day, 7_day, 14_day, urgent |
scheduled_for | TIMESTAMP | When to send reminder |
sent_at | TIMESTAMP | When reminder was actually sent |
status | ENUM | pending, sent, failed, cancelled |
email_template_id | VARCHAR | Reference to email template used |
created_at | TIMESTAMP | Creation timestamp |
Index Strategy:
idx_scheduled_statuson(scheduled_for, status)- Cron job queries
6. evaluation_exports
Tracks data exports for graduated/departed residents.
| Column | Type | Description |
|---|---|---|
id | INTEGER (PK) | Unique identifier |
resident_id | VARCHAR (FK) | User ID of resident |
institution_id | INTEGER (FK) | Institution |
exported_by | VARCHAR (FK) | Admin who initiated export |
export_format | ENUM | pdf, csv, combined |
file_path | VARCHAR | Cloud storage path (GCS bucket) |
file_size_bytes | BIGINT | File size |
download_link | VARCHAR | Temporary download URL |
link_expires_at | TIMESTAMP | Download link expiry (30 days) |
retention_period_end | TIMESTAMP | When to hard-delete (90 days) |
exported_at | TIMESTAMP | Export timestamp |
downloaded_at | TIMESTAMP | When resident downloaded |
status | ENUM | generated, downloaded, expired, deleted |
Index Strategy:
idx_retention_statuson(retention_period_end, status)- Cleanup job
7. evaluation_field_types (Reference Data)
Predefined field types for form builder.
| Column | Type | Description |
|---|---|---|
id | INTEGER (PK) | Unique identifier |
field_type | VARCHAR | text_input, textarea, rating_scale, etc. |
display_name | VARCHAR | "Text Input", "Rating Scale (1-5)" |
description | TEXT | Usage instructions |
default_config | JSONB | Default settings for this field type |
icon | VARCHAR | Icon identifier for UI |
category | VARCHAR | "Basic", "Advanced", "Clinical-Specific" |
is_active | BOOLEAN | Enabled/disabled |
Seed Data:
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_exportsPostgreSQL-Specific Optimizations
1. JSONB Indexing for Fast Queries
-- 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
-- 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
-- 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:
{
"name": "Monthly Clinical Competency Assessment",
"description": "Standard evaluation for all residents",
"form_schema": {
"sections": [...],
"metadata": {...}
}
}Response: 201 Created
{
"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:
- Validate all mandatory fields are present in form_schema
- Set
status = 'published' - Set
published_at = NOW() - If previous version exists, link via
parent_template_id - Increment
version_number - Create audit log entry
Response: 200 OK
{
"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
{
"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:
{
"form_template_id": 123,
"evaluated_resident_id": "resident_user_id_456",
"encounter_date": "2025-10-23",
"rotation": "Family Medicine Clinic"
}Response: 201 Created
{
"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:
{
"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:
- Validate all required fields completed
- If
evaluator_type = 'self', setstatus = 'pending_signoff'and trigger email to preceptor - If
evaluator_type = 'preceptor'or'senior_resident', setstatus = 'submitted'and notify resident - Create
evaluation_remindersrecord if pending sign-off
Response: 200 OK
{
"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:
{
"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:
- Create record in
evaluation_signoffs - Update
evaluations.status:approved→status = 'approved',approved_at = NOW()rejected→status = 'rejected'requested_changes→status = 'draft', send notification
- If approved, make evaluation visible to resident
- Cancel pending reminders
GET /api/evaluations
Purpose: List evaluations (role-based filtering) Permission: All authenticated users Query Parameters:
evaluated_resident_id: Filter by residentevaluator_id: Filter by evaluatorstatus: Filter by statusdate_from,date_to: Date rangeform_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.idOR 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:
{
"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:
{
"preceptor_email": "[email protected]",
"form_template_id": 123,
"evaluated_resident_id": "resident_user_id_456",
"encounter_date": "2025-10-23"
}Business Logic:
- Create evaluation with
evaluator_type = 'external_preceptor' - Generate magic link token (JWT, 48-hour expiry)
- Send email with link:
https://app.noumaris.com/evaluations/guest/{token} - 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:
- Verify token signature and expiry
- Decode token to get
evaluation_id - Return evaluation form + prefilled data
- Allow form completion without login
POST /api/evaluations/guest/{token}/submit
Purpose: External preceptor submits evaluation Permission: Valid magic link token Business Logic:
- Validate token
- Save responses to evaluation
- Set
status = 'submitted' - Invalidate token (single-use)
- 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:
- Query all approved evaluations for resident
- Generate PDF (formatted reports) using ReportLab/WeasyPrint
- Generate CSV (structured data)
- Zip files, upload to GCS bucket
- Create temporary signed URL (30-day expiry)
- Create record in
evaluation_exports - Send email with download link
Response: 202 Accepted
{
"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:
- Trigger export workflow first
- Set
resident_profiles.status = 'archived' - Anonymize PII in evaluations (replace with "Archived Resident [ID]")
- Schedule hard-delete job for 90 days later
- Create audit log entry
Permission Matrix
| Endpoint | Superadmin | Inst. Admin | Preceptor | Senior Resident | Resident |
|---|---|---|---|---|---|
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
{
"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 templateEVAL_002: Evaluation not found or access deniedEVAL_003: Invalid form schema (validation failed)EVAL_004: Missing required fields in evaluationEVAL_005: Invalid sign-off permissionEVAL_006: Magic link expired or invalidEVAL_007: Senior resident insufficient PGY levelEVAL_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 configurationKey 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:
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:
// 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:
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:
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
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
// 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:
# 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
# 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_logtable
# 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 response3. 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
Magic Link Security
Token Structure: JWT with short expiry
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
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_typeswith 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
DynamicFormRenderercomponent - [ ] Build
FormFieldFactorywith all field types - [ ] Implement
CanMEDSTagSelectorcomponent - [ ] Implement
SignatureFieldcomponent - [ ] 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
SelfAssessmentcomponent - [ ] Implement submit-for-signoff flow
- [ ] Create
SignOffModalcomponent (approve/reject/edit) - [ ] Build
EvaluationTimelinecomponent - [ ] 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
EmailServiceclass - [ ] Build
ReminderSchedulerwith 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/preceptorsCRUD endpoints - [ ] Implement magic link token generation (JWT)
- [ ] Create
ExternalPreceptorViewcomponent (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
# 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
-- 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_gin3. Materialized Views for Dashboards
-- 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:
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
// Lazy load form builder (large dependency: React DnD)
const FormBuilder = lazy(() => import('./components/FormBuilder'));
<Suspense fallback={<LoadingSpinner />}>
<FormBuilder />
</Suspense>3. React Query for Caching
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
| Risk | Likelihood | Impact | Mitigation Strategy |
|---|---|---|---|
| Complexity Creep: Form builder becomes too complex for admins | Medium | High | Provide 10+ pre-built templates; offer "clone and modify" workflow instead of building from scratch |
| Low Preceptor Adoption: Preceptors ignore email reminders | High | High | Integrate with existing clinical workflow (link from scribe session); escalate to program director after 7 days |
| Data Migration: Existing Elentra data needs import | Medium | Medium | Build one-time CSV import tool; provide manual entry option; prioritize new evaluations over historical data |
| Performance: JSONB queries slow at scale (10,000+ evaluations) | Low | Medium | Use materialized views for dashboards; implement pagination (50 items/page); consider read replicas |
| External Preceptor Security: Magic links shared/leaked | Medium | High | Single-use tokens; 48-hour expiry; rate limiting; audit log for suspicious access |
| Form Versioning Confusion: Admins accidentally retire active forms | Medium | Medium | Require confirmation modal; show "X active evaluations" warning; allow un-retire within 24 hours |
| Email Deliverability: Reminders go to spam | Medium | High | Use 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
- Competency Frameworks: Should we support ACGME milestones in addition to CanMEDS?
- Multi-Evaluator Forms: Should we support forms requiring sign-off from multiple preceptors?
- Integration with AFMC (Canadian): Should we integrate with AFMC's Passport for resident portfolios?
- 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):
Medical Expert (Central Role)
- Apply medical knowledge
- Perform clinical assessments
- Develop management plans
Communicator
- Develop rapport with patients
- Effectively gather and share information
- Work with patients to develop care plans
Collaborator
- Work effectively within a healthcare team
- Participate in productive conflict resolution
Leader
- Contribute to healthcare system improvement
- Manage resources wisely
- Demonstrate leadership skills
Health Advocate
- Respond to individual patient health needs
- Identify determinants of health
- Promote health equity
Scholar
- Engage in lifelong learning
- Teach patients, families, and colleagues
- Contribute to scholarship
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
| Term | Definition |
|---|---|
| CanMEDS | Competency framework for physician training (Canada) |
| EPA | Entrustable Professional Activity - tasks residents can perform independently |
| PGY | Post-Graduate Year (PGY-1 = first year resident, PGY-3 = third year) |
| Preceptor | Staff physician who supervises and evaluates residents |
| Chief Resident | Senior resident with administrative responsibilities |
| CFPC | College of Family Physicians of Canada |
| ACGME | Accreditation Council for Graduate Medical Education (US) |
| Magic Link | Secure, single-use URL for external preceptor access |
| Form Snapshot | Frozen 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.