feat(todos): make product optional in createTodoAction; fix promote scopes

- createTodoAction: productId is now optional; validates with
  productAccessFilter when provided so team members can link todos
- promoteTodoToPbiAction: use productAccessFilter for product lookup;
  remove product_id from todo WHERE (was breaking unlinked todos)
- promoteTodoToStoryAction: only enforce product match when todo has
  a product_id (null means unlinked, any product is acceptable)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-25 19:17:00 +02:00
parent 43718c133e
commit d03df529d3

View file

@ -18,15 +18,16 @@ export async function createTodoAction(_prevState: unknown, formData: FormData)
if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' }
const title = (formData.get('title') as string)?.trim()
const productId = (formData.get('productId') as string)?.trim()
const productId = (formData.get('productId') as string)?.trim() || null
if (!title) return { error: 'Titel is verplicht' }
if (!productId) return { error: 'Product is verplicht' }
const product = await prisma.product.findFirst({
where: { id: productId, user_id: session.userId, archived: false },
})
if (!product) return { error: 'Product niet gevonden' }
if (productId) {
const product = await prisma.product.findFirst({
where: { id: productId, ...productAccessFilter(session.userId), archived: false },
})
if (!product) return { error: 'Product niet gevonden' }
}
await prisma.todo.create({ data: { user_id: session.userId, product_id: productId, title } })
revalidatePath('/todos')
@ -78,12 +79,12 @@ export async function promoteTodoToPbiAction(_prevState: unknown, formData: Form
if (!parsed.success) return { error: parsed.error.flatten().fieldErrors }
const product = await prisma.product.findFirst({
where: { id: parsed.data.productId, user_id: session.userId },
where: { id: parsed.data.productId, ...productAccessFilter(session.userId) },
})
if (!product) return { error: 'Product niet gevonden' }
const todo = await prisma.todo.findFirst({
where: { id: parsed.data.todoId, user_id: session.userId, product_id: parsed.data.productId },
where: { id: parsed.data.todoId, user_id: session.userId },
})
if (!todo) return { error: 'Todo niet gevonden' }
@ -140,7 +141,7 @@ export async function promoteTodoToStoryAction(_prevState: unknown, formData: Fo
where: { id: parsed.data.pbiId, product: productAccessFilter(session.userId) },
})
if (!pbi) return { error: 'PBI niet gevonden' }
if (todo.product_id !== pbi.product_id) return { error: 'Todo hoort niet bij dit product' }
if (todo.product_id !== null && todo.product_id !== pbi.product_id) return { error: 'Todo hoort niet bij dit product' }
const last = await prisma.story.findFirst({
where: { pbi_id: parsed.data.pbiId, priority: parsed.data.priority },