MeshiTrack/docs/phases/phase-3-regimens-pill-organizer.md

11 KiB

Phase 3 — Regimens & Pill Organizer

Goal: Define daily medication schedules (regimens) and batch-dispense from the medicine cabinet into a pill organizer. This is the core convenience feature: instead of tracking individual pill consumption daily, users fill their organizer for N days in a single action.

Depends on: Phase 0, Phase 1 (medicines), Phase 2 (cabinet)


Deliverables

  1. Regimen MongoDB schema and CRUD API
  2. OrganizerFill schema and fill/undo API
  3. Pill organizer fill flow with shortage detection
  4. Burn rate calculation (days until empty per medicine)
  5. Regimen and pill organizer web UI

Data Model

Regimen Schema

// packages/shared/src/types/regimen.ts
export interface Regimen {
  id: string;
  householdId: string;
  userId: string; // Regimens are per-person
  name: string; // e.g., "Daily medications", "Morning routine"
  isActive: boolean;
  medications: RegimenMedication[];
  createdBy: string;
  createdAt: Date;
  updatedAt: Date;
}

export interface RegimenMedication {
  medicineId: string;
  medicineName: string; // Denormalized
  medicineStrength: number; // Denormalized
  medicineStrengthUnit: StrengthUnit; // Denormalized
  medicineForm: MedicineForm; // Denormalized
  dosage: number; // e.g., 2 (pills per dose)
  dosageUnit: DosageUnit;
  frequency: DosageFrequency;
  customFrequencyPerDay?: number; // When frequency is 'custom'
  timeOfDay?: TimeOfDay;
  instructions?: string; // e.g., "Take with food", "Do not crush"
}

export enum DosageFrequency {
  DAILY = 'daily', // 1x per day
  TWICE_DAILY = 'twice_daily', // 2x per day
  THREE_TIMES_DAILY = 'three_times_daily', // 3x per day
  WEEKLY = 'weekly', // 1x per week
  EVERY_OTHER_DAY = 'every_other_day',
  AS_NEEDED = 'as_needed', // Excluded from organizer fill calculations
  CUSTOM = 'custom', // Uses customFrequencyPerDay
}

export enum TimeOfDay {
  MORNING = 'morning',
  AFTERNOON = 'afternoon',
  EVENING = 'evening',
  BEDTIME = 'bedtime',
}

OrganizerFill Schema

// packages/shared/src/types/organizer-fill.ts
export interface OrganizerFill {
  id: string;
  householdId: string;
  userId: string;
  regimenId: string;
  regimenName: string; // Denormalized
  numberOfDays: number; // Flexible: 1, 6, 7, 14, etc.
  fillDate: Date;
  items: OrganizerFillItem[];
  status: OrganizerFillStatus;
  notes?: string;
  createdAt: Date;
  updatedAt: Date;
}

export interface OrganizerFillItem {
  medicineId: string;
  medicineName: string;
  quantityNeeded: number; // Total pills needed for N days
  quantityTaken: number; // Actual pills taken from cabinet
  wasShort: boolean; // quantityTaken < quantityNeeded
  shortage: number; // quantityNeeded - quantityTaken (0 if not short)
  deductions: OrganizerDeduction[];
}

export interface OrganizerDeduction {
  cabinetItemId: string;
  quantityTaken: number;
}

export enum OrganizerFillStatus {
  COMPLETED = 'completed', // All medicines fully dispensed
  PARTIAL = 'partial', // Some medicines were short
  REVERSED = 'reversed', // Fill was undone
}

BurnRate (Computed, not stored)

// Calculated from active regimens + cabinet stock
export interface BurnRate {
  medicineId: string;
  medicineName: string;
  dailyConsumption: number; // Total pills per day across all regimens
  totalInCabinet: number;
  daysUntilEmpty: number | null; // null if dailyConsumption is 0
  earliestExpiry: Date | null;
}

MongoDB Indexes

// Regimen
{ householdId: 1, userId: 1, isActive: 1 }
{ householdId: 1, 'medications.medicineId': 1 }

// OrganizerFill
{ householdId: 1, userId: 1, fillDate: -1 }
{ householdId: 1, regimenId: 1, fillDate: -1 }
{ householdId: 1, status: 1 }

API Endpoints

RegimensModule

Method Path Description Auth
GET /regimens List user's regimens member
GET /regimens/:id Get single regimen member
POST /regimens Create regimen member
PATCH /regimens/:id Update regimen member
DELETE /regimens/:id Delete regimen member
GET /regimens/burn-rate Burn rate for all active regimens member

OrganizerModule

Method Path Description Auth
GET /organizer/fills List fill history (paginated) member
GET /organizer/fills/:id Get single fill details member
POST /organizer/preview Preview a fill (shows quantities, shortages) member
POST /organizer/fill Execute a fill (deduct from cabinet) member
POST /organizer/fills/:id/undo Reverse a fill (restore cabinet quantities) member

Preview Request/Response

// POST /organizer/preview
interface OrganizerPreviewRequest {
  regimenId: string;
  numberOfDays: number; // Default: 7
}

interface OrganizerPreviewResponse {
  regimenName: string;
  numberOfDays: number;
  items: {
    medicineId: string;
    medicineName: string;
    quantityNeeded: number;
    quantityAvailable: number;
    isShort: boolean;
    shortage: number;
    cabinetBreakdown: {
      cabinetItemId: string;
      expirationDate: Date | null;
      quantityToTake: number;
    }[];
  }[];
  canFillCompletely: boolean;
  hasShortages: boolean;
}

Fill Request

// POST /organizer/fill
interface OrganizerFillRequest {
  regimenId: string;
  numberOfDays: number;
  allowPartial: boolean; // If false, reject when any medicine is short
  notes?: string;
}

Tasks

3.1 — Shared Types & Validation

  • Add regimen types to packages/shared/src/types/regimen.ts
  • Add organizer fill types to packages/shared/src/types/organizer-fill.ts
  • Zod schemas:
    • CreateRegimenSchema
    • UpdateRegimenSchema
    • OrganizerPreviewSchema
    • OrganizerFillSchema

3.2 — Regimen CRUD

  • RegimensRepository and RegimensService
  • Standard CRUD scoped to householdId + userId
  • On create/update: validate that all medicineId references exist in the medicine library
  • Denormalize medicine fields (name, strength, unit, form)

3.3 — Frequency Multiplier Logic

/**
 * Calculate total pills needed for N days based on frequency.
 *
 * daily:             dosage * numberOfDays
 * twice_daily:       dosage * 2 * numberOfDays
 * three_times_daily: dosage * 3 * numberOfDays
 * weekly:            dosage * ceil(numberOfDays / 7)
 * every_other_day:   dosage * ceil(numberOfDays / 2)
 * as_needed:         0 (excluded from organizer fills)
 * custom:            dosage * customFrequencyPerDay * numberOfDays
 */
function calculateQuantityNeeded(
  medication: RegimenMedication,
  numberOfDays: number,
): number;

3.4 — Organizer Fill Service

class OrganizerService {
  /**
   * Preview: calculate what would happen without deducting.
   * For each medicine in the regimen:
   * 1. Calculate quantity needed (via frequency multiplier)
   * 2. Check cabinet stock (via CabinetService.getAggregateSummary)
   * 3. Plan FEFO deductions (earliest expiry first)
   * 4. Flag shortages
   */
  preview(householdId: string, regimenId: string, numberOfDays: number): Promise<OrganizerPreviewResponse>;

  /**
   * Fill: execute the preview plan.
   * 1. Re-validate stock (may have changed since preview)
   * 2. If allowPartial=false and any shortage, reject
   * 3. Call CabinetService.deductStock for each medicine
   * 4. Create OrganizerFill record
   * 5. Return fill details
   */
  fill(householdId: string, userId: string, request: OrganizerFillRequest): Promise<OrganizerFill>;

  /**
   * Undo: reverse a fill.
   * 1. Verify fill is not already reversed
   * 2. For each deduction, call CabinetService.restoreStock
   * 3. Mark fill as reversed
   */
  undoFill(householdId: string, fillId: string): Promise<OrganizerFill>;
}

3.5 — Burn Rate Calculation

class BurnRateService {
  /**
   * For each medicine across all active regimens for a user:
   * 1. Sum daily consumption: dosage * daily_frequency_multiplier
   * 2. Get total cabinet stock for that medicine
   * 3. daysUntilEmpty = floor(totalInCabinet / dailyConsumption)
   * 4. Include earliest expiry date from cabinet
   *
   * Note: 'as_needed' frequency is excluded from burn rate.
   * Note: If multiple users in household have regimens, each sees their own burn rate.
   */
  calculateBurnRates(householdId: string, userId: string): Promise<BurnRate[]>;
}

3.6 — Web UI: Regimens

  • /regimens page:
    • List of user's regimens with active/inactive toggle
    • Each regimen shows: name, medication count, active status
    • Expand/click to see all medications with dosage details
  • Add/Edit regimen form:
    • Name, active toggle
    • Medications list:
      • Medicine autocomplete (from library)
      • Dosage (number + unit)
      • Frequency dropdown
      • Time of day (optional)
      • Instructions (optional)
      • Add/remove medications

3.7 — Web UI: Pill Organizer

  • /organizer page:
    • Fill organizer section:
      • Select regimen dropdown
      • Number of days input (default: 7, adjustable)
      • "Preview" button -> shows:
        • Per-medicine breakdown: needed vs available
        • Shortage warnings (highlighted)
        • Which cabinet items will be drawn from (FEFO order)
      • "Fill" button -> executes the fill, shows confirmation
      • Option for partial fill when shortages exist
    • Burn rate section:
      • Table: medicine name, daily consumption, total in cabinet, days until empty
      • Color-coded: green (>14 days), yellow (7-14 days), red (<7 days)
      • Links to refill alerts (Phase 4)
    • Fill history section:
      • Recent fills with date, regimen, day count, status
      • Expand to see per-medicine details
      • "Undo" button on recent fills (with confirmation)

Acceptance Criteria

  • Can create and manage regimens with multiple medications
  • Frequency multiplier correctly calculates quantities for all frequency types
  • Preview accurately shows needed quantities and shortages
  • Fill deducts from cabinet using FEFO (earliest expiry first)
  • Partial fills work when allowPartial is true
  • Fill is rejected when allowPartial is false and any medicine is short
  • Undo fully restores cabinet quantities
  • Undo is idempotent (cannot undo an already-reversed fill)
  • Burn rate correctly accounts for all active regimens
  • as_needed frequency is excluded from fill calculations and burn rate
  • All queries scoped to householdId; regimens additionally scoped to userId

Estimated Effort

Medium-large. The fill/undo transactional logic with FEFO, shortage handling, and burn rate calculations are the most complex parts. UI is moderately complex with the preview/fill flow.