11 KiB
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
RegimenMongoDB schema and CRUD APIOrganizerFillschema and fill/undo API- Pill organizer fill flow with shortage detection
- Burn rate calculation (days until empty per medicine)
- 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:
CreateRegimenSchemaUpdateRegimenSchemaOrganizerPreviewSchemaOrganizerFillSchema
3.2 — Regimen CRUD
RegimensRepositoryandRegimensService- Standard CRUD scoped to
householdId+userId - On create/update: validate that all
medicineIdreferences 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
/regimenspage:- 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
/organizerpage:- 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)
- Fill organizer section:
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
allowPartialis true - Fill is rejected when
allowPartialis 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_neededfrequency is excluded from fill calculations and burn rate- All queries scoped to
householdId; regimens additionally scoped touserId
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.