lots of changes leave me alone its better now
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import api from '@/lib/api';
|
||||
import { PlusIcon, CheckIcon, PillIcon, ClockIcon, TrashIcon } from '@/components/ui/Icons';
|
||||
import Link from 'next/link';
|
||||
import PushNotificationToggle from '@/components/notifications/PushNotificationToggle';
|
||||
|
||||
interface Medication {
|
||||
id: string;
|
||||
@@ -32,6 +33,8 @@ interface TodaysMedication {
|
||||
scheduled_times: string[];
|
||||
taken_times: string[];
|
||||
is_prn?: boolean;
|
||||
is_next_day?: boolean;
|
||||
is_previous_day?: boolean;
|
||||
}
|
||||
|
||||
interface AdherenceEntry {
|
||||
@@ -41,6 +44,23 @@ interface AdherenceEntry {
|
||||
is_prn?: boolean;
|
||||
}
|
||||
|
||||
type TimeStatus = 'overdue' | 'due_now' | 'upcoming' | 'taken';
|
||||
|
||||
function getTimeStatus(scheduledTime: string, takenTimes: string[], now: Date): TimeStatus {
|
||||
if (takenTimes.includes(scheduledTime)) return 'taken';
|
||||
|
||||
const [h, m] = scheduledTime.split(':').map(Number);
|
||||
const scheduled = new Date(now);
|
||||
scheduled.setHours(h, m, 0, 0);
|
||||
|
||||
const diffMs = now.getTime() - scheduled.getTime();
|
||||
const diffMin = diffMs / 60000;
|
||||
|
||||
if (diffMin > 15) return 'overdue';
|
||||
if (diffMin >= -15) return 'due_now';
|
||||
return 'upcoming';
|
||||
}
|
||||
|
||||
const formatSchedule = (med: Medication): string => {
|
||||
if (med.frequency === 'specific_days' && med.days_of_week?.length) {
|
||||
return med.days_of_week.map(d => d.charAt(0).toUpperCase() + d.slice(1)).join(', ');
|
||||
@@ -53,12 +73,19 @@ const formatSchedule = (med: Medication): string => {
|
||||
return 'Daily';
|
||||
};
|
||||
|
||||
interface DueEntry {
|
||||
item: TodaysMedication;
|
||||
time: string;
|
||||
status: TimeStatus;
|
||||
}
|
||||
|
||||
export default function MedicationsPage() {
|
||||
const router = useRouter();
|
||||
const [medications, setMedications] = useState<Medication[]>([]);
|
||||
const [todayMeds, setTodayMeds] = useState<TodaysMedication[]>([]);
|
||||
const [adherence, setAdherence] = useState<AdherenceEntry[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [tick, setTick] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
@@ -80,6 +107,51 @@ export default function MedicationsPage() {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
// Auto-refresh grouping every 60s
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => setTick(t => t + 1), 60000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const { dueEntries, upcomingEntries, prnEntries } = useMemo(() => {
|
||||
const now = new Date();
|
||||
const due: DueEntry[] = [];
|
||||
const upcoming: DueEntry[] = [];
|
||||
const prn: TodaysMedication[] = [];
|
||||
|
||||
for (const item of todayMeds) {
|
||||
if (item.is_prn) {
|
||||
prn.push(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Next-day meds are always upcoming
|
||||
if (item.is_next_day) {
|
||||
for (const time of item.scheduled_times) {
|
||||
upcoming.push({ item, time, status: 'upcoming' });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const time of item.scheduled_times) {
|
||||
const status = getTimeStatus(time, item.taken_times, now);
|
||||
if (status === 'upcoming') {
|
||||
upcoming.push({ item, time, status });
|
||||
} else {
|
||||
due.push({ item, time, status });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort due: overdue first, then due_now, then by time
|
||||
const statusOrder: Record<TimeStatus, number> = { overdue: 0, due_now: 1, taken: 2, upcoming: 3 };
|
||||
due.sort((a, b) => statusOrder[a.status] - statusOrder[b.status] || a.time.localeCompare(b.time));
|
||||
upcoming.sort((a, b) => a.time.localeCompare(b.time));
|
||||
|
||||
return { dueEntries: due, upcomingEntries: upcoming, prnEntries: prn };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [todayMeds, tick]);
|
||||
|
||||
const handleTake = async (medId: string, time?: string) => {
|
||||
try {
|
||||
await api.medications.take(medId, time);
|
||||
@@ -121,6 +193,13 @@ export default function MedicationsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const borderColor = (status: TimeStatus) => {
|
||||
if (status === 'overdue') return 'border-l-4 border-l-red-500';
|
||||
if (status === 'due_now') return 'border-l-4 border-l-amber-500';
|
||||
if (status === 'taken') return 'border-l-4 border-l-green-500';
|
||||
return '';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -130,63 +209,117 @@ export default function MedicationsPage() {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Today's Schedule */}
|
||||
{todayMeds.length > 0 && (
|
||||
{/* Push Notification Toggle */}
|
||||
<PushNotificationToggle />
|
||||
|
||||
{/* Due Now Section */}
|
||||
{dueEntries.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-3">Today</h2>
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-3">Due</h2>
|
||||
<div className="space-y-3">
|
||||
{todayMeds.map((item) => (
|
||||
{dueEntries.map((entry) => (
|
||||
<div
|
||||
key={`${entry.item.medication.id}-${entry.time}`}
|
||||
className={`bg-white rounded-xl p-4 shadow-sm ${borderColor(entry.status)}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold text-gray-900">{entry.item.medication.name}</h3>
|
||||
{entry.item.is_previous_day && (
|
||||
<span className="text-xs bg-purple-100 text-purple-700 px-2 py-0.5 rounded">Yesterday</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">{entry.item.medication.dosage} {entry.item.medication.unit}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between bg-gray-50 rounded-lg p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<ClockIcon size={16} className="text-gray-500" />
|
||||
<span className="font-medium">{entry.time}</span>
|
||||
{entry.status === 'overdue' && (
|
||||
<span className="text-xs text-red-600 font-medium">Overdue</span>
|
||||
)}
|
||||
</div>
|
||||
{entry.status === 'taken' ? (
|
||||
<span className="text-green-600 font-medium flex items-center gap-1">
|
||||
<CheckIcon size={16} /> Taken
|
||||
</span>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleTake(entry.item.medication.id, entry.time)}
|
||||
className="bg-green-600 text-white px-3 py-1 rounded-lg text-sm font-medium"
|
||||
>
|
||||
Take
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSkip(entry.item.medication.id, entry.time)}
|
||||
className="text-gray-500 px-2 py-1"
|
||||
>
|
||||
Skip
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* PRN Section */}
|
||||
{prnEntries.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-3">As Needed</h2>
|
||||
<div className="space-y-3">
|
||||
{prnEntries.map((item) => (
|
||||
<div key={item.medication.id} className="bg-white rounded-xl p-4 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900">{item.medication.name}</h3>
|
||||
<p className="text-sm text-gray-500">{item.medication.dosage} {item.medication.unit}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{item.is_prn ? (
|
||||
<div className="flex items-center justify-between bg-gray-50 rounded-lg p-3">
|
||||
<span className="text-gray-500 text-sm">As needed</span>
|
||||
<button
|
||||
onClick={() => handleTake(item.medication.id)}
|
||||
className="bg-green-600 text-white px-3 py-1 rounded-lg text-sm font-medium"
|
||||
>
|
||||
Log Dose
|
||||
</button>
|
||||
<div className="flex items-center justify-between bg-gray-50 rounded-lg p-3">
|
||||
<span className="text-gray-500 text-sm">As needed</span>
|
||||
<button
|
||||
onClick={() => handleTake(item.medication.id)}
|
||||
className="bg-green-600 text-white px-3 py-1 rounded-lg text-sm font-medium"
|
||||
>
|
||||
Log Dose
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Upcoming Section */}
|
||||
{upcomingEntries.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-500 mb-3">Upcoming</h2>
|
||||
<div className="space-y-3">
|
||||
{upcomingEntries.map((entry) => (
|
||||
<div
|
||||
key={`${entry.item.medication.id}-${entry.time}`}
|
||||
className="bg-gray-50 rounded-xl p-4 shadow-sm opacity-75"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-medium text-gray-700">{entry.item.medication.name}</h3>
|
||||
{entry.item.is_next_day && (
|
||||
<span className="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded">Tomorrow</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
item.scheduled_times.map((time) => {
|
||||
const isTaken = item.taken_times.includes(time);
|
||||
return (
|
||||
<div key={time} className="flex items-center justify-between bg-gray-50 rounded-lg p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<ClockIcon size={16} className="text-gray-500" />
|
||||
<span className="font-medium">{time}</span>
|
||||
</div>
|
||||
{isTaken ? (
|
||||
<span className="text-green-600 font-medium flex items-center gap-1">
|
||||
<CheckIcon size={16} /> Taken
|
||||
</span>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleTake(item.medication.id, time)}
|
||||
className="bg-green-600 text-white px-3 py-1 rounded-lg text-sm font-medium"
|
||||
>
|
||||
Take
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSkip(item.medication.id, time)}
|
||||
className="text-gray-500 px-2 py-1"
|
||||
>
|
||||
Skip
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
<p className="text-sm text-gray-400">{entry.item.medication.dosage} {entry.item.medication.unit}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-gray-400">
|
||||
<ClockIcon size={16} />
|
||||
<span className="font-medium">{entry.time}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -88,7 +88,7 @@ export default function RoutineDetailPage() {
|
||||
name: newStepName,
|
||||
duration_minutes: newStepDuration,
|
||||
});
|
||||
setSteps([...steps, { ...step, position: steps.length + 1 }]);
|
||||
setSteps([...steps, { ...step, name: newStepName, step_type: 'generic', position: steps.length + 1 }]);
|
||||
setNewStepName('');
|
||||
} catch (err) {
|
||||
console.error('Failed to add step:', err);
|
||||
|
||||
Reference in New Issue
Block a user