2026-03-27 14:50:34 +09:00
|
|
|
# Phase 2 — Medicine Cabinet
|
|
|
|
|
|
2026-03-28 08:19:48 +09:00
|
|
|
**Goal**: Track medicine inventory — what you have, how much of each, and when it expires.
|
2026-03-27 14:50:34 +09:00
|
|
|
|
|
|
|
|
**Depends on**: Phase 0, Phase 1 (medicines)
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Deliverables
|
|
|
|
|
|
|
|
|
|
1. `CabinetItem` MongoDB schema and full CRUD API
|
|
|
|
|
2. Aggregate quantity view per medicine
|
2026-03-28 08:19:48 +09:00
|
|
|
3. Expiry date tracking with visual indicators
|
|
|
|
|
4. Medicine cabinet web UI with status indicators
|
2026-03-27 14:50:34 +09:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Data Model
|
|
|
|
|
|
|
|
|
|
### CabinetItem Schema
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// packages/shared/src/types/cabinet.ts
|
|
|
|
|
export interface CabinetItem {
|
|
|
|
|
id: string;
|
|
|
|
|
householdId: string;
|
2026-03-28 08:19:48 +09:00
|
|
|
medicineId: string; // Reference to Medicine (generic level)
|
2026-03-27 14:50:34 +09:00
|
|
|
medicineName: string; // Denormalized
|
|
|
|
|
medicineStrength: number; // Denormalized for display
|
|
|
|
|
medicineStrengthUnit: StrengthUnit; // Denormalized
|
|
|
|
|
medicineForm: MedicineForm; // Denormalized
|
2026-03-28 08:19:48 +09:00
|
|
|
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)
|
2026-03-27 14:50:34 +09:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
```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)
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### MongoDB Indexes
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
{ householdId: 1, medicineId: 1, status: 1 }
|
|
|
|
|
{ householdId: 1, status: 1 }
|
2026-03-28 08:19:48 +09:00
|
|
|
{ householdId: 1, expirationDate: 1 }
|
2026-03-27 14:50:34 +09:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 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 |
|
2026-03-28 08:19:48 +09:00
|
|
|
| DELETE | `/cabinet/:id` | Delete cabinet item | member |
|
2026-03-27 14:50:34 +09:00
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// POST /cabinet/:id/adjust
|
|
|
|
|
interface AdjustQuantityRequest {
|
|
|
|
|
delta: number; // Positive to add, negative to subtract
|
2026-03-28 08:19:48 +09:00
|
|
|
reason?: string; // e.g., "Correcting count"
|
2026-03-27 14:50:34 +09:00
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 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)`
|
2026-03-28 08:19:48 +09:00
|
|
|
- `adjustQuantity(id, householdId, delta)` — atomic adjust with floor at 0
|
2026-03-27 14:50:34 +09:00
|
|
|
- `findExpiringSoon(householdId, withinDays)`
|
2026-03-28 08:19:48 +09:00
|
|
|
- `softDelete(id, householdId)`
|
2026-03-27 14:50:34 +09:00
|
|
|
|
|
|
|
|
### 2.3 — Cabinet Service
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
class CabinetService {
|
2026-03-28 08:19:48 +09:00
|
|
|
/** Add item, denormalizing medicine and product fields */
|
2026-03-27 14:50:34 +09:00
|
|
|
addItem(data: CreateCabinetItem): Promise<CabinetItem>;
|
|
|
|
|
|
|
|
|
|
/** Adjust quantity with floor at 0, auto-set depleted status */
|
2026-03-28 08:19:48 +09:00
|
|
|
adjustQuantity(id: string, householdId: string, delta: number): Promise<CabinetItem>;
|
2026-03-27 14:50:34 +09:00
|
|
|
|
2026-03-28 08:19:48 +09:00
|
|
|
/** Get aggregate summary */
|
2026-03-27 14:50:34 +09:00
|
|
|
getSummary(householdId: string): Promise<CabinetSummary[]>;
|
|
|
|
|
|
|
|
|
|
/** Find items expiring within N days */
|
|
|
|
|
getExpiringSoon(householdId: string, withinDays: number): Promise<CabinetItem[]>;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2026-03-28 08:19:48 +09:00
|
|
|
### 2.4 — Web UI: Medicine Cabinet
|
2026-03-27 14:50:34 +09:00
|
|
|
|
2026-03-28 08:19:48 +09:00
|
|
|
- `/medicines/cabinet` page:
|
2026-03-27 14:50:34 +09:00
|
|
|
- **Summary view** (default): aggregated per medicine
|
2026-03-28 08:19:48 +09:00
|
|
|
- Medicine name, total quantity, earliest expiry
|
2026-03-27 14:50:34 +09:00
|
|
|
- Expand to see individual items (bottles/boxes)
|
2026-03-28 08:19:48 +09:00
|
|
|
- **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
|
2026-03-27 14:50:34 +09:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Acceptance Criteria
|
|
|
|
|
|
2026-03-28 08:19:48 +09:00
|
|
|
- [x] Can add items to cabinet linked to medicines
|
|
|
|
|
- [x] Aggregate summary shows total quantity per medicine
|
|
|
|
|
- [x] Quantity adjustments are atomic and floor at 0
|
|
|
|
|
- [x] Items auto-transition to `depleted` when quantity reaches 0
|
|
|
|
|
- [x] Expiring-soon endpoint returns items within N days
|
|
|
|
|
- [x] Web UI shows color-coded expiry indicators
|
|
|
|
|
- [x] All cabinet queries are scoped to `householdId`
|
|
|
|
|
- [x] Medicine detail page shows inventory for that medicine
|
|
|
|
|
- [x] Concentration is denormalized from product for injection vials
|