Refactor medication model to use blisters and pills instead of strips and tabs

- Updated medication schema to replace stripsPerPack and tabsPerStrip with blistersPerPack and pillsPerBlister.
- Adjusted medication routes to handle new blister and pill structure, including calculations for total pills.
- Modified frontend components to reflect changes in medication data structure and ensure compatibility with new backend logic.
- Updated reminder scheduler and share routes to utilize the new medication model.
- Enhanced Docker configuration for better permissions handling during development.
This commit is contained in:
Daniel Volz
2025-12-29 19:18:14 +01:00
parent dc0e364830
commit 666306b416
26 changed files with 169 additions and 492 deletions
+8 -138
View File
@@ -159,9 +159,8 @@ Each blister defines a recurring intake:
### Key Medication Fields
```typescript
{
name, genericName, takenBy, // Identity
packCount, stripsPerPack, tabsPerStrip, looseTablets, // Inventory
count, strips, stripSize, // Derived/legacy
name, genericName, takenByJson, // Identity (takenByJson is JSON array)
packCount, blistersPerPack, pillsPerBlister, looseTablets, // Inventory
pillWeightMg, // For mg display
usageJson, everyJson, startJson, // Intake schedules as JSON arrays
imageUrl, expiryDate, notes, // Optional metadata
@@ -207,141 +206,14 @@ Example: `5-0-1735344000000` = Medication 5, Blister 0, timestamp
- **Environment**: Copy `.env.example``.env`, secrets must be 10+ chars
- **i18n**: All UI text via `t('key')` function, translations in `frontend/src/i18n/*.json`
## ⚠️⚠️⚠️ Database Migrations (ABSOLUTELY CRITICAL) ⚠️⚠️⚠️
## Database Schema Changes
**THIS IS NON-NEGOTIABLE: ALL database changes MUST work for EXISTING production databases!**
When adding new database columns:
Users update their Docker containers and expect the app to work with their existing data. If migrations don't run automatically, the app crashes with `SQLITE_ERROR: no such column` errors.
### The Migration System
The app uses **auto-migrations at startup** in `backend/src/db/client.ts`. This file:
1. Creates tables if they don't exist (fresh install)
2. Runs `ALTER TABLE ADD COLUMN` for each new column (existing databases)
3. Ignores "duplicate column" errors (migration already applied)
### When adding/modifying database columns or tables, ALWAYS do ALL of the following:
#### 1. Update schema: `backend/src/db/schema.ts`
```typescript
// Add the new column to the Drizzle schema
stockCalculationMode: text("stock_calculation_mode").notNull().default("automatic"),
```
#### 2. Update client.ts TABLE CREATION: `backend/src/db/client.ts`
Find the `CREATE TABLE IF NOT EXISTS` statement and add the new column:
```sql
CREATE TABLE IF NOT EXISTS user_settings (
...
stock_calculation_mode text NOT NULL DEFAULT 'automatic', -- ADD THIS LINE
...
)
```
**This is for FRESH installs** - new databases get all columns from the start.
#### 3. Update client.ts MIGRATIONS ARRAY: `backend/src/db/client.ts`
Add an entry to the `migrations` array:
```typescript
const migrations = [
...existing migrations...
{ name: "user_settings_stock_calculation_mode", sql: "ALTER TABLE user_settings ADD COLUMN stock_calculation_mode TEXT NOT NULL DEFAULT 'automatic'" },
];
```
**This is for EXISTING databases** - the ALTER TABLE adds the column to old databases.
#### 4. Create migration SQL file (for documentation): `backend/src/db/migrations/XXXX_description.sql`
```sql
-- Add stock calculation mode setting
ALTER TABLE user_settings ADD COLUMN stock_calculation_mode TEXT NOT NULL DEFAULT 'automatic';
```
#### 5. Update journal: `backend/src/db/migrations/meta/_journal.json`
```json
{ "idx": X, "version": 1, "when": TIMESTAMP, "tag": "XXXX_description", "breakpoint": false }
```
#### 6. Update migrate.ts: `backend/src/db/migrate.ts`
Add the column to the `CREATE TABLE` statement AND to the `migrations` array.
### ⚠️ CRITICAL CHECKLIST - DO NOT SKIP ANY STEP:
| Step | File | Purpose | If Missing |
|------|------|---------|------------|
| 1 | `schema.ts` | Drizzle ORM knows about column | TypeScript errors |
| 2 | `client.ts` (CREATE TABLE) | Fresh installs have column | Fresh installs crash |
| 3 | `client.ts` (migrations array) | Existing DBs get column | **PRODUCTION CRASHES** |
| 4 | `migrations/*.sql` | Documentation | None (but keep for history) |
| 5 | `_journal.json` | Migration tracking | None (but keep for history) |
| 6 | `migrate.ts` | CLI migration tool | CLI tool fails |
**Step 3 is the most critical!** Without it, users who update their Docker container will get `SQLITE_ERROR: no such column` and the app will not start.
### Testing Migrations
Before pushing changes:
1. Test with fresh database: Delete `backend/data/medassist-ng.db` and restart
2. Test with existing database: Keep old DB and restart - new columns should be added automatically
## ⚠️ Defensive Coding (CRITICAL for Production)
**ALL new optional fields MUST be handled defensively in both Backend AND Frontend!**
When a user updates their app, old data in the database may not have new fields. The frontend receives this data and crashes with `TypeError: Cannot read property 'length' of undefined`.
### Rules for New Optional/Array Fields:
#### Backend (routes/*.ts):
Always provide default values when returning data:
```typescript
// ✅ CORRECT - Always return array, even if DB value is null/undefined
takenBy: parseTakenByJson(row.takenByJson), // Returns [] if null/undefined
// Parser function example:
function parseTakenByJson(value: string | null | undefined): string[] {
if (!value) return [];
try { return JSON.parse(value) || []; }
catch { return []; }
}
```
#### Frontend (App.tsx):
Always use defensive checks when accessing optional properties:
```typescript
// ✅ CORRECT - Defensive checks
med?.takenBy && med.takenBy.length > 0
(m.takenBy || []).includes(selectedUser)
(d.takenBy || []).length > 0 ? d.takenBy : [null]
const personCount = Math.max(1, m.takenBy?.length || 1);
// ❌ WRONG - Will crash if takenBy is undefined
m.takenBy.includes(selectedUser) // TypeError!
m.takenBy.length > 0 // TypeError!
```
### Checklist for New Optional Fields:
| Location | Action |
|----------|--------|
| Backend route | Return default value (`[]`, `null`, `0`, etc.) |
| Frontend type | Mark as optional: `takenBy?: string[]` |
| Frontend access | ALWAYS use `?.`, `|| []`, or null-check before `.length`, `.map()`, `.includes()` |
| Schedule builders | Pass default: `takenBy: med.takenBy || []` |
### Common Patterns:
```typescript
// Arrays - always default to []
const people = (dose.takenBy || []).length > 0 ? dose.takenBy : [null];
meds.flatMap(m => m.takenBy || []);
// Optional chaining for nested access
med?.takenBy?.length > 0
// Filter with optional check
meds.filter(m => (m.takenBy || []).includes(name))
// Conditional rendering
{med?.takenBy && med.takenBy.length > 0 && med.takenBy.map(...)}
```
1. **Update schema**: `backend/src/db/schema.ts` - Add the Drizzle column definition
2. **Update client.ts**: `backend/src/db/client.ts` - Add column to `CREATE TABLE IF NOT EXISTS`
3. **Update migrate.ts**: `backend/src/db/migrate.ts` - Same as client.ts
4. **Delete old DB**: `rm backend/data/medassist-ng.db` and restart
## File Locations
@@ -349,8 +221,6 @@ meds.filter(m => (m.takenBy || []).includes(name))
|---------|----------|
| Backend entry | `backend/src/index.ts` |
| Database schema | `backend/src/db/schema.ts` |
| Migrations | `backend/src/db/migrations/*.sql` |
| Migration journal | `backend/src/db/migrations/meta/_journal.json` |
| Backend routes | `backend/src/routes/*.ts` |
| Backend services | `backend/src/services/*.ts` |
| Frontend app | `frontend/src/App.tsx` |