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 { useEffect, useState, useRef, useMemo } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
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 { PlusIcon, PlayIcon, ClockIcon, CheckIcon } from '@/components/ui/Icons';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
@@ -208,6 +208,7 @@ export default function RoutinesPage() {
|
|||||||
const [allRoutines, setAllRoutines] = useState<Routine[]>([]);
|
const [allRoutines, setAllRoutines] = useState<Routine[]>([]);
|
||||||
const [allSchedules, setAllSchedules] = useState<ScheduleEntry[]>([]);
|
const [allSchedules, setAllSchedules] = useState<ScheduleEntry[]>([]);
|
||||||
const [todayMeds, setTodayMeds] = useState<TodaysMedication[]>([]);
|
const [todayMeds, setTodayMeds] = useState<TodaysMedication[]>([]);
|
||||||
|
const [allTasks, setAllTasks] = useState<Task[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [selectedDate, setSelectedDate] = useState(() => new Date());
|
const [selectedDate, setSelectedDate] = useState(() => new Date());
|
||||||
const [nowMinutes, setNowMinutes] = useState(() => {
|
const [nowMinutes, setNowMinutes] = useState(() => {
|
||||||
@@ -232,6 +233,14 @@ export default function RoutinesPage() {
|
|||||||
.filter((s) => s.days.includes(dayKey))
|
.filter((s) => s.days.includes(dayKey))
|
||||||
.sort((a, b) => timeToMinutes(a.time) - timeToMinutes(b.time));
|
.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 scheduledRoutineIds = new Set(allSchedules.map((s) => s.routine_id));
|
||||||
const unscheduledRoutines = allRoutines.filter(
|
const unscheduledRoutines = allRoutines.filter(
|
||||||
(r) => !scheduledRoutineIds.has(r.id)
|
(r) => !scheduledRoutineIds.has(r.id)
|
||||||
@@ -299,6 +308,10 @@ export default function RoutinesPage() {
|
|||||||
const allEventMins = [
|
const allEventMins = [
|
||||||
...scheduledForDay.map((e) => timeToMinutes(e.time)),
|
...scheduledForDay.map((e) => timeToMinutes(e.time)),
|
||||||
...groupedMedEntries.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 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;
|
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,
|
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);
|
return computeLanes(items);
|
||||||
}, [scheduledForDay, groupedMedEntries]);
|
}, [scheduledForDay, groupedMedEntries, tasksForDay]);
|
||||||
|
|
||||||
// ── Handlers ──────────────────────────────────────────────────
|
// ── Handlers ──────────────────────────────────────────────────
|
||||||
const handleTakeMed = async (medicationId: string, scheduledTime: string) => {
|
const handleTakeMed = async (medicationId: string, scheduledTime: string) => {
|
||||||
@@ -416,11 +438,13 @@ export default function RoutinesPage() {
|
|||||||
api.routines.list(),
|
api.routines.list(),
|
||||||
api.routines.listAllSchedules(),
|
api.routines.listAllSchedules(),
|
||||||
api.medications.getToday().catch(() => []),
|
api.medications.getToday().catch(() => []),
|
||||||
|
api.tasks.list('all').catch(() => []),
|
||||||
])
|
])
|
||||||
.then(([routines, schedules, meds]) => {
|
.then(([routines, schedules, meds, tasks]) => {
|
||||||
setAllRoutines(routines);
|
setAllRoutines(routines);
|
||||||
setAllSchedules(schedules);
|
setAllSchedules(schedules);
|
||||||
setTodayMeds(meds);
|
setTodayMeds(meds);
|
||||||
|
setAllTasks(tasks);
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.finally(() => setIsLoading(false));
|
.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 */}
|
{/* 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">
|
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||||
<p className="text-gray-400 text-sm">
|
<p className="text-gray-400 text-sm">
|
||||||
No routines or medications for this day
|
No routines or medications for this day
|
||||||
|
|||||||
Reference in New Issue
Block a user