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:
@@ -65,8 +65,9 @@ async function runMigrations() {
|
||||
id integer PRIMARY KEY AUTOINCREMENT,
|
||||
username text NOT NULL UNIQUE,
|
||||
password_hash text,
|
||||
email text,
|
||||
avatar_url text,
|
||||
auth_provider text NOT NULL DEFAULT 'local',
|
||||
oidc_subject text,
|
||||
is_active integer NOT NULL DEFAULT 1,
|
||||
last_login_at integer,
|
||||
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
|
||||
@@ -77,19 +78,15 @@ async function runMigrations() {
|
||||
user_id integer NOT NULL,
|
||||
name text NOT NULL,
|
||||
generic_name text,
|
||||
taken_by text,
|
||||
taken_by_json text NOT NULL DEFAULT '[]',
|
||||
count integer NOT NULL DEFAULT 0,
|
||||
strips integer NOT NULL DEFAULT 0,
|
||||
pack_count integer NOT NULL DEFAULT 1,
|
||||
strips_per_pack integer NOT NULL DEFAULT 1,
|
||||
tabs_per_strip integer NOT NULL DEFAULT 1,
|
||||
blisters_per_pack integer NOT NULL DEFAULT 1,
|
||||
pills_per_blister integer NOT NULL DEFAULT 1,
|
||||
loose_tablets integer NOT NULL DEFAULT 0,
|
||||
pill_weight_mg integer,
|
||||
usage_json text NOT NULL DEFAULT '[]',
|
||||
every_json text NOT NULL DEFAULT '[]',
|
||||
start_json text NOT NULL DEFAULT '[]',
|
||||
strip_size integer NOT NULL DEFAULT 1,
|
||||
image_url text,
|
||||
expiry_date text,
|
||||
notes text,
|
||||
@@ -161,36 +158,6 @@ async function runMigrations() {
|
||||
}
|
||||
console.log(`[DB] Tables verified/created`);
|
||||
|
||||
// Then run column migrations for existing databases
|
||||
const migrations = [
|
||||
{ name: "image_url", sql: "ALTER TABLE medications ADD COLUMN image_url TEXT" },
|
||||
{ name: "expiry_date", sql: "ALTER TABLE medications ADD COLUMN expiry_date TEXT" },
|
||||
{ name: "notes", sql: "ALTER TABLE medications ADD COLUMN notes TEXT" },
|
||||
{ name: "generic_name", sql: "ALTER TABLE medications ADD COLUMN generic_name TEXT" },
|
||||
{ name: "intake_reminders_enabled", sql: "ALTER TABLE medications ADD COLUMN intake_reminders_enabled INTEGER NOT NULL DEFAULT 0" },
|
||||
{ name: "pill_weight_mg", sql: "ALTER TABLE medications ADD COLUMN pill_weight_mg REAL" },
|
||||
{ name: "taken_by", sql: "ALTER TABLE medications ADD COLUMN taken_by TEXT" },
|
||||
{ name: "taken_by_json", sql: "ALTER TABLE medications ADD COLUMN taken_by_json TEXT NOT NULL DEFAULT '[]'" },
|
||||
{ name: "users_email", sql: "ALTER TABLE users ADD COLUMN email TEXT" },
|
||||
{ name: "users_avatar_url", sql: "ALTER TABLE users ADD COLUMN avatar_url TEXT" },
|
||||
{ name: "users_oidc_subject", sql: "ALTER TABLE users ADD COLUMN oidc_subject TEXT" },
|
||||
{ name: "user_settings_expiry_warning_days", sql: "ALTER TABLE user_settings ADD COLUMN expiry_warning_days INTEGER NOT NULL DEFAULT 90" },
|
||||
{ name: "user_settings_stock_calculation_mode", sql: "ALTER TABLE user_settings ADD COLUMN stock_calculation_mode TEXT NOT NULL DEFAULT 'automatic'" },
|
||||
{ name: "share_tokens_expires_at", sql: "ALTER TABLE share_tokens ADD COLUMN expires_at INTEGER" },
|
||||
];
|
||||
|
||||
for (const migration of migrations) {
|
||||
try {
|
||||
await client.execute(migration.sql);
|
||||
console.log(`[DB] Migration applied: ${migration.name}`);
|
||||
} catch (e: any) {
|
||||
// Ignore "duplicate column" errors - column already exists
|
||||
if (!e.message?.includes("duplicate column")) {
|
||||
console.error(`[DB] Migration error (${migration.name}):`, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If auth is disabled, ensure a default user exists (ID=1)
|
||||
const authEnabled = process.env.AUTH_ENABLED === "true";
|
||||
if (!authEnabled) {
|
||||
|
||||
@@ -33,19 +33,15 @@ async function main() {
|
||||
user_id integer NOT NULL,
|
||||
name text NOT NULL,
|
||||
generic_name text,
|
||||
taken_by text,
|
||||
taken_by_json text NOT NULL DEFAULT '[]',
|
||||
count integer NOT NULL DEFAULT 0,
|
||||
strips integer NOT NULL DEFAULT 0,
|
||||
pack_count integer NOT NULL DEFAULT 1,
|
||||
strips_per_pack integer NOT NULL DEFAULT 1,
|
||||
tabs_per_strip integer NOT NULL DEFAULT 1,
|
||||
blisters_per_pack integer NOT NULL DEFAULT 1,
|
||||
pills_per_blister integer NOT NULL DEFAULT 1,
|
||||
loose_tablets integer NOT NULL DEFAULT 0,
|
||||
pill_weight_mg integer,
|
||||
usage_json text NOT NULL DEFAULT '[]',
|
||||
every_json text NOT NULL DEFAULT '[]',
|
||||
start_json text NOT NULL DEFAULT '[]',
|
||||
strip_size integer NOT NULL DEFAULT 1,
|
||||
image_url text,
|
||||
expiry_date text,
|
||||
notes text,
|
||||
@@ -119,52 +115,6 @@ async function main() {
|
||||
await client.execute(stmt);
|
||||
}
|
||||
|
||||
console.log("Base tables created. Running migrations for existing databases...");
|
||||
|
||||
// Run incremental migrations for existing databases
|
||||
// These ALTER TABLE statements are safe to run multiple times (they'll fail silently if column exists)
|
||||
const migrations = [
|
||||
// 0003: Add image_url to medications
|
||||
{ name: "0003_add_image_url", sql: "ALTER TABLE medications ADD COLUMN image_url text" },
|
||||
// 0004: Add expiry_date to medications
|
||||
{ name: "0004_add_expiry_date", sql: "ALTER TABLE medications ADD COLUMN expiry_date text" },
|
||||
// 0005: Add notes to medications
|
||||
{ name: "0005_add_notes", sql: "ALTER TABLE medications ADD COLUMN notes text" },
|
||||
// 0006: Add generic_name to medications
|
||||
{ name: "0006_add_generic_name", sql: "ALTER TABLE medications ADD COLUMN generic_name text" },
|
||||
// 0007: Add intake_reminders_enabled to medications
|
||||
{ name: "0007_add_intake_reminders", sql: "ALTER TABLE medications ADD COLUMN intake_reminders_enabled integer NOT NULL DEFAULT 0" },
|
||||
// 0008: Add pill_weight_mg to medications
|
||||
{ name: "0008_add_pill_weight", sql: "ALTER TABLE medications ADD COLUMN pill_weight_mg integer" },
|
||||
// 0009: Add taken_by to medications
|
||||
{ name: "0009_add_taken_by", sql: "ALTER TABLE medications ADD COLUMN taken_by text" },
|
||||
// 0012: Add avatar_url to users
|
||||
{ name: "0012_add_user_avatar", sql: "ALTER TABLE users ADD COLUMN avatar_url text" },
|
||||
// 0013: Add oidc_subject to users
|
||||
{ name: "0013_add_oidc_subject", sql: "ALTER TABLE users ADD COLUMN oidc_subject text" },
|
||||
// 0014: Add stock_calculation_mode to user_settings
|
||||
{ name: "0014_add_stock_calculation_mode", sql: "ALTER TABLE user_settings ADD COLUMN stock_calculation_mode text NOT NULL DEFAULT 'automatic'" },
|
||||
// 0015: Add expires_at to share_tokens
|
||||
{ name: "0015_add_share_token_expiry", sql: "ALTER TABLE share_tokens ADD COLUMN expires_at integer" },
|
||||
// 0016: Add taken_by_json to medications
|
||||
{ name: "0016_taken_by_json_array", sql: "ALTER TABLE medications ADD COLUMN taken_by_json text NOT NULL DEFAULT '[]'" },
|
||||
];
|
||||
|
||||
for (const migration of migrations) {
|
||||
try {
|
||||
await client.execute(migration.sql);
|
||||
console.log(`Migration ${migration.name}: applied`);
|
||||
} catch (err: unknown) {
|
||||
// Ignore "duplicate column" errors - means migration was already applied
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
if (errorMessage.includes("duplicate column") || errorMessage.includes("already exists")) {
|
||||
console.log(`Migration ${migration.name}: already applied (skipped)`);
|
||||
} else {
|
||||
console.error(`Migration ${migration.name}: failed - ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Database setup complete!");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
-- Migration 0003: Add image_url column for medication photos
|
||||
ALTER TABLE medications ADD COLUMN image_url TEXT;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- Migration 0004: Add expiry_date column for medication expiration tracking
|
||||
ALTER TABLE medications ADD COLUMN expiry_date TEXT;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- Add notes column for medication instructions
|
||||
ALTER TABLE medications ADD COLUMN notes TEXT;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- Add generic_name column for medication active ingredient
|
||||
ALTER TABLE medications ADD COLUMN generic_name TEXT;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- Add intake_reminders_enabled column to medications table
|
||||
ALTER TABLE medications ADD COLUMN intake_reminders_enabled INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- Add pill weight column (in mg)
|
||||
ALTER TABLE medications ADD COLUMN pill_weight_mg INTEGER;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- Add taken_by column for family member tracking
|
||||
ALTER TABLE medications ADD COLUMN taken_by TEXT;
|
||||
@@ -1,28 +0,0 @@
|
||||
-- Add user_id to medications (for existing databases)
|
||||
-- First, add the column as nullable
|
||||
ALTER TABLE medications ADD COLUMN user_id INTEGER REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
-- Create user_settings table for per-user notification settings
|
||||
CREATE TABLE IF NOT EXISTS user_settings (
|
||||
id integer PRIMARY KEY AUTOINCREMENT,
|
||||
user_id integer NOT NULL UNIQUE,
|
||||
email_enabled integer NOT NULL DEFAULT 0,
|
||||
notification_email text,
|
||||
email_stock_reminders integer NOT NULL DEFAULT 1,
|
||||
email_intake_reminders integer NOT NULL DEFAULT 1,
|
||||
shoutrrr_enabled integer NOT NULL DEFAULT 0,
|
||||
shoutrrr_url text,
|
||||
shoutrrr_stock_reminders integer NOT NULL DEFAULT 1,
|
||||
shoutrrr_intake_reminders integer NOT NULL DEFAULT 1,
|
||||
reminder_days_before integer NOT NULL DEFAULT 7,
|
||||
repeat_daily_reminders integer NOT NULL DEFAULT 0,
|
||||
low_stock_days integer NOT NULL DEFAULT 30,
|
||||
normal_stock_days integer NOT NULL DEFAULT 90,
|
||||
high_stock_days integer NOT NULL DEFAULT 180,
|
||||
language text NOT NULL DEFAULT 'en',
|
||||
last_auto_email_sent text,
|
||||
last_notification_type text,
|
||||
last_notification_channel text,
|
||||
updated_at integer NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
@@ -1,11 +0,0 @@
|
||||
-- Dose tracking table for syncing taken doses between users and share links
|
||||
CREATE TABLE IF NOT EXISTS dose_tracking (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
dose_id TEXT NOT NULL,
|
||||
taken_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
marked_by TEXT
|
||||
);
|
||||
|
||||
-- Index for fast lookups by user and dose
|
||||
CREATE INDEX IF NOT EXISTS idx_dose_tracking_user_dose ON dose_tracking(user_id, dose_id);
|
||||
@@ -1,2 +0,0 @@
|
||||
-- Add avatar URL column to users table
|
||||
ALTER TABLE users ADD COLUMN avatar_url TEXT;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- Add OIDC subject column for SSO user identification
|
||||
ALTER TABLE users ADD COLUMN oidc_subject TEXT;
|
||||
@@ -1,4 +0,0 @@
|
||||
-- Add stock calculation mode setting
|
||||
-- "automatic" = stock decreases based on schedule from start date
|
||||
-- "manual" = stock only decreases when doses are marked as taken
|
||||
ALTER TABLE user_settings ADD COLUMN stock_calculation_mode TEXT NOT NULL DEFAULT 'automatic';
|
||||
@@ -1,3 +0,0 @@
|
||||
-- Add expiration date to share tokens (default 1 year from creation)
|
||||
-- NULL means no expiration (for backwards compatibility with existing tokens)
|
||||
ALTER TABLE share_tokens ADD COLUMN expires_at INTEGER;
|
||||
@@ -1,17 +0,0 @@
|
||||
-- Convert taken_by from single string to JSON array
|
||||
-- This allows multiple people to share the same medication
|
||||
|
||||
-- Add new column for JSON array
|
||||
ALTER TABLE medications ADD COLUMN taken_by_json TEXT NOT NULL DEFAULT '[]';
|
||||
|
||||
-- Migrate existing data: convert single string to JSON array
|
||||
-- If taken_by is not null/empty, convert to ["value"], otherwise keep as []
|
||||
UPDATE medications
|
||||
SET taken_by_json = CASE
|
||||
WHEN taken_by IS NOT NULL AND taken_by != ''
|
||||
THEN json_array(taken_by)
|
||||
ELSE '[]'
|
||||
END;
|
||||
|
||||
-- Note: We keep the old taken_by column for backwards compatibility during migration
|
||||
-- It can be dropped in a future migration once all code uses taken_by_json
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"entries": [
|
||||
{ "idx": 0, "version": 1, "when": 1734633120, "tag": "0000_init", "breakpoint": false },
|
||||
{ "idx": 1, "version": 1, "when": 1734700000, "tag": "0001_add_strips", "breakpoint": false },
|
||||
{ "idx": 2, "version": 1, "when": 1734800000, "tag": "0002_pack_inventory", "breakpoint": false },
|
||||
{ "idx": 3, "version": 1, "when": 1734900000, "tag": "0003_add_image_url", "breakpoint": false },
|
||||
{ "idx": 4, "version": 1, "when": 1735000000, "tag": "0004_add_expiry_date", "breakpoint": false },
|
||||
{ "idx": 5, "version": 1, "when": 1735100000, "tag": "0005_add_notes", "breakpoint": false },
|
||||
{ "idx": 6, "version": 1, "when": 1735200000, "tag": "0006_add_generic_name", "breakpoint": false },
|
||||
{ "idx": 7, "version": 1, "when": 1735300000, "tag": "0007_add_intake_reminders", "breakpoint": false },
|
||||
{ "idx": 8, "version": 1, "when": 1735400000, "tag": "0008_add_pill_weight", "breakpoint": false },
|
||||
{ "idx": 9, "version": 1, "when": 1735500000, "tag": "0009_add_taken_by", "breakpoint": false },
|
||||
{ "idx": 10, "version": 1, "when": 1735600000, "tag": "0010_add_user_settings", "breakpoint": false },
|
||||
{ "idx": 11, "version": 1, "when": 1735700000, "tag": "0011_add_dose_tracking", "breakpoint": false },
|
||||
{ "idx": 12, "version": 1, "when": 1735800000, "tag": "0012_add_user_avatar", "breakpoint": false },
|
||||
{ "idx": 13, "version": 1, "when": 1735900000, "tag": "0013_add_oidc_subject", "breakpoint": false },
|
||||
{ "idx": 14, "version": 1, "when": 1735400000, "tag": "0014_add_stock_calculation_mode", "breakpoint": false },
|
||||
{ "idx": 15, "version": 1, "when": 1735400001, "tag": "0015_add_share_token_expiry", "breakpoint": false },
|
||||
{ "idx": 16, "version": 1, "when": 1735400002, "tag": "0016_taken_by_json_array", "breakpoint": false }
|
||||
]
|
||||
}
|
||||
@@ -25,19 +25,15 @@ export const medications = sqliteTable("medications", {
|
||||
userId: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
name: text("name", { length: 100 }).notNull(),
|
||||
genericName: text("generic_name", { length: 100 }),
|
||||
takenBy: text("taken_by", { length: 100 }), // Deprecated: use takenByJson
|
||||
takenByJson: text("taken_by_json").notNull().default("[]"), // JSON array of person names
|
||||
count: integer("count").notNull().default(0),
|
||||
strips: integer("strips").notNull().default(0),
|
||||
packCount: integer("pack_count").notNull().default(1),
|
||||
stripsPerPack: integer("strips_per_pack").notNull().default(1),
|
||||
tabsPerStrip: integer("tabs_per_strip").notNull().default(1),
|
||||
blistersPerPack: integer("blisters_per_pack").notNull().default(1),
|
||||
pillsPerBlister: integer("pills_per_blister").notNull().default(1),
|
||||
looseTablets: integer("loose_tablets").notNull().default(0),
|
||||
pillWeightMg: integer("pill_weight_mg"),
|
||||
usageJson: text("usage_json").notNull().default("[]"),
|
||||
everyJson: text("every_json").notNull().default("[]"),
|
||||
startJson: text("start_json").notNull().default("[]"),
|
||||
stripSize: integer("strip_size").notNull().default(1),
|
||||
imageUrl: text("image_url"),
|
||||
expiryDate: text("expiry_date"),
|
||||
notes: text("notes"),
|
||||
|
||||
Reference in New Issue
Block a user