MeshiTrack/docs/phases/phase-2-medicine-cabinet.md

5.9 KiB

Phase 2 — Medicine Cabinet

Goal: Track medicine inventory — what you have, how much of each, and when it expires.

Depends on: Phase 0, Phase 1 (medicines)


Deliverables

  1. CabinetItem MongoDB schema and full CRUD API
  2. Aggregate quantity view per medicine
  3. Expiry date tracking with visual indicators
  4. Medicine cabinet web UI with status indicators

Data Model

CabinetItem Schema

// packages/shared/src/types/cabinet.ts
export interface CabinetItem {
  id: string;
  householdId: string;
  medicineId: string; // Reference to Medicine (generic level)
  medicineName: string; // Denormalized
  medicineStrength: number; // Denormalized for display
  medicineStrengthUnit: StrengthUnit; // Denormalized
  medicineForm: MedicineForm; // Denormalized
  medicineProductId?: string; // Optional reference to MedicineProduct (what was purchased)
  medicineProductBrand?: string; // Denormalized
  concentration?: number; // Denormalized from product (injection vials)
  concentrationUnit?: ConcentrationUnit; // Denormalized from product
  quantity: number; // Current quantity (independent of package size)
  unit: DosageUnit;
  expirationDate?: Date;
  status: CabinetItemStatus;
  notes?: string;
  createdBy: string;
  createdAt: Date;
  updatedAt: Date;
}

export enum CabinetItemStatus {
  ACTIVE = 'active',
  DEPLETED = 'depleted',
  EXPIRED = 'expired',
}

CabinetSummary (Computed, not stored)

// Aggregate view — total per medicine across all cabinet items
export interface CabinetSummary {
  medicineId: string;
  medicineName: string;
  medicineStrength: number;
  medicineStrengthUnit: StrengthUnit;
  medicineForm: MedicineForm;
  totalQuantity: number;
  unit: DosageUnit;
  earliestExpiry: Date | null;
  itemCount: number; // How many cabinet items (bottles/boxes)
}

MongoDB Indexes

{ householdId: 1, medicineId: 1, status: 1 }
{ householdId: 1, status: 1 }
{ householdId: 1, expirationDate: 1 }

API Endpoints

CabinetModule

Method Path Description Auth
GET /cabinet List cabinet items (filtered, paginated) member
GET /cabinet/summary Aggregate quantities per medicine member
GET /cabinet/:id Get single cabinet item member
POST /cabinet Add item to cabinet member
PATCH /cabinet/:id Update item (quantity, notes, etc.) member
POST /cabinet/:id/adjust Adjust quantity (add/subtract without full edit) member
DELETE /cabinet/:id Delete cabinet item member
GET /cabinet/expiring-soon Items expiring within N days member

Query Parameters for GET /cabinet

?medicineId=abc123            # Filter by medicine
&status=active                # Filter by status
&expiringWithin=30            # Days until expiry
&cursor=abc123
&limit=20

Adjust Quantity Request

// POST /cabinet/:id/adjust
interface AdjustQuantityRequest {
  delta: number; // Positive to add, negative to subtract
  reason?: string; // e.g., "Correcting count"
}

Tasks

2.1 — Shared Types & Validation

  • Add cabinet types to packages/shared/src/types/cabinet.ts
  • Zod schemas:
    • CreateCabinetItemSchema
    • UpdateCabinetItemSchema
    • AdjustQuantitySchema
    • CabinetQuerySchema

2.2 — Mongoose Schema & Repository

  • packages/api/src/modules/cabinet/cabinet.repository.ts
  • CabinetRepository with:
    • findByHousehold(householdId, query) — filtered, paginated
    • findById(id, householdId)
    • getAggregateSummary(householdId) — MongoDB aggregation pipeline
    • create(data)
    • update(id, householdId, data)
    • adjustQuantity(id, householdId, delta) — atomic adjust with floor at 0
    • findExpiringSoon(householdId, withinDays)
    • softDelete(id, householdId)

2.3 — Cabinet Service

class CabinetService {
  /** Add item, denormalizing medicine and product fields */
  addItem(data: CreateCabinetItem): Promise<CabinetItem>;

  /** Adjust quantity with floor at 0, auto-set depleted status */
  adjustQuantity(id: string, householdId: string, delta: number): Promise<CabinetItem>;

  /** Get aggregate summary */
  getSummary(householdId: string): Promise<CabinetSummary[]>;

  /** Find items expiring within N days */
  getExpiringSoon(householdId: string, withinDays: number): Promise<CabinetItem[]>;
}

2.4 — Web UI: Medicine Cabinet

  • /medicines/cabinet page:
    • Summary view (default): aggregated per medicine
      • Medicine name, total quantity, earliest expiry
      • Expand to see individual items (bottles/boxes)
    • Detail view: all individual cabinet items with status filter
    • Each item shows: quantity, unit, expiry date, status badge
    • Color-coded expiry: green (>30 days), yellow (7-30 days), red (<7 days), bold red (expired)
    • Quick actions: adjust quantity (+/-), delete
    • "Add to Cabinet" form with medicine search/select
  • /medicines/[id] detail page:
    • Inventory section showing cabinet items for that medicine
    • Adjust and delete actions inline

Acceptance Criteria

  • Can add items to cabinet linked to medicines
  • Aggregate summary shows total quantity per medicine
  • Quantity adjustments are atomic and floor at 0
  • Items auto-transition to depleted when quantity reaches 0
  • Expiring-soon endpoint returns items within N days
  • Web UI shows color-coded expiry indicators
  • All cabinet queries are scoped to householdId
  • Medicine detail page shows inventory for that medicine
  • Concentration is denormalized from product for injection vials