From e0fb77d494a51a1b9a9a5aeb4e879a8d35f6e1b4 Mon Sep 17 00:00:00 2001 From: Daniel Volz Date: Sat, 14 Mar 2026 20:26:17 +0100 Subject: [PATCH] feat: embed medication overview into shared links Closes #424 --- README.md | 1 + .../0013_add_share_medication_overview.sql | 1 + backend/drizzle/meta/0013_snapshot.json | 1228 ++++++++++++++++ backend/drizzle/meta/_journal.json | 7 + backend/src/db/db-utils.ts | 2 + backend/src/db/schema.ts | 2 + backend/src/routes/export.ts | 3 + backend/src/routes/settings.ts | 9 + backend/src/routes/share.ts | 18 +- backend/src/services/coverage.ts | 33 +- backend/src/test/e2e-routes.test.ts | 1 + backend/src/test/integration.test.ts | 1 + backend/src/test/planner.test.ts | 1 + backend/src/test/services.test.ts | 11 + backend/src/test/setup.ts | 55 +- backend/src/utils/scheduler-utils.ts | 1 + frontend/src/components/ShareDialog.tsx | 66 +- .../SharedMedicationOverviewSection.tsx | 194 +++ frontend/src/components/SharedSchedule.tsx | 1257 ++++++++--------- frontend/src/components/index.ts | 1 + frontend/src/hooks/useSettings.ts | 178 ++- frontend/src/hooks/useShare.ts | 20 +- frontend/src/i18n/de.json | 8 +- frontend/src/i18n/en.json | 8 +- frontend/src/pages/SettingsPage.tsx | 13 +- frontend/src/pages/SharedOverviewPage.tsx | 353 +---- frontend/src/styles/schedule-mobile-edit.css | 109 +- .../src/test/components/ShareDialog.test.tsx | 20 +- .../test/components/SharedSchedule.test.tsx | 118 ++ .../SharedScheduleTodayOnly.test.tsx | 3 +- frontend/src/test/hooks/useSettings.test.ts | 50 +- frontend/src/test/hooks/useShare.test.ts | 2 +- frontend/src/test/pages/SettingsPage.test.tsx | 26 +- .../test/pages/SharedOverviewPage.test.tsx | 100 +- frontend/src/types/index.ts | 4 +- 35 files changed, 2607 insertions(+), 1297 deletions(-) create mode 100644 backend/drizzle/0013_add_share_medication_overview.sql create mode 100644 backend/drizzle/meta/0013_snapshot.json create mode 100644 frontend/src/components/SharedMedicationOverviewSection.tsx diff --git a/README.md b/README.md index 64f7005..d52f28d 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,7 @@ Share your medication schedule with others via a public link. ### Multi-Person Support - Manage medications for multiple people - Share schedules via link. Recipients can mark doses as taken, you see it live +- Optionally embed the medication overview directly on shared links via a settings toggle ### Data Export & Import - Export all your data (medications, dose history, settings) as JSON diff --git a/backend/drizzle/0013_add_share_medication_overview.sql b/backend/drizzle/0013_add_share_medication_overview.sql new file mode 100644 index 0000000..e474265 --- /dev/null +++ b/backend/drizzle/0013_add_share_medication_overview.sql @@ -0,0 +1 @@ +ALTER TABLE `user_settings` ADD `share_medication_overview` integer DEFAULT false NOT NULL; \ No newline at end of file diff --git a/backend/drizzle/meta/0013_snapshot.json b/backend/drizzle/meta/0013_snapshot.json new file mode 100644 index 0000000..4e89724 --- /dev/null +++ b/backend/drizzle/meta/0013_snapshot.json @@ -0,0 +1,1228 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "be57622d-1bd6-425e-90ba-41952f2f15d6", + "prevId": "66cf7f2e-59bf-41dd-ad1b-4fcd81519019", + "tables": { + "api_keys": { + "name": "api_keys", + "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 + }, + "key_hash": { + "name": "key_hash", + "type": "text(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text(24)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "scope": { + "name": "scope", + "type": "text(10)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'write'" + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_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": { + "api_keys_key_hash_unique": { + "name": "api_keys_key_hash_unique", + "columns": [ + "key_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "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'" + }, + "medication_form": { + "name": "medication_form", + "type": "text(20)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'tablet'" + }, + "pill_form": { + "name": "pill_form", + "type": "text(20)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lifecycle_category": { + "name": "lifecycle_category", + "type": "text(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'refill_when_empty'" + }, + "package_amount_value": { + "name": "package_amount_value", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "package_amount_unit": { + "name": "package_amount_unit", + "type": "text(10)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ml'" + }, + "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": "''" + }, + "medication_end_date": { + "name": "medication_end_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_mark_obsolete_after_end_date": { + "name": "auto_mark_obsolete_after_end_date", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "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 + }, + "share_medication_overview": { + "name": "share_medication_overview", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "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 8de4b25..9b2cefe 100644 --- a/backend/drizzle/meta/_journal.json +++ b/backend/drizzle/meta/_journal.json @@ -92,6 +92,13 @@ "when": 1772881208026, "tag": "0012_add_api_keys_and_package_amount_columns", "breakpoints": true + }, + { + "idx": 13, + "version": "6", + "when": 1773348659979, + "tag": "0013_add_share_medication_overview", + "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 65e5484..7488341 100644 --- a/backend/src/db/db-utils.ts +++ b/backend/src/db/db-utils.ts @@ -149,6 +149,8 @@ export async function runAlterMigrations(client: Client): Promise<{ success: boo `ALTER TABLE user_settings ADD COLUMN last_stock_reminder_med_names text`, // Added for share stock visibility toggle `ALTER TABLE user_settings ADD COLUMN share_stock_status integer NOT NULL DEFAULT 1`, + // Added for integrated share overview visibility on shared links + `ALTER TABLE user_settings ADD COLUMN share_medication_overview integer NOT NULL DEFAULT 0`, // Added for timeline visibility toggles (dashboard + shared schedule) `ALTER TABLE user_settings ADD COLUMN upcoming_today_only integer NOT NULL DEFAULT 0`, `ALTER TABLE user_settings ADD COLUMN share_schedule_today_only integer NOT NULL DEFAULT 0`, diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index 4b0337b..f4310aa 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -109,6 +109,8 @@ export const userSettings = sqliteTable("user_settings", { stockCalculationMode: text("stock_calculation_mode", { length: 20 }).notNull().default("automatic"), // Whether shared schedule links show stock status (Critical/Low/Normal) to intake users shareStockStatus: integer("share_stock_status", { mode: "boolean" }).notNull().default(true), + // Whether shared schedule links also embed the medication overview section + shareMedicationOverview: integer("share_medication_overview", { mode: "boolean" }).notNull().default(false), // UI timeline visibility preferences upcomingTodayOnly: integer("upcoming_today_only", { mode: "boolean" }).notNull().default(false), shareScheduleTodayOnly: integer("share_schedule_today_only", { mode: "boolean" }).notNull().default(false), diff --git a/backend/src/routes/export.ts b/backend/src/routes/export.ts index 7a7b4ec..02470dc 100644 --- a/backend/src/routes/export.ts +++ b/backend/src/routes/export.ts @@ -136,6 +136,7 @@ const settingsExportSchema = z language: z.string().default("en"), stockCalculationMode: z.enum(["automatic", "manual"]).default("automatic"), shareStockStatus: z.boolean().default(true), + shareMedicationOverview: z.boolean().default(false), }) .optional(); @@ -503,6 +504,7 @@ export async function exportRoutes(app: FastifyInstance) { language: settings.language, stockCalculationMode: settings.stockCalculationMode, shareStockStatus: settings.shareStockStatus, + shareMedicationOverview: settings.shareMedicationOverview ?? false, } : undefined; @@ -793,6 +795,7 @@ export async function exportRoutes(app: FastifyInstance) { language: importData.settings.language ?? "en", stockCalculationMode: importData.settings.stockCalculationMode ?? "automatic", shareStockStatus: importData.settings.shareStockStatus ?? true, + shareMedicationOverview: importData.settings.shareMedicationOverview ?? false, }); } diff --git a/backend/src/routes/settings.ts b/backend/src/routes/settings.ts index a1ebd24..42617e8 100644 --- a/backend/src/routes/settings.ts +++ b/backend/src/routes/settings.ts @@ -33,6 +33,7 @@ export type UserSettings = { language: Language; stockCalculationMode: "automatic" | "manual"; shareStockStatus: boolean; + shareMedicationOverview: boolean; upcomingTodayOnly: boolean; shareScheduleTodayOnly: boolean; swapDashboardMainSections: boolean; @@ -72,6 +73,7 @@ type SettingsBody = { language: string; stockCalculationMode: "automatic" | "manual"; shareStockStatus: boolean; + shareMedicationOverview: boolean; upcomingTodayOnly: boolean; shareScheduleTodayOnly: boolean; swapDashboardMainSections: boolean; @@ -221,6 +223,7 @@ function getDefaultSettings() { language: (process.env.DEFAULT_LANGUAGE as "en" | "de") || "en", stockCalculationMode: (process.env.DEFAULT_STOCK_CALCULATION_MODE as "automatic" | "manual") || "automatic", shareStockStatus: envBool("DEFAULT_SHARE_STOCK_STATUS", true), + shareMedicationOverview: envBool("DEFAULT_SHARE_MEDICATION_OVERVIEW", false), upcomingTodayOnly: envBool("DEFAULT_UPCOMING_TODAY_ONLY", false), shareScheduleTodayOnly: envBool("DEFAULT_SHARE_SCHEDULE_TODAY_ONLY", false), swapDashboardMainSections: false, @@ -283,6 +286,7 @@ export async function loadUserSettings(userId: number): Promise { language: settings.language as Language, stockCalculationMode: (settings.stockCalculationMode as "automatic" | "manual") ?? "automatic", shareStockStatus: settings.shareStockStatus ?? true, + shareMedicationOverview: settings.shareMedicationOverview ?? false, upcomingTodayOnly: settings.upcomingTodayOnly ?? false, shareScheduleTodayOnly: settings.shareScheduleTodayOnly ?? false, swapDashboardMainSections: settings.swapDashboardMainSections ?? false, @@ -327,6 +331,7 @@ export async function getAllUserSettings(): Promise { language: settings.language as Language, stockCalculationMode: (settings.stockCalculationMode as "automatic" | "manual") ?? "automatic", shareStockStatus: settings.shareStockStatus ?? true, + shareMedicationOverview: settings.shareMedicationOverview ?? false, upcomingTodayOnly: settings.upcomingTodayOnly ?? false, shareScheduleTodayOnly: settings.shareScheduleTodayOnly ?? false, swapDashboardMainSections: settings.swapDashboardMainSections ?? false, @@ -411,6 +416,7 @@ export async function settingsRoutes(app: FastifyInstance) { language: settings.language, stockCalculationMode: settings.stockCalculationMode ?? "automatic", shareStockStatus: settings.shareStockStatus ?? true, + shareMedicationOverview: settings.shareMedicationOverview ?? false, upcomingTodayOnly: settings.upcomingTodayOnly ?? false, shareScheduleTodayOnly: settings.shareScheduleTodayOnly ?? false, swapDashboardMainSections: settings.swapDashboardMainSections ?? false, @@ -477,6 +483,7 @@ export async function settingsRoutes(app: FastifyInstance) { language: { type: "string", enum: ["en", "de"] }, stockCalculationMode: { type: "string", enum: ["automatic", "manual"] }, shareStockStatus: { type: "boolean" }, + shareMedicationOverview: { type: "boolean" }, upcomingTodayOnly: { type: "boolean" }, shareScheduleTodayOnly: { type: "boolean" }, swapDashboardMainSections: { type: "boolean" }, @@ -504,6 +511,7 @@ export async function settingsRoutes(app: FastifyInstance) { language: "en", stockCalculationMode: "automatic", shareStockStatus: true, + shareMedicationOverview: false, upcomingTodayOnly: false, shareScheduleTodayOnly: false, swapDashboardMainSections: false, @@ -554,6 +562,7 @@ export async function settingsRoutes(app: FastifyInstance) { language: body.language ?? "en", stockCalculationMode: body.stockCalculationMode ?? "automatic", shareStockStatus: body.shareStockStatus ?? true, + shareMedicationOverview: body.shareMedicationOverview ?? false, upcomingTodayOnly: body.upcomingTodayOnly ?? false, shareScheduleTodayOnly: body.shareScheduleTodayOnly ?? false, swapDashboardMainSections: body.swapDashboardMainSections ?? false, diff --git a/backend/src/routes/share.ts b/backend/src/routes/share.ts index c7c21a1..60b45e3 100644 --- a/backend/src/routes/share.ts +++ b/backend/src/routes/share.ts @@ -56,6 +56,10 @@ const shareReadResponseSchema = { sharedBy: { type: "string" }, scheduleDays: { type: "integer" }, medications: { type: "array", items: { type: "object", additionalProperties: true } }, + shareMedicationOverview: { type: "boolean" }, + medicationOverview: { + anyOf: [{ type: "array", items: { type: "object", additionalProperties: true } }, { type: "null" }], + }, stockThresholds: { type: "object", additionalProperties: { type: "number" } }, stockCalculationMode: { type: "string", enum: ["automatic", "manual"] }, shareStockStatus: { type: "boolean" }, @@ -241,11 +245,23 @@ export async function shareRoutes(app: FastifyInstance) { }; }); + const shareMedicationOverview = settings?.shareMedicationOverview ?? false; + const medicationOverview = shareMedicationOverview + ? buildSharedMedicationOverview({ + medications: meds, + doses: await db.select().from(doseTracking).where(eq(doseTracking.userId, share.userId)), + thresholdDays: settings?.lowStockDays ?? 30, + showStockStatus: settings?.shareStockStatus ?? true, + }) + : null; + return { takenBy: share.takenBy, sharedBy: owner?.username ?? null, scheduleDays: share.scheduleDays, medications: medicationsWithBlisters, + shareMedicationOverview, + medicationOverview, stockThresholds: { lowStockDays: settings?.lowStockDays ?? 30, normalStockDays: settings?.normalStockDays ?? 60, @@ -328,7 +344,7 @@ export async function shareRoutes(app: FastifyInstance) { medications: meds, doses, thresholdDays: settings?.lowStockDays ?? 30, - shareStockStatus: settings?.shareStockStatus ?? true, + showStockStatus: settings?.shareStockStatus ?? true, }); return { diff --git a/backend/src/services/coverage.ts b/backend/src/services/coverage.ts index 660cf31..d68dfb9 100644 --- a/backend/src/services/coverage.ts +++ b/backend/src/services/coverage.ts @@ -29,7 +29,7 @@ export type SharedMedicationOverviewItem = { daysLeft: number | null; nextIntakeDate: string | null; depletionDate: string | null; - priority: "normal" | "high" | null; + priority: "normal" | "high" | "out-of-stock" | null; expiryDate: string | null; medicationStartDate: string | null; prescriptionEnabled: boolean; @@ -135,13 +135,23 @@ function toNullableDate(value: string | null): string | null { return value.trim() ? value : null; } +function computeOverviewPriority( + currentStock: number, + daysLeft: number | null, + thresholdDays: number +): "normal" | "high" | "out-of-stock" { + if (currentStock <= 0 || daysLeft === 0) return "out-of-stock"; + if (daysLeft !== null && daysLeft <= thresholdDays) return "high"; + return "normal"; +} + export function buildSharedMedicationOverview(options: { medications: MedicationRow[]; doses: DoseRow[]; thresholdDays: number; - shareStockStatus: boolean; + showStockStatus?: boolean; }): SharedMedicationOverviewItem[] { - const { medications: medicationRows, doses, thresholdDays, shareStockStatus } = options; + const { medications: medicationRows, doses, thresholdDays, showStockStatus = true } = options; const dosesByMedication = new Map(); for (const dose of doses) { @@ -178,7 +188,12 @@ export function buildSharedMedicationOverview(options: { const daysLeft = dailyDoseRate > 0 ? Math.floor(currentStock / dailyDoseRate) : null; const depletionDate = daysLeft === null ? null : toDateOnlyString(new Date(todayDate.getTime() + daysLeft * MS_PER_DAY)); - const priority: "normal" | "high" = daysLeft !== null && daysLeft <= thresholdDays ? "high" : "normal"; + const priority = computeOverviewPriority(currentStock, daysLeft, thresholdDays); + const visibleCurrentStock = showStockStatus ? currentStock : null; + const visibleCapacity = showStockStatus ? capacity : null; + const visibleDaysLeft = showStockStatus ? daysLeft : null; + const visibleDepletionDate = showStockStatus ? depletionDate : null; + const visiblePriority = showStockStatus ? priority : null; return { name: medication.name, @@ -190,12 +205,12 @@ export function buildSharedMedicationOverview(options: { pillsPerBlister: medication.pillsPerBlister, totalPills: medication.totalPills, looseTablets: medication.looseTablets, - currentStock: shareStockStatus ? currentStock : null, - capacity: shareStockStatus ? capacity : null, - daysLeft: shareStockStatus ? daysLeft : null, + currentStock: visibleCurrentStock, + capacity: visibleCapacity, + daysLeft: visibleDaysLeft, nextIntakeDate: computeNextIntakeDate(intakes, todayDateOnly), - depletionDate: shareStockStatus ? depletionDate : null, - priority: shareStockStatus ? priority : null, + depletionDate: visibleDepletionDate, + priority: visiblePriority, expiryDate: toNullableDate(medication.expiryDate), medicationStartDate: toNullableDate(medication.medicationStartDate), prescriptionEnabled: medication.prescriptionEnabled ?? false, diff --git a/backend/src/test/e2e-routes.test.ts b/backend/src/test/e2e-routes.test.ts index d750607..6c034b9 100644 --- a/backend/src/test/e2e-routes.test.ts +++ b/backend/src/test/e2e-routes.test.ts @@ -146,6 +146,7 @@ async function createSchema(client: Client) { language text NOT NULL DEFAULT 'en', stock_calculation_mode text NOT NULL DEFAULT 'automatic', share_stock_status integer NOT NULL DEFAULT 1, + share_medication_overview integer NOT NULL DEFAULT 0, upcoming_today_only integer NOT NULL DEFAULT 0, share_schedule_today_only integer NOT NULL DEFAULT 0, swap_dashboard_main_sections integer NOT NULL DEFAULT 0, diff --git a/backend/src/test/integration.test.ts b/backend/src/test/integration.test.ts index fae8546..a066d38 100644 --- a/backend/src/test/integration.test.ts +++ b/backend/src/test/integration.test.ts @@ -140,6 +140,7 @@ async function createSchema(client: Client) { language text NOT NULL DEFAULT 'en', stock_calculation_mode text NOT NULL DEFAULT 'automatic', share_stock_status integer NOT NULL DEFAULT 1, + share_medication_overview integer NOT NULL DEFAULT 0, upcoming_today_only integer NOT NULL DEFAULT 0, share_schedule_today_only integer NOT NULL DEFAULT 0, swap_dashboard_main_sections integer NOT NULL DEFAULT 0, diff --git a/backend/src/test/planner.test.ts b/backend/src/test/planner.test.ts index 9262ecc..9282e73 100644 --- a/backend/src/test/planner.test.ts +++ b/backend/src/test/planner.test.ts @@ -157,6 +157,7 @@ async function createSchema(client: Client) { language text NOT NULL DEFAULT 'en', stock_calculation_mode text NOT NULL DEFAULT 'automatic', share_stock_status integer NOT NULL DEFAULT 1, + share_medication_overview integer NOT NULL DEFAULT 0, upcoming_today_only integer NOT NULL DEFAULT 0, share_schedule_today_only integer NOT NULL DEFAULT 0, swap_dashboard_main_sections integer NOT NULL DEFAULT 0, diff --git a/backend/src/test/services.test.ts b/backend/src/test/services.test.ts index cbe11c1..19f3186 100644 --- a/backend/src/test/services.test.ts +++ b/backend/src/test/services.test.ts @@ -21,6 +21,7 @@ import { parseIntakeReminderState, parseReminderState, parseTakenByJson, + personTakesMedication, } from "../utils/scheduler-utils.js"; // Helper to convert Blister to Intake for tests @@ -151,6 +152,16 @@ describe("Scheduler Utils - Timezone Functions", () => { }); }); +describe("Scheduler Utils - Sharing", () => { + it("treats the all-share sentinel as matching intake-specific assignees", () => { + const intakes = [blisterToIntake({ usage: 1, every: 1, start: "2025-01-01T08:00:00.000Z" }, "Max")]; + + expect(personTakesMedication("all", [], intakes)).toBe(true); + expect(personTakesMedication("Max", [], intakes)).toBe(true); + expect(personTakesMedication("Anna", [], intakes)).toBe(false); + }); +}); + describe("Scheduler Utils - Blister Parsing", () => { describe("parseBlisters", () => { it("should parse valid blister JSON arrays", () => { diff --git a/backend/src/test/setup.ts b/backend/src/test/setup.ts index ba996de..d38e205 100644 --- a/backend/src/test/setup.ts +++ b/backend/src/test/setup.ts @@ -218,13 +218,20 @@ export interface UpdateUserSettingsOptions { stockCalculationMode?: "automatic" | "manual"; lowStockDays?: number; shareStockStatus?: boolean; + shareMedicationOverview?: boolean; } /** * Create or update user settings */ export async function setUserSettings(client: Client, options: UpdateUserSettingsOptions): Promise { - const { userId, stockCalculationMode = "automatic", lowStockDays = 30, shareStockStatus } = options; + const { + userId, + stockCalculationMode = "automatic", + lowStockDays = 30, + shareStockStatus, + shareMedicationOverview, + } = options; // Check if settings exist const existing = await client.execute({ @@ -233,20 +240,46 @@ export async function setUserSettings(client: Client, options: UpdateUserSetting }); if (existing.rows.length > 0) { + const updateArgs = [stockCalculationMode, lowStockDays] as Array; + let updateSql = "UPDATE user_settings SET stock_calculation_mode = ?, low_stock_days = ?"; + + if (shareStockStatus !== undefined) { + updateSql += ", share_stock_status = ?"; + updateArgs.push(shareStockStatus ? 1 : 0); + } + + if (shareMedicationOverview !== undefined) { + updateSql += ", share_medication_overview = ?"; + updateArgs.push(shareMedicationOverview ? 1 : 0); + } + + updateSql += " WHERE user_id = ?"; + updateArgs.push(userId); + await client.execute({ - sql: `UPDATE user_settings SET stock_calculation_mode = ?, low_stock_days = ?${shareStockStatus !== undefined ? ", share_stock_status = ?" : ""} WHERE user_id = ?`, - args: - shareStockStatus !== undefined - ? [stockCalculationMode, lowStockDays, shareStockStatus ? 1 : 0, userId] - : [stockCalculationMode, lowStockDays, userId], + sql: updateSql, + args: updateArgs, }); } else { + const insertColumns = ["user_id", "stock_calculation_mode", "low_stock_days"]; + const insertPlaceholders = ["?", "?", "?"]; + const insertArgs = [userId, stockCalculationMode, lowStockDays] as Array; + + if (shareStockStatus !== undefined) { + insertColumns.push("share_stock_status"); + insertPlaceholders.push("?"); + insertArgs.push(shareStockStatus ? 1 : 0); + } + + if (shareMedicationOverview !== undefined) { + insertColumns.push("share_medication_overview"); + insertPlaceholders.push("?"); + insertArgs.push(shareMedicationOverview ? 1 : 0); + } + await client.execute({ - sql: `INSERT INTO user_settings (user_id, stock_calculation_mode, low_stock_days${shareStockStatus !== undefined ? ", share_stock_status" : ""}) VALUES (?, ?, ?${shareStockStatus !== undefined ? ", ?" : ""})`, - args: - shareStockStatus !== undefined - ? [userId, stockCalculationMode, lowStockDays, shareStockStatus ? 1 : 0] - : [userId, stockCalculationMode, lowStockDays], + sql: `INSERT INTO user_settings (${insertColumns.join(", ")}) VALUES (${insertPlaceholders.join(", ")})`, + args: insertArgs, }); } } diff --git a/backend/src/utils/scheduler-utils.ts b/backend/src/utils/scheduler-utils.ts index dd1a208..4e2aa47 100644 --- a/backend/src/utils/scheduler-utils.ts +++ b/backend/src/utils/scheduler-utils.ts @@ -292,6 +292,7 @@ export function getAllTakenByForMedication(medicationTakenBy: string[], intakes: * Check if a person takes this medication (either via medication-level or intake-level takenBy). */ export function personTakesMedication(person: string, medicationTakenBy: string[], intakes: Intake[]): boolean { + if (person === "all") return medicationTakenBy.length > 0 || intakes.some((intake) => intake.takenBy !== null); if (medicationTakenBy.includes(person)) return true; return intakes.some((intake) => intake.takenBy === person); } diff --git a/frontend/src/components/ShareDialog.tsx b/frontend/src/components/ShareDialog.tsx index 166b596..83f6966 100644 --- a/frontend/src/components/ShareDialog.tsx +++ b/frontend/src/components/ShareDialog.tsx @@ -4,7 +4,6 @@ */ import { Check, Copy, Link2, X } from "lucide-react"; -import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; export interface ShareDialogProps { @@ -41,49 +40,9 @@ export function ShareDialog({ onCopyShareLink, }: ShareDialogProps) { const { t } = useTranslation(); - const [overviewCopied, setOverviewCopied] = useState(false); const closeLabel = t("common.close"); const copyLabel = shareCopied ? t("share.copied") : t("share.copyLink"); - const overviewCopyLabel = overviewCopied ? t("share.copied") : t("share.copyOverviewLink"); - const overviewLink = shareLink ? `${shareLink}/overview` : null; - - useEffect(() => { - if (!shareLink) { - setOverviewCopied(false); - } - }, [shareLink]); - - const copyOverviewLink = async () => { - if (!overviewLink) return; - - const markCopied = () => { - setOverviewCopied(true); - setTimeout(() => setOverviewCopied(false), 2000); - }; - - if (navigator.clipboard?.writeText) { - try { - await navigator.clipboard.writeText(overviewLink); - markCopied(); - return; - } catch { - // Fall back to textarea-based copy. - } - } - - const textarea = document.createElement("textarea"); - textarea.value = overviewLink; - textarea.style.position = "fixed"; - textarea.style.opacity = "0"; - document.body.appendChild(textarea); - textarea.select(); - try { - document.execCommand("copy"); - markCopied(); - } finally { - document.body.removeChild(textarea); - } - }; + const getPersonLabel = (person: string) => (person === "all" ? t("share.allPeople") : person); // ESC is handled by the global handler in App.tsx to avoid double history.back() @@ -152,34 +111,13 @@ export function ShareDialog({ {shareCopied ?