Allow scheduling multiple medications at once, remove time conflicts
- Multi-med creation form: add any number of medication cards in one session, each with independent name/dosage/unit/frequency/times settings - Submit button labels dynamically (Add 1 / Add N Medications) - Removed all schedule conflict checking — medications can now coexist at the same time slot as each other and as routines Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -145,56 +145,6 @@ def register(app):
|
|||||||
meds = postgres.select("medications", where={"user_uuid": user_uuid}, order_by="name")
|
meds = postgres.select("medications", where={"user_uuid": user_uuid}, order_by="name")
|
||||||
return flask.jsonify(meds), 200
|
return flask.jsonify(meds), 200
|
||||||
|
|
||||||
def _time_str_to_minutes(time_str):
|
|
||||||
"""Convert 'HH:MM' to minutes since midnight."""
|
|
||||||
parts = time_str.split(":")
|
|
||||||
return int(parts[0]) * 60 + int(parts[1])
|
|
||||||
|
|
||||||
def _get_routine_duration_minutes(routine_id):
|
|
||||||
"""Get total duration of a routine from its steps."""
|
|
||||||
steps = postgres.select("routine_steps", where={"routine_id": routine_id})
|
|
||||||
total = sum(s.get("duration_minutes", 0) or 0 for s in steps)
|
|
||||||
return max(total, 1)
|
|
||||||
|
|
||||||
def _check_med_schedule_conflicts(user_uuid, new_times, new_days=None, exclude_med_id=None):
|
|
||||||
"""Check if the proposed medication schedule conflicts with existing routines or medications.
|
|
||||||
Returns (has_conflict, conflict_message) tuple.
|
|
||||||
"""
|
|
||||||
if not new_times:
|
|
||||||
return False, None
|
|
||||||
|
|
||||||
# Check conflicts with routines (duration-aware)
|
|
||||||
user_routines = postgres.select("routines", {"user_uuid": user_uuid})
|
|
||||||
for r in user_routines:
|
|
||||||
sched = postgres.select_one("routine_schedules", {"routine_id": r["id"]})
|
|
||||||
if not sched or not sched.get("time"):
|
|
||||||
continue
|
|
||||||
routine_days = sched.get("days", [])
|
|
||||||
if isinstance(routine_days, str):
|
|
||||||
routine_days = json.loads(routine_days)
|
|
||||||
if new_days and not any(d in routine_days for d in new_days):
|
|
||||||
continue
|
|
||||||
routine_start = _time_str_to_minutes(sched["time"])
|
|
||||||
routine_dur = _get_routine_duration_minutes(r["id"])
|
|
||||||
for t in new_times:
|
|
||||||
med_start = _time_str_to_minutes(t)
|
|
||||||
# Med falls within routine time range
|
|
||||||
if routine_start <= med_start < routine_start + routine_dur:
|
|
||||||
return True, f"Time conflicts with routine: {r.get('name', 'Unnamed routine')}"
|
|
||||||
|
|
||||||
# Check conflicts with other medications
|
|
||||||
user_meds = postgres.select("medications", {"user_uuid": user_uuid, "active": True})
|
|
||||||
for med in user_meds:
|
|
||||||
if med["id"] == exclude_med_id:
|
|
||||||
continue
|
|
||||||
med_times = med.get("times", [])
|
|
||||||
if isinstance(med_times, str):
|
|
||||||
med_times = json.loads(med_times)
|
|
||||||
if any(t in med_times for t in new_times):
|
|
||||||
return True, f"Time conflicts with medication: {med.get('name', 'Unnamed medication')}"
|
|
||||||
|
|
||||||
return False, None
|
|
||||||
|
|
||||||
@app.route("/api/medications", methods=["POST"])
|
@app.route("/api/medications", methods=["POST"])
|
||||||
def api_addMedication():
|
def api_addMedication():
|
||||||
"""Add a medication. Body: {name, dosage, unit, frequency, times?, days_of_week?, interval_days?, start_date?, notes?}"""
|
"""Add a medication. Body: {name, dosage, unit, frequency, times?, days_of_week?, interval_days?, start_date?, notes?}"""
|
||||||
@@ -214,15 +164,6 @@ def register(app):
|
|||||||
if not data.get("start_date") or not data.get("interval_days"):
|
if not data.get("start_date") or not data.get("interval_days"):
|
||||||
return flask.jsonify({"error": "every_n_days frequency requires both start_date and interval_days"}), 400
|
return flask.jsonify({"error": "every_n_days frequency requires both start_date and interval_days"}), 400
|
||||||
|
|
||||||
# Check for schedule conflicts
|
|
||||||
new_times = data.get("times", [])
|
|
||||||
new_days = data.get("days_of_week", [])
|
|
||||||
has_conflict, conflict_msg = _check_med_schedule_conflicts(
|
|
||||||
user_uuid, new_times, new_days
|
|
||||||
)
|
|
||||||
if has_conflict:
|
|
||||||
return flask.jsonify({"error": conflict_msg}), 409
|
|
||||||
|
|
||||||
row = {
|
row = {
|
||||||
"id": str(uuid.uuid4()),
|
"id": str(uuid.uuid4()),
|
||||||
"user_uuid": user_uuid,
|
"user_uuid": user_uuid,
|
||||||
@@ -283,16 +224,6 @@ def register(app):
|
|||||||
"days_of_week", "interval_days", "start_date", "next_dose_date",
|
"days_of_week", "interval_days", "start_date", "next_dose_date",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Check for schedule conflicts if times are being updated
|
|
||||||
if "times" in data:
|
|
||||||
new_times = data.get("times", [])
|
|
||||||
new_days = data.get("days_of_week") or existing.get("days_of_week", [])
|
|
||||||
has_conflict, conflict_msg = _check_med_schedule_conflicts(
|
|
||||||
user_uuid, new_times, new_days, exclude_med_id=med_id
|
|
||||||
)
|
|
||||||
if has_conflict:
|
|
||||||
return flask.jsonify({"error": conflict_msg}), 409
|
|
||||||
|
|
||||||
updates = {k: v for k, v in data.items() if k in allowed}
|
updates = {k: v for k, v in data.items() if k in allowed}
|
||||||
if not updates:
|
if not updates:
|
||||||
return flask.jsonify({"error": "no valid fields to update"}), 400
|
return flask.jsonify({"error": "no valid fields to update"}), 400
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import api from '@/lib/api';
|
import api from '@/lib/api';
|
||||||
import { ArrowLeftIcon } from '@/components/ui/Icons';
|
import { ArrowLeftIcon, PlusIcon, TrashIcon } from '@/components/ui/Icons';
|
||||||
|
|
||||||
const DAY_OPTIONS = [
|
const DAY_OPTIONS = [
|
||||||
{ value: 'mon', label: 'Mon' },
|
{ value: 'mon', label: 'Mon' },
|
||||||
@@ -15,96 +15,78 @@ const DAY_OPTIONS = [
|
|||||||
{ value: 'sun', label: 'Sun' },
|
{ value: 'sun', label: 'Sun' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function NewMedicationPage() {
|
interface MedEntry {
|
||||||
const router = useRouter();
|
id: string;
|
||||||
const [name, setName] = useState('');
|
name: string;
|
||||||
const [dosage, setDosage] = useState('');
|
dosage: string;
|
||||||
const [unit, setUnit] = useState('mg');
|
unit: string;
|
||||||
const [frequency, setFrequency] = useState('daily');
|
frequency: string;
|
||||||
const [times, setTimes] = useState<string[]>(['08:00']);
|
times: string[];
|
||||||
const [daysOfWeek, setDaysOfWeek] = useState<string[]>([]);
|
daysOfWeek: string[];
|
||||||
const [intervalDays, setIntervalDays] = useState(7);
|
intervalDays: number;
|
||||||
const [startDate, setStartDate] = useState(new Date().toISOString().slice(0, 10));
|
startDate: string;
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [error, setError] = useState('');
|
|
||||||
|
|
||||||
const handleAddTime = () => {
|
|
||||||
setTimes([...times, '12:00']);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveTime = (index: number) => {
|
|
||||||
setTimes(times.filter((_, i) => i !== index));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTimeChange = (index: number, value: string) => {
|
|
||||||
const newTimes = [...times];
|
|
||||||
newTimes[index] = value;
|
|
||||||
setTimes(newTimes);
|
|
||||||
};
|
|
||||||
|
|
||||||
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 of the week');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
function blankEntry(): MedEntry {
|
||||||
setError('');
|
return {
|
||||||
|
id: `med-${Date.now()}-${Math.random()}`,
|
||||||
|
name: '',
|
||||||
|
dosage: '',
|
||||||
|
unit: 'mg',
|
||||||
|
frequency: 'daily',
|
||||||
|
times: ['08:00'],
|
||||||
|
daysOfWeek: [],
|
||||||
|
intervalDays: 7,
|
||||||
|
startDate: new Date().toISOString().slice(0, 10),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
function MedCard({
|
||||||
await api.medications.create({
|
entry,
|
||||||
name,
|
index,
|
||||||
dosage,
|
total,
|
||||||
unit,
|
onChange,
|
||||||
frequency,
|
onRemove,
|
||||||
times: frequency === 'as_needed' ? [] : times,
|
}: {
|
||||||
...(frequency === 'specific_days' && { days_of_week: daysOfWeek }),
|
entry: MedEntry;
|
||||||
...(frequency === 'every_n_days' && { interval_days: intervalDays, start_date: startDate }),
|
index: number;
|
||||||
|
total: number;
|
||||||
|
onChange: (updates: Partial<MedEntry>) => void;
|
||||||
|
onRemove: () => void;
|
||||||
|
}) {
|
||||||
|
const handleAddTime = () => onChange({ times: [...entry.times, '12:00'] });
|
||||||
|
const handleRemoveTime = (i: number) => onChange({ times: entry.times.filter((_, idx) => idx !== i) });
|
||||||
|
const handleTimeChange = (i: number, val: string) => {
|
||||||
|
const t = [...entry.times];
|
||||||
|
t[i] = val;
|
||||||
|
onChange({ times: t });
|
||||||
|
};
|
||||||
|
const toggleDay = (day: string) =>
|
||||||
|
onChange({
|
||||||
|
daysOfWeek: entry.daysOfWeek.includes(day)
|
||||||
|
? entry.daysOfWeek.filter(d => d !== day)
|
||||||
|
: [...entry.daysOfWeek, day],
|
||||||
});
|
});
|
||||||
router.push('/dashboard/medications');
|
|
||||||
} catch (err) {
|
|
||||||
setError((err as Error).message || 'Failed to add medication');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
|
|
||||||
<header className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-10">
|
|
||||||
<div className="flex items-center gap-3 px-4 py-3">
|
|
||||||
<button onClick={() => router.back()} className="p-1 text-gray-600 dark:text-gray-400">
|
|
||||||
<ArrowLeftIcon size={24} />
|
|
||||||
</button>
|
|
||||||
<h1 className="text-xl font-bold text-gray-900 dark:text-gray-100">Add Medication</h1>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="p-4 space-y-6">
|
|
||||||
{error && (
|
|
||||||
<div className="bg-red-50 dark:bg-red-900/30 text-red-600 dark:text-red-400 px-4 py-3 rounded-lg text-sm">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm space-y-4">
|
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-semibold text-gray-500 dark:text-gray-400">
|
||||||
|
Medication {index + 1}
|
||||||
|
</span>
|
||||||
|
{total > 1 && (
|
||||||
|
<button type="button" onClick={onRemove} className="text-red-500 dark:text-red-400 p-1">
|
||||||
|
<TrashIcon size={18} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Medication Name</label>
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Name</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={name}
|
value={entry.name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={e => onChange({ name: e.target.value })}
|
||||||
placeholder="e.g., Vitamin D"
|
placeholder="e.g., Vitamin D"
|
||||||
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 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 outline-none"
|
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 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 outline-none"
|
||||||
/>
|
/>
|
||||||
@@ -115,8 +97,8 @@ export default function NewMedicationPage() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Dosage</label>
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Dosage</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={dosage}
|
value={entry.dosage}
|
||||||
onChange={(e) => setDosage(e.target.value)}
|
onChange={e => onChange({ dosage: e.target.value })}
|
||||||
placeholder="e.g., 1000"
|
placeholder="e.g., 1000"
|
||||||
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 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 outline-none"
|
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 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 outline-none"
|
||||||
/>
|
/>
|
||||||
@@ -124,8 +106,8 @@ export default function NewMedicationPage() {
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Unit</label>
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Unit</label>
|
||||||
<select
|
<select
|
||||||
value={unit}
|
value={entry.unit}
|
||||||
onChange={(e) => setUnit(e.target.value)}
|
onChange={e => onChange({ unit: 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"
|
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"
|
||||||
>
|
>
|
||||||
<option value="mg">mg</option>
|
<option value="mg">mg</option>
|
||||||
@@ -142,8 +124,8 @@ export default function NewMedicationPage() {
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Frequency</label>
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Frequency</label>
|
||||||
<select
|
<select
|
||||||
value={frequency}
|
value={entry.frequency}
|
||||||
onChange={(e) => setFrequency(e.target.value)}
|
onChange={e => onChange({ frequency: 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"
|
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"
|
||||||
>
|
>
|
||||||
<option value="daily">Daily</option>
|
<option value="daily">Daily</option>
|
||||||
@@ -153,8 +135,7 @@ export default function NewMedicationPage() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Day-of-week picker for specific_days */}
|
{entry.frequency === 'specific_days' && (
|
||||||
{frequency === 'specific_days' && (
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Days</label>
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Days</label>
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
@@ -164,7 +145,7 @@ export default function NewMedicationPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => toggleDay(value)}
|
onClick={() => toggleDay(value)}
|
||||||
className={`px-3 py-2 rounded-lg text-sm font-medium border transition-colors ${
|
className={`px-3 py-2 rounded-lg text-sm font-medium border transition-colors ${
|
||||||
daysOfWeek.includes(value)
|
entry.daysOfWeek.includes(value)
|
||||||
? 'bg-indigo-600 text-white border-indigo-600'
|
? 'bg-indigo-600 text-white border-indigo-600'
|
||||||
: 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600'
|
: 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600'
|
||||||
}`}
|
}`}
|
||||||
@@ -176,33 +157,31 @@ export default function NewMedicationPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Interval settings for every_n_days */}
|
{entry.frequency === 'every_n_days' && (
|
||||||
{frequency === 'every_n_days' && (
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Every N Days</label>
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Every N Days</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
value={intervalDays}
|
value={entry.intervalDays}
|
||||||
onChange={(e) => setIntervalDays(parseInt(e.target.value) || 1)}
|
onChange={e => onChange({ intervalDays: 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 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 outline-none"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Starting From</label>
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Starting From</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={startDate}
|
value={entry.startDate}
|
||||||
onChange={(e) => setStartDate(e.target.value)}
|
onChange={e => onChange({ startDate: 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"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Times picker — hidden for as_needed */}
|
{entry.frequency !== 'as_needed' && (
|
||||||
{frequency !== 'as_needed' && (
|
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Times</label>
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Times</label>
|
||||||
@@ -214,22 +193,19 @@ export default function NewMedicationPage() {
|
|||||||
+ Add Time
|
+ Add Time
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{frequency === 'daily' && (
|
|
||||||
<p className="text-xs text-gray-400 dark:text-gray-500 mb-2">Add multiple times for 2x, 3x, or more doses per day</p>
|
|
||||||
)}
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{times.map((time, index) => (
|
{entry.times.map((time, i) => (
|
||||||
<div key={index} className="flex gap-2">
|
<div key={i} className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="time"
|
type="time"
|
||||||
value={time}
|
value={time}
|
||||||
onChange={(e) => handleTimeChange(index, e.target.value)}
|
onChange={e => 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"
|
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 && (
|
{entry.times.length > 1 && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleRemoveTime(index)}
|
onClick={() => handleRemoveTime(i)}
|
||||||
className="text-red-500 dark:text-red-400 px-3"
|
className="text-red-500 dark:text-red-400 px-3"
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
@@ -241,13 +217,114 @@ export default function NewMedicationPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NewMedicationPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [entries, setEntries] = useState<MedEntry[]>([blankEntry()]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
const updateEntry = (index: number, updates: Partial<MedEntry>) => {
|
||||||
|
setEntries(prev => prev.map((e, i) => (i === index ? { ...e, ...updates } : e)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeEntry = (index: number) => {
|
||||||
|
setEntries(prev => prev.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
for (let i = 0; i < entries.length; i++) {
|
||||||
|
const entry = entries[i];
|
||||||
|
if (!entry.name.trim() || !entry.dosage.trim()) {
|
||||||
|
setError(`Medication ${i + 1}: name and dosage are required`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (entry.frequency === 'specific_days' && entry.daysOfWeek.length === 0) {
|
||||||
|
setError(`Medication ${i + 1}: select at least one day of the week`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const entry of entries) {
|
||||||
|
await api.medications.create({
|
||||||
|
name: entry.name,
|
||||||
|
dosage: entry.dosage,
|
||||||
|
unit: entry.unit,
|
||||||
|
frequency: entry.frequency,
|
||||||
|
times: entry.frequency === 'as_needed' ? [] : entry.times,
|
||||||
|
...(entry.frequency === 'specific_days' && { days_of_week: entry.daysOfWeek }),
|
||||||
|
...(entry.frequency === 'every_n_days' && {
|
||||||
|
interval_days: entry.intervalDays,
|
||||||
|
start_date: entry.startDate,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
router.push('/dashboard/medications');
|
||||||
|
} catch (err) {
|
||||||
|
setError((err as Error).message || 'Failed to add medication');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const count = entries.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
|
||||||
|
<header className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-10">
|
||||||
|
<div className="flex items-center gap-3 px-4 py-3">
|
||||||
|
<button onClick={() => router.back()} className="p-1 text-gray-600 dark:text-gray-400">
|
||||||
|
<ArrowLeftIcon size={24} />
|
||||||
|
</button>
|
||||||
|
<h1 className="text-xl font-bold text-gray-900 dark:text-gray-100">Add Medications</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="p-4 space-y-4">
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 dark:bg-red-900/30 text-red-600 dark:text-red-400 px-4 py-3 rounded-lg text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{entries.map((entry, index) => (
|
||||||
|
<MedCard
|
||||||
|
key={entry.id}
|
||||||
|
entry={entry}
|
||||||
|
index={index}
|
||||||
|
total={count}
|
||||||
|
onChange={updates => updateEntry(index, updates)}
|
||||||
|
onRemove={() => removeEntry(index)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setEntries(prev => [...prev, blankEntry()])}
|
||||||
|
className="w-full flex items-center justify-center gap-2 py-3 border-2 border-dashed border-indigo-300 dark:border-indigo-700 rounded-xl text-indigo-600 dark:text-indigo-400 font-medium hover:border-indigo-400 dark:hover:border-indigo-600 transition-colors"
|
||||||
|
>
|
||||||
|
<PlusIcon size={18} />
|
||||||
|
Add Another Medication
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="w-full bg-indigo-600 text-white font-semibold py-4 rounded-xl hover:bg-indigo-700 disabled:opacity-50"
|
className="w-full bg-indigo-600 text-white font-semibold py-4 rounded-xl hover:bg-indigo-700 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{isLoading ? 'Adding...' : 'Add Medication'}
|
{isLoading
|
||||||
|
? 'Adding...'
|
||||||
|
: count === 1
|
||||||
|
? 'Add Medication'
|
||||||
|
: `Add ${count} Medications`}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user