feat(ST-512): extend REST API with code, description and implementation_plan

- GET /api/products returns code, description and definition_of_done
- GET /api/products/:id/next-story returns story.code and per-task code + implementation_plan
- GET /api/sprints/:id/tasks returns description, implementation_plan, story_code and derived per-task code
- POST /api/todos accepts and returns optional description (max 2000)

All changes are additive — existing clients ignore unknown keys.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-26 21:36:55 +02:00
parent 80d1f30615
commit 03fda05be3
4 changed files with 60 additions and 7 deletions

View file

@ -25,8 +25,16 @@ export async function GET(
orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }],
include: {
tasks: {
orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }],
select: { id: true, title: true, description: true, priority: true, sort_order: true, status: true },
orderBy: { sort_order: 'asc' },
select: {
id: true,
title: true,
description: true,
implementation_plan: true,
priority: true,
sort_order: true,
status: true,
},
},
},
})
@ -35,11 +43,17 @@ export async function GET(
return Response.json({ error: 'Geen open stories in de Sprint' }, { status: 404 })
}
const tasks = story.tasks.map((t, idx) => ({
...t,
code: story.code ? `${story.code}.${idx + 1}` : null,
}))
return Response.json({
id: story.id,
code: story.code,
title: story.title,
description: story.description,
acceptance_criteria: story.acceptance_criteria,
tasks: story.tasks,
tasks,
})
}

View file

@ -11,7 +11,7 @@ export async function GET(request: Request) {
const products = await prisma.product.findMany({
where: { archived: false, ...productAccessFilter(auth.userId) },
orderBy: { created_at: 'desc' },
select: { id: true, name: true, repo_url: true },
select: { id: true, code: true, name: true, description: true, repo_url: true, definition_of_done: true },
})
return Response.json(products)

View file

@ -31,8 +31,40 @@ export async function GET(
{ sort_order: 'asc' },
],
take: limit,
select: { id: true, title: true, story_id: true, priority: true, sort_order: true, status: true },
select: {
id: true,
title: true,
description: true,
implementation_plan: true,
story_id: true,
priority: true,
sort_order: true,
status: true,
story: {
select: {
code: true,
tasks: { select: { id: true }, orderBy: { sort_order: 'asc' } },
},
},
},
})
return Response.json(tasks)
const enriched = tasks.map((t) => {
const positionInStory = t.story.tasks.findIndex((st) => st.id === t.id)
const code = t.story.code && positionInStory >= 0 ? `${t.story.code}.${positionInStory + 1}` : null
return {
id: t.id,
code,
title: t.title,
description: t.description,
implementation_plan: t.implementation_plan,
story_id: t.story_id,
story_code: t.story.code,
priority: t.priority,
sort_order: t.sort_order,
status: t.status,
}
})
return Response.json(enriched)
}

View file

@ -4,6 +4,7 @@ import { z } from 'zod'
const bodySchema = z.object({
title: z.string().min(1, 'Titel is verplicht').max(500),
description: z.string().max(2000, 'Beschrijving mag maximaal 2000 tekens bevatten').optional(),
product_id: z.string().min(1, 'Product is verplicht'),
})
@ -29,13 +30,19 @@ export async function POST(request: Request) {
return Response.json({ error: 'Product niet gevonden' }, { status: 404 })
}
const description = parsed.data.description?.trim() || null
const todo = await prisma.todo.create({
data: {
user_id: auth.userId,
product_id: parsed.data.product_id,
title: parsed.data.title,
description,
},
})
return Response.json({ id: todo.id, title: todo.title, created_at: todo.created_at }, { status: 201 })
return Response.json(
{ id: todo.id, title: todo.title, description: todo.description, created_at: todo.created_at },
{ status: 201 },
)
}