diff --git a/src/app/actions/comments.ts b/src/app/actions/comments.ts new file mode 100644 index 0000000..84b1d38 --- /dev/null +++ b/src/app/actions/comments.ts @@ -0,0 +1,49 @@ +"use server"; + +import { revalidatePath } from "next/cache"; + +import { getServerPb } from "@/lib/pb/server"; +import { requireSession } from "@/lib/auth/session"; +import { logActivity } from "@/app/actions/activity"; +import type { CardRecord } from "@/lib/pb/types"; + +/** + * The OWNER's comment path only. + * + * A client manager comments through POST /api/client/cards/:id/comments, built + * in Plan 1a. Do not route them here: that route verifies the card belongs to + * their client and runs under a superuser with an explicit whitelist, which is + * the boundary this action deliberately does not have. + */ +export async function addOwnerComment(input: { + cardId: string; + body: string; + clientSlug: string; + projectSlug: string; +}): Promise { + const session = await requireSession(); + if (session.role !== "owner") throw new Error("Not found"); + + const body = input.body.trim().slice(0, 20000); + if (!body) return; + + const pb = await getServerPb(); + const card = await pb.collection("cards").getOne(input.cardId); + + await pb.collection("comments").create({ + card: input.cardId, + author: session.userId, + body, + }); + + await logActivity({ + project: card.project, + card: card.id, + actor: session.userId, + action: "comment_added", + }); + + revalidatePath( + `/p/${input.clientSlug}/${input.projectSlug}/c/${input.cardId}` + ); +} \ No newline at end of file diff --git a/src/app/actions/subtasks.ts b/src/app/actions/subtasks.ts new file mode 100644 index 0000000..0c89ad3 --- /dev/null +++ b/src/app/actions/subtasks.ts @@ -0,0 +1,97 @@ +"use server"; + +import { revalidatePath } from "next/cache"; + +import { getServerPb } from "@/lib/pb/server"; +import { requireSession } from "@/lib/auth/session"; +import { logActivity } from "@/app/actions/activity"; +import { positionBetween } from "@/lib/ordering/fractional"; +import type { CardRecord, SubtaskRecord } from "@/lib/pb/types"; + +/** See the security note at the top of src/app/actions/cards.ts. */ +async function requireOwner() { + const session = await requireSession(); + if (session.role !== "owner") throw new Error("Not found"); + return session; +} + +function cardPath(clientSlug: string, projectSlug: string, cardId: string) { + return `/p/${clientSlug}/${projectSlug}/c/${cardId}`; +} + +export async function addSubtask(input: { + cardId: string; + title: string; + clientSlug: string; + projectSlug: string; +}): Promise { + await requireOwner(); + const pb = await getServerPb(); + + const title = input.title.trim().slice(0, 200); + if (!title) return; + + const last = await pb + .collection("subtasks") + .getFirstListItem(`card = "${input.cardId}"`, { + sort: "-position", + }) + .catch(() => null); + + await pb.collection("subtasks").create({ + card: input.cardId, + title, + done: false, + position: positionBetween(last ? last.position : null, null), + }); + + revalidatePath(cardPath(input.clientSlug, input.projectSlug, input.cardId)); +} + +export async function toggleSubtask(input: { + subtaskId: string; + done: boolean; + clientSlug: string; + projectSlug: string; +}): Promise { + const session = await requireOwner(); + const pb = await getServerPb(); + + const subtask = await pb + .collection("subtasks") + .getOne(input.subtaskId); + + await pb.collection("subtasks").update(input.subtaskId, { done: input.done }); + + const card = await pb.collection("cards").getOne(subtask.card); + + await logActivity({ + project: card.project, + card: card.id, + actor: session.userId, + action: "subtask_toggled", + // `field` carries the subtask's title so the timeline can name it without + // a second lookup, and still reads correctly after the subtask is deleted. + field: subtask.title, + newValue: String(input.done), + }); + + revalidatePath(cardPath(input.clientSlug, input.projectSlug, subtask.card)); +} + +export async function deleteSubtask(input: { + subtaskId: string; + clientSlug: string; + projectSlug: string; +}): Promise { + await requireOwner(); + const pb = await getServerPb(); + + const subtask = await pb + .collection("subtasks") + .getOne(input.subtaskId); + + await pb.collection("subtasks").delete(input.subtaskId); + + revalidatePath(cardPath(input.clientSlug, input.projectSlug, subtask.card)); +} \ No newline at end of file