diff --git a/backend/src/routes/share.ts b/backend/src/routes/share.ts index 3a114a9..c277658 100644 --- a/backend/src/routes/share.ts +++ b/backend/src/routes/share.ts @@ -107,6 +107,9 @@ export async function shareRoutes(app: FastifyInstance) { blisters = []; } + // Parse takenBy JSON array + const takenByArray = parseTakenByJson(med.takenByJson); + const totalPills = med.packCount * med.blistersPerPack * med.pillsPerBlister + med.looseTablets; return { id: med.id, @@ -115,7 +118,11 @@ export async function shareRoutes(app: FastifyInstance) { pillWeightMg: med.pillWeightMg, imageUrl: med.imageUrl, totalPills, + packCount: med.packCount, + blistersPerPack: med.blistersPerPack, + looseTablets: med.looseTablets, pillsPerBlister: med.pillsPerBlister, + takenBy: takenByArray, blisters, }; }); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5b88e38..97fcf92 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -348,7 +348,7 @@ function AppContent() { const [manuallyCollapsedDays, setManuallyCollapsedDays] = useState>(new Set()); const [manuallyExpandedDays, setManuallyExpandedDays] = useState>(new Set()); - // Load user-specific scheduleDays and takenDoses when user changes + // Load user-specific scheduleDays when user changes useEffect(() => { if (typeof window !== "undefined" && user?.id) { const storedDays = localStorage.getItem(userStorageKey(user.id, "scheduleDays")); @@ -364,28 +364,30 @@ function AppContent() { setManuallyCollapsedDays(new Set()); setManuallyExpandedDays(new Set()); } - - // Load taken doses from server - async function loadTakenDoses() { - try { - const res = await fetch("/api/doses/taken", { credentials: "include" }); - if (res.ok) { - const data = await res.json(); - setTakenDoses(new Set(data.doses.map((d: { doseId: string }) => d.doseId))); - } - // Don't reset on error - keep current state - } catch { - // Don't reset on error - keep current state - } - } - loadTakenDoses(); - - // Poll for updates every 5 seconds (real-time sync with share links) - const interval = setInterval(loadTakenDoses, 5000); - return () => clearInterval(interval); } }, [user?.id]); + // Poll for taken doses from server (works with or without auth) + useEffect(() => { + async function loadTakenDoses() { + try { + const res = await fetch("/api/doses/taken", { credentials: "include" }); + if (res.ok) { + const data = await res.json(); + setTakenDoses(new Set(data.doses.map((d: { doseId: string }) => d.doseId))); + } + // Don't reset on error - keep current state + } catch { + // Don't reset on error - keep current state + } + } + loadTakenDoses(); + + // Poll for updates every 5 seconds (real-time sync with share links) + const interval = setInterval(loadTakenDoses, 5000); + return () => clearInterval(interval); + }, []); + // Get dose ID with optional person suffix function getDoseId(baseDoseId: string, person: string | null): string { return person ? `${baseDoseId}-${person}` : baseDoseId; @@ -1521,8 +1523,9 @@ function AppContent() { const isFutureDose = doseDate.getTime() > todayMidnight.getTime(); // If no takenBy, show single checkbox; otherwise show one per person const people = (dose.takenBy || []).length > 0 ? dose.takenBy : [null]; + const allTaken = people.every((person) => takenDoses.has(getDoseId(dose.id, person))); return ( -
+
{dose.timeStr} {dose.usage} {dose.usage !== 1 ? t('common.pills') : t('common.pill')}{med?.pillWeightMg && ` (${dose.usage * med.pillWeightMg} mg)`}
@@ -2488,13 +2491,17 @@ function AppContent() {

{t('modal.intakeSchedule')} {selectedMed.intakeRemindersEnabled && 🔔}

- {selectedMed.blisters.map((blister, idx) => ( + {selectedMed.blisters.map((blister, idx) => { + const personCount = Math.max(1, selectedMed.takenBy?.length || 1); + const totalUsage = blister.usage * personCount; + return (
- {blister.usage} {blister.usage !== 1 ? t('common.pills') : t('common.pill')}{selectedMed.pillWeightMg && ` (${blister.usage * selectedMed.pillWeightMg} mg)`} + {totalUsage} {totalUsage !== 1 ? t('common.pills') : t('common.pill')}{selectedMed.pillWeightMg && ` (${totalUsage * selectedMed.pillWeightMg} mg)`} {t('form.blisters.every')} {blister.every} {blister.every !== 1 ? t('common.days') : t('common.day')} {t('modal.at')} {new Date(blister.start).toLocaleTimeString(i18n.language, { hour: "2-digit", minute: "2-digit" })}
- ))} + ); + })}
)} @@ -3372,8 +3379,12 @@ type SharedMedication = { genericName?: string | null; pillWeightMg?: number | null; imageUrl?: string | null; - count?: number; - pillsPerBlister?: number; + totalPills: number; + packCount: number; + blistersPerPack: number; + looseTablets: number; + pillsPerBlister: number; + takenBy: string[]; blisters: Blister[]; }; @@ -3910,8 +3921,9 @@ function SharedSchedule() {
{item.doses.map((dose) => { const people = (dose.takenBy || []).length > 0 ? dose.takenBy : [null]; + const allTaken = people.every((person) => takenDoses.has(getDoseId(dose.id, person))); return ( -
+
{dose.timeStr} {dose.usage} {dose.usage !== 1 ? t('common.pills') : t('common.pill')} @@ -4028,6 +4040,7 @@ function SharedSchedule() {
{item.doses.map((dose) => { const people = (dose.takenBy || []).length > 0 ? dose.takenBy : [null]; + const allTaken = people.every((person) => takenDoses.has(getDoseId(dose.id, person))); // Only disable doses on future DAYS, not later today const doseDate = new Date(dose.when); doseDate.setHours(0, 0, 0, 0); @@ -4035,7 +4048,7 @@ function SharedSchedule() { todayMidnight.setHours(0, 0, 0, 0); const isFutureDose = doseDate.getTime() > todayMidnight.getTime(); return ( -
+
{dose.timeStr} {dose.usage} {dose.usage !== 1 ? t('common.pills') : t('common.pill')} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 34d5f16..008cfee 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -809,6 +809,19 @@ textarea.auto-resize { color: var(--text-secondary); } +/* All persons have taken this dose */ +.dose-item.all-taken { + background: var(--success-bg); + border-color: rgba(57, 217, 138, 0.3); + opacity: 0.7; +} + +.dose-item.all-taken .dose-time, +.dose-item.all-taken .dose-usage { + text-decoration: line-through; + color: var(--text-secondary); +} + /* Overdue (past, not taken) doses */ .dose-item.overdue { background: var(--warning-bg);