diff --git a/backend/package-lock.json b/backend/package-lock.json
index 1b7dc45..2601107 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "medassist-ng-backend",
- "version": "1.15.1",
+ "version": "1.16.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "medassist-ng-backend",
- "version": "1.15.1",
+ "version": "1.16.0",
"dependencies": {
"@fastify/cookie": "^11.0.2",
"@fastify/cors": "^11.2.0",
diff --git a/backend/src/routes/medications.ts b/backend/src/routes/medications.ts
index 7d62f02..24e7f42 100644
--- a/backend/src/routes/medications.ts
+++ b/backend/src/routes/medications.ts
@@ -42,7 +42,7 @@ const medicationStartDateSchema = z
const medicationSchema = z
.object({
- name: z.string().trim().min(1).max(100),
+ name: z.string().trim().max(100).default(""),
genericName: z.string().trim().max(100).nullable().optional(),
takenBy: z.array(z.string().trim().max(100)).default([]), // Medication-level takenBy (fallback)
packageType: packageTypeSchema,
@@ -66,6 +66,10 @@ const medicationSchema = z
intakes: z.array(intakeSchema).min(1).max(12).optional(),
blisters: z.array(blisterSchema).min(1).max(12).optional(), // Legacy format
})
+ .refine(
+ (data) => (data.name && data.name.length > 0) || (data.genericName && data.genericName.length > 0),
+ { message: "Either 'name' or 'genericName' must be provided", path: ["name"] }
+ )
.refine((data) => data.intakes || data.blisters, { message: "Either 'intakes' or 'blisters' must be provided" })
.refine(
(data) => {
diff --git a/backend/src/routes/planner.ts b/backend/src/routes/planner.ts
index 6876cf8..6be7d23 100644
--- a/backend/src/routes/planner.ts
+++ b/backend/src/routes/planner.ts
@@ -371,10 +371,10 @@ ${getFooterPlain(language)}`;
// Load user settings
const userId = await getUserId(request);
const activeMeds = await db
- .select({ name: medications.name })
+ .select({ name: medications.name, genericName: medications.genericName })
.from(medications)
.where(and(eq(medications.userId, userId), eq(medications.isObsolete, false)));
- const activeMedNames = new Set(activeMeds.map((med) => med.name));
+ const activeMedNames = new Set(activeMeds.map((med) => med.name || med.genericName || ""));
const filteredLowStock = lowStock.filter((item) => activeMedNames.has(item.name));
if (filteredLowStock.length === 0) {
return reply.status(400).send({ error: "No active medications to notify" });
@@ -641,10 +641,10 @@ ${getFooterPlain(language)}`;
const userId = await getUserId(request);
const activeMeds = await db
- .select({ name: medications.name })
+ .select({ name: medications.name, genericName: medications.genericName })
.from(medications)
.where(and(eq(medications.userId, userId), eq(medications.isObsolete, false)));
- const activeMedNames = new Set(activeMeds.map((med) => med.name));
+ const activeMedNames = new Set(activeMeds.map((med) => med.name || med.genericName || ""));
const filteredPrescriptionLow = prescriptionLow.filter((item) => activeMedNames.has(item.name));
if (filteredPrescriptionLow.length === 0) {
return reply.status(400).send({ error: "No active medications to notify" });
diff --git a/backend/src/services/intake-reminder-scheduler.ts b/backend/src/services/intake-reminder-scheduler.ts
index 7d8deaf..df06b78 100644
--- a/backend/src/services/intake-reminder-scheduler.ts
+++ b/backend/src/services/intake-reminder-scheduler.ts
@@ -106,8 +106,9 @@ async function autoMarkDueIntakesAsTaken(
}
const medicationTakenBy = parseTakenByJson(med.takenByJson);
+ const medDisplayName = med.name || med.genericName || "";
const todaysIntakes = getTodaysIntakes(
- med.name,
+ medDisplayName,
intakes,
medicationTakenBy,
med.pillWeightMg,
@@ -415,9 +416,10 @@ async function checkAndSendIntakeRemindersForUser(
);
// Medication-level takenBy (for fallback/display purposes)
const medicationTakenBy = parseTakenByJson(med.takenByJson);
+ const medDisplayName = med.name || med.genericName || "";
logger.debug(
- `[IntakeReminder] User ${settings.userId}: Processing medication "${med.name}" with ${intakes.length} intakes`
+ `[IntakeReminder] User ${settings.userId}: Processing medication "${medDisplayName}" with ${intakes.length} intakes`
);
// Filter intakes that have reminders enabled (per-intake setting or medication-level)
@@ -438,7 +440,7 @@ async function checkAndSendIntakeRemindersForUser(
// Always get upcoming intakes (15 min before) for first reminders
const upcomingIntakes = getUpcomingIntakes(
- med.name,
+ medDisplayName,
[intake],
REMINDER_MINUTES_BEFORE,
medicationTakenBy,
@@ -465,7 +467,7 @@ async function checkAndSendIntakeRemindersForUser(
// If repeat reminders enabled, also check for missed intakes (past the intake time)
if (settings.repeatRemindersEnabled) {
const allTodaysIntakes = getTodaysIntakes(
- med.name,
+ medDisplayName,
[intake],
medicationTakenBy,
med.pillWeightMg,
diff --git a/backend/src/services/reminder-scheduler.ts b/backend/src/services/reminder-scheduler.ts
index 32eee03..0a7dbb7 100644
--- a/backend/src/services/reminder-scheduler.ts
+++ b/backend/src/services/reminder-scheduler.ts
@@ -353,7 +353,7 @@ async function getMedicationsNeedingReminder(
if (isCritical || isLow) {
lowStock.push({
- name: row.name,
+ name: row.name || row.genericName || "",
medsLeft: currentPills,
daysLeft,
depletionDate,
@@ -379,7 +379,7 @@ async function getMedicationsNeedingPrescriptionReminder(userId: number): Promis
(row.prescriptionRemainingRefills ?? 0) <= (row.prescriptionLowRefillThreshold ?? 1)
)
.map((row) => ({
- name: row.name,
+ name: row.name || row.genericName || "",
remainingRefills: row.prescriptionRemainingRefills ?? 0,
lowThreshold: row.prescriptionLowRefillThreshold ?? 1,
expiryDate: row.prescriptionExpiryDate ?? null,
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 13a60e8..69dc297 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "medassist-ng-frontend",
- "version": "1.15.1",
+ "version": "1.16.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "medassist-ng-frontend",
- "version": "1.15.1",
+ "version": "1.16.0",
"dependencies": {
"i18next": "^25.8.13",
"i18next-browser-languagedetector": "^8.2.1",
diff --git a/frontend/src/components/MedDetailModal.tsx b/frontend/src/components/MedDetailModal.tsx
index af8c08e..8639f21 100644
--- a/frontend/src/components/MedDetailModal.tsx
+++ b/frontend/src/components/MedDetailModal.tsx
@@ -15,7 +15,7 @@ import { useTranslation } from "react-i18next";
import { Lightbox, MedicationAvatar } from "../components";
import { useEscapeKey } from "../hooks";
import type { Coverage, Medication, RefillEntry, StockThresholds } from "../types";
-import { getMedTotal, getPackageSize } from "../types";
+import { getMedDisplayName, getMedTotal, getPackageSize } from "../types";
import { formatNumber, generateICS, getExpiryClass, getSystemLocale } from "../utils";
import { getStockStatus } from "../utils/schedule";
import { splitCurrentBlisterStock } from "../utils/stock";
@@ -193,7 +193,7 @@ export function MedDetailModal({
if (!selectedMed) return null;
- const medCoverage = coverage.all.find((c) => c.name === selectedMed.name);
+ const medCoverage = coverage.all.find((c) => c.name === getMedDisplayName(selectedMed));
const packageSize = getPackageSize(selectedMed);
// Structural max = sealed package capacity only (excludes pre-existing looseTablets).
const structuralMax =
@@ -380,7 +380,7 @@ export function MedDetailModal({
{t("editStock.title")}
- {selectedMed.name}
+ {getMedDisplayName(selectedMed)}
{t("editStock.hint")}
{selectedMed.packageType === "blister" && (
@@ -667,12 +667,12 @@ export function MedDetailModal({
}
}}
>
-
+
{selectedMed.imageUrl && 🔍}
-
{selectedMed.name}
- {selectedMed.genericName &&
{selectedMed.genericName}}
+
{getMedDisplayName(selectedMed)}
+ {selectedMed.name && selectedMed.genericName &&
{selectedMed.genericName}}
{selectedMed.takenBy && (selectedMed.takenBy || []).length > 0 && (
{t("modal.for")}{" "}
@@ -1017,7 +1017,7 @@ export function MedDetailModal({
{/* Image Lightbox */}
{showImageLightbox && selectedMed.imageUrl && (
-
+
)}
{/* Refill Modal */}
@@ -1049,7 +1049,7 @@ export function MedDetailModal({
{t("refill.title")}
- {selectedMed.name}
+ {getMedDisplayName(selectedMed)}
{selectedMed.packageType === "blister" ? (
diff --git a/frontend/src/components/MobileEditModal.tsx b/frontend/src/components/MobileEditModal.tsx
index 651dad5..380f860 100644
--- a/frontend/src/components/MobileEditModal.tsx
+++ b/frontend/src/components/MobileEditModal.tsx
@@ -253,7 +253,7 @@ export function MobileEditModal({
const mobileTitle = (() => {
if (!editingId) return t("form.newEntry");
if (readOnlyMode) return t("form.viewEntry");
- const medicationName = currentMed?.name?.trim() || form.name.trim();
+ const medicationName = (currentMed ? (currentMed.name?.trim() || currentMed.genericName?.trim()) : null) || form.name.trim() || form.genericName.trim();
if (!medicationName) return t("form.editEntry");
return t("form.editEntryWithName", { name: medicationName });
})();
@@ -361,21 +361,26 @@ export function MobileEditModal({
onBlur={() => setShowNameValidation(true)}
placeholder={t("form.placeholders.commercial")}
maxLength={FIELD_LIMITS.name.max}
- required={!readOnlyMode}
/>
{!readOnlyMode && showNameValidation && fieldErrors.name && (
{fieldErrors.name}
)}
-