From d45929ddc07f8232e59eb7b6a47027726c4e062a Mon Sep 17 00:00:00 2001 From: chelsea Date: Thu, 19 Feb 2026 16:49:12 -0600 Subject: [PATCH] Fix off-day med reminders and add medication editing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scheduler: check_nagging() now calls _is_med_due_today() before creating on-demand schedules or processing existing ones — prevents nagging for specific_days / every_n_days meds on days they are not scheduled. Web client: add Edit button (pencil icon) on each medication card linking to /dashboard/medications/[id]/edit — new page pre-populates the full form (name, dosage, unit, frequency, times, days, interval, notes) and submits PUT /api/medications/:id on save. Co-Authored-By: Claude Sonnet 4.6 --- scheduler/daemon.py | 8 +- .../dashboard/medications/[id]/edit/page.tsx | 262 ++++++++++++++++++ .../src/app/dashboard/medications/page.tsx | 22 +- 3 files changed, 284 insertions(+), 8 deletions(-) create mode 100644 synculous-client/src/app/dashboard/medications/[id]/edit/page.tsx diff --git a/scheduler/daemon.py b/scheduler/daemon.py index 5bfd13f..b87119b 100644 --- a/scheduler/daemon.py +++ b/scheduler/daemon.py @@ -317,6 +317,10 @@ def check_nagging(): now = _user_now_for(user_uuid) today = now.date() + # Skip nagging if medication is not due today + if not _is_med_due_today(med, today): + continue + # Get today's schedules try: schedules = postgres.select( @@ -333,8 +337,10 @@ def check_nagging(): # Table may not exist yet continue - # If no schedules exist, try to create them + # If no schedules exist, try to create them — but only if med is due today if not schedules: + if not _is_med_due_today(med, today): + continue logger.info(f"No schedules found for medication {med_id}, attempting to create") times = med.get("times", []) if times: diff --git a/synculous-client/src/app/dashboard/medications/[id]/edit/page.tsx b/synculous-client/src/app/dashboard/medications/[id]/edit/page.tsx new file mode 100644 index 0000000..8c1f156 --- /dev/null +++ b/synculous-client/src/app/dashboard/medications/[id]/edit/page.tsx @@ -0,0 +1,262 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useRouter, useParams } from 'next/navigation'; +import api from '@/lib/api'; +import { ArrowLeftIcon } from '@/components/ui/Icons'; + +const DAY_OPTIONS = [ + { value: 'mon', label: 'Mon' }, + { value: 'tue', label: 'Tue' }, + { value: 'wed', label: 'Wed' }, + { value: 'thu', label: 'Thu' }, + { value: 'fri', label: 'Fri' }, + { value: 'sat', label: 'Sat' }, + { value: 'sun', label: 'Sun' }, +]; + +export default function EditMedicationPage() { + const router = useRouter(); + const params = useParams(); + const medId = params.id as string; + + const [isLoadingMed, setIsLoadingMed] = useState(true); + const [name, setName] = useState(''); + const [dosage, setDosage] = useState(''); + const [unit, setUnit] = useState('mg'); + const [frequency, setFrequency] = useState('daily'); + const [times, setTimes] = useState(['08:00']); + const [daysOfWeek, setDaysOfWeek] = useState([]); + const [intervalDays, setIntervalDays] = useState(7); + const [startDate, setStartDate] = useState(new Date().toISOString().slice(0, 10)); + const [notes, setNotes] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(''); + + useEffect(() => { + api.medications.get(medId) + .then(med => { + setName(med.name); + setDosage(String(med.dosage)); + setUnit(med.unit); + setFrequency((med as any).frequency || 'daily'); + setTimes((med as any).times?.length ? (med as any).times : ['08:00']); + setDaysOfWeek((med as any).days_of_week || []); + setIntervalDays((med as any).interval_days || 7); + setStartDate((med as any).start_date?.slice(0, 10) || new Date().toISOString().slice(0, 10)); + setNotes(med.notes || ''); + }) + .catch(() => setError('Failed to load medication.')) + .finally(() => setIsLoadingMed(false)); + }, [medId]); + + const handleAddTime = () => setTimes([...times, '12:00']); + const handleRemoveTime = (i: number) => setTimes(times.filter((_, idx) => idx !== i)); + const handleTimeChange = (i: number, val: string) => { + const t = [...times]; t[i] = val; setTimes(t); + }; + const toggleDay = (day: string) => + setDaysOfWeek(prev => prev.includes(day) ? prev.filter(d => d !== day) : [...prev, day]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!name.trim() || !dosage.trim()) { setError('Name and dosage are required.'); return; } + if (frequency === 'specific_days' && daysOfWeek.length === 0) { setError('Select at least one day.'); return; } + + setIsSubmitting(true); + setError(''); + try { + await api.medications.update(medId, { + name: name.trim(), + dosage: dosage.trim(), + unit, + frequency, + times: frequency === 'as_needed' ? [] : times, + ...(frequency === 'specific_days' && { days_of_week: daysOfWeek }), + ...(frequency === 'every_n_days' && { interval_days: intervalDays, start_date: startDate }), + ...(notes.trim() && { notes: notes.trim() }), + }); + router.push('/dashboard/medications'); + } catch (err) { + setError((err as Error).message || 'Failed to save changes.'); + setIsSubmitting(false); + } + }; + + if (isLoadingMed) { + return ( +
+
+
+ ); + } + + return ( +
+
+
+ +

Edit Medication

+
+
+ +
+ {error && ( +
+ {error} +
+ )} + +
+
+ + setName(e.target.value)} + className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-indigo-500 outline-none" + /> +
+ +
+
+ + setDosage(e.target.value)} + className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-indigo-500 outline-none" + /> +
+
+ + +
+
+ +
+ + +
+ + {frequency === 'specific_days' && ( +
+ +
+ {DAY_OPTIONS.map(({ value, label }) => ( + + ))} +
+
+ )} + + {frequency === 'every_n_days' && ( +
+
+ + setIntervalDays(parseInt(e.target.value) || 1)} + className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-indigo-500 outline-none" + /> +
+
+ + setStartDate(e.target.value)} + className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-indigo-500 outline-none" + /> +
+
+ )} + + {frequency !== 'as_needed' && ( +
+
+ + +
+
+ {times.map((time, i) => ( +
+ handleTimeChange(i, e.target.value)} + className="flex-1 px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-indigo-500 outline-none" + /> + {times.length > 1 && ( + + )} +
+ ))} +
+
+ )} + +
+ +