Scrum4Me/app/api/tasks/[id]/route.ts
janpeter visser c7bd42c4e4 feat(ui): add verify_required select to TaskDetailDialog
SoloTask interface, solo page mapping, solo store, PATCH route handler
and TaskDetailDialog all updated to expose the three-level verify gate
(ALIGNED / ALIGNED_OR_PARTIAL / ANY) as a native select. Disabled with
DemoTooltip in demo mode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 17:39:27 +02:00

137 lines
4.7 KiB
TypeScript

import { authenticateApiRequest } from '@/lib/api-auth'
import { prisma } from '@/lib/prisma'
import { z } from 'zod'
import { TASK_STATUS_API_VALUES, taskStatusFromApi, taskStatusToApi } from '@/lib/task-status'
import { updateTaskStatusWithStoryPromotion } from '@/lib/tasks-status-update'
// `review` is a valid TaskStatus in the DB and the kanban-board UI, but the
// sprint task list (components/sprint/task-list.tsx) does not yet render it.
// Reject it here until the sprint UI handles REVIEW so external clients don't
// drive tasks into a state the shared UI can't display.
const PATCHABLE_TASK_STATUS = TASK_STATUS_API_VALUES.filter((s) => s !== 'review')
const VERIFY_REQUIRED_VALUES = ['ALIGNED', 'ALIGNED_OR_PARTIAL', 'ANY'] as const
const patchSchema = z
.object({
status: z.enum(PATCHABLE_TASK_STATUS as [string, ...string[]]).optional(),
implementation_plan: z.string().optional(),
verify_only: z.boolean().optional(),
verify_required: z.enum(VERIFY_REQUIRED_VALUES).optional(),
})
.refine(
(data) =>
data.status !== undefined ||
data.implementation_plan !== undefined ||
data.verify_only !== undefined ||
data.verify_required !== undefined,
{ message: 'Geef minimaal status, implementation_plan, verify_only of verify_required mee' },
)
export async function PATCH(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const auth = await authenticateApiRequest(request)
if ('error' in auth) {
return Response.json({ error: auth.error }, { status: auth.status })
}
if (auth.isDemo) {
return Response.json({ error: 'Niet beschikbaar in demo-modus' }, { status: 403 })
}
const { id } = await params
const task = await prisma.task.findFirst({
where: { id },
include: {
story: {
include: {
product: {
include: {
members: {
where: { user_id: auth.userId },
select: { id: true },
},
},
},
},
},
},
})
if (!task) {
return Response.json({ error: 'Taak niet gevonden' }, { status: 404 })
}
const hasAccess =
task.story.product.user_id === auth.userId ||
(task.story.product.members?.length ?? 0) > 0
if (!hasAccess) {
return Response.json({ error: 'Geen toegang' }, { status: 403 })
}
let body: unknown
try {
body = await request.json()
} catch {
return Response.json({ error: 'Malformed JSON' }, { status: 400 })
}
const parsed = patchSchema.safeParse(body)
if (!parsed.success) {
return Response.json({ error: parsed.error.flatten() }, { status: 422 })
}
let dbStatus: ReturnType<typeof taskStatusFromApi> | undefined
if (parsed.data.status !== undefined) {
dbStatus = taskStatusFromApi(parsed.data.status)
if (dbStatus === null) {
return Response.json(
{ error: { fieldErrors: { status: ['Onbekende status'] } } },
{ status: 422 },
)
}
}
// Combine simple field writes (plan, verify_only, verify_required) into one update call
const simpleData: { implementation_plan?: string; verify_only?: boolean; verify_required?: typeof VERIFY_REQUIRED_VALUES[number] } = {}
if (parsed.data.implementation_plan !== undefined)
simpleData.implementation_plan = parsed.data.implementation_plan
if (parsed.data.verify_only !== undefined)
simpleData.verify_only = parsed.data.verify_only
if (parsed.data.verify_required !== undefined)
simpleData.verify_required = parsed.data.verify_required
const updated = await prisma.$transaction(async (tx) => {
const simpleUpdate = Object.keys(simpleData).length > 0
? await tx.task.update({
where: { id },
data: simpleData,
select: { id: true, status: true, implementation_plan: true, verify_only: true, verify_required: true },
})
: null
if (dbStatus !== undefined && dbStatus !== null) {
const result = await updateTaskStatusWithStoryPromotion(id, dbStatus, tx)
return {
id: result.task.id,
status: result.task.status,
implementation_plan: result.task.implementation_plan,
verify_only: simpleUpdate?.verify_only,
verify_required: simpleUpdate?.verify_required,
}
}
if (simpleUpdate) return simpleUpdate
// Should not reach here — patchSchema rejects bodies without recognized fields.
throw new Error('Geen wijzigingen')
})
return Response.json({
id: updated.id,
status: taskStatusToApi(updated.status),
implementation_plan: updated.implementation_plan,
...(updated.verify_only !== undefined && { verify_only: updated.verify_only }),
...(updated.verify_required !== undefined && { verify_required: updated.verify_required }),
})
}