@@ -0,0 +1 @@
|
||||
ALTER TABLE `user_settings` ADD `share_medication_overview` integer DEFAULT false NOT NULL;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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`,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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<UserSettings> {
|
||||
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<UserSettings[]> {
|
||||
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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<number, DoseRow[]>();
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
+44
-11
@@ -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<void> {
|
||||
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<string | number>;
|
||||
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<string | number>;
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user