feat(codes): server actions + seed/scripts gebruiken code overal
- actions/tasks.ts: saveTask + createTaskAction (legacy form) gebruiken
createWithCodeRetry + generateNextTaskCode; persisten product_id
denormalisatie. P2002 op user-supplied code wordt 422 met fieldError
- actions/pbis.ts + stories.ts: insert-helpers nemen verplichte string;
update laat code-veld weg uit data wanneer null (kan niet meer leeg
worden gemaakt nu DB NOT NULL is)
- actions/todos.ts: promoteTodoToPbi/Story genereren expliciet een code
voor het transactiestart (kan niet binnen $transaction array retryen)
- prisma/seed.ts: per-product task counter geeft elke task een T-N code
- scripts/insert-milestone.ts: createMany berekent maxN voor product
en assigneert T-{maxN+i+1} per nieuwe task
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
829122d437
commit
7c82a736f5
6 changed files with 82 additions and 32 deletions
|
|
@ -68,7 +68,7 @@ export async function createPbiAction(_prevState: unknown, formData: FormData) {
|
|||
|
||||
const status = parsed.data.status ? pbiStatusFromApi(parsed.data.status) ?? undefined : undefined
|
||||
|
||||
const insert = (code: string | null) =>
|
||||
const insert = (code: string) =>
|
||||
prisma.pbi.create({
|
||||
data: {
|
||||
product_id: parsed.data.productId,
|
||||
|
|
@ -156,7 +156,7 @@ export async function updatePbiAction(_prevState: unknown, formData: FormData) {
|
|||
await prisma.pbi.update({
|
||||
where: { id: parsed.data.id },
|
||||
data: {
|
||||
code,
|
||||
...(code ? { code } : {}),
|
||||
title: parsed.data.title,
|
||||
description: parsed.data.description ?? null,
|
||||
priority: parsed.data.priority,
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export async function createStoryAction(_prevState: unknown, formData: FormData)
|
|||
})
|
||||
const sort_order = (last?.sort_order ?? 0) + 1.0
|
||||
|
||||
const insert = (code: string | null) =>
|
||||
const insert = (code: string) =>
|
||||
prisma.story.create({
|
||||
data: {
|
||||
pbi_id: parsed.data.pbiId,
|
||||
|
|
@ -163,7 +163,7 @@ export async function updateStoryAction(_prevState: unknown, formData: FormData)
|
|||
await prisma.story.update({
|
||||
where: { id: parsed.data.id },
|
||||
data: {
|
||||
code,
|
||||
...(code ? { code } : {}),
|
||||
title: parsed.data.title,
|
||||
description: parsed.data.description ?? null,
|
||||
acceptance_criteria: parsed.data.acceptance_criteria ?? null,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import { productAccessFilter } from '@/lib/product-access'
|
|||
import { requireProductWriter } from '@/lib/auth'
|
||||
import { taskSchema as sharedTaskSchema, type TaskInput } from '@/lib/schemas/task'
|
||||
import { updateTaskStatusWithStoryPromotion } from '@/lib/tasks-status-update'
|
||||
import { normalizeCode } from '@/lib/code'
|
||||
import { createWithCodeRetry, generateNextTaskCode, isCodeUniqueConflict } from '@/lib/code-server'
|
||||
|
||||
async function getSession() {
|
||||
return getIronSession<SessionData>(await cookies(), sessionOptions)
|
||||
|
|
@ -47,7 +49,8 @@ export async function saveTask(
|
|||
}
|
||||
}
|
||||
|
||||
const { title, description, implementation_plan, priority, status } = parsed.data
|
||||
const { title, description, implementation_plan, priority, status, code: rawCode } = parsed.data
|
||||
const inputCode = normalizeCode(rawCode ?? null)
|
||||
const scope = productAccessFilter(session.userId)
|
||||
|
||||
try {
|
||||
|
|
@ -91,7 +94,7 @@ export async function saveTask(
|
|||
|
||||
const story = await prisma.story.findFirst({
|
||||
where: { id: context.storyId, product: scope },
|
||||
select: { sprint_id: true },
|
||||
select: { sprint_id: true, product_id: true },
|
||||
})
|
||||
if (!story) return { ok: false, code: 403, error: 'forbidden' }
|
||||
|
||||
|
|
@ -101,24 +104,43 @@ export async function saveTask(
|
|||
select: { sort_order: true },
|
||||
})
|
||||
|
||||
const task = await prisma.task.create({
|
||||
data: {
|
||||
story_id: context.storyId,
|
||||
sprint_id: story.sprint_id ?? null,
|
||||
title,
|
||||
description: description ?? null,
|
||||
implementation_plan: implementation_plan ?? null,
|
||||
priority,
|
||||
sort_order: (last?.sort_order ?? 0) + 1.0,
|
||||
status: 'TO_DO',
|
||||
},
|
||||
select: { id: true, title: true, status: true },
|
||||
})
|
||||
const productId = story.product_id
|
||||
const sprintId = story.sprint_id ?? null
|
||||
const sortOrder = (last?.sort_order ?? 0) + 1.0
|
||||
const storyId = context.storyId
|
||||
|
||||
const task = await createWithCodeRetry(
|
||||
() => (inputCode ? Promise.resolve(inputCode) : generateNextTaskCode(productId)),
|
||||
(code) =>
|
||||
prisma.task.create({
|
||||
data: {
|
||||
story_id: storyId,
|
||||
product_id: productId,
|
||||
sprint_id: sprintId,
|
||||
code,
|
||||
title,
|
||||
description: description ?? null,
|
||||
implementation_plan: implementation_plan ?? null,
|
||||
priority,
|
||||
sort_order: sortOrder,
|
||||
status: 'TO_DO',
|
||||
},
|
||||
select: { id: true, title: true, status: true },
|
||||
}),
|
||||
)
|
||||
|
||||
revalidatePath(`/products/${context.productId}/sprint`)
|
||||
revalidatePath(`/products/${context.productId}`)
|
||||
return { ok: true, task: { ...task, status: task.status.toString() } }
|
||||
} catch {
|
||||
} catch (e) {
|
||||
if (inputCode && isCodeUniqueConflict(e)) {
|
||||
return {
|
||||
ok: false,
|
||||
code: 422,
|
||||
error: 'validation',
|
||||
fieldErrors: { code: ['Code bestaat al binnen dit product'] },
|
||||
}
|
||||
}
|
||||
return { ok: false, code: 500, error: 'server_error' }
|
||||
}
|
||||
}
|
||||
|
|
@ -179,17 +201,24 @@ export async function createTaskAction(_prevState: unknown, formData: FormData)
|
|||
orderBy: { sort_order: 'desc' },
|
||||
})
|
||||
|
||||
const task = await prisma.task.create({
|
||||
data: {
|
||||
story_id: storyId,
|
||||
sprint_id: sprintId || null,
|
||||
title: parsed.data.title,
|
||||
description: parsed.data.description ?? null,
|
||||
priority: parsed.data.priority,
|
||||
sort_order: (last?.sort_order ?? 0) + 1.0,
|
||||
status: 'TO_DO',
|
||||
},
|
||||
})
|
||||
const productId = story.product_id
|
||||
const task = await createWithCodeRetry(
|
||||
() => generateNextTaskCode(productId),
|
||||
(code) =>
|
||||
prisma.task.create({
|
||||
data: {
|
||||
story_id: storyId,
|
||||
product_id: productId,
|
||||
sprint_id: sprintId || null,
|
||||
code,
|
||||
title: parsed.data.title,
|
||||
description: parsed.data.description ?? null,
|
||||
priority: parsed.data.priority,
|
||||
sort_order: (last?.sort_order ?? 0) + 1.0,
|
||||
status: 'TO_DO',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
revalidatePath(`/products/${story.product_id}/sprint/planning`)
|
||||
return { success: true, task }
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { z } from 'zod'
|
|||
import { prisma } from '@/lib/prisma'
|
||||
import { SessionData, sessionOptions } from '@/lib/session'
|
||||
import { productAccessFilter } from '@/lib/product-access'
|
||||
import { generateNextPbiCode, generateNextStoryCode } from '@/lib/code-server'
|
||||
|
||||
async function getSession() {
|
||||
return getIronSession<SessionData>(await cookies(), sessionOptions)
|
||||
|
|
@ -154,10 +155,13 @@ export async function promoteTodoToPbiAction(_prevState: unknown, formData: Form
|
|||
orderBy: { sort_order: 'desc' },
|
||||
})
|
||||
|
||||
const pbiCode = await generateNextPbiCode(parsed.data.productId)
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.pbi.create({
|
||||
data: {
|
||||
product_id: parsed.data.productId,
|
||||
code: pbiCode,
|
||||
title: parsed.data.title,
|
||||
priority: parsed.data.priority,
|
||||
sort_order: (last?.sort_order ?? 0) + 1.0,
|
||||
|
|
@ -209,11 +213,14 @@ export async function promoteTodoToStoryAction(_prevState: unknown, formData: Fo
|
|||
orderBy: { sort_order: 'desc' },
|
||||
})
|
||||
|
||||
const storyCode = await generateNextStoryCode(pbi.product_id)
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.story.create({
|
||||
data: {
|
||||
pbi_id: parsed.data.pbiId,
|
||||
product_id: pbi.product_id,
|
||||
code: storyCode,
|
||||
title: parsed.data.title,
|
||||
priority: parsed.data.priority,
|
||||
sort_order: (last?.sort_order ?? 0) + 1.0,
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ async function main() {
|
|||
const milestones = await loadBacklog(root)
|
||||
console.log(`Loaded backlog: ${milestones.length} milestones, ${milestones.reduce((acc, m) => acc + m.stories.length, 0)} stories`)
|
||||
|
||||
let productTaskCounter = 0
|
||||
for (const ms of milestones) {
|
||||
const pbi = await prisma.pbi.create({
|
||||
data: {
|
||||
|
|
@ -174,10 +175,13 @@ async function main() {
|
|||
})
|
||||
|
||||
for (const t of s.tasks) {
|
||||
productTaskCounter += 1
|
||||
await prisma.task.create({
|
||||
data: {
|
||||
story_id: story.id,
|
||||
product_id: product.id,
|
||||
sprint_id: inSprint ? sprint.id : null,
|
||||
code: `T-${productTaskCounter}`,
|
||||
title: t.title,
|
||||
description: t.description,
|
||||
priority: ms.priority,
|
||||
|
|
|
|||
|
|
@ -163,9 +163,19 @@ async function main() {
|
|||
// Tasks: alleen als de story op dit moment 0 tasks had
|
||||
if (!hadTasks && s.tasks.length > 0) {
|
||||
if (!args.dryRun) {
|
||||
const allTasks = await prisma.task.findMany({
|
||||
where: { product_id: product.id },
|
||||
select: { code: true },
|
||||
})
|
||||
const maxN = allTasks.reduce((m, t) => {
|
||||
const match = /^T-(\d+)$/.exec(t.code)
|
||||
return match ? Math.max(m, Number(match[1])) : m
|
||||
}, 0)
|
||||
await prisma.task.createMany({
|
||||
data: s.tasks.map((t) => ({
|
||||
data: s.tasks.map((t, i) => ({
|
||||
story_id: storyId,
|
||||
product_id: product.id,
|
||||
code: `T-${maxN + i + 1}`,
|
||||
title: t.title,
|
||||
description: t.description || null,
|
||||
priority: ms.priority,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue