Show tasks on the routines timeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user