Implement medicine library and cabinet
This commit is contained in:
parent
db79af06f7
commit
1f66fab30f
72 changed files with 7642 additions and 319 deletions
|
|
@ -74,7 +74,8 @@ MeshiTrack/
|
|||
│ │ │ │ ├── health/
|
||||
│ │ │ │ ├── users/
|
||||
│ │ │ │ ├── households/
|
||||
│ │ │ │ ├── medicines/ # Phase 1: Medicine catalog
|
||||
│ │ │ │ ├── medicines/ # Phase 1: Medicine catalog (generic level)
|
||||
│ │ │ │ ├── medicine-products/ # Phase 1: Purchasable brands/packages
|
||||
│ │ │ │ ├── cabinet/ # Phase 2: Medicine inventory
|
||||
│ │ │ │ ├── regimens/ # Phase 3: Medication schedules
|
||||
│ │ │ │ ├── organizer/ # Phase 3: Pill organizer fills
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Phase 1 — Medicine Library
|
||||
|
||||
**Goal**: A searchable catalog of medicines with dosage and form information. Medicines are the atomic building blocks for regimens, cabinet inventory, and refill tracking.
|
||||
**Goal**: A two-level catalog of medicines. The **Medicine** level represents the generic substance you take (what regimens reference). The **MedicineProduct** level represents a specific purchasable item from a brand/manufacturer (what you buy and track prices for). Cabinet inventory tracks current quantity at the Medicine level, with an optional link back to which product it came from.
|
||||
|
||||
**Depends on**: Phase 0 (auth, households, shared types)
|
||||
|
||||
|
|
@ -8,37 +8,31 @@
|
|||
|
||||
## Deliverables
|
||||
|
||||
1. `Medicine` MongoDB schema and full CRUD API
|
||||
2. Full-text search with filters
|
||||
3. Barcode lookup (future: integration with drug database APIs)
|
||||
4. Bulk import (CSV/JSON)
|
||||
5. Medicine library web UI (search, add, edit)
|
||||
1. `Medicine` and `MedicineProduct` MongoDB schemas with full CRUD APIs
|
||||
2. Full-text search with category/form filters
|
||||
3. Cascade delete protection (cannot delete medicine with linked products)
|
||||
4. Medicine library web UI (search, filter, add, edit, delete -- both levels)
|
||||
|
||||
---
|
||||
|
||||
## Data Model
|
||||
|
||||
### Medicine Schema
|
||||
### Medicine Schema (Generic Level)
|
||||
|
||||
The generic substance -- what you take. Regimens and cabinet items reference this.
|
||||
|
||||
```typescript
|
||||
// packages/shared/src/types/medicine.ts
|
||||
export interface Medicine {
|
||||
id: string;
|
||||
householdId: string;
|
||||
name: string;
|
||||
genericName?: string;
|
||||
brand?: string;
|
||||
barcode?: string;
|
||||
name: string; // Display name, e.g., "Metformin" or "Vitamin D3"
|
||||
form: MedicineForm;
|
||||
strength: number;
|
||||
strengthUnit: StrengthUnit;
|
||||
strength: number; // e.g., 500
|
||||
strengthUnit: StrengthUnit; // e.g., 'mg' (weight/count only)
|
||||
category: MedicineCategory;
|
||||
activeIngredient?: string;
|
||||
manufacturer?: string;
|
||||
notes?: string;
|
||||
imageUrl?: string;
|
||||
tags: string[];
|
||||
source: MedicineSource;
|
||||
createdBy: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
|
|
@ -72,45 +66,99 @@ export enum MedicineCategory {
|
|||
PRESCRIPTION = 'prescription',
|
||||
OTC = 'otc',
|
||||
SUPPLEMENT = 'supplement',
|
||||
VITAMIN = 'vitamin',
|
||||
HERBAL = 'herbal',
|
||||
OTHER = 'other',
|
||||
}
|
||||
```
|
||||
|
||||
export enum MedicineSource {
|
||||
### MedicineProduct Schema (Purchasable Level)
|
||||
|
||||
A specific brand/package you can buy. References a Medicine. Used for price tracking and ordering. Vial products can include concentration data.
|
||||
|
||||
```typescript
|
||||
// packages/shared/src/types/medicine.ts
|
||||
export interface MedicineProduct {
|
||||
id: string;
|
||||
householdId: string;
|
||||
medicineId: string; // Reference to Medicine
|
||||
medicineName: string; // Denormalized for display
|
||||
brand: string; // e.g., "CVS Health", "Kirkland"
|
||||
manufacturer?: string;
|
||||
packageSize: number; // e.g., 90 (pills per bottle)
|
||||
packageUnit: DosageUnit; // e.g., 'pill', 'ml', 'vial'
|
||||
concentration?: number; // For vials only, e.g., 100
|
||||
concentrationUnit?: ConcentrationUnit; // For vials only, e.g., 'units/mL'
|
||||
imageUrl?: string;
|
||||
notes?: string;
|
||||
source: MedicineProductSource;
|
||||
createdBy: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export enum MedicineProductSource {
|
||||
MANUAL = 'manual',
|
||||
BARCODE_LOOKUP = 'barcode_lookup',
|
||||
IMPORT = 'import',
|
||||
}
|
||||
|
||||
export enum ConcentrationUnit {
|
||||
MG_PER_ML = 'mg/mL',
|
||||
MCG_PER_ML = 'mcg/mL',
|
||||
UNITS_PER_ML = 'units/mL',
|
||||
}
|
||||
```
|
||||
|
||||
### Relationship Diagram
|
||||
|
||||
```
|
||||
Medicine (generic) MedicineProduct (purchasable)
|
||||
┌─────────────────────┐ ┌──────────────────────────────┐
|
||||
│ Metformin 500mg tab │◄────────│ CVS Metformin 500mg, 90ct │
|
||||
│ │◄────────│ Kirkland Metformin 500mg, 60ct│
|
||||
└─────────────────────┘ └──────────────────────────────┘
|
||||
▲ ▲
|
||||
│ │
|
||||
Referenced by: Referenced by:
|
||||
- Regimens (Phase 3) - PriceRecords (Phase 4)
|
||||
- CabinetItems (Phase 2) - CabinetItems (Phase 2, optional)
|
||||
```
|
||||
|
||||
### MongoDB Indexes
|
||||
|
||||
```javascript
|
||||
// Text index for search
|
||||
{ name: 'text', genericName: 'text', brand: 'text', activeIngredient: 'text', tags: 'text' }
|
||||
|
||||
// Compound indexes
|
||||
// Medicine
|
||||
{ householdId: 1, name: 'text', tags: 'text' }
|
||||
{ householdId: 1, category: 1 }
|
||||
{ householdId: 1, barcode: 1 } // unique within household
|
||||
{ householdId: 1, name: 1, strength: 1, form: 1 } // near-unique for dedup
|
||||
{ householdId: 1, name: 1, strength: 1, strengthUnit: 1, form: 1 } // dedup
|
||||
|
||||
// MedicineProduct
|
||||
{ householdId: 1, medicineId: 1 }
|
||||
{ householdId: 1, brand: 'text' }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### MedicinesModule
|
||||
### MedicinesModule (Generic Level)
|
||||
|
||||
| Method | Path | Description | Auth |
|
||||
| ------ | -------------------------- | -------------------------------- | ------ |
|
||||
| GET | `/medicines` | List/search medicines (paginated)| member |
|
||||
| GET | `/medicines/:id` | Get single medicine | member |
|
||||
| POST | `/medicines` | Create medicine | member |
|
||||
| PATCH | `/medicines/:id` | Update medicine | member |
|
||||
| DELETE | `/medicines/:id` | Soft-delete medicine | admin |
|
||||
| GET | `/medicines/barcode/:code` | Lookup by barcode | member |
|
||||
| POST | `/medicines/import` | Bulk import from CSV/JSON | admin |
|
||||
| Method | Path | Description | Auth |
|
||||
| ------ | --------------------- | --------------------------------- | ------ |
|
||||
| GET | `/medicines` | List/search medicines (paginated) | member |
|
||||
| GET | `/medicines/:id` | Get single medicine | member |
|
||||
| POST | `/medicines` | Create medicine | member |
|
||||
| PATCH | `/medicines/:id` | Update medicine | member |
|
||||
| DELETE | `/medicines/:id` | Soft-delete medicine | admin |
|
||||
| POST | `/medicines/import` | Bulk import from CSV/JSON | admin |
|
||||
|
||||
### MedicineProductsModule (Purchasable Level)
|
||||
|
||||
| Method | Path | Description | Auth |
|
||||
| ------ | --------------------------------------- | ------------------------------------------ | ------ |
|
||||
| GET | `/medicines/:medicineId/products` | List products for a medicine | member |
|
||||
| GET | `/medicine-products/:id` | Get single product | member |
|
||||
| POST | `/medicines/:medicineId/products` | Create product under a medicine | member |
|
||||
| PATCH | `/medicine-products/:id` | Update product | member |
|
||||
| DELETE | `/medicine-products/:id` | Soft-delete product | admin |
|
||||
|
||||
### Query Parameters for GET `/medicines`
|
||||
|
||||
|
|
@ -141,74 +189,76 @@ interface PaginatedResponse<T> {
|
|||
|
||||
## Tasks
|
||||
|
||||
### 1.1 — Shared Types & Validation
|
||||
### 1.1 -- Shared Types & Validation
|
||||
|
||||
- Add all types above to `packages/shared/src/types/medicine.ts`
|
||||
- Add enums to `packages/shared/src/enums/`
|
||||
- Create Zod schemas:
|
||||
- `CreateMedicineSchema` — validates create payload
|
||||
- `UpdateMedicineSchema` — partial, validates update payload
|
||||
- `MedicineQuerySchema` — validates query params
|
||||
- [x] Add Medicine types to `packages/shared/src/types/medicine.ts`
|
||||
- [x] Add MedicineProduct types (same file)
|
||||
- [x] Add enums to `packages/shared/src/enums/medicine.enums.ts`
|
||||
- [x] Create Zod schemas:
|
||||
- `CreateMedicineSchema`, `UpdateMedicineSchema`, `MedicineQuerySchema`
|
||||
- `CreateMedicineProductSchema`, `UpdateMedicineProductSchema`
|
||||
|
||||
### 1.2 — Mongoose Schema & Repository
|
||||
### 1.2 -- Medicine Mongoose Schema & Repository
|
||||
|
||||
- `packages/api/src/modules/medicines/medicines.repository.ts`
|
||||
- `MedicinesRepository` with:
|
||||
- `findByHousehold(householdId, query)` — supports text search, filters, cursor pagination
|
||||
- `findByBarcode(householdId, barcode)`
|
||||
- [x] `packages/api/src/modules/medicines/medicines.repository.ts`
|
||||
- [x] `MedicinesRepository` with:
|
||||
- `findByHousehold(householdId, query)` -- supports text search, filters, cursor pagination
|
||||
- `findById(id, householdId)`
|
||||
- `findDuplicate(householdId, name, strength, strengthUnit, form, excludeId?)`
|
||||
- `create(data)`
|
||||
- `update(id, householdId, data)`
|
||||
- `softDelete(id, householdId)`
|
||||
- `bulkCreate(items[])`
|
||||
|
||||
### 1.3 — Service & Routes
|
||||
### 1.3 -- MedicineProduct Mongoose Schema & Repository
|
||||
|
||||
- `MedicinesService` with business logic (dedup check on create, validation)
|
||||
- `MedicinesRoutes` with Fastify route plugin registering all endpoints
|
||||
- Register Awilix dependencies via `fp()` plugin
|
||||
- [x] `packages/api/src/modules/medicine-products/medicine-products.repository.ts`
|
||||
- [x] `MedicineProductsRepository` with:
|
||||
- `findByMedicine(householdId, medicineId, query)` -- paginated
|
||||
- `findById(id, householdId)`
|
||||
- `countByMedicineId(medicineId)` -- for cascade delete check
|
||||
- `create(data)`
|
||||
- `update(id, householdId, data)`
|
||||
- `softDelete(id, householdId)`
|
||||
|
||||
### 1.4 — Barcode Lookup
|
||||
### 1.4 -- Services & Routes
|
||||
|
||||
- `BarcodeLookupService`:
|
||||
- First check local DB for matching barcode
|
||||
- Placeholder for external drug database API integration (manual entry fallback)
|
||||
- Cache results in local DB with `source: 'barcode_lookup'`
|
||||
- [x] `MedicinesService` with business logic:
|
||||
- Dedup check on create (same name + strength + strengthUnit + form within household)
|
||||
- Cascade: when deleting a medicine, check for linked products and block if any exist
|
||||
- [x] `MedicineProductsService` with business logic:
|
||||
- Validate medicineId exists on create
|
||||
- Denormalize medicineName
|
||||
- [x] Route plugins for both modules, registered via `fp()`
|
||||
|
||||
### 1.5 — Import Endpoint
|
||||
### 1.5 -- Web UI: Medicine Library
|
||||
|
||||
- `POST /medicines/import` accepts multipart CSV or JSON file
|
||||
- Validate each row against `CreateMedicineSchema`
|
||||
- Return summary: `{ imported: N, skipped: M, errors: [...] }`
|
||||
- CSV column mapping: `name, genericName, brand, barcode, form, strength, strengthUnit, category, activeIngredient, manufacturer`
|
||||
|
||||
### 1.6 — Web UI: Medicine Library
|
||||
|
||||
- `/medicines` page:
|
||||
- Search bar with debounced full-text search
|
||||
- [x] `/medicines` page:
|
||||
- Search bar with text search
|
||||
- Category and form filter dropdowns
|
||||
- Tag filter chips
|
||||
- Medicine grid/list view (toggle)
|
||||
- Each medicine card shows: name, strength + unit, form, brand, category badge
|
||||
- Add/Edit medicine modal:
|
||||
- Form fields for all medicine properties
|
||||
- Barcode field with "Lookup" button
|
||||
- Import dialog: file upload with preview and error display
|
||||
- Medicine list view
|
||||
- Each medicine card shows: name, strength + unit, form, category badge
|
||||
- Click a medicine to see its detail page with products
|
||||
- [x] Add/Edit medicine form
|
||||
- [x] Add/Edit product form (nested under a medicine detail page)
|
||||
- Brand, manufacturer, package size, concentration (vials only)
|
||||
- [x] Delete with confirmation (blocked if products exist)
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Can create, read, update, delete medicines via API
|
||||
- [ ] Full-text search returns relevant results
|
||||
- [ ] Barcode lookup checks local DB first
|
||||
- [ ] Bulk import processes a CSV with 100+ medicines
|
||||
- [ ] Web UI allows searching, filtering, adding, and editing medicines
|
||||
- [ ] All medicine queries are scoped to `householdId`
|
||||
- [ ] Dedup check prevents creating duplicate medicines (same name + strength + form)
|
||||
|
||||
- [x] Can create, read, update, delete medicines (generic level) via API
|
||||
- [x] Can create, read, update, delete medicine products (purchasable level) via API
|
||||
- [x] Medicine products are correctly linked to their parent medicine
|
||||
- [x] Full-text search returns relevant results
|
||||
- [x] Category and form filters work on the medicines list
|
||||
- [x] Cascade delete check prevents deleting medicines with linked products
|
||||
- [x] Web UI shows two-level hierarchy: medicines with nested products
|
||||
- [x] All queries are scoped to `householdId`
|
||||
- [x] Dedup check prevents creating duplicate medicines (same name + strength + unit + form)
|
||||
- [x] Vial products support concentration + concentration unit fields
|
||||
---
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
Medium. Straightforward CRUD with search, following the same patterns established in Phase 0.
|
||||
Medium. Two related CRUD modules with search. The two-level structure adds a bit of complexity over a flat model but keeps the domain clean.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 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.
|
||||
**Goal**: Track medicine inventory — what you have, how much of each, and when it expires.
|
||||
|
||||
**Depends on**: Phase 0, Phase 1 (medicines)
|
||||
|
||||
|
|
@ -10,9 +10,8 @@
|
|||
|
||||
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
|
||||
3. Expiry date tracking with visual indicators
|
||||
4. Medicine cabinet web UI with status indicators
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -25,19 +24,18 @@
|
|||
export interface CabinetItem {
|
||||
id: string;
|
||||
householdId: string;
|
||||
medicineId: string;
|
||||
medicineId: string; // Reference to Medicine (generic level)
|
||||
medicineName: string; // Denormalized
|
||||
medicineStrength: number; // Denormalized for display
|
||||
medicineStrengthUnit: StrengthUnit; // Denormalized
|
||||
medicineForm: MedicineForm; // Denormalized
|
||||
quantity: number;
|
||||
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;
|
||||
lotNumber?: string;
|
||||
purchaseDate?: Date;
|
||||
purchasePrice?: number;
|
||||
storeId?: string;
|
||||
storeName?: string; // Denormalized
|
||||
status: CabinetItemStatus;
|
||||
notes?: string;
|
||||
createdBy: string;
|
||||
|
|
@ -45,23 +43,10 @@ export interface CabinetItem {
|
|||
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',
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -79,8 +64,6 @@ export interface CabinetSummary {
|
|||
unit: DosageUnit;
|
||||
earliestExpiry: Date | null;
|
||||
itemCount: number; // How many cabinet items (bottles/boxes)
|
||||
lowStockThreshold?: number; // From household settings
|
||||
isLowStock: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -89,8 +72,7 @@ export interface CabinetSummary {
|
|||
```javascript
|
||||
{ householdId: 1, medicineId: 1, status: 1 }
|
||||
{ householdId: 1, status: 1 }
|
||||
{ householdId: 1, expirationDate: 1 } // For expiry warnings
|
||||
{ householdId: 1, 'quantity': 1 }
|
||||
{ householdId: 1, expirationDate: 1 }
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -107,9 +89,8 @@ export interface CabinetSummary {
|
|||
| 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 |
|
||||
| DELETE | `/cabinet/:id` | Delete cabinet item | member |
|
||||
| GET | `/cabinet/expiring-soon` | Items expiring within N days | member |
|
||||
| GET | `/cabinet/low-stock` | Medicines below threshold quantity | member |
|
||||
|
||||
### Query Parameters for GET `/cabinet`
|
||||
|
||||
|
|
@ -117,7 +98,6 @@ export interface CabinetSummary {
|
|||
?medicineId=abc123 # Filter by medicine
|
||||
&status=active # Filter by status
|
||||
&expiringWithin=30 # Days until expiry
|
||||
&sort=-expirationDate|name # Sort field
|
||||
&cursor=abc123
|
||||
&limit=20
|
||||
```
|
||||
|
|
@ -128,7 +108,7 @@ export interface CabinetSummary {
|
|||
// POST /cabinet/:id/adjust
|
||||
interface AdjustQuantityRequest {
|
||||
delta: number; // Positive to add, negative to subtract
|
||||
reason?: string; // e.g., "Correcting count", "Dropped a pill"
|
||||
reason?: string; // e.g., "Correcting count"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -151,102 +131,56 @@ interface AdjustQuantityRequest {
|
|||
- `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`
|
||||
- `adjustQuantity(id, householdId, delta)` — atomic adjust with floor at 0
|
||||
- `findExpiringSoon(householdId, withinDays)`
|
||||
- `delete(id, householdId)`
|
||||
- `softDelete(id, householdId)`
|
||||
|
||||
### 2.3 — Cabinet Service
|
||||
|
||||
```typescript
|
||||
class CabinetService {
|
||||
/** Add item, denormalizing medicine fields */
|
||||
/** 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, reason?: string): Promise<CabinetItem>;
|
||||
adjustQuantity(id: string, householdId: string, delta: number): Promise<CabinetItem>;
|
||||
|
||||
/** Get aggregate summary with low stock flags */
|
||||
/** Get aggregate summary */
|
||||
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
|
||||
### 2.4 — Web UI: Medicine Cabinet
|
||||
|
||||
- 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:
|
||||
- `/medicines/cabinet` page:
|
||||
- **Summary view** (default): aggregated per medicine
|
||||
- Medicine name, total quantity, earliest expiry, low stock indicator
|
||||
- Medicine name, total quantity, earliest expiry
|
||||
- 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
|
||||
- **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
|
||||
- [ ] 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).
|
||||
- [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
|
||||
|
|
|
|||
|
|
@ -49,13 +49,15 @@ export interface Store {
|
|||
export interface MedicinePriceRecord {
|
||||
id: string;
|
||||
householdId: string;
|
||||
medicineId: string;
|
||||
medicineProductId: string; // Reference to MedicineProduct (purchasable level)
|
||||
medicineProductBrand: string; // Denormalized
|
||||
medicineId: string; // Reference to Medicine (generic level, for cross-brand comparison)
|
||||
medicineName: string; // Denormalized
|
||||
storeId: string;
|
||||
storeName: string; // Denormalized
|
||||
price: number;
|
||||
currency: string; // Default from household settings
|
||||
quantity: number; // How many pills/units for this price
|
||||
quantity: number; // How many pills/units for this price (package size)
|
||||
unit: DosageUnit;
|
||||
pricePerUnit: number; // Computed: price / quantity
|
||||
date: Date;
|
||||
|
|
@ -139,8 +141,8 @@ export enum RefillListStatus {
|
|||
{ householdId: 1, tags: 1 }
|
||||
|
||||
// MedicinePriceRecord
|
||||
{ householdId: 1, medicineId: 1, storeId: 1, date: -1 }
|
||||
{ householdId: 1, medicineId: 1, date: -1 }
|
||||
{ householdId: 1, medicineProductId: 1, storeId: 1, date: -1 }
|
||||
{ householdId: 1, medicineId: 1, date: -1 } // Cross-brand comparison
|
||||
{ householdId: 1, storeId: 1, date: -1 }
|
||||
|
||||
// RefillList
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue