Add Brili-style scheduled routines timeline view
Replaces the flat routine card list with a day-oriented timeline showing scheduled routines at their time slots, with week strip navigation and a live "now" indicator. Adds bulk schedules API endpoint. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -506,6 +506,34 @@ def register(app):
|
|||||||
|
|
||||||
# ── Routine Scheduling ────────────────────────────────────────
|
# ── Routine Scheduling ────────────────────────────────────────
|
||||||
|
|
||||||
|
@app.route("/api/routines/schedules", methods=["GET"])
|
||||||
|
def api_listAllSchedules():
|
||||||
|
"""Get all schedules for the user's routines with routine metadata."""
|
||||||
|
user_uuid = _auth(flask.request)
|
||||||
|
if not user_uuid:
|
||||||
|
return flask.jsonify({"error": "unauthorized"}), 401
|
||||||
|
routines = postgres.select("routines", where={"user_uuid": user_uuid})
|
||||||
|
if not routines:
|
||||||
|
return flask.jsonify([]), 200
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for r in routines:
|
||||||
|
sched = postgres.select_one("routine_schedules", {"routine_id": r["id"]})
|
||||||
|
if not sched:
|
||||||
|
continue
|
||||||
|
steps = postgres.select("routine_steps", where={"routine_id": r["id"]})
|
||||||
|
total_duration = sum(s.get("duration_minutes") or 0 for s in steps)
|
||||||
|
result.append({
|
||||||
|
"routine_id": r["id"],
|
||||||
|
"routine_name": r.get("name", ""),
|
||||||
|
"routine_icon": r.get("icon", ""),
|
||||||
|
"days": sched.get("days", []),
|
||||||
|
"time": sched.get("time"),
|
||||||
|
"remind": sched.get("remind", True),
|
||||||
|
"total_duration_minutes": total_duration,
|
||||||
|
})
|
||||||
|
return flask.jsonify(result), 200
|
||||||
|
|
||||||
@app.route("/api/routines/<routine_id>/schedule", methods=["PUT"])
|
@app.route("/api/routines/<routine_id>/schedule", methods=["PUT"])
|
||||||
def api_setRoutineSchedule(routine_id):
|
def api_setRoutineSchedule(routine_id):
|
||||||
"""Set when this routine should run. Body: {days: ["mon","tue",...], time: "08:00", remind: true}"""
|
"""Set when this routine should run. Body: {days: ["mon","tue",...], time: "08:00", remind: true}"""
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState, useRef } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import api from '@/lib/api';
|
import api from '@/lib/api';
|
||||||
import { PlusIcon, PlayIcon, EditIcon, TrashIcon, FlameIcon } from '@/components/ui/Icons';
|
import { PlusIcon, PlayIcon, ClockIcon } from '@/components/ui/Icons';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
interface Routine {
|
interface Routine {
|
||||||
@@ -13,42 +13,135 @@ interface Routine {
|
|||||||
icon?: string;
|
icon?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ScheduleEntry {
|
||||||
|
routine_id: string;
|
||||||
|
routine_name: string;
|
||||||
|
routine_icon: string;
|
||||||
|
days: string[];
|
||||||
|
time: string;
|
||||||
|
remind: boolean;
|
||||||
|
total_duration_minutes: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HOUR_HEIGHT = 80;
|
||||||
|
const START_HOUR = 5;
|
||||||
|
const END_HOUR = 23;
|
||||||
|
const DAY_LABELS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||||
|
const DAY_KEYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
|
||||||
|
|
||||||
|
function getWeekDays(anchor: Date): Date[] {
|
||||||
|
const d = new Date(anchor);
|
||||||
|
const day = d.getDay();
|
||||||
|
const diff = day === 0 ? 6 : day - 1;
|
||||||
|
d.setDate(d.getDate() - diff);
|
||||||
|
return Array.from({ length: 7 }, (_, i) => {
|
||||||
|
const dd = new Date(d);
|
||||||
|
dd.setDate(d.getDate() + i);
|
||||||
|
return dd;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSameDay(a: Date, b: Date): boolean {
|
||||||
|
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
function timeToMinutes(t: string): number {
|
||||||
|
const [h, m] = t.split(':').map(Number);
|
||||||
|
return h * 60 + m;
|
||||||
|
}
|
||||||
|
|
||||||
|
function minutesToTop(minutes: number): number {
|
||||||
|
return ((minutes - START_HOUR * 60) / 60) * HOUR_HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
function durationToHeight(minutes: number): number {
|
||||||
|
return Math.max(48, (minutes / 60) * HOUR_HEIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(t: string): string {
|
||||||
|
const [h, m] = t.split(':').map(Number);
|
||||||
|
const period = h >= 12 ? 'PM' : 'AM';
|
||||||
|
const dh = h > 12 ? h - 12 : h === 0 ? 12 : h;
|
||||||
|
return `${dh}:${String(m).padStart(2, '0')} ${period}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMinutesToTime(t: string, mins: number): string {
|
||||||
|
const [h, m] = t.split(':').map(Number);
|
||||||
|
const total = h * 60 + m + mins;
|
||||||
|
const nh = Math.floor(total / 60) % 24;
|
||||||
|
const nm = total % 60;
|
||||||
|
return `${String(nh).padStart(2, '0')}:${String(nm).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDayKey(date: Date): string {
|
||||||
|
const day = date.getDay();
|
||||||
|
return DAY_KEYS[day === 0 ? 6 : day - 1];
|
||||||
|
}
|
||||||
|
|
||||||
export default function RoutinesPage() {
|
export default function RoutinesPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [routines, setRoutines] = useState<Routine[]>([]);
|
const timelineRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [allRoutines, setAllRoutines] = useState<Routine[]>([]);
|
||||||
|
const [allSchedules, setAllSchedules] = useState<ScheduleEntry[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [deleteModal, setDeleteModal] = useState<string | null>(null);
|
const [selectedDate, setSelectedDate] = useState(() => new Date());
|
||||||
|
const [nowMinutes, setNowMinutes] = useState(() => {
|
||||||
|
const n = new Date();
|
||||||
|
return n.getHours() * 60 + n.getMinutes();
|
||||||
|
});
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
const weekDays = getWeekDays(selectedDate);
|
||||||
|
const isToday = isSameDay(selectedDate, today);
|
||||||
|
const dayKey = getDayKey(selectedDate);
|
||||||
|
|
||||||
|
const scheduledForDay = allSchedules
|
||||||
|
.filter(s => s.days.includes(dayKey))
|
||||||
|
.sort((a, b) => timeToMinutes(a.time) - timeToMinutes(b.time));
|
||||||
|
|
||||||
|
const scheduledRoutineIds = new Set(allSchedules.map(s => s.routine_id));
|
||||||
|
const unscheduledRoutines = allRoutines.filter(r => !scheduledRoutineIds.has(r.id));
|
||||||
|
|
||||||
|
const nowTopPx = minutesToTop(nowMinutes);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchRoutines = async () => {
|
Promise.all([
|
||||||
try {
|
api.routines.list(),
|
||||||
const data = await api.routines.list();
|
api.routines.listAllSchedules(),
|
||||||
setRoutines(data);
|
])
|
||||||
} catch (err) {
|
.then(([routines, schedules]) => {
|
||||||
console.error('Failed to fetch routines:', err);
|
setAllRoutines(routines);
|
||||||
} finally {
|
setAllSchedules(schedules);
|
||||||
setIsLoading(false);
|
})
|
||||||
}
|
.catch(() => {})
|
||||||
};
|
.finally(() => setIsLoading(false));
|
||||||
fetchRoutines();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDelete = async (routineId: string) => {
|
useEffect(() => {
|
||||||
try {
|
const timer = setInterval(() => {
|
||||||
await api.routines.delete(routineId);
|
const n = new Date();
|
||||||
setRoutines(routines.filter(r => r.id !== routineId));
|
setNowMinutes(n.getHours() * 60 + n.getMinutes());
|
||||||
setDeleteModal(null);
|
}, 30_000);
|
||||||
} catch (err) {
|
return () => clearInterval(timer);
|
||||||
console.error('Failed to delete routine:', err);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading && isToday && timelineRef.current) {
|
||||||
|
const scrollTarget = nowTopPx - window.innerHeight / 3;
|
||||||
|
timelineRef.current.scrollTo({ top: Math.max(0, scrollTarget), behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
};
|
}, [isLoading, isToday]);
|
||||||
|
|
||||||
const handleStartRoutine = async (routineId: string) => {
|
const handleStartRoutine = async (routineId: string) => {
|
||||||
try {
|
try {
|
||||||
await api.sessions.start(routineId);
|
await api.sessions.start(routineId);
|
||||||
router.push(`/dashboard/routines/${routineId}/run`);
|
router.push(`/dashboard/routines/${routineId}/run`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to start routine:', err);
|
const msg = (err as Error).message;
|
||||||
|
if (msg.includes('already have active session')) {
|
||||||
|
router.push(`/dashboard/routines/${routineId}/run`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -61,97 +154,192 @@ export default function RoutinesPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 space-y-4">
|
<div className="flex flex-col h-full min-h-screen bg-gray-50">
|
||||||
<div className="flex items-center justify-between">
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between px-4 pt-4 pb-2 bg-white border-b border-gray-100">
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Routines</h1>
|
<h1 className="text-2xl font-bold text-gray-900">Routines</h1>
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/routines/new"
|
href="/dashboard/routines/new"
|
||||||
className="bg-indigo-600 text-white p-2 rounded-full"
|
className="bg-indigo-600 text-white p-2 rounded-full"
|
||||||
>
|
>
|
||||||
<PlusIcon size={24} />
|
<PlusIcon size={20} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{routines.length === 0 ? (
|
{/* Week Strip */}
|
||||||
<div className="bg-white rounded-xl p-8 shadow-sm text-center mt-4">
|
<div className="flex bg-white px-2 pb-3 pt-2 gap-1 border-b border-gray-100">
|
||||||
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
{weekDays.map((day, i) => {
|
||||||
<FlameIcon className="text-gray-400" size={32} />
|
const selected = isSameDay(day, selectedDate);
|
||||||
</div>
|
const isTodayDay = isSameDay(day, today);
|
||||||
<h3 className="font-semibold text-gray-900 mb-1">No routines yet</h3>
|
return (
|
||||||
<p className="text-gray-500 text-sm mb-4">Create your first routine to get started</p>
|
<button
|
||||||
<Link
|
key={i}
|
||||||
href="/dashboard/routines/new"
|
onClick={() => setSelectedDate(day)}
|
||||||
className="inline-block bg-indigo-600 text-white px-4 py-2 rounded-lg font-medium"
|
className="flex-1 flex flex-col items-center py-1 rounded-xl"
|
||||||
>
|
|
||||||
Create Routine
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{routines.map((routine) => (
|
|
||||||
<div
|
|
||||||
key={routine.id}
|
|
||||||
className="bg-white rounded-xl p-4 shadow-sm"
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<span className="text-xs text-gray-500 mb-1">{DAY_LABELS[i]}</span>
|
||||||
<div className="w-12 h-12 bg-gradient-to-br from-indigo-100 to-pink-100 rounded-xl flex items-center justify-center flex-shrink-0">
|
<span
|
||||||
<span className="text-2xl">{routine.icon || '✨'}</span>
|
className={`w-8 h-8 flex items-center justify-center rounded-full text-sm font-semibold transition-colors ${
|
||||||
|
isTodayDay && selected
|
||||||
|
? 'bg-indigo-600 text-white'
|
||||||
|
: isTodayDay
|
||||||
|
? 'text-indigo-600 font-bold'
|
||||||
|
: selected
|
||||||
|
? 'bg-gray-200 text-gray-900'
|
||||||
|
: 'text-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{day.getDate()}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Timeline */}
|
||||||
|
<div ref={timelineRef} className="flex-1 overflow-y-auto">
|
||||||
|
{allRoutines.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-20 px-4">
|
||||||
|
<div className="w-16 h-16 bg-indigo-100 rounded-full flex items-center justify-center mb-4">
|
||||||
|
<ClockIcon size={32} className="text-indigo-400" />
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-500 text-center mb-4">No routines yet</p>
|
||||||
|
<Link
|
||||||
|
href="/dashboard/routines/new"
|
||||||
|
className="bg-indigo-600 text-white px-6 py-3 rounded-xl font-medium"
|
||||||
|
>
|
||||||
|
Create Routine
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Timeline grid */}
|
||||||
|
<div
|
||||||
|
className="relative"
|
||||||
|
style={{ height: `${(END_HOUR - START_HOUR) * HOUR_HEIGHT}px` }}
|
||||||
|
>
|
||||||
|
{/* Hour lines */}
|
||||||
|
{Array.from({ length: END_HOUR - START_HOUR }, (_, i) => {
|
||||||
|
const hour = START_HOUR + i;
|
||||||
|
const label =
|
||||||
|
hour === 0 ? '12 AM' :
|
||||||
|
hour < 12 ? `${hour} AM` :
|
||||||
|
hour === 12 ? '12 PM' :
|
||||||
|
`${hour - 12} PM`;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={hour}
|
||||||
|
className="absolute w-full flex items-start"
|
||||||
|
style={{ top: `${i * HOUR_HEIGHT}px`, height: `${HOUR_HEIGHT}px` }}
|
||||||
|
>
|
||||||
|
<span className="w-14 text-xs text-gray-400 pr-2 text-right flex-shrink-0 -mt-2">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<div className="flex-1 border-t border-gray-200 h-full" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Now indicator */}
|
||||||
|
{isToday && nowTopPx >= 0 && nowTopPx <= (END_HOUR - START_HOUR) * HOUR_HEIGHT && (
|
||||||
|
<div
|
||||||
|
className="absolute flex items-center pointer-events-none z-10"
|
||||||
|
style={{ top: `${nowTopPx}px`, left: '48px', right: '0' }}
|
||||||
|
>
|
||||||
|
<div className="w-3 h-3 rounded-full bg-red-500 -ml-1.5 flex-shrink-0" />
|
||||||
|
<div className="flex-1 h-0.5 bg-red-500" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
)}
|
||||||
<h3 className="font-semibold text-gray-900 truncate">{routine.name}</h3>
|
|
||||||
{routine.description && (
|
{/* Routine cards */}
|
||||||
<p className="text-gray-500 text-sm truncate">{routine.description}</p>
|
{scheduledForDay.map((entry) => {
|
||||||
)}
|
const startMin = timeToMinutes(entry.time);
|
||||||
|
const topPx = minutesToTop(startMin);
|
||||||
|
const heightPx = durationToHeight(entry.total_duration_minutes);
|
||||||
|
const endMin = startMin + entry.total_duration_minutes;
|
||||||
|
const isPast = isToday && nowMinutes > endMin;
|
||||||
|
const isCurrent = isToday && nowMinutes >= startMin && nowMinutes < endMin;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={entry.routine_id}
|
||||||
|
onClick={() => router.push(`/dashboard/routines/${entry.routine_id}`)}
|
||||||
|
style={{
|
||||||
|
top: `${topPx}px`,
|
||||||
|
height: `${heightPx}px`,
|
||||||
|
left: '60px',
|
||||||
|
right: '8px',
|
||||||
|
}}
|
||||||
|
className={`absolute rounded-xl px-3 py-2 text-left shadow-sm border transition-all overflow-hidden ${
|
||||||
|
isPast
|
||||||
|
? 'bg-green-50 border-green-200 opacity-75'
|
||||||
|
: 'bg-indigo-50 border-indigo-200'
|
||||||
|
} ${isCurrent ? 'ring-2 ring-indigo-500 opacity-100' : ''}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-lg leading-none flex-shrink-0">{entry.routine_icon || '✨'}</span>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="font-semibold text-gray-900 text-sm truncate">{entry.routine_name}</p>
|
||||||
|
<p className="text-xs text-gray-500 truncate">
|
||||||
|
{formatTime(entry.time)}
|
||||||
|
{entry.total_duration_minutes > 0 && (
|
||||||
|
<> - {formatTime(addMinutesToTime(entry.time, entry.total_duration_minutes))}</>
|
||||||
|
)}
|
||||||
|
{entry.total_duration_minutes > 0 && ` · ${entry.total_duration_minutes}m`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Empty day */}
|
||||||
|
{scheduledForDay.length === 0 && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||||
|
<p className="text-gray-400 text-sm">No routines scheduled for this day</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
)}
|
||||||
<button
|
</div>
|
||||||
onClick={() => handleStartRoutine(routine.id)}
|
|
||||||
className="bg-indigo-600 text-white p-2 rounded-full"
|
{/* Unscheduled routines */}
|
||||||
>
|
{unscheduledRoutines.length > 0 && (
|
||||||
<PlayIcon size={18} />
|
<div className="border-t border-gray-200 bg-white px-4 pt-3 pb-4">
|
||||||
</button>
|
<h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
||||||
<Link
|
Unscheduled
|
||||||
href={`/dashboard/routines/${routine.id}`}
|
</h2>
|
||||||
className="text-gray-500 p-2"
|
<div className="space-y-2">
|
||||||
>
|
{unscheduledRoutines.map((r) => (
|
||||||
<EditIcon size={18} />
|
<div
|
||||||
</Link>
|
key={r.id}
|
||||||
<button
|
className="flex items-center gap-3 bg-gray-50 rounded-xl p-3"
|
||||||
onClick={() => setDeleteModal(routine.id)}
|
>
|
||||||
className="text-red-500 p-2"
|
<span className="text-xl flex-shrink-0">{r.icon || '✨'}</span>
|
||||||
>
|
<div className="flex-1 min-w-0">
|
||||||
<TrashIcon size={18} />
|
<p className="font-medium text-gray-900 text-sm truncate">{r.name}</p>
|
||||||
</button>
|
{r.description && (
|
||||||
|
<p className="text-xs text-gray-500 truncate">{r.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => handleStartRoutine(r.id)}
|
||||||
|
className="bg-indigo-600 text-white p-2 rounded-lg flex-shrink-0"
|
||||||
|
>
|
||||||
|
<PlayIcon size={16} />
|
||||||
|
</button>
|
||||||
|
<Link
|
||||||
|
href={`/dashboard/routines/${r.id}`}
|
||||||
|
className="text-indigo-600 text-sm font-medium flex-shrink-0"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
))}
|
</>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{/* Delete Modal */}
|
|
||||||
{deleteModal && (
|
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
||||||
<div className="bg-white rounded-2xl p-6 max-w-sm w-full">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Delete Routine?</h3>
|
|
||||||
<p className="text-gray-500 mb-4">This action cannot be undone.</p>
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<button
|
|
||||||
onClick={() => setDeleteModal(null)}
|
|
||||||
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg font-medium"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDelete(deleteModal)}
|
|
||||||
className="flex-1 px-4 py-2 bg-red-600 text-white rounded-lg font-medium"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -265,6 +265,18 @@ export const api = {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
listAllSchedules: async () => {
|
||||||
|
return request<Array<{
|
||||||
|
routine_id: string;
|
||||||
|
routine_name: string;
|
||||||
|
routine_icon: string;
|
||||||
|
days: string[];
|
||||||
|
time: string;
|
||||||
|
remind: boolean;
|
||||||
|
total_duration_minutes: number;
|
||||||
|
}>>('/api/routines/schedules', { method: 'GET' });
|
||||||
|
},
|
||||||
|
|
||||||
// History
|
// History
|
||||||
getHistory: async (routineId: string, days = 7) => {
|
getHistory: async (routineId: string, days = 7) => {
|
||||||
return request<Array<{
|
return request<Array<{
|
||||||
|
|||||||
Reference in New Issue
Block a user