"use server"; import { revalidatePath } from "next/cache"; import { prisma } from "@/lib/db"; import { requireUserId } from "@/lib/auth-helpers"; import { GroupColorSchema, GroupTitleSchema } from "@/lib/validation/group"; import type { GroupDTO } from "@/types/board"; export async function createGroup( categoryId: string, title: string, color: string ): Promise { const userId = await requireUserId(); const parsedTitle = GroupTitleSchema.parse(title); const parsedColor = GroupColorSchema.parse(color); const category = await prisma.category.findFirst({ where: { id: categoryId, userId }, select: { id: true }, }); if (!category) throw new Error("Category not found"); const count = await prisma.group.count({ where: { categoryId } }); const group = await prisma.group.create({ data: { title: parsedTitle, color: parsedColor, order: count, categoryId }, }); revalidatePath("/"); return { id: group.id, title: group.title, color: group.color, order: group.order, noteContent: group.noteContent, categoryId: group.categoryId, todos: [], }; } export async function updateGroup( groupId: string, data: { title?: string; color?: string } ): Promise { const userId = await requireUserId(); const update: { title?: string; color?: string } = {}; if (data.title !== undefined) update.title = GroupTitleSchema.parse(data.title); if (data.color !== undefined) update.color = GroupColorSchema.parse(data.color); const { count } = await prisma.group.updateMany({ where: { id: groupId, category: { userId } }, data: update, }); if (count === 0) throw new Error("Group not found"); revalidatePath("/"); } export async function deleteGroup(groupId: string): Promise { const userId = await requireUserId(); const { count } = await prisma.group.deleteMany({ where: { id: groupId, category: { userId } }, }); if (count === 0) throw new Error("Group not found"); revalidatePath("/"); } /** Persists a new card order within a single lane. */ export async function reorderGroupsInCategory( categoryId: string, orderedGroupIds: string[] ): Promise { const userId = await requireUserId(); const category = await prisma.category.findFirst({ where: { id: categoryId, userId }, include: { groups: { select: { id: true } } }, }); if (!category) throw new Error("Category not found"); const ownedIds = new Set(category.groups.map((g) => g.id)); if ( orderedGroupIds.length !== ownedIds.size || !orderedGroupIds.every((id) => ownedIds.has(id)) ) { throw new Error("Group list does not match this category's groups"); } await prisma.$transaction( orderedGroupIds.map((id, index) => prisma.group.update({ where: { id }, data: { order: index } }) ) ); revalidatePath("/"); } /** * Moves a card to a different lane and persists the resulting order for * both the source and destination lanes. */ export async function moveGroupToCategory( groupId: string, targetCategoryId: string, orderedGroupIdsInTargetLane: string[], orderedGroupIdsInSourceLane: string[] ): Promise { const userId = await requireUserId(); const [group, targetCategory] = await Promise.all([ prisma.group.findFirst({ where: { id: groupId, category: { userId } }, select: { id: true, categoryId: true }, }), prisma.category.findFirst({ where: { id: targetCategoryId, userId }, select: { id: true } }), ]); if (!group) throw new Error("Group not found"); if (!targetCategory) throw new Error("Target category not found"); const targetIndex = orderedGroupIdsInTargetLane.indexOf(groupId); if (targetIndex === -1) { throw new Error("Moved group must be present in the target lane's order list"); } await prisma.$transaction([ prisma.group.update({ where: { id: groupId }, data: { categoryId: targetCategoryId, order: targetIndex }, }), ...orderedGroupIdsInTargetLane .map((id, index) => ({ id, index })) .filter(({ id }) => id !== groupId) .map(({ id, index }) => prisma.group.update({ where: { id }, data: { order: index } })), ...orderedGroupIdsInSourceLane.map((id, index) => prisma.group.update({ where: { id }, data: { order: index } }) ), ]); revalidatePath("/"); }