Organize/lib/actions/scheduled-todos.ts

155 lines
5.2 KiB
TypeScript

"use server";
import { revalidatePath } from "next/cache";
import { prisma } from "@/lib/db";
import { requireUserId } from "@/lib/auth-helpers";
import { boardPath, projectAccessFilter, scheduledTodoAccessFilter } from "@/lib/access";
import {
RecurrenceInputSchema,
ScheduledTodoDetailsSchema,
ScheduledTodoTitleSchema,
type RecurrenceInput,
} from "@/lib/validation/scheduled-todo";
import { buildRRuleString, parseRRuleString } from "@/lib/rrule-utils";
import { getScheduledTodoBoard } from "@/lib/scheduled-todos";
import type { ScheduledBoardDTO, ScheduledTodoEditDTO } from "@/types/scheduled-todo";
/** Verifies the project (Home scope is always allowed for its own user). */
async function assertProjectAccess(userId: string, projectId: string) {
const project = await prisma.project.findFirst({
where: { id: projectId, ...projectAccessFilter(userId) },
select: { id: true },
});
if (!project) throw new Error("Project not found");
}
async function findOwnedScheduledTodo(userId: string, id: string) {
const todo = await prisma.scheduledTodo.findFirst({
where: { id, ...scheduledTodoAccessFilter(userId) },
select: { id: true, projectId: true },
});
if (!todo) throw new Error("Scheduled to-do not found");
return todo;
}
/** The action the client Scheduled panel calls -- it derives Home vs.
* Project scope from the current route itself (see ScheduledPanel), so this
* is the single entry point regardless of which board is showing. */
export async function getScheduledBoard(projectId: string | null): Promise<ScheduledBoardDTO> {
const userId = await requireUserId();
if (projectId) {
await assertProjectAccess(userId, projectId);
return getScheduledTodoBoard({ projectId });
}
return getScheduledTodoBoard({ userId, projectId: null });
}
export async function getScheduledTodoForEdit(id: string): Promise<ScheduledTodoEditDTO> {
const userId = await requireUserId();
const todo = await prisma.scheduledTodo.findFirst({
where: { id, ...scheduledTodoAccessFilter(userId) },
});
if (!todo) throw new Error("Scheduled to-do not found");
return {
id: todo.id,
title: todo.title,
details: todo.details,
startDate: todo.startDate.toISOString().slice(0, 10),
recurrence: todo.rrule ? parseRRuleString(todo.rrule) : null,
};
}
export async function createScheduledTodo(
scope: { projectId: string | null },
data: { title: string; details?: string; startDate: string; recurrence?: RecurrenceInput }
): Promise<void> {
const userId = await requireUserId();
const title = ScheduledTodoTitleSchema.parse(data.title);
const details = data.details ? ScheduledTodoDetailsSchema.parse(data.details) : undefined;
const recurrence = data.recurrence ? RecurrenceInputSchema.parse(data.recurrence) : undefined;
if (scope.projectId) await assertProjectAccess(userId, scope.projectId);
await prisma.scheduledTodo.create({
data: {
title,
details: details ?? null,
userId: scope.projectId ? null : userId,
projectId: scope.projectId,
startDate: new Date(data.startDate),
rrule: recurrence ? buildRRuleString(recurrence) : null,
},
});
revalidatePath(boardPath(scope.projectId));
}
export async function updateScheduledTodo(
id: string,
data: {
title?: string;
details?: string | null;
startDate?: string;
recurrence?: RecurrenceInput | null;
}
): Promise<void> {
const userId = await requireUserId();
const todo = await findOwnedScheduledTodo(userId, id);
const update: {
title?: string;
details?: string | null;
startDate?: Date;
rrule?: string | null;
} = {};
if (data.title !== undefined) update.title = ScheduledTodoTitleSchema.parse(data.title);
if (data.details !== undefined) {
update.details = data.details ? ScheduledTodoDetailsSchema.parse(data.details) : null;
}
if (data.startDate !== undefined) update.startDate = new Date(data.startDate);
if (data.recurrence !== undefined) {
update.rrule = data.recurrence ? buildRRuleString(RecurrenceInputSchema.parse(data.recurrence)) : null;
}
await prisma.scheduledTodo.update({ where: { id }, data: update });
revalidatePath(boardPath(todo.projectId));
}
export async function deleteScheduledTodo(id: string): Promise<void> {
const userId = await requireUserId();
const todo = await findOwnedScheduledTodo(userId, id);
await prisma.scheduledTodo.delete({ where: { id } });
revalidatePath(boardPath(todo.projectId));
}
/** Toggles just one date's occurrence -- a recurring to-do's other dates
* are unaffected (see ScheduledTodoCompletion). */
export async function toggleScheduledOccurrence(
scheduledTodoId: string,
occurrenceDate: string,
completed: boolean
): Promise<void> {
const userId = await requireUserId();
const todo = await findOwnedScheduledTodo(userId, scheduledTodoId);
const date = new Date(occurrenceDate);
if (completed) {
await prisma.scheduledTodoCompletion.upsert({
where: { scheduledTodoId_occurrenceDate: { scheduledTodoId, occurrenceDate: date } },
create: { scheduledTodoId, occurrenceDate: date },
update: {},
});
} else {
await prisma.scheduledTodoCompletion.deleteMany({
where: { scheduledTodoId, occurrenceDate: date },
});
}
revalidatePath(boardPath(todo.projectId));
}