feat(dose-tracking): implement dose tracking functionality with API routes for marking and unmarking doses
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
-- Dose tracking table for syncing taken doses between users and share links
|
||||||
|
CREATE TABLE IF NOT EXISTS dose_tracking (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
dose_id TEXT NOT NULL,
|
||||||
|
taken_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||||
|
marked_by TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index for fast lookups by user and dose
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dose_tracking_user_dose ON dose_tracking(user_id, dose_id);
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
{ "idx": 7, "version": 1, "when": 1735300000, "tag": "0007_add_intake_reminders", "breakpoint": false },
|
{ "idx": 7, "version": 1, "when": 1735300000, "tag": "0007_add_intake_reminders", "breakpoint": false },
|
||||||
{ "idx": 8, "version": 1, "when": 1735400000, "tag": "0008_add_pill_weight", "breakpoint": false },
|
{ "idx": 8, "version": 1, "when": 1735400000, "tag": "0008_add_pill_weight", "breakpoint": false },
|
||||||
{ "idx": 9, "version": 1, "when": 1735500000, "tag": "0009_add_taken_by", "breakpoint": false },
|
{ "idx": 9, "version": 1, "when": 1735500000, "tag": "0009_add_taken_by", "breakpoint": false },
|
||||||
{ "idx": 10, "version": 1, "when": 1735600000, "tag": "0010_add_user_settings", "breakpoint": false }
|
{ "idx": 10, "version": 1, "when": 1735600000, "tag": "0010_add_user_settings", "breakpoint": false },
|
||||||
|
{ "idx": 11, "version": 1, "when": 1735700000, "tag": "0011_add_dose_tracking", "breakpoint": false }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,3 +99,14 @@ export const shareTokens = sqliteTable("share_tokens", {
|
|||||||
scheduleDays: integer("schedule_days").notNull().default(30),
|
scheduleDays: integer("schedule_days").notNull().default(30),
|
||||||
createdAt: integer("created_at", { mode: "timestamp" }).notNull().default(sql`CURRENT_TIMESTAMP`),
|
createdAt: integer("created_at", { mode: "timestamp" }).notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Dose Tracking - Tracks when doses are marked as taken
|
||||||
|
// =============================================================================
|
||||||
|
export const doseTracking = sqliteTable("dose_tracking", {
|
||||||
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
|
userId: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||||
|
doseId: text("dose_id", { length: 255 }).notNull(), // e.g. "med-5-1-86400000-1735200000000"
|
||||||
|
takenAt: integer("taken_at", { mode: "timestamp" }).notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||||
|
markedBy: text("marked_by", { length: 100 }), // null = user, "Daniel" = via share link
|
||||||
|
});
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { medicationRoutes } from "./routes/medications.js";
|
|||||||
import { settingsRoutes } from "./routes/settings.js";
|
import { settingsRoutes } from "./routes/settings.js";
|
||||||
import { plannerRoutes } from "./routes/planner.js";
|
import { plannerRoutes } from "./routes/planner.js";
|
||||||
import { shareRoutes } from "./routes/share.js";
|
import { shareRoutes } from "./routes/share.js";
|
||||||
|
import { doseRoutes } from "./routes/doses.js";
|
||||||
import { startReminderScheduler } from "./services/reminder-scheduler.js";
|
import { startReminderScheduler } from "./services/reminder-scheduler.js";
|
||||||
import { startIntakeReminderScheduler } from "./services/intake-reminder-scheduler.js";
|
import { startIntakeReminderScheduler } from "./services/intake-reminder-scheduler.js";
|
||||||
|
|
||||||
@@ -101,6 +102,7 @@ await app.register(medicationRoutes);
|
|||||||
await app.register(settingsRoutes);
|
await app.register(settingsRoutes);
|
||||||
await app.register(plannerRoutes);
|
await app.register(plannerRoutes);
|
||||||
await app.register(shareRoutes);
|
await app.register(shareRoutes);
|
||||||
|
await app.register(doseRoutes);
|
||||||
|
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
import { FastifyInstance } from "fastify";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { db } from "../db/client.js";
|
||||||
|
import { doseTracking, shareTokens } from "../db/schema.js";
|
||||||
|
import { eq, and, gte } from "drizzle-orm";
|
||||||
|
import { requireAuth } from "../plugins/auth.js";
|
||||||
|
import type { AuthUser } from "../types/fastify.js";
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Validation Schemas
|
||||||
|
// =============================================================================
|
||||||
|
const markDoseSchema = z.object({
|
||||||
|
doseId: z.string().min(1, "doseId is required"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const shareDoseSchema = z.object({
|
||||||
|
doseId: z.string().min(1, "doseId is required"),
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Dose Tracking Routes
|
||||||
|
// =============================================================================
|
||||||
|
export async function doseRoutes(app: FastifyInstance) {
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// GET /doses/taken - PROTECTED: Get all taken doses for the user
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
app.get(
|
||||||
|
"/doses/taken",
|
||||||
|
{ preHandler: requireAuth },
|
||||||
|
async (request, reply) => {
|
||||||
|
const authUser = request.user as unknown as AuthUser | null;
|
||||||
|
if (!authUser) {
|
||||||
|
return reply.status(401).send({ error: "Not authenticated" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get doses from last 30 days (to avoid loading too much data)
|
||||||
|
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
const doses = await db.select()
|
||||||
|
.from(doseTracking)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(doseTracking.userId, authUser.id),
|
||||||
|
gte(doseTracking.takenAt, thirtyDaysAgo)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
doses: doses.map((d) => ({
|
||||||
|
doseId: d.doseId,
|
||||||
|
takenAt: d.takenAt?.getTime() ?? Date.now(),
|
||||||
|
markedBy: d.markedBy,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// POST /doses/taken - PROTECTED: Mark a dose as taken
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
app.post<{ Body: z.infer<typeof markDoseSchema> }>(
|
||||||
|
"/doses/taken",
|
||||||
|
{ preHandler: requireAuth },
|
||||||
|
async (request, reply) => {
|
||||||
|
const authUser = request.user as unknown as AuthUser | null;
|
||||||
|
if (!authUser) {
|
||||||
|
return reply.status(401).send({ error: "Not authenticated" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = markDoseSchema.safeParse(request.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return reply.status(400).send({
|
||||||
|
error: parsed.error.errors[0]?.message ?? "Invalid input",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { doseId } = parsed.data;
|
||||||
|
|
||||||
|
// Check if already marked
|
||||||
|
const [existing] = await db.select()
|
||||||
|
.from(doseTracking)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(doseTracking.userId, authUser.id),
|
||||||
|
eq(doseTracking.doseId, doseId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return { success: true, message: "Already marked" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert new record
|
||||||
|
await db.insert(doseTracking).values({
|
||||||
|
userId: authUser.id,
|
||||||
|
doseId,
|
||||||
|
markedBy: null, // Marked by the user themselves
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// DELETE /doses/taken/:doseId - PROTECTED: Unmark a dose
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
app.delete<{ Params: { doseId: string } }>(
|
||||||
|
"/doses/taken/:doseId",
|
||||||
|
{ preHandler: requireAuth },
|
||||||
|
async (request, reply) => {
|
||||||
|
const authUser = request.user as unknown as AuthUser | null;
|
||||||
|
if (!authUser) {
|
||||||
|
return reply.status(401).send({ error: "Not authenticated" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { doseId } = request.params;
|
||||||
|
|
||||||
|
await db.delete(doseTracking).where(
|
||||||
|
and(
|
||||||
|
eq(doseTracking.userId, authUser.id),
|
||||||
|
eq(doseTracking.doseId, doseId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// GET /share/:token/doses - PUBLIC: Get taken doses for a share link
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
app.get<{ Params: { token: string } }>(
|
||||||
|
"/share/:token/doses",
|
||||||
|
async (request, reply) => {
|
||||||
|
const { token } = request.params;
|
||||||
|
|
||||||
|
// Find share token
|
||||||
|
const [share] = await db.select().from(shareTokens).where(eq(shareTokens.token, token));
|
||||||
|
if (!share) {
|
||||||
|
return reply.notFound("Share link not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get doses from last 30 days
|
||||||
|
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
const doses = await db.select()
|
||||||
|
.from(doseTracking)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(doseTracking.userId, share.userId),
|
||||||
|
gte(doseTracking.takenAt, thirtyDaysAgo)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
doses: doses.map((d) => ({
|
||||||
|
doseId: d.doseId,
|
||||||
|
takenAt: d.takenAt?.getTime() ?? Date.now(),
|
||||||
|
markedBy: d.markedBy,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// POST /share/:token/doses - PUBLIC: Mark a dose as taken via share link
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
app.post<{ Params: { token: string }; Body: z.infer<typeof shareDoseSchema> }>(
|
||||||
|
"/share/:token/doses",
|
||||||
|
async (request, reply) => {
|
||||||
|
const { token } = request.params;
|
||||||
|
|
||||||
|
const parsed = shareDoseSchema.safeParse(request.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return reply.status(400).send({
|
||||||
|
error: parsed.error.errors[0]?.message ?? "Invalid input",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { doseId } = parsed.data;
|
||||||
|
|
||||||
|
// Find share token
|
||||||
|
const [share] = await db.select().from(shareTokens).where(eq(shareTokens.token, token));
|
||||||
|
if (!share) {
|
||||||
|
return reply.notFound("Share link not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already marked
|
||||||
|
const [existing] = await db.select()
|
||||||
|
.from(doseTracking)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(doseTracking.userId, share.userId),
|
||||||
|
eq(doseTracking.doseId, doseId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return { success: true, message: "Already marked" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert new record - marked by the takenBy person
|
||||||
|
await db.insert(doseTracking).values({
|
||||||
|
userId: share.userId,
|
||||||
|
doseId,
|
||||||
|
markedBy: share.takenBy, // e.g. "Daniel"
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// DELETE /share/:token/doses/:doseId - PUBLIC: Unmark a dose via share link
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
app.delete<{ Params: { token: string; doseId: string } }>(
|
||||||
|
"/share/:token/doses/:doseId",
|
||||||
|
async (request, reply) => {
|
||||||
|
const { token, doseId } = request.params;
|
||||||
|
|
||||||
|
// Find share token
|
||||||
|
const [share] = await db.select().from(shareTokens).where(eq(shareTokens.token, token));
|
||||||
|
if (!share) {
|
||||||
|
return reply.notFound("Share link not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.delete(doseTracking).where(
|
||||||
|
and(
|
||||||
|
eq(doseTracking.userId, share.userId),
|
||||||
|
eq(doseTracking.doseId, doseId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
+94
-42
@@ -241,13 +241,13 @@ function AppContent() {
|
|||||||
const storedDays = localStorage.getItem(userStorageKey(user.id, "scheduleDays"));
|
const storedDays = localStorage.getItem(userStorageKey(user.id, "scheduleDays"));
|
||||||
setScheduleDays(storedDays ? Number(storedDays) : 30);
|
setScheduleDays(storedDays ? Number(storedDays) : 30);
|
||||||
|
|
||||||
|
// Load taken doses from server
|
||||||
|
async function loadTakenDoses() {
|
||||||
try {
|
try {
|
||||||
const storedDoses = localStorage.getItem(userStorageKey(user.id, "takenDoses"));
|
const res = await fetch("/api/doses/taken", { credentials: "include" });
|
||||||
if (storedDoses) {
|
if (res.ok) {
|
||||||
const parsed = JSON.parse(storedDoses);
|
const data = await res.json();
|
||||||
const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
setTakenDoses(new Set(data.doses.map((d: { doseId: string }) => d.doseId)));
|
||||||
const filtered = parsed.filter((item: { id: string; timestamp: number }) => item.timestamp > weekAgo);
|
|
||||||
setTakenDoses(new Set(filtered.map((item: { id: string }) => item.id)));
|
|
||||||
} else {
|
} else {
|
||||||
setTakenDoses(new Set());
|
setTakenDoses(new Set());
|
||||||
}
|
}
|
||||||
@@ -255,32 +255,59 @@ function AppContent() {
|
|||||||
setTakenDoses(new Set());
|
setTakenDoses(new Set());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
loadTakenDoses();
|
||||||
|
}
|
||||||
}, [user?.id]);
|
}, [user?.id]);
|
||||||
|
|
||||||
function markDoseTaken(doseId: string) {
|
async function markDoseTaken(doseId: string) {
|
||||||
|
// Optimistic update
|
||||||
setTakenDoses((prev) => {
|
setTakenDoses((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
next.add(doseId);
|
next.add(doseId);
|
||||||
// Persist with timestamp for cleanup
|
|
||||||
const items = Array.from(next).map((id) => ({ id, timestamp: Date.now() }));
|
|
||||||
if (user?.id) {
|
|
||||||
localStorage.setItem(userStorageKey(user.id, "takenDoses"), JSON.stringify(items));
|
|
||||||
}
|
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function undoDoseTaken(doseId: string) {
|
// Send to server
|
||||||
|
try {
|
||||||
|
await fetch("/api/doses/taken", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({ doseId }),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Revert on error
|
||||||
setTakenDoses((prev) => {
|
setTakenDoses((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
next.delete(doseId);
|
next.delete(doseId);
|
||||||
const items = Array.from(next).map((id) => ({ id, timestamp: Date.now() }));
|
|
||||||
if (user?.id) {
|
|
||||||
localStorage.setItem(userStorageKey(user.id, "takenDoses"), JSON.stringify(items));
|
|
||||||
}
|
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function undoDoseTaken(doseId: string) {
|
||||||
|
// Optimistic update
|
||||||
|
setTakenDoses((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(doseId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send to server
|
||||||
|
try {
|
||||||
|
await fetch(`/api/doses/taken/${encodeURIComponent(doseId)}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Revert on error
|
||||||
|
setTakenDoses((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.add(doseId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Close modal on Escape key
|
// Close modal on Escape key
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -2303,48 +2330,73 @@ function SharedSchedule() {
|
|||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [lightboxImage]);
|
}, [lightboxImage]);
|
||||||
|
|
||||||
// Load taken doses from localStorage
|
// Load taken doses from server
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (token) {
|
if (token) {
|
||||||
|
async function loadTakenDoses() {
|
||||||
try {
|
try {
|
||||||
const storedDoses = localStorage.getItem(`share_${token}_takenDoses`);
|
const res = await fetch(`/api/share/${token}/doses`);
|
||||||
if (storedDoses) {
|
if (res.ok) {
|
||||||
const parsed = JSON.parse(storedDoses);
|
const data = await res.json();
|
||||||
// Clean up old doses (older than 7 days)
|
setTakenDoses(new Set(data.doses.map((d: { doseId: string }) => d.doseId)));
|
||||||
const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
} else {
|
||||||
const filtered = parsed.filter((item: { id: string; timestamp: number }) => item.timestamp > weekAgo);
|
setTakenDoses(new Set());
|
||||||
setTakenDoses(new Set(filtered.map((item: { id: string }) => item.id)));
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setTakenDoses(new Set());
|
setTakenDoses(new Set());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
loadTakenDoses();
|
||||||
|
}
|
||||||
}, [token]);
|
}, [token]);
|
||||||
|
|
||||||
function markDoseTaken(doseId: string) {
|
async function markDoseTaken(doseId: string) {
|
||||||
|
// Optimistic update
|
||||||
setTakenDoses((prev) => {
|
setTakenDoses((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
next.add(doseId);
|
next.add(doseId);
|
||||||
// Persist with timestamp for cleanup
|
|
||||||
const items = Array.from(next).map((id) => ({ id, timestamp: Date.now() }));
|
|
||||||
if (token) {
|
|
||||||
localStorage.setItem(`share_${token}_takenDoses`, JSON.stringify(items));
|
|
||||||
}
|
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function undoDoseTaken(doseId: string) {
|
// Send to server
|
||||||
|
try {
|
||||||
|
await fetch(`/api/share/${token}/doses`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ doseId }),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Revert on error
|
||||||
setTakenDoses((prev) => {
|
setTakenDoses((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
next.delete(doseId);
|
next.delete(doseId);
|
||||||
const items = Array.from(next).map((id) => ({ id, timestamp: Date.now() }));
|
|
||||||
if (token) {
|
|
||||||
localStorage.setItem(`share_${token}_takenDoses`, JSON.stringify(items));
|
|
||||||
}
|
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function undoDoseTaken(doseId: string) {
|
||||||
|
// Optimistic update
|
||||||
|
setTakenDoses((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(doseId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send to server
|
||||||
|
try {
|
||||||
|
await fetch(`/api/share/${token}/doses/${encodeURIComponent(doseId)}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Revert on error
|
||||||
|
setTakenDoses((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.add(doseId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
@@ -2382,7 +2434,7 @@ function SharedSchedule() {
|
|||||||
const doses: { id: string; when: number; medName: string; usage: number; timeStr: string }[] = [];
|
const doses: { id: string; when: number; medName: string; usage: number; timeStr: string }[] = [];
|
||||||
|
|
||||||
for (const med of data.medications) {
|
for (const med of data.medications) {
|
||||||
for (const slice of med.slices) {
|
med.slices.forEach((slice, sliceIdx) => {
|
||||||
const startDate = new Date(slice.start);
|
const startDate = new Date(slice.start);
|
||||||
const intervalMs = slice.every * 24 * 60 * 60 * 1000;
|
const intervalMs = slice.every * 24 * 60 * 60 * 1000;
|
||||||
let t = startDate.getTime();
|
let t = startDate.getTime();
|
||||||
@@ -2397,8 +2449,8 @@ function SharedSchedule() {
|
|||||||
|
|
||||||
while (t <= endTime) {
|
while (t <= endTime) {
|
||||||
const d = new Date(t);
|
const d = new Date(t);
|
||||||
// Generate unique dose ID
|
// Generate dose ID matching Dashboard format: ${med.id}-${sliceIdx}-${whenMs}
|
||||||
const doseId = `share-${med.id}-${slice.usage}-${slice.every}-${t}`;
|
const doseId = `${med.id}-${sliceIdx}-${t}`;
|
||||||
doses.push({
|
doses.push({
|
||||||
id: doseId,
|
id: doseId,
|
||||||
when: t,
|
when: t,
|
||||||
@@ -2408,7 +2460,7 @@ function SharedSchedule() {
|
|||||||
});
|
});
|
||||||
t += intervalMs;
|
t += intervalMs;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
doses.sort((a, b) => a.when - b.when);
|
doses.sort((a, b) => a.when - b.when);
|
||||||
|
|||||||
Reference in New Issue
Block a user