Organize/lib/scheduled-todos.ts

82 lines
2.8 KiB
TypeScript

import "server-only";
import { prisma } from "@/lib/db";
import { addUTCDays, getWeekStart, startOfUTCDate, toDateKey } from "@/lib/dates";
import { expandOccurrences } from "@/lib/rrule-utils";
import type { ScheduledBoardDTO, ScheduledDayDTO, ScheduledOccurrenceDTO } from "@/types/scheduled-todo";
// A recurring to-do that's never checked off would otherwise surface a
// missed occurrence for every day since it was created, forever -- cap how
// far back "overdue" looks so the column stays meaningful instead of
// endless.
const OVERDUE_LOOKBACK_DAYS = 30;
/**
* Fetches one Scheduled board's worth of occurrences, bucketed the way the
* panel renders them -- shared by the Home board and every Project board,
* which differ only in which ScheduledTodos they scope to (mirrors
* lib/board.ts's getBoard).
*/
export async function getScheduledTodoBoard(
where: { userId: string; projectId: null } | { projectId: string }
): Promise<ScheduledBoardDTO> {
const today = startOfUTCDate(new Date());
const todayKey = toDateKey(today);
const weekStart = getWeekStart(today);
const weekEnd = addUTCDays(weekStart, 6);
const windowStart = addUTCDays(today, -OVERDUE_LOOKBACK_DAYS);
const scheduledTodos = await prisma.scheduledTodo.findMany({
where,
include: { completions: true },
orderBy: { createdAt: "asc" },
});
const overdue: ScheduledOccurrenceDTO[] = [];
const todayOccurrences: ScheduledOccurrenceDTO[] = [];
// Seeded up front so every remaining weekday gets a header even with
// nothing scheduled that day.
const upcomingByDate = new Map<string, ScheduledOccurrenceDTO[]>();
for (let d = addUTCDays(today, 1); d <= weekEnd; d = addUTCDays(d, 1)) {
upcomingByDate.set(toDateKey(d), []);
}
for (const todo of scheduledTodos) {
const completedDates = new Set(todo.completions.map((c) => toDateKey(c.occurrenceDate)));
const occurrences = expandOccurrences(todo.startDate, todo.rrule, windowStart, weekEnd);
for (const occurrenceDate of occurrences) {
const key = toDateKey(occurrenceDate);
const dto: ScheduledOccurrenceDTO = {
scheduledTodoId: todo.id,
title: todo.title,
details: todo.details,
occurrenceDate: key,
completed: completedDates.has(key),
isRecurring: !!todo.rrule,
};
if (key < todayKey) {
if (!dto.completed) overdue.push(dto);
} else if (key === todayKey) {
todayOccurrences.push(dto);
} else {
upcomingByDate.get(key)?.push(dto);
}
}
}
overdue.sort((a, b) => a.occurrenceDate.localeCompare(b.occurrenceDate));
const upcoming: ScheduledDayDTO[] = Array.from(upcomingByDate, ([date, occurrences]) => ({
date,
occurrences,
})).sort((a, b) => a.date.localeCompare(b.date));
return {
overdue,
today: { date: todayKey, occurrences: todayOccurrences },
upcoming,
};
}