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

253 lines
7.9 KiB
Markdown
Raw Normal View History

2026-03-27 14:50:34 +09:00
# Phase 2 — Medicine Cabinet
**Goal**: Track medicine inventory — what you have, how much of each, and when it expires. Provide aggregate views and low-stock/expiry warnings.
**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 and warnings
4. Low stock alerts (based on configurable thresholds)
5. Medicine cabinet web UI with status indicators
---
## Data Model
### CabinetItem Schema
```typescript
// packages/shared/src/types/cabinet.ts
export interface CabinetItem {
id: string;
householdId: string;
medicineId: string;
medicineName: string; // Denormalized
medicineStrength: number; // Denormalized for display
medicineStrengthUnit: StrengthUnit; // Denormalized
medicineForm: MedicineForm; // Denormalized
quantity: number;
unit: DosageUnit;
expirationDate?: Date;
lotNumber?: string;
purchaseDate?: Date;
purchasePrice?: number;
storeId?: string;
storeName?: string; // Denormalized
status: CabinetItemStatus;
notes?: string;
createdBy: string;
createdAt: Date;
updatedAt: Date;
}
export enum DosageUnit {
PILL = 'pill',
CAPSULE = 'capsule',
ML = 'ml',
G = 'g',
PATCH = 'patch',
DOSE = 'dose',
PUFF = 'puff',
DROP = 'drop',
APPLICATION = 'application',
}
export enum CabinetItemStatus {
ACTIVE = 'active',
DEPLETED = 'depleted',
EXPIRED = 'expired',
DISCARDED = 'discarded',
}
```
### CabinetSummary (Computed, not stored)
```typescript
// 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)
lowStockThreshold?: number; // From household settings
isLowStock: boolean;
}
```
### MongoDB Indexes
```javascript
{ householdId: 1, medicineId: 1, status: 1 }
{ householdId: 1, status: 1 }
{ householdId: 1, expirationDate: 1 } // For expiry warnings
{ householdId: 1, 'quantity': 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` | Hard delete (admin) | admin |
| GET | `/cabinet/expiring-soon` | Items expiring within N days | member |
| GET | `/cabinet/low-stock` | Medicines below threshold quantity | member |
### Query Parameters for GET `/cabinet`
```
?medicineId=abc123 # Filter by medicine
&status=active # Filter by status
&expiringWithin=30 # Days until expiry
&sort=-expirationDate|name # Sort field
&cursor=abc123
&limit=20
```
### Adjust Quantity Request
```typescript
// POST /cabinet/:id/adjust
interface AdjustQuantityRequest {
delta: number; // Positive to add, negative to subtract
reason?: string; // e.g., "Correcting count", "Dropped a pill"
}
```
---
## 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)`
- `findByMedicine(householdId, medicineId)` — all items for a medicine
- `getAggregateSummary(householdId)` — MongoDB aggregation pipeline
- `create(data)`
- `update(id, householdId, data)`
- `adjustQuantity(id, householdId, delta)` — atomic `$inc`
- `findExpiringSoon(householdId, withinDays)`
- `delete(id, householdId)`
### 2.3 — Cabinet Service
```typescript
class CabinetService {
/** Add item, denormalizing medicine fields */
addItem(data: CreateCabinetItem): Promise<CabinetItem>;
/** Adjust quantity with floor at 0, auto-set depleted status */
adjustQuantity(id: string, householdId: string, delta: number, reason?: string): Promise<CabinetItem>;
/** Get aggregate summary with low stock flags */
getSummary(householdId: string): Promise<CabinetSummary[]>;
/** Find items expiring within N days */
getExpiringSoon(householdId: string, withinDays: number): Promise<CabinetItem[]>;
/** Find medicines below low stock threshold */
getLowStock(householdId: string): Promise<CabinetSummary[]>;
/**
* Deduct quantity from cabinet items for a medicine (used by Pill Organizer in Phase 3).
* Uses FEFO (First Expiry, First Out) — draws from items with earliest expiry first.
* Returns actual quantity deducted (may be less than requested if insufficient).
*/
deductStock(householdId: string, medicineId: string, quantity: number): Promise<DeductionResult>;
/** Reverse a deduction (used by Pill Organizer undo) */
restoreStock(householdId: string, cabinetItemId: string, quantity: number): Promise<CabinetItem>;
}
interface DeductionResult {
totalDeducted: number;
requested: number;
isShort: boolean;
deductions: {
cabinetItemId: string;
quantityTaken: number;
remainingInItem: number;
}[];
}
```
### 2.4 — Expiry Check Job
- Scheduled job (daily at 6 AM, configurable):
1. Query all active cabinet items with `expirationDate <= today`
2. Update status to `expired`
3. Create in-app notifications for expired items
4. Query items expiring within 7 days, create warning notifications
### 2.5 — Web UI: Medicine Cabinet
- `/cabinet` page:
- **Summary view** (default): aggregated per medicine
- Medicine name, total quantity, earliest expiry, low stock indicator
- Expand to see individual items (bottles/boxes)
- **Detail view**: all individual cabinet items
- Each item shows: medicine name, quantity, expiry date, status badge
- Color-coded expiry: green (>30 days), yellow (7-30 days), red (<7 days), grey (expired)
- Low stock badge on medicines below threshold
- Quick actions: adjust quantity (+/-), discard
- "Add to Cabinet" button -> modal:
- Medicine autocomplete (from library)
- Quantity + unit
- Expiration date (optional)
- Lot number (optional)
- Purchase date, price, store (optional)
- `/cabinet/alerts` or notification panel:
- Expiring soon items
- Low stock warnings
---
## 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
- [ ] Items auto-transition to `expired` when past expiration date
- [ ] Expiring-soon endpoint returns items within N days
- [ ] Low-stock endpoint compares against configurable thresholds
- [ ] FEFO deduction draws from earliest-expiring items first
- [ ] Web UI shows color-coded expiry indicators
- [ ] All cabinet queries are scoped to `householdId`
---
## Estimated Effort
Medium. CRUD with aggregation pipeline, FEFO logic, and scheduled expiry job. Simpler than food pantry tracking (no freshness estimation).