"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 { 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 { 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 { 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 { 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 { 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 { 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)); }