feat(sort-order): derive story/task sort_order from parseCodeNumber(code)

All create paths (createStoryAction, saveTask, createTaskAction,
materializeIdeaPlanAction) and code-edit paths (updateStoryAction, saveTask
update) now set sort_order = parseCodeNumber(code) instead of last+1.
Removes stale last-record queries from create paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scrum4Me Agent 2026-05-14 16:04:42 +02:00
parent 5085fac31d
commit fec03dd257
3 changed files with 14 additions and 28 deletions

View file

@ -21,6 +21,7 @@ import { canTransition, isGrillMdEditable, isIdeaEditable, isPlanMdEditable } fr
import { nextIdeaCode } from '@/lib/idea-code-server'
import { parsePlanMd } from '@/lib/idea-plan-parser'
import { ACTIVE_JOB_STATUSES } from '@/lib/job-status'
import { parseCodeNumber } from '@/lib/code'
import type { ClaudeJobKind, Idea, IdeaStatus } from '@prisma/client'
@ -720,16 +721,17 @@ export async function materializeIdeaPlanAction(
for (let si = 0; si < plan.stories.length; si++) {
const s = plan.stories[si]
const storyCode = `ST-${String(nextStoryN++).padStart(3, '0')}`
const story = await tx.story.create({
data: {
pbi_id: pbi.id,
product_id: productId,
code: `ST-${String(nextStoryN++).padStart(3, '0')}`,
code: storyCode,
title: s.title,
description: s.description ?? null,
acceptance_criteria: s.acceptance_criteria ?? null,
priority: s.priority,
sort_order: si + 1, // sequential within PBI
sort_order: parseCodeNumber(storyCode),
status: 'OPEN',
},
select: { id: true },
@ -738,11 +740,12 @@ export async function materializeIdeaPlanAction(
for (let ti = 0; ti < s.tasks.length; ti++) {
const t = s.tasks[ti]
const taskCode = `T-${nextTaskN++}`
const task = await tx.task.create({
data: {
story_id: story.id,
product_id: productId,
code: `T-${nextTaskN++}`,
code: taskCode,
title: t.title,
description: t.description ?? null,
implementation_plan: t.implementation_plan ?? null,
@ -751,7 +754,7 @@ export async function materializeIdeaPlanAction(
// gemixte task-priorities binnen één story zouden anders de
// YAML-volgorde verstoren (zie plan-fix task-volgorde-na-upload).
priority: s.priority,
sort_order: ti + 1,
sort_order: parseCodeNumber(taskCode),
status: 'TO_DO',
verify_required: t.verify_required ?? 'ALIGNED_OR_PARTIAL',
verify_only: t.verify_only ?? false,

View file

@ -7,7 +7,7 @@ import { prisma } from '@/lib/prisma'
import { SessionData, sessionOptions } from '@/lib/session'
import { getAccessibleProduct, productAccessFilter } from '@/lib/product-access'
import { requireProductWriter } from '@/lib/auth'
import { isValidCode, normalizeCode } from '@/lib/code'
import { isValidCode, normalizeCode, parseCodeNumber } from '@/lib/code'
import { createWithCodeRetry, generateNextStoryCode } from '@/lib/code-server'
import { createStorySchema, updateStorySchema } from '@/lib/schemas/story'
import { enforceUserRateLimit } from '@/lib/rate-limit'
@ -78,12 +78,6 @@ export async function createStoryAction(_prevState: unknown, formData: FormData)
}
}
const last = await prisma.story.findFirst({
where: { pbi_id: parsed.data.pbiId, priority: parsed.data.priority },
orderBy: { sort_order: 'desc' },
})
const sort_order = (last?.sort_order ?? 0) + 1.0
const insert = (code: string) =>
prisma.story.create({
data: {
@ -94,7 +88,7 @@ export async function createStoryAction(_prevState: unknown, formData: FormData)
description: parsed.data.description ?? null,
acceptance_criteria: parsed.data.acceptance_criteria ?? null,
priority: parsed.data.priority,
sort_order,
sort_order: parseCodeNumber(code),
status: 'OPEN',
},
})
@ -167,7 +161,7 @@ export async function updateStoryAction(_prevState: unknown, formData: FormData)
await prisma.story.update({
where: { id: parsed.data.id },
data: {
...(code ? { code } : {}),
...(code ? { code, sort_order: parseCodeNumber(code) } : {}),
title: parsed.data.title,
description: parsed.data.description ?? null,
acceptance_criteria: parsed.data.acceptance_criteria ?? null,

View file

@ -10,7 +10,7 @@ import { productAccessFilter } from '@/lib/product-access'
import { requireProductWriter } from '@/lib/auth'
import { taskSchema as sharedTaskSchema, type TaskInput } from '@/lib/schemas/task'
import { propagateStatusUpwards } from '@/lib/tasks-status-update'
import { normalizeCode } from '@/lib/code'
import { normalizeCode, parseCodeNumber } from '@/lib/code'
import { createWithCodeRetry, generateNextTaskCode, isCodeUniqueConflict } from '@/lib/code-server'
import { enforceUserRateLimit } from '@/lib/rate-limit'
@ -80,6 +80,7 @@ export async function saveTask(
description: description ?? null,
implementation_plan: implementation_plan ?? null,
priority,
...(inputCode ? { code: inputCode, sort_order: parseCodeNumber(inputCode) } : {}),
},
select: { id: true, title: true, status: true },
})
@ -106,15 +107,8 @@ export async function saveTask(
})
if (!story) return { ok: false, code: 403, error: 'forbidden' }
const last = await prisma.task.findFirst({
where: { story_id: context.storyId },
orderBy: { sort_order: 'desc' },
select: { sort_order: 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(
@ -130,7 +124,7 @@ export async function saveTask(
description: description ?? null,
implementation_plan: implementation_plan ?? null,
priority,
sort_order: sortOrder,
sort_order: parseCodeNumber(code),
status: 'TO_DO',
},
select: { id: true, title: true, status: true },
@ -207,11 +201,6 @@ export async function createTaskAction(_prevState: unknown, formData: FormData)
})
if (!story) return { error: 'Story niet gevonden' }
const last = await prisma.task.findFirst({
where: { story_id: storyId },
orderBy: { sort_order: 'desc' },
})
const productId = story.product_id
const task = await createWithCodeRetry(
() => generateNextTaskCode(productId),
@ -225,7 +214,7 @@ export async function createTaskAction(_prevState: unknown, formData: FormData)
title: parsed.data.title,
description: parsed.data.description ?? null,
priority: parsed.data.priority,
sort_order: (last?.sort_order ?? 0) + 1.0,
sort_order: parseCodeNumber(code),
status: 'TO_DO',
},
}),