Add past days toggle and update terminology for blisters
- Added translations for showing/hiding past days and past days count in German and English. - Renamed "slices" to "blisters" in both translation files. - Updated CSS styles to reflect the change from slices to blisters, including layout and hover effects. - Introduced new styles for past days toggle button and past day blocks.
This commit is contained in:
@@ -12,7 +12,7 @@ import type { AuthUser } from "../types/fastify.js";
|
||||
|
||||
const IMAGES_DIR = resolve(process.cwd(), "data/images");
|
||||
|
||||
const sliceSchema = z.object({
|
||||
const blisterSchema = z.object({
|
||||
usage: z.number().nonnegative(),
|
||||
every: z.number().int().min(1),
|
||||
start: z.string().datetime(),
|
||||
@@ -30,24 +30,24 @@ const medicationSchema = z.object({
|
||||
expiryDate: z.string().nullable().optional(),
|
||||
notes: z.string().max(500).nullable().optional(),
|
||||
intakeRemindersEnabled: z.boolean().default(false),
|
||||
slices: z.array(sliceSchema).min(1).max(12),
|
||||
blisters: z.array(blisterSchema).min(1).max(12),
|
||||
});
|
||||
|
||||
function zipSlices(usage: number[], every: number[], start: string[]) {
|
||||
function zipBlisters(usage: number[], every: number[], start: string[]) {
|
||||
const len = Math.min(usage.length, every.length, start.length);
|
||||
const slices: Array<{ usage: number; every: number; start: string }> = [];
|
||||
const blisters: Array<{ usage: number; every: number; start: string }> = [];
|
||||
for (let i = 0; i < len; i++) {
|
||||
slices.push({ usage: usage[i], every: every[i], start: start[i] });
|
||||
blisters.push({ usage: usage[i], every: every[i], start: start[i] });
|
||||
}
|
||||
return slices;
|
||||
return blisters;
|
||||
}
|
||||
|
||||
function parseSlices(row: typeof medications.$inferSelect) {
|
||||
function parseBlisters(row: typeof medications.$inferSelect) {
|
||||
try {
|
||||
const usage = JSON.parse(row.usageJson) as number[];
|
||||
const every = JSON.parse(row.everyJson) as number[];
|
||||
const start = JSON.parse(row.startJson) as string[];
|
||||
return zipSlices(usage, every, start);
|
||||
return zipBlisters(usage, every, start);
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
@@ -90,7 +90,7 @@ export async function medicationRoutes(app: FastifyInstance) {
|
||||
tabsPerStrip: row.tabsPerStrip ?? row.stripSize ?? 1,
|
||||
looseTablets: row.looseTablets ?? 0,
|
||||
pillWeightMg: row.pillWeightMg,
|
||||
slices: parseSlices(row),
|
||||
blisters: parseBlisters(row),
|
||||
imageUrl: row.imageUrl,
|
||||
expiryDate: row.expiryDate,
|
||||
notes: row.notes,
|
||||
@@ -104,10 +104,10 @@ export async function medicationRoutes(app: FastifyInstance) {
|
||||
if (!parsed.success) return reply.status(400).send(parsed.error.format());
|
||||
|
||||
const userId = getUserId(req, reply);
|
||||
const { name, genericName, takenBy, packCount, stripsPerPack, tabsPerStrip, looseTablets, pillWeightMg, expiryDate, notes, intakeRemindersEnabled, slices } = parsed.data;
|
||||
const usageJson = JSON.stringify(slices.map((s) => s.usage));
|
||||
const everyJson = JSON.stringify(slices.map((s) => s.every));
|
||||
const startJson = JSON.stringify(slices.map((s) => s.start));
|
||||
const { name, genericName, takenBy, packCount, stripsPerPack, tabsPerStrip, looseTablets, pillWeightMg, expiryDate, notes, intakeRemindersEnabled, blisters } = parsed.data;
|
||||
const usageJson = JSON.stringify(blisters.map((s) => s.usage));
|
||||
const everyJson = JSON.stringify(blisters.map((s) => s.every));
|
||||
const startJson = JSON.stringify(blisters.map((s) => s.start));
|
||||
|
||||
const derivedCount = deriveTotalTablets(packCount, stripsPerPack, tabsPerStrip, looseTablets);
|
||||
|
||||
@@ -148,7 +148,7 @@ export async function medicationRoutes(app: FastifyInstance) {
|
||||
tabsPerStrip: inserted.tabsPerStrip,
|
||||
looseTablets: inserted.looseTablets,
|
||||
pillWeightMg: inserted.pillWeightMg,
|
||||
slices,
|
||||
blisters,
|
||||
imageUrl: inserted.imageUrl,
|
||||
expiryDate: inserted.expiryDate,
|
||||
notes: inserted.notes,
|
||||
@@ -169,10 +169,10 @@ export async function medicationRoutes(app: FastifyInstance) {
|
||||
const [existing] = await db.select().from(medications).where(and(eq(medications.id, idNum), eq(medications.userId, userId)));
|
||||
if (!existing) return reply.notFound();
|
||||
|
||||
const { name, genericName, takenBy, packCount, stripsPerPack, tabsPerStrip, looseTablets, pillWeightMg, expiryDate, notes, intakeRemindersEnabled, slices } = parsed.data;
|
||||
const usageJson = JSON.stringify(slices.map((s) => s.usage));
|
||||
const everyJson = JSON.stringify(slices.map((s) => s.every));
|
||||
const startJson = JSON.stringify(slices.map((s) => s.start));
|
||||
const { name, genericName, takenBy, packCount, stripsPerPack, tabsPerStrip, looseTablets, pillWeightMg, expiryDate, notes, intakeRemindersEnabled, blisters } = parsed.data;
|
||||
const usageJson = JSON.stringify(blisters.map((s) => s.usage));
|
||||
const everyJson = JSON.stringify(blisters.map((s) => s.every));
|
||||
const startJson = JSON.stringify(blisters.map((s) => s.start));
|
||||
|
||||
const derivedCount = deriveTotalTablets(packCount, stripsPerPack, tabsPerStrip, looseTablets);
|
||||
|
||||
@@ -216,7 +216,7 @@ export async function medicationRoutes(app: FastifyInstance) {
|
||||
tabsPerStrip: result[0].tabsPerStrip,
|
||||
looseTablets: result[0].looseTablets,
|
||||
pillWeightMg: result[0].pillWeightMg,
|
||||
slices,
|
||||
blisters,
|
||||
imageUrl: result[0].imageUrl,
|
||||
expiryDate: result[0].expiryDate,
|
||||
notes: result[0].notes,
|
||||
@@ -313,8 +313,8 @@ export async function medicationRoutes(app: FastifyInstance) {
|
||||
const now = new Date();
|
||||
|
||||
const payload = rows.map((row) => {
|
||||
const slices = parseSlices(row);
|
||||
const usageTotal = calculateUsageInRange(slices, start, end);
|
||||
const blisters = parseBlisters(row);
|
||||
const usageTotal = calculateUsageInRange(blisters, start, end);
|
||||
const tabsPerStrip = row.tabsPerStrip ?? row.stripSize ?? 1;
|
||||
const packCount = row.packCount ?? 1;
|
||||
const stripsPerPack = row.stripsPerPack ?? row.strips ?? 1;
|
||||
@@ -323,13 +323,13 @@ export async function medicationRoutes(app: FastifyInstance) {
|
||||
|
||||
// Calculate consumption up to now (same logic as frontend)
|
||||
let consumedUntilNow = 0;
|
||||
slices.forEach((slice) => {
|
||||
const sliceStart = new Date(slice.start);
|
||||
if (Number.isNaN(sliceStart.getTime()) || sliceStart > now) return;
|
||||
blisters.forEach((blister) => {
|
||||
const blisterStart = new Date(blister.start);
|
||||
if (Number.isNaN(blisterStart.getTime()) || blisterStart > now) return;
|
||||
const msPerDay = 86400000;
|
||||
const period = Math.max(1, slice.every) * msPerDay;
|
||||
const occurrences = Math.floor((now.getTime() - sliceStart.getTime()) / period) + 1;
|
||||
consumedUntilNow += occurrences * slice.usage;
|
||||
const period = Math.max(1, blister.every) * msPerDay;
|
||||
const occurrences = Math.floor((now.getTime() - blisterStart.getTime()) / period) + 1;
|
||||
consumedUntilNow += occurrences * blister.usage;
|
||||
});
|
||||
|
||||
const currentPills = Math.max(0, originalTotalPills - consumedUntilNow);
|
||||
@@ -365,14 +365,14 @@ export async function medicationRoutes(app: FastifyInstance) {
|
||||
});
|
||||
}
|
||||
|
||||
function calculateUsageInRange(slices: Array<{ usage: number; every: number; start: string }>, start: Date, end: Date) {
|
||||
function calculateUsageInRange(blisters: Array<{ usage: number; every: number; start: string }>, start: Date, end: Date) {
|
||||
let total = 0;
|
||||
slices.forEach((slice) => {
|
||||
const sliceStart = new Date(slice.start);
|
||||
if (Number.isNaN(sliceStart.getTime())) return;
|
||||
// iterate occurrences from sliceStart up to end
|
||||
for (let dt = new Date(sliceStart); dt < end; dt.setDate(dt.getDate() + slice.every)) {
|
||||
if (dt >= start && dt < end) total += slice.usage;
|
||||
blisters.forEach((blister) => {
|
||||
const blisterStart = new Date(blister.start);
|
||||
if (Number.isNaN(blisterStart.getTime())) return;
|
||||
// iterate occurrences from blisterStart up to end
|
||||
for (let dt = new Date(blisterStart); dt < end; dt.setDate(dt.getDate() + blister.every)) {
|
||||
if (dt >= start && dt < end) total += blister.usage;
|
||||
}
|
||||
});
|
||||
return Number(total.toFixed(2));
|
||||
|
||||
@@ -56,20 +56,20 @@ export async function shareRoutes(app: FastifyInstance) {
|
||||
)
|
||||
);
|
||||
|
||||
// Parse slices and build schedule data
|
||||
const medicationsWithSlices = meds.map((med) => {
|
||||
let slices: { usage: number; every: number; start: string }[] = [];
|
||||
// Parse blisters and build schedule data
|
||||
const medicationsWithBlisters = meds.map((med) => {
|
||||
let blisters: { usage: number; every: number; start: string }[] = [];
|
||||
try {
|
||||
const usageArr = JSON.parse(med.usageJson || "[]");
|
||||
const everyArr = JSON.parse(med.everyJson || "[]");
|
||||
const startArr = JSON.parse(med.startJson || "[]");
|
||||
slices = usageArr.map((usage: number, i: number) => ({
|
||||
blisters = usageArr.map((usage: number, i: number) => ({
|
||||
usage,
|
||||
every: everyArr[i] ?? 1,
|
||||
start: startArr[i] ?? new Date().toISOString(),
|
||||
}));
|
||||
} catch {
|
||||
slices = [];
|
||||
blisters = [];
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -78,14 +78,14 @@ export async function shareRoutes(app: FastifyInstance) {
|
||||
genericName: med.genericName,
|
||||
pillWeightMg: med.pillWeightMg,
|
||||
imageUrl: med.imageUrl,
|
||||
slices,
|
||||
blisters,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
takenBy: share.takenBy,
|
||||
scheduleDays: share.scheduleDays,
|
||||
medications: medicationsWithSlices,
|
||||
medications: medicationsWithBlisters,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getAllUserSettings, sendShoutrrrNotification, type UserSettings } from
|
||||
import { getTranslations, t, getDateLocale, type Language } from "../i18n/translations.js";
|
||||
import { getReminderState, updateReminderSentTime } from "./reminder-scheduler.js";
|
||||
|
||||
type Slice = { usage: number; every: number; start: string };
|
||||
type Blister = { usage: number; every: number; start: string };
|
||||
|
||||
type IntakeReminderState = {
|
||||
sentReminders: string[]; // Array of "medName:timestamp" to track sent reminders
|
||||
@@ -42,17 +42,17 @@ function saveIntakeReminderState(state: IntakeReminderState): void {
|
||||
writeFileSync(intakeReminderStateFile, JSON.stringify(state, null, 2));
|
||||
}
|
||||
|
||||
function parseSlices(row: { usageJson: string; everyJson: string; startJson: string }): Slice[] {
|
||||
function parseBlisters(row: { usageJson: string; everyJson: string; startJson: string }): Blister[] {
|
||||
try {
|
||||
const usage = JSON.parse(row.usageJson) as number[];
|
||||
const every = JSON.parse(row.everyJson) as number[];
|
||||
const start = JSON.parse(row.startJson) as string[];
|
||||
const len = Math.min(usage.length, every.length, start.length);
|
||||
const slices: Slice[] = [];
|
||||
const blisters: Blister[] = [];
|
||||
for (let i = 0; i < len; i++) {
|
||||
slices.push({ usage: usage[i], every: every[i], start: start[i] });
|
||||
blisters.push({ usage: usage[i], every: every[i], start: start[i] });
|
||||
}
|
||||
return slices;
|
||||
return blisters;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
@@ -67,7 +67,7 @@ type UpcomingIntake = {
|
||||
pillWeightMg: number | null;
|
||||
};
|
||||
|
||||
function getUpcomingIntakes(medName: string, slices: Slice[], minutesBefore: number, takenBy: string | null, pillWeightMg: number | null, locale: string): UpcomingIntake[] {
|
||||
function getUpcomingIntakes(medName: string, blisters: Blister[], minutesBefore: number, takenBy: string | null, pillWeightMg: number | null, locale: string): UpcomingIntake[] {
|
||||
const now = Date.now();
|
||||
// Window to detect if "now" is the right time to send reminder
|
||||
// We check if the notify time (intake - 15min) falls within current minute ±1
|
||||
@@ -76,9 +76,9 @@ function getUpcomingIntakes(medName: string, slices: Slice[], minutesBefore: num
|
||||
|
||||
const upcoming: UpcomingIntake[] = [];
|
||||
|
||||
for (const slice of slices) {
|
||||
const startTime = new Date(slice.start).getTime();
|
||||
const intervalMs = slice.every * 24 * 60 * 60 * 1000;
|
||||
for (const blister of blisters) {
|
||||
const startTime = new Date(blister.start).getTime();
|
||||
const intervalMs = blister.every * 24 * 60 * 60 * 1000;
|
||||
|
||||
if (intervalMs <= 0) continue;
|
||||
|
||||
@@ -112,7 +112,7 @@ function getUpcomingIntakes(medName: string, slices: Slice[], minutesBefore: num
|
||||
const intakeDate = new Date(nextTime);
|
||||
upcoming.push({
|
||||
medName,
|
||||
usage: slice.usage,
|
||||
usage: blister.usage,
|
||||
intakeTime: intakeDate,
|
||||
intakeTimeStr: intakeDate.toLocaleTimeString(locale, {
|
||||
hour: "2-digit",
|
||||
@@ -303,8 +303,8 @@ async function checkAndSendIntakeRemindersForUser(
|
||||
|
||||
// Find all upcoming intakes across all medications for this user
|
||||
for (const med of medsWithReminders) {
|
||||
const slices = parseSlices(med);
|
||||
const upcoming = getUpcomingIntakes(med.name, slices, REMINDER_MINUTES_BEFORE, med.takenBy, med.pillWeightMg, locale);
|
||||
const blisters = parseBlisters(med);
|
||||
const upcoming = getUpcomingIntakes(med.name, blisters, REMINDER_MINUTES_BEFORE, med.takenBy, med.pillWeightMg, locale);
|
||||
allUpcoming.push(...upcoming);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { resolve } from "path";
|
||||
import { loadUserSettings, getAllUserSettings, sendShoutrrrNotification, type UserSettings } from "../routes/settings.js";
|
||||
import { getTranslations, t, getDateLocale, type Language } from "../i18n/translations.js";
|
||||
|
||||
type Slice = { usage: number; every: number; start: string };
|
||||
type Blister = { usage: number; every: number; start: string };
|
||||
|
||||
type ReminderState = {
|
||||
lastAutoEmailSent: string | null; // ISO date string
|
||||
@@ -172,28 +172,28 @@ export function updateReminderSentTime(type: "stock" | "intake" = "stock", chann
|
||||
});
|
||||
}
|
||||
|
||||
function parseSlices(row: { usageJson: string; everyJson: string; startJson: string }): Slice[] {
|
||||
function parseBlisters(row: { usageJson: string; everyJson: string; startJson: string }): Blister[] {
|
||||
try {
|
||||
const usage = JSON.parse(row.usageJson) as number[];
|
||||
const every = JSON.parse(row.everyJson) as number[];
|
||||
const start = JSON.parse(row.startJson) as string[];
|
||||
const len = Math.min(usage.length, every.length, start.length);
|
||||
const slices: Slice[] = [];
|
||||
const blisters: Blister[] = [];
|
||||
for (let i = 0; i < len; i++) {
|
||||
slices.push({ usage: usage[i], every: every[i], start: start[i] });
|
||||
blisters.push({ usage: usage[i], every: every[i], start: start[i] });
|
||||
}
|
||||
return slices;
|
||||
return blisters;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function calculateDailyUsage(slices: Slice[]): number {
|
||||
return slices.reduce((sum, s) => sum + s.usage / s.every, 0);
|
||||
function calculateDailyUsage(blisters: Blister[]): number {
|
||||
return blisters.reduce((sum, s) => sum + s.usage / s.every, 0);
|
||||
}
|
||||
|
||||
function calculateDepletionInfo(med: { count: number; slices: Slice[] }, language: Language): { daysLeft: number | null; depletionDate: string | null } {
|
||||
const dailyUsage = calculateDailyUsage(med.slices);
|
||||
function calculateDepletionInfo(med: { count: number; blisters: Blister[] }, language: Language): { daysLeft: number | null; depletionDate: string | null } {
|
||||
const dailyUsage = calculateDailyUsage(med.blisters);
|
||||
if (dailyUsage <= 0) return { daysLeft: null, depletionDate: null };
|
||||
|
||||
const daysLeft = Math.floor(med.count / dailyUsage);
|
||||
@@ -220,8 +220,8 @@ async function getMedicationsNeedingReminder(userId: number, reminderDaysBefore:
|
||||
const lowStock: LowStockItem[] = [];
|
||||
|
||||
for (const row of rows) {
|
||||
const slices = parseSlices(row);
|
||||
const { daysLeft, depletionDate } = calculateDepletionInfo({ count: row.count, slices }, language);
|
||||
const blisters = parseBlisters(row);
|
||||
const { daysLeft, depletionDate } = calculateDepletionInfo({ count: row.count, blisters }, language);
|
||||
|
||||
// Check if medication runs out within reminderDaysBefore days
|
||||
if (daysLeft !== null && daysLeft <= reminderDaysBefore) {
|
||||
|
||||
Reference in New Issue
Block a user