115 lines
3.6 KiB
TypeScript
115 lines
3.6 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 patchSchema = z
|
|
.object({
|
|
status: z.enum(PATCHABLE_TASK_STATUS as [string, ...string[]]).optional(),
|
|
implementation_plan: z.string().optional(),
|
|
})
|
|
.refine((data) => data.status !== undefined || data.implementation_plan !== undefined, {
|
|
message: 'Geef minimaal status of implementation_plan 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 },
|
|
)
|
|
}
|
|
}
|
|
|
|
const updated = await prisma.$transaction(async (tx) => {
|
|
const planUpdate = parsed.data.implementation_plan !== undefined
|
|
? await tx.task.update({
|
|
where: { id },
|
|
data: { implementation_plan: parsed.data.implementation_plan },
|
|
select: { id: true, status: true, implementation_plan: 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,
|
|
}
|
|
}
|
|
|
|
if (planUpdate) return planUpdate
|
|
|
|
// Should not reach here — patchSchema rejects bodies without status or implementation_plan.
|
|
throw new Error('Geen wijzigingen')
|
|
})
|
|
|
|
return Response.json({
|
|
id: updated.id,
|
|
status: taskStatusToApi(updated.status),
|
|
implementation_plan: updated.implementation_plan,
|
|
})
|
|
}
|