diff --git a/backend/drizzle/0010_mean_spot.sql b/backend/drizzle/0010_mean_spot.sql new file mode 100644 index 0000000..afa82c0 --- /dev/null +++ b/backend/drizzle/0010_mean_spot.sql @@ -0,0 +1 @@ +ALTER TABLE `dose_tracking` ADD `taken_source` text DEFAULT 'manual' NOT NULL; \ No newline at end of file diff --git a/backend/drizzle/meta/0010_snapshot.json b/backend/drizzle/meta/0010_snapshot.json new file mode 100644 index 0000000..2fae59a --- /dev/null +++ b/backend/drizzle/meta/0010_snapshot.json @@ -0,0 +1,1052 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "41342657-afb5-479d-bc93-2ba8d784c09b", + "prevId": "b6f1ee4b-cc31-4060-a4d4-bcd4fdc5bd87", + "tables": { + "dose_tracking": { + "name": "dose_tracking", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dose_id": { + "name": "dose_id", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "taken_at": { + "name": "taken_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%s','now'))" + }, + "marked_by": { + "name": "marked_by", + "type": "text(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "taken_source": { + "name": "taken_source", + "type": "text(20)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "dismissed": { + "name": "dismissed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "dose_tracking_user_id_users_id_fk": { + "name": "dose_tracking_user_id_users_id_fk", + "tableFrom": "dose_tracking", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "medications": { + "name": "medications", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "generic_name": { + "name": "generic_name", + "type": "text(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "taken_by_json": { + "name": "taken_by_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "package_type": { + "name": "package_type", + "type": "text(20)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'blister'" + }, + "pack_count": { + "name": "pack_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "blisters_per_pack": { + "name": "blisters_per_pack", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "pills_per_blister": { + "name": "pills_per_blister", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "total_pills": { + "name": "total_pills", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "loose_tablets": { + "name": "loose_tablets", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "stock_adjustment": { + "name": "stock_adjustment", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_stock_correction_at": { + "name": "last_stock_correction_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pill_weight_mg": { + "name": "pill_weight_mg", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dose_unit": { + "name": "dose_unit", + "type": "text(20)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'mg'" + }, + "usage_json": { + "name": "usage_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "every_json": { + "name": "every_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "start_json": { + "name": "start_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "intakes_json": { + "name": "intakes_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expiry_date": { + "name": "expiry_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "intake_reminders_enabled": { + "name": "intake_reminders_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "medication_start_date": { + "name": "medication_start_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "is_obsolete": { + "name": "is_obsolete", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "obsolete_at": { + "name": "obsolete_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prescription_enabled": { + "name": "prescription_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "prescription_authorized_refills": { + "name": "prescription_authorized_refills", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prescription_remaining_refills": { + "name": "prescription_remaining_refills", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prescription_low_refill_threshold": { + "name": "prescription_low_refill_threshold", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "prescription_expiry_date": { + "name": "prescription_expiry_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dismissed_until": { + "name": "dismissed_until", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "medications_user_id_users_id_fk": { + "name": "medications_user_id_users_id_fk", + "tableFrom": "medications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "refill_history": { + "name": "refill_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "medication_id": { + "name": "medication_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "packs_added": { + "name": "packs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "loose_pills_added": { + "name": "loose_pills_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "used_prescription": { + "name": "used_prescription", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "refill_date": { + "name": "refill_date", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%s','now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "refill_history_medication_id_medications_id_fk": { + "name": "refill_history_medication_id_medications_id_fk", + "tableFrom": "refill_history", + "tableTo": "medications", + "columnsFrom": [ + "medication_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "refill_history_user_id_users_id_fk": { + "name": "refill_history_user_id_users_id_fk", + "tableFrom": "refill_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "refresh_tokens": { + "name": "refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_id": { + "name": "token_id", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rotated_at": { + "name": "rotated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked": { + "name": "revoked", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "refresh_tokens_token_id_unique": { + "name": "refresh_tokens_token_id_unique", + "columns": [ + "token_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "refresh_tokens_user_id_users_id_fk": { + "name": "refresh_tokens_user_id_users_id_fk", + "tableFrom": "refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "share_tokens": { + "name": "share_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "taken_by": { + "name": "taken_by", + "type": "text(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schedule_days": { + "name": "schedule_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 30 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "share_tokens_token_unique": { + "name": "share_tokens_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "share_tokens_user_id_users_id_fk": { + "name": "share_tokens_user_id_users_id_fk", + "tableFrom": "share_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_settings": { + "name": "user_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_enabled": { + "name": "email_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "notification_email": { + "name": "notification_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email_stock_reminders": { + "name": "email_stock_reminders", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "email_intake_reminders": { + "name": "email_intake_reminders", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "email_prescription_reminders": { + "name": "email_prescription_reminders", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "shoutrrr_enabled": { + "name": "shoutrrr_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "shoutrrr_url": { + "name": "shoutrrr_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shoutrrr_stock_reminders": { + "name": "shoutrrr_stock_reminders", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "shoutrrr_intake_reminders": { + "name": "shoutrrr_intake_reminders", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "shoutrrr_prescription_reminders": { + "name": "shoutrrr_prescription_reminders", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "reminder_days_before": { + "name": "reminder_days_before", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 7 + }, + "repeat_daily_reminders": { + "name": "repeat_daily_reminders", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "skip_reminders_for_taken_doses": { + "name": "skip_reminders_for_taken_doses", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "repeat_reminders_enabled": { + "name": "repeat_reminders_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "reminder_repeat_interval_minutes": { + "name": "reminder_repeat_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 30 + }, + "max_nagging_reminders": { + "name": "max_nagging_reminders", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 5 + }, + "low_stock_days": { + "name": "low_stock_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 30 + }, + "normal_stock_days": { + "name": "normal_stock_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "high_stock_days": { + "name": "high_stock_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 180 + }, + "expiry_warning_days": { + "name": "expiry_warning_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "language": { + "name": "language", + "type": "text(10)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'en'" + }, + "stock_calculation_mode": { + "name": "stock_calculation_mode", + "type": "text(20)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'automatic'" + }, + "share_stock_status": { + "name": "share_stock_status", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "upcoming_today_only": { + "name": "upcoming_today_only", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_schedule_today_only": { + "name": "share_schedule_today_only", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "swap_dashboard_main_sections": { + "name": "swap_dashboard_main_sections", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "last_auto_email_sent": { + "name": "last_auto_email_sent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_notification_type": { + "name": "last_notification_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_notification_channel": { + "name": "last_notification_channel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_reminder_med_name": { + "name": "last_reminder_med_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_reminder_taken_by": { + "name": "last_reminder_taken_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_stock_reminder_sent": { + "name": "last_stock_reminder_sent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_stock_reminder_channel": { + "name": "last_stock_reminder_channel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_stock_reminder_med_names": { + "name": "last_stock_reminder_med_names", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_prescription_reminder_sent": { + "name": "last_prescription_reminder_sent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_prescription_reminder_channel": { + "name": "last_prescription_reminder_channel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_prescription_reminder_med_names": { + "name": "last_prescription_reminder_med_names", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "user_settings_user_id_unique": { + "name": "user_settings_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_settings_user_id_users_id_fk": { + "name": "user_settings_user_id_users_id_fk", + "tableFrom": "user_settings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_provider": { + "name": "auth_provider", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'local'" + }, + "oidc_subject": { + "name": "oidc_subject", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "last_login_at": { + "name": "last_login_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "users_username_unique": { + "name": "users_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/backend/drizzle/meta/_journal.json b/backend/drizzle/meta/_journal.json index f5dd8ad..b1f7176 100644 --- a/backend/drizzle/meta/_journal.json +++ b/backend/drizzle/meta/_journal.json @@ -71,6 +71,13 @@ "when": 1771164000000, "tag": "0009_add_medication_start_date", "breakpoints": true + }, + { + "idx": 10, + "version": "6", + "when": 1771694832866, + "tag": "0010_mean_spot", + "breakpoints": true } ] } \ No newline at end of file diff --git a/backend/src/db/db-utils.ts b/backend/src/db/db-utils.ts index e3da5d5..277de07 100644 --- a/backend/src/db/db-utils.ts +++ b/backend/src/db/db-utils.ts @@ -111,6 +111,8 @@ export async function runAlterMigrations(client: Client): Promise<{ success: boo `ALTER TABLE user_settings ADD COLUMN max_nagging_reminders integer NOT NULL DEFAULT 5`, // Added in v1.2.3 - dismiss missed doses without deducting stock `ALTER TABLE dose_tracking ADD COLUMN dismissed integer NOT NULL DEFAULT 0`, + // Added for intake automation auditability (manual vs automatic taken) + `ALTER TABLE dose_tracking ADD COLUMN taken_source text NOT NULL DEFAULT 'manual'`, // Added in v1.3.x - stock calculation mode (automatic/manual) `ALTER TABLE user_settings ADD COLUMN stock_calculation_mode text NOT NULL DEFAULT 'automatic'`, // Added for stock correction - hidden offset that doesn't affect looseTablets diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index 84db3a3..333b358 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -163,6 +163,7 @@ export const doseTracking = sqliteTable("dose_tracking", { doseId: text("dose_id", { length: 255 }).notNull(), // e.g. "med-5-1-86400000-1735200000000" takenAt: integer("taken_at", { mode: "timestamp" }).notNull().default(sql`(strftime('%s','now'))`), markedBy: text("marked_by", { length: 100 }), // null = user, "Daniel" = via share link + takenSource: text("taken_source", { length: 20 }).notNull().default("manual"), // manual or automatic dismissed: integer("dismissed", { mode: "boolean" }).notNull().default(false), // true = missed dose acknowledged without taking }); diff --git a/backend/src/routes/doses.ts b/backend/src/routes/doses.ts index 04ccb5e..cf6bd44 100644 --- a/backend/src/routes/doses.ts +++ b/backend/src/routes/doses.ts @@ -56,6 +56,7 @@ export async function doseRoutes(app: FastifyInstance) { doseId: d.doseId, takenAt: d.takenAt?.getTime() ?? Date.now(), markedBy: d.markedBy, + takenSource: d.takenSource ?? "manual", dismissed: d.dismissed ?? false, })), }; @@ -94,6 +95,7 @@ export async function doseRoutes(app: FastifyInstance) { userId, doseId, markedBy: null, // Marked by the user themselves + takenSource: "manual", }); return { success: true }; @@ -227,6 +229,7 @@ export async function doseRoutes(app: FastifyInstance) { doseId: d.doseId, takenAt: d.takenAt?.getTime() ?? Date.now(), markedBy: d.markedBy, + takenSource: d.takenSource ?? "manual", dismissed: d.dismissed ?? false, })), }; @@ -270,6 +273,7 @@ export async function doseRoutes(app: FastifyInstance) { userId: share.userId, doseId, markedBy: share.takenBy, // e.g. "Daniel" + takenSource: "manual", }); return { success: true }; diff --git a/backend/src/routes/export.ts b/backend/src/routes/export.ts index ecab9ab..d4903ef 100644 --- a/backend/src/routes/export.ts +++ b/backend/src/routes/export.ts @@ -72,6 +72,7 @@ const doseHistorySchema = z.object({ scheduledTime: z.string(), // ISO datetime takenAt: z.string(), // ISO datetime markedBy: z.string().nullable().optional(), + takenSource: z.enum(["manual", "automatic"]).default("manual"), dismissed: z.boolean().default(false), takenByPerson: z.string().nullable().optional(), // Person suffix from dose ID (e.g., "Daniel") }); @@ -364,6 +365,7 @@ export async function exportRoutes(app: FastifyInstance) { scheduledTime: scheduledTimeIso, takenAt: takenAtIso, markedBy: dose.markedBy, + takenSource: dose.takenSource === "automatic" ? "automatic" : "manual", dismissed: dose.dismissed ?? false, takenByPerson: parsed.person, }; @@ -625,6 +627,7 @@ export async function exportRoutes(app: FastifyInstance) { doseId, takenAt: new Date(dose.takenAt), markedBy: dose.markedBy || null, + takenSource: dose.takenSource ?? "manual", dismissed: dose.dismissed ?? false, }); } diff --git a/backend/src/routes/report.ts b/backend/src/routes/report.ts index 72f6adb..c40f251 100644 --- a/backend/src/routes/report.ts +++ b/backend/src/routes/report.ts @@ -51,17 +51,22 @@ export async function reportRoutes(app: FastifyInstance) { doseId: doseTracking.doseId, takenAt: doseTracking.takenAt, dismissed: doseTracking.dismissed, + takenSource: doseTracking.takenSource, }) .from(doseTracking) .where(eq(doseTracking.userId, userId)); // Group doses by medication ID - const dosesByMed = new Map(); + const dosesByMed = new Map(); for (const dose of allDoses) { const medId = Number.parseInt(dose.doseId.split("-")[0], 10); if (Number.isNaN(medId) || !medicationIds.includes(medId)) continue; if (!dosesByMed.has(medId)) dosesByMed.set(medId, []); - dosesByMed.get(medId)!.push({ takenAt: dose.takenAt, dismissed: dose.dismissed }); + dosesByMed.get(medId)!.push({ + takenAt: dose.takenAt, + dismissed: dose.dismissed, + takenSource: dose.takenSource ?? "manual", + }); } // Fetch refill history for requested medications @@ -69,6 +74,7 @@ export async function reportRoutes(app: FastifyInstance) { number, { dosesTaken: number; + automaticDosesTaken: number; dosesDismissed: number; firstDoseAt: string | null; lastDoseAt: string | null; @@ -79,6 +85,7 @@ export async function reportRoutes(app: FastifyInstance) { for (const medId of medicationIds) { const doses = dosesByMed.get(medId) ?? []; const takenDoses = doses.filter((d) => !d.dismissed); + const automaticTakenDoses = takenDoses.filter((d) => d.takenSource === "automatic"); const dismissedDoses = doses.filter((d) => d.dismissed); const sortedTaken = takenDoses.map((d) => d.takenAt.getTime()).sort((a, b) => a - b); @@ -88,6 +95,7 @@ export async function reportRoutes(app: FastifyInstance) { result[medId] = { dosesTaken: takenDoses.length, + automaticDosesTaken: automaticTakenDoses.length, dosesDismissed: dismissedDoses.length, firstDoseAt: sortedTaken.length > 0 ? new Date(sortedTaken[0]).toISOString() : null, lastDoseAt: sortedTaken.length > 0 ? new Date(sortedTaken[sortedTaken.length - 1]).toISOString() : null, diff --git a/backend/src/services/intake-reminder-scheduler.ts b/backend/src/services/intake-reminder-scheduler.ts index 5dc9fcb..7d8deaf 100644 --- a/backend/src/services/intake-reminder-scheduler.ts +++ b/backend/src/services/intake-reminder-scheduler.ts @@ -50,6 +50,113 @@ function saveIntakeReminderState(state: IntakeReminderState): void { writeFileSync(intakeReminderStateFile, JSON.stringify(state, null, 2)); } +function buildDoseIdForIntake(intake: UpcomingIntake & { medicationId: number; blisterIndex: number }): string { + const intakeDate = intake.intakeTime; + const dateOnlyMs = new Date(intakeDate.getFullYear(), intakeDate.getMonth(), intakeDate.getDate()).getTime(); + if (intake.takenBy) { + return `${intake.medicationId}-${intake.blisterIndex}-${dateOnlyMs}-${intake.takenBy}`; + } + return `${intake.medicationId}-${intake.blisterIndex}-${dateOnlyMs}`; +} + +async function autoMarkDueIntakesAsTaken( + settings: UserSettings & { userId: number }, + rows: (typeof medications.$inferSelect)[], + locale: string, + tz: string, + logger: ServiceLogger +): Promise { + if (settings.stockCalculationMode !== "automatic") { + return 0; + } + + const now = new Date(); + const nowInTimezone = new Date(now.toLocaleString("en-US", { timeZone: tz })); + const todayStart = new Date(now.toLocaleString("en-US", { timeZone: tz })); + todayStart.setHours(0, 0, 0, 0); + const todayEnd = new Date(now.toLocaleString("en-US", { timeZone: tz })); + todayEnd.setHours(23, 59, 59, 999); + + const existingToday = await db + .select({ doseId: doseTracking.doseId }) + .from(doseTracking) + .where( + and( + eq(doseTracking.userId, settings.userId), + gte(doseTracking.takenAt, todayStart), + lte(doseTracking.takenAt, todayEnd) + ) + ); + const existingDoseIds = new Set(existingToday.map((d) => d.doseId)); + + let inserted = 0; + + for (const med of rows) { + if (med.isObsolete) { + continue; + } + + const intakes = parseIntakesJson( + med.intakesJson, + { usageJson: med.usageJson, everyJson: med.everyJson, startJson: med.startJson }, + med.intakeRemindersEnabled ?? false + ); + if (intakes.length === 0) { + continue; + } + + const medicationTakenBy = parseTakenByJson(med.takenByJson); + const todaysIntakes = getTodaysIntakes( + med.name, + intakes, + medicationTakenBy, + med.pillWeightMg, + locale, + tz, + med.id, + med.doseUnit ?? "mg" + ); + + for (const intake of todaysIntakes) { + const intakeTimeInTimezone = new Date(intake.intakeTime.toLocaleString("en-US", { timeZone: tz })); + if (intakeTimeInTimezone.getTime() > nowInTimezone.getTime()) { + continue; + } + if (intake.medicationId === undefined || intake.blisterIndex === undefined) { + continue; + } + + const doseId = buildDoseIdForIntake({ + ...intake, + medicationId: intake.medicationId, + blisterIndex: intake.blisterIndex, + }); + + if (existingDoseIds.has(doseId)) { + continue; + } + + await db.insert(doseTracking).values({ + userId: settings.userId, + doseId, + takenAt: intake.intakeTime, + markedBy: null, + takenSource: "automatic", + dismissed: false, + }); + + existingDoseIds.add(doseId); + inserted++; + } + } + + if (inserted > 0) { + logger.info(`[IntakeReminder] User ${settings.userId}: Auto-marked ${inserted} due intake dose(s) as taken`); + } + + return inserted; +} + async function sendIntakeReminderEmail( email: string, intakes: UpcomingIntake[], @@ -246,6 +353,17 @@ async function checkAndSendIntakeRemindersForUser( `[IntakeReminder] Checking user ${settings.userId} - repeat:${settings.repeatRemindersEnabled} skip:${settings.skipRemindersForTakenDoses}` ); + const rows = await db + .select() + .from(medications) + .where(eq(medications.userId, settings.userId)) + .orderBy(medications.id); + + const locale = getDateLocale(language); + const tz = getTimezone(); + + await autoMarkDueIntakesAsTaken(settings, rows, locale, tz, logger); + // Check if any intake reminder notifications are enabled (granular check) const emailEnabled = settings.emailEnabled && settings.notificationEmail && settings.emailIntakeReminders; const shoutrrrEnabled = settings.shoutrrrEnabled && settings.shoutrrrUrl && settings.shoutrrrIntakeReminders; @@ -262,11 +380,6 @@ async function checkAndSendIntakeRemindersForUser( ); // Get all medications with intake reminders enabled for this user - const rows = await db - .select() - .from(medications) - .where(eq(medications.userId, settings.userId)) - .orderBy(medications.id); const medsWithReminders = rows.filter((row) => row.intakeRemindersEnabled); if (medsWithReminders.length === 0) { @@ -280,9 +393,6 @@ async function checkAndSendIntakeRemindersForUser( const state = loadIntakeReminderState(); const allUpcoming: (UpcomingIntake & { medicationId: number; blisterIndex: number })[] = []; - const locale = getDateLocale(language); - const tz = getTimezone(); - // Get start and end of today in user's timezone (for filtering today's doses only) const now = new Date(); const todayStart = new Date(now.toLocaleString("en-US", { timeZone: tz })); diff --git a/backend/src/test/e2e-routes.test.ts b/backend/src/test/e2e-routes.test.ts index 2498186..77d3504 100644 --- a/backend/src/test/e2e-routes.test.ts +++ b/backend/src/test/e2e-routes.test.ts @@ -171,6 +171,7 @@ async function createSchema(client: Client) { dose_id text NOT NULL, taken_at integer NOT NULL DEFAULT (strftime('%s','now')), marked_by text, + taken_source text NOT NULL DEFAULT 'manual', dismissed integer NOT NULL DEFAULT 0, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE )`, diff --git a/backend/src/test/integration.test.ts b/backend/src/test/integration.test.ts index f15ef7b..5cdde40 100644 --- a/backend/src/test/integration.test.ts +++ b/backend/src/test/integration.test.ts @@ -165,6 +165,7 @@ async function createSchema(client: Client) { dose_id text NOT NULL, taken_at integer NOT NULL DEFAULT (strftime('%s','now')), marked_by text, + taken_source text NOT NULL DEFAULT 'manual', dismissed integer NOT NULL DEFAULT 0, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE )`, diff --git a/frontend/src/components/ReportModal.tsx b/frontend/src/components/ReportModal.tsx index b71570a..7605ce2 100644 --- a/frontend/src/components/ReportModal.tsx +++ b/frontend/src/components/ReportModal.tsx @@ -16,6 +16,7 @@ type ReportData = Record< number, { dosesTaken: number; + automaticDosesTaken: number; dosesDismissed: number; firstDoseAt: string | null; lastDoseAt: string | null; @@ -382,6 +383,9 @@ function generateTextReport( lines.push(h3(t("report.docIntakeHistory"))); if (data.dosesTaken > 0 || data.dosesDismissed > 0) { lines.push(item(t("report.docDosesTaken"), String(data.dosesTaken))); + if (data.automaticDosesTaken > 0) { + lines.push(item(`🤖 ${t("report.docDosesTakenAutomatic")}`, String(data.automaticDosesTaken))); + } if (data.dosesDismissed > 0) lines.push(item(t("report.docDosesDismissed"), String(data.dosesDismissed))); if (data.firstDoseAt) lines.push(item(t("report.docFirstDose"), fmtDate(data.firstDoseAt))); if (data.lastDoseAt) lines.push(item(t("report.docLastDose"), fmtDate(data.lastDoseAt))); @@ -580,6 +584,9 @@ function buildPrintHtml( if (data.dosesTaken > 0 || data.dosesDismissed > 0) { s += ``; s += ``; + if (data.automaticDosesTaken > 0) { + s += ``; + } if (data.dosesDismissed > 0) s += ``; if (data.firstDoseAt) diff --git a/frontend/src/context/AppContext.tsx b/frontend/src/context/AppContext.tsx index 789e181..d917100 100644 --- a/frontend/src/context/AppContext.tsx +++ b/frontend/src/context/AppContext.tsx @@ -3,7 +3,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useState } import { useTranslation } from "react-i18next"; import { useAuth } from "../components/Auth"; import { useCollapsedDays, useDoses, useMedications, useRefill, useSettings, useShare } from "../hooks"; -import type { Coverage, Medication, ScheduleEvent, StockThresholds } from "../types"; +import type { Coverage, FormState, Medication, ScheduleEvent, StockThresholds } from "../types"; import { getSystemLocale } from "../utils/formatters"; import { log } from "../utils/logger"; import { buildSchedulePreview, calculateCoverage, computeMissedPastDoseIds } from "../utils/schedule"; @@ -72,6 +72,7 @@ export interface AppContextValue { showClearMissedConfirm: boolean; setShowClearMissedConfirm: (show: boolean) => void; getDoseId: (baseDoseId: string, person: string | null) => string; + isDoseTakenAutomatically: (doseId: string) => boolean; countTakenDoses: (doses: Array<{ id: string; takenBy: string[] }>) => { total: number; taken: number }; markDoseTaken: (doseId: string) => Promise; undoDoseTaken: (doseId: string) => Promise; @@ -127,7 +128,7 @@ export interface AppContextValue { submitRefill: ( medId: number, editingId: number | null, - setForm: React.Dispatch>, + setForm: React.Dispatch>, loadMeds: () => void, usePrescription?: boolean ) => Promise; @@ -742,6 +743,7 @@ export function AppProvider({ children }: { children: React.ReactNode }) { showClearMissedConfirm: doses.showClearMissedConfirm, setShowClearMissedConfirm: doses.setShowClearMissedConfirm, getDoseId: doses.getDoseId, + isDoseTakenAutomatically: doses.isDoseTakenAutomatically, countTakenDoses: doses.countTakenDoses, markDoseTaken: doses.markDoseTaken, undoDoseTaken: doses.undoDoseTaken, diff --git a/frontend/src/hooks/useDoses.ts b/frontend/src/hooks/useDoses.ts index 94d7914..718164d 100644 --- a/frontend/src/hooks/useDoses.ts +++ b/frontend/src/hooks/useDoses.ts @@ -8,10 +8,12 @@ export interface UseDosesReturn { takenDoses: Set; setTakenDoses: React.Dispatch>>; takenDoseTimestamps: Map; + takenDoseSources: Map; dismissedDoses: Set; showClearMissedConfirm: boolean; setShowClearMissedConfirm: (show: boolean) => void; getDoseId: (baseDoseId: string, person: string | null) => string; + isDoseTakenAutomatically: (doseId: string) => boolean; countTakenDoses: (doses: Array<{ id: string; takenBy: string[] }>) => { total: number; taken: number }; markDoseTaken: (doseId: string) => Promise; undoDoseTaken: (doseId: string) => Promise; @@ -21,6 +23,7 @@ export interface UseDosesReturn { export function useDoses(): UseDosesReturn { const [takenDoses, setTakenDoses] = useState>(new Set()); const [takenDoseTimestamps, setTakenDoseTimestamps] = useState>(new Map()); + const [takenDoseSources, setTakenDoseSources] = useState>(new Map()); const [dismissedDoses, setDismissedDoses] = useState>(new Set()); const [showClearMissedConfirm, setShowClearMissedConfirm] = useState(false); @@ -42,6 +45,7 @@ export function useDoses(): UseDosesReturn { const data = await res.json(); const taken = new Set(); const timestamps = new Map(); + const sources = new Map(); const dismissed = new Set(); for (const d of data.doses) { if (d.dismissed) { @@ -49,10 +53,12 @@ export function useDoses(): UseDosesReturn { } else { taken.add(d.doseId); timestamps.set(d.doseId, d.takenAt); + sources.set(d.doseId, d.takenSource === "automatic" ? "automatic" : "manual"); } } setTakenDoses(taken); setTakenDoseTimestamps(timestamps); + setTakenDoseSources(sources); setDismissedDoses(dismissed); } // Don't reset on error - keep current state @@ -75,6 +81,13 @@ export function useDoses(): UseDosesReturn { return person ? `${baseDoseId}-${person}` : baseDoseId; }, []); + const isDoseTakenAutomatically = useCallback( + (doseId: string): boolean => { + return takenDoseSources.get(doseId) === "automatic"; + }, + [takenDoseSources] + ); + // Count taken doses for a day/item const countTakenDoses = useCallback( (doses: Array<{ id: string; takenBy: string[] }>): { total: number; taken: number } => { @@ -106,6 +119,11 @@ export function useDoses(): UseDosesReturn { next.set(doseId, Date.now()); return next; }); + setTakenDoseSources((prev) => { + const next = new Map(prev); + next.set(doseId, "manual"); + return next; + }); // Send to server try { @@ -127,6 +145,11 @@ export function useDoses(): UseDosesReturn { next.delete(doseId); return next; }); + setTakenDoseSources((prev) => { + const next = new Map(prev); + next.delete(doseId); + return next; + }); } finally { mutationInFlightRef.current--; // Re-sync with server after mutation completes @@ -150,6 +173,11 @@ export function useDoses(): UseDosesReturn { next.delete(doseId); return next; }); + setTakenDoseSources((prev) => { + const next = new Map(prev); + next.delete(doseId); + return next; + }); // Send to server try { @@ -177,10 +205,12 @@ export function useDoses(): UseDosesReturn { takenDoses, setTakenDoses, takenDoseTimestamps, + takenDoseSources, dismissedDoses, showClearMissedConfirm, setShowClearMissedConfirm, getDoseId, + isDoseTakenAutomatically, countTakenDoses, markDoseTaken, undoDoseTaken, diff --git a/frontend/src/i18n/de.json b/frontend/src/i18n/de.json index 9df1c9e..ece0b4c 100644 --- a/frontend/src/i18n/de.json +++ b/frontend/src/i18n/de.json @@ -351,6 +351,7 @@ }, "tooltips": { "intakeReminders": "Einnahme-Erinnerungen aktiviert", + "automaticTaken": "Automatisch eingenommen", "hasNotes": "Hat Notizen", "stockExceedsCapacity": "Bestand überschreitet Packungskapazität — Packungsanzahl anpassen", "lightMode": "Zum hellen Modus wechseln", @@ -648,6 +649,7 @@ "docPrescriptionExpiry": "Rezeptablauf", "docIntakeHistory": "Einnahme-Verlauf", "docDosesTaken": "Eingenommene Dosen", + "docDosesTakenAutomatic": "Automatisch eingenommen", "docDosesDismissed": "Verworfene Dosen", "docFirstDose": "Erste Dosis", "docLastDose": "Letzte Dosis", diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index e26cbaa..b75b527 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -351,6 +351,7 @@ }, "tooltips": { "intakeReminders": "Intake reminders enabled", + "automaticTaken": "Automatically taken", "hasNotes": "Has notes", "stockExceedsCapacity": "Stock exceeds package capacity — consider updating pack count", "lightMode": "Switch to light mode", @@ -648,6 +649,7 @@ "docPrescriptionExpiry": "Prescription expiry", "docIntakeHistory": "Intake History", "docDosesTaken": "Doses taken", + "docDosesTakenAutomatic": "Automatically taken", "docDosesDismissed": "Doses dismissed", "docFirstDose": "First dose", "docLastDose": "Last dose", diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index ffa83e7..77003ac 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -64,6 +64,7 @@ export function DashboardPage() { missedPastDoseIds, getDayStockStatus, getDoseId, + isDoseTakenAutomatically, showClearMissedConfirm, setShowClearMissedConfirm, clearingMissed, @@ -767,6 +768,8 @@ export function DashboardPage() { {people.map((person) => { const doseId = getDoseId(dose.id, person); const isTaken = takenDoses.has(doseId); + const isAutomaticallyTaken = + isTaken && isDoseTakenAutomatically(doseId) && dose.when <= Date.now(); return (
{person && ( @@ -786,6 +789,14 @@ export function DashboardPage() { onClick={() => undoDoseTaken(doseId)} title={t("common.undo")} > + {isAutomaticallyTaken && ( + + 🤖 + + )} ↩ ) : ( @@ -1013,6 +1024,8 @@ export function DashboardPage() { {people.map((person) => { const doseId = getDoseId(dose.id, person); const isTaken = takenDoses.has(doseId); + const isAutomaticallyTaken = + isTaken && isDoseTakenAutomatically(doseId) && dose.when <= Date.now(); return (
{person && ( @@ -1032,6 +1045,14 @@ export function DashboardPage() { onClick={() => undoDoseTaken(doseId)} title={t("common.undo")} > + {isAutomaticallyTaken && ( + + 🤖 + + )} ↩ ) : ( @@ -1222,6 +1243,8 @@ export function DashboardPage() { {people.map((person) => { const doseId = getDoseId(dose.id, person); const isTaken = takenDoses.has(doseId); + const isAutomaticallyTaken = + isTaken && isDoseTakenAutomatically(doseId) && dose.when <= Date.now(); return (
{person && ( @@ -1241,6 +1264,14 @@ export function DashboardPage() { onClick={() => undoDoseTaken(doseId)} title={t("common.undo")} > + {isAutomaticallyTaken && ( + + 🤖 + + )} ↩ ) : ( diff --git a/frontend/src/pages/SchedulePage.tsx b/frontend/src/pages/SchedulePage.tsx index 8e5fc4e..93d6cd8 100644 --- a/frontend/src/pages/SchedulePage.tsx +++ b/frontend/src/pages/SchedulePage.tsx @@ -66,6 +66,7 @@ export function SchedulePage() { pastDays, futureDays, takenDoses, + isDoseTakenAutomatically, dismissedDoses, markDoseTaken, undoDoseTaken, @@ -212,6 +213,8 @@ export function SchedulePage() { {people.map((person) => { const doseId = getDoseId(dose.id, person); const isTaken = takenDoses.has(doseId); + const isAutomaticallyTaken = + isTaken && isDoseTakenAutomatically(doseId) && dose.when <= Date.now(); return (
{person && ( @@ -231,6 +234,14 @@ export function SchedulePage() { onClick={() => undoDoseTaken(doseId)} title={t("common.undo")} > + {isAutomaticallyTaken && ( + + 🤖 + + )} ↩ ) : ( @@ -373,6 +384,8 @@ export function SchedulePage() { {people.map((person) => { const doseId = getDoseId(dose.id, person); const isTaken = takenDoses.has(doseId); + const isAutomaticallyTaken = + isTaken && isDoseTakenAutomatically(doseId) && dose.when <= now; const isOverdue = !isTaken && dose.when < now && !isPastDay; return (
undoDoseTaken(doseId)} title={t("common.undo")} > + {isAutomaticallyTaken && ( + + 🤖 + + )} ↩ ) : ( diff --git a/frontend/src/test/pages/DashboardPage.test.tsx b/frontend/src/test/pages/DashboardPage.test.tsx index 24b788f..43efb5f 100644 --- a/frontend/src/test/pages/DashboardPage.test.tsx +++ b/frontend/src/test/pages/DashboardPage.test.tsx @@ -181,6 +181,7 @@ const createMockAppContext = (overrides = {}) => ({ missedPastDoseIds: [], getDayStockStatus: vi.fn(() => "success"), getDoseId: vi.fn((id, person) => (person ? `${id}-${person}` : id)), + isDoseTakenAutomatically: vi.fn(() => false), showClearMissedConfirm: false, setShowClearMissedConfirm: vi.fn(), clearingMissed: false, diff --git a/frontend/src/test/pages/SchedulePage.test.tsx b/frontend/src/test/pages/SchedulePage.test.tsx index 5bd3d37..b600a0c 100644 --- a/frontend/src/test/pages/SchedulePage.test.tsx +++ b/frontend/src/test/pages/SchedulePage.test.tsx @@ -111,6 +111,7 @@ const createMockContext = (overrides = {}) => ({ manuallyExpandedDays: new Set(), toggleDayCollapse: vi.fn(), openUserFilter: vi.fn(), + isDoseTakenAutomatically: vi.fn(() => false), missedPastDoseIds: [], ...overrides, });
${escHtml(t("report.docDosesTaken"))}${data.dosesTaken}
${escHtml(`🤖 ${t("report.docDosesTakenAutomatic")}`)}${data.automaticDosesTaken}
${escHtml(t("report.docDosesDismissed"))}${data.dosesDismissed}