Show tasks on the routines timeline

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 17:02:30 -06:00
parent d45929ddc0
commit 24a1d18b25

View File

@@ -2,7 +2,7 @@
import { useEffect, useState, useRef, useMemo } from 'react';
import { useRouter } from 'next/navigation';
import api from '@/lib/api';
import api, { type Task } from '@/lib/api';
import { PlusIcon, PlayIcon, ClockIcon, CheckIcon } from '@/components/ui/Icons';
import Link from 'next/link';
@@ -208,6 +208,7 @@ export default function RoutinesPage() {
const [allRoutines, setAllRoutines] = useState<Routine[]>([]);
const [allSchedules, setAllSchedules] = useState<ScheduleEntry[]>([]);
const [todayMeds, setTodayMeds] = useState<TodaysMedication[]>([]);
const [allTasks, setAllTasks] = useState<Task[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [selectedDate, setSelectedDate] = useState(() => new Date());
const [nowMinutes, setNowMinutes] = useState(() => {
@@ -232,6 +233,14 @@ export default function RoutinesPage() {
.filter((s) => s.days.includes(dayKey))
.sort((a, b) => timeToMinutes(a.time) - timeToMinutes(b.time));
const tasksForDay = allTasks.filter((t) => {
if (t.status === 'cancelled') return false;
const d = new Date(t.scheduled_datetime);
return d.getFullYear() === selectedDate.getFullYear() &&
d.getMonth() === selectedDate.getMonth() &&
d.getDate() === selectedDate.getDate();
});
const scheduledRoutineIds = new Set(allSchedules.map((s) => s.routine_id));
const unscheduledRoutines = allRoutines.filter(
(r) => !scheduledRoutineIds.has(r.id)
@@ -299,6 +308,10 @@ export default function RoutinesPage() {
const allEventMins = [
...scheduledForDay.map((e) => timeToMinutes(e.time)),
...groupedMedEntries.map((e) => timeToMinutes(e.time)),
...tasksForDay.map((t) => {
const d = new Date(t.scheduled_datetime);
return d.getHours() * 60 + d.getMinutes();
}),
];
const eventStartHour = allEventMins.length > 0 ? Math.floor(Math.min(...allEventMins) / 60) : DEFAULT_START_HOUR;
const eventEndHour = allEventMins.length > 0 ? Math.ceil(Math.max(...allEventMins) / 60) : DEFAULT_END_HOUR;
@@ -325,9 +338,18 @@ export default function RoutinesPage() {
endMin: timeToMinutes(g.time) + durationMin,
};
}),
...tasksForDay.map((t) => {
const d = new Date(t.scheduled_datetime);
const startMin = d.getHours() * 60 + d.getMinutes();
return {
id: `t-${t.id}`,
startMin,
endMin: startMin + (48 / HOUR_HEIGHT) * 60,
};
}),
];
return computeLanes(items);
}, [scheduledForDay, groupedMedEntries]);
}, [scheduledForDay, groupedMedEntries, tasksForDay]);
// ── Handlers ──────────────────────────────────────────────────
const handleTakeMed = async (medicationId: string, scheduledTime: string) => {
@@ -416,11 +438,13 @@ export default function RoutinesPage() {
api.routines.list(),
api.routines.listAllSchedules(),
api.medications.getToday().catch(() => []),
api.tasks.list('all').catch(() => []),
])
.then(([routines, schedules, meds]) => {
.then(([routines, schedules, meds, tasks]) => {
setAllRoutines(routines);
setAllSchedules(schedules);
setTodayMeds(meds);
setAllTasks(tasks);
})
.catch(() => {})
.finally(() => setIsLoading(false));
@@ -776,8 +800,56 @@ export default function RoutinesPage() {
);
})}
{/* Task cards */}
{tasksForDay.map((task) => {
const d = new Date(task.scheduled_datetime);
const startMin = d.getHours() * 60 + d.getMinutes();
const topPx = minutesToTop(startMin, displayStartHour);
const isPast = task.status === 'completed';
const layout = timelineLayout.get(`t-${task.id}`) ?? {
lane: 0,
totalLanes: 1,
};
const timeStr = `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
return (
<div
key={task.id}
onClick={() => router.push('/dashboard/tasks')}
style={{
top: `${topPx}px`,
height: '48px',
...laneStyle(layout.lane, layout.totalLanes),
}}
className={`absolute rounded-xl px-3 py-2 text-left shadow-sm border transition-all overflow-hidden cursor-pointer ${
isPast
? 'bg-green-50 dark:bg-green-900/30 border-green-200 dark:border-green-800 opacity-75'
: 'bg-purple-50 dark:bg-purple-900/30 border-purple-200 dark:border-purple-800'
}`}
>
<div className="flex items-center gap-2">
<span className="text-lg leading-none flex-shrink-0">📋</span>
<div className="min-w-0 flex-1">
<p className="font-semibold text-gray-900 dark:text-gray-100 text-sm truncate">
{task.title}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">
{formatTime(timeStr)}
{task.description && ` · ${task.description}`}
</p>
</div>
{isPast && (
<span className="text-green-600 flex-shrink-0">
<CheckIcon size={16} />
</span>
)}
</div>
</div>
);
})}
{/* Empty day */}
{scheduledForDay.length === 0 && medEntries.length === 0 && (
{scheduledForDay.length === 0 && medEntries.length === 0 && tasksForDay.length === 0 && (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<p className="text-gray-400 text-sm">
No routines or medications for this day