feat(ST-352): add claimStoryAction, unclaimStoryAction, reassignStoryAction, claimAllUnassignedInActiveSprintAction

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-26 16:19:15 +02:00
parent ef5c7c7675
commit 4ae4edb033

View file

@ -7,6 +7,7 @@ import { z } from 'zod'
import { prisma } from '@/lib/prisma'
import { SessionData, sessionOptions } from '@/lib/session'
import { getAccessibleProduct, productAccessFilter } from '@/lib/product-access'
import { requireProductWriter } from '@/lib/auth'
async function getSession() {
return getIronSession<SessionData>(await cookies(), sessionOptions)
@ -208,6 +209,95 @@ export async function getStoryLogsAction(storyId: string) {
}
}
export async function claimStoryAction(storyId: string, productId: string) {
try {
await requireProductWriter(productId)
} catch (e: unknown) {
return { error: e instanceof Error ? e.message : 'Geen toegang' }
}
const session = await getSession()
const story = await prisma.story.findFirst({ where: { id: storyId, product_id: productId } })
if (!story) return { error: 'Story niet gevonden' }
await prisma.story.update({ where: { id: storyId }, data: { assignee_id: session.userId } })
revalidatePath(`/products/${productId}/sprint`)
revalidatePath('/solo')
return { success: true }
}
export async function unclaimStoryAction(storyId: string, productId: string) {
try {
await requireProductWriter(productId)
} catch (e: unknown) {
return { error: e instanceof Error ? e.message : 'Geen toegang' }
}
const story = await prisma.story.findFirst({ where: { id: storyId, product_id: productId } })
if (!story) return { error: 'Story niet gevonden' }
await prisma.story.update({ where: { id: storyId }, data: { assignee_id: null } })
revalidatePath(`/products/${productId}/sprint`)
revalidatePath('/solo')
return { success: true }
}
export async function reassignStoryAction(storyId: string, productId: string, targetUserId: string) {
try {
await requireProductWriter(productId)
} catch (e: unknown) {
return { error: e instanceof Error ? e.message : 'Geen toegang' }
}
// Validate target user is owner or member of the product
const product = await prisma.product.findFirst({
where: {
id: productId,
OR: [
{ user_id: targetUserId },
{ members: { some: { user_id: targetUserId } } },
],
},
})
if (!product) return { error: 'Gebruiker is geen lid van dit product' }
const story = await prisma.story.findFirst({ where: { id: storyId, product_id: productId } })
if (!story) return { error: 'Story niet gevonden' }
await prisma.story.update({ where: { id: storyId }, data: { assignee_id: targetUserId } })
revalidatePath(`/products/${productId}/sprint`)
revalidatePath('/solo')
return { success: true }
}
export async function claimAllUnassignedInActiveSprintAction(productId: string) {
try {
await requireProductWriter(productId)
} catch (e: unknown) {
return { error: e instanceof Error ? e.message : 'Geen toegang' }
}
const session = await getSession()
const userId = session.userId
const sprint = await prisma.sprint.findFirst({
where: { product_id: productId, status: 'ACTIVE' },
})
if (!sprint) return { error: 'Geen actieve sprint gevonden' }
const result = await prisma.story.updateMany({
where: { sprint_id: sprint.id, product_id: productId, assignee_id: null },
data: { assignee_id: userId },
})
revalidatePath(`/products/${productId}/sprint`)
revalidatePath('/solo')
return { success: true, count: result.count }
}
export async function reorderStoriesAction(
pbiId: string,
productId: string,