* feat(schema): add Task.verify_required enum (ALIGNED / ALIGNED_OR_PARTIAL / ANY) Adds VerifyRequired enum and verify_required field (default ALIGNED_OR_PARTIAL) to the Task model. Also declares the claude_jobs_status_finished_at_idx index in the schema to match the live DB. Applied via db execute + migrate resolve. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * 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> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
137 lines
4.7 KiB
TypeScript
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 }),
|
|
})
|
|
}
|