feat(PBI-59): Sprint.code (SP-N sequentieel per product)
Voegt een verplicht code-veld toe aan Sprint, sequentieel per product (consistent met PBI-N, ST-NNN, T-N). - **Schema** — `Sprint.code String @db.VarChar(30)` + `@@unique([product_id, code])` - **Migratie** — voegt kolom toe als nullable, backfillt bestaande sprints via `ROW_NUMBER() OVER (PARTITION BY product_id ORDER BY created_at)` als `SP-N`, en zet daarna NOT NULL + UNIQUE. - **Generator** — `generateNextSprintCode(productId)` in lib/code-server.ts volgt het patroon van story/pbi/task; createSprintAction gebruikt `createWithCodeRetry` voor race-bescherming. - **Seed** — sprint-counter per product (`SP-1`, `SP-2`, ...). Zichtbaar in: - Sprint-header (`Product › Sprint actief · SP-3`) - JobCard + JobDetailPane voor SPRINT_IMPLEMENTATION jobs - Insights: VelocityChart x-axis (compacter dan goal-truncated), AlignmentTrend tooltip, SprintInfoStrip - actions/jobs-page.ts: `sprintCode` is weer een echte code i.p.v. null Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
16f01283ef
commit
9b299198cd
20 changed files with 97 additions and 29 deletions
|
|
@ -37,7 +37,7 @@ const JOB_INCLUDE = {
|
|||
task: { select: { code: true, title: true, description: true, implementation_plan: true } },
|
||||
idea: { select: { code: true, title: true, description: true, grill_md: true, plan_md: true } },
|
||||
product: { select: { name: true } },
|
||||
sprint_run: { include: { sprint: { select: { sprint_goal: true } } } },
|
||||
sprint_run: { include: { sprint: { select: { sprint_goal: true, code: true } } } },
|
||||
} as const
|
||||
|
||||
type RawJob = {
|
||||
|
|
@ -72,7 +72,7 @@ type RawJob = {
|
|||
plan_md: string | null
|
||||
} | null
|
||||
product: { name: string }
|
||||
sprint_run: { sprint: { sprint_goal: string } } | null
|
||||
sprint_run: { sprint: { sprint_goal: string; code: string } } | null
|
||||
}
|
||||
|
||||
type PriceRow = {
|
||||
|
|
@ -122,7 +122,7 @@ function mapJob(j: RawJob, priceMap: Map<string, PriceRow>): JobWithRelations {
|
|||
ideaCode: j.idea?.code ?? null,
|
||||
ideaTitle: j.idea?.title ?? null,
|
||||
sprintGoal: j.sprint_run?.sprint.sprint_goal ?? null,
|
||||
sprintCode: null,
|
||||
sprintCode: j.sprint_run?.sprint.code ?? null,
|
||||
productName: j.product.name,
|
||||
modelId: j.model_id,
|
||||
inputTokens: j.input_tokens,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
} from '@/lib/schemas/sprint'
|
||||
import { enforceUserRateLimit } from '@/lib/rate-limit'
|
||||
import { propagateStatusUpwards } from '@/lib/tasks-status-update'
|
||||
import { createWithCodeRetry, generateNextSprintCode } from '@/lib/code-server'
|
||||
|
||||
async function getSession() {
|
||||
return getIronSession<SessionData>(await cookies(), sessionOptions)
|
||||
|
|
@ -54,15 +55,20 @@ export async function createSprintAction(_prevState: unknown, formData: FormData
|
|||
})
|
||||
if (existing) return { error: 'Er is al een actieve Sprint voor dit product', sprintId: existing.id, code: 422 }
|
||||
|
||||
const sprint = await prisma.sprint.create({
|
||||
data: {
|
||||
product_id: parsed.data.productId,
|
||||
sprint_goal: parsed.data.sprint_goal,
|
||||
status: 'ACTIVE',
|
||||
start_date: parsed.data.start_date,
|
||||
end_date: parsed.data.end_date,
|
||||
},
|
||||
})
|
||||
const sprint = await createWithCodeRetry(
|
||||
() => generateNextSprintCode(parsed.data.productId),
|
||||
(code) =>
|
||||
prisma.sprint.create({
|
||||
data: {
|
||||
product_id: parsed.data.productId,
|
||||
code,
|
||||
sprint_goal: parsed.data.sprint_goal,
|
||||
status: 'ACTIVE',
|
||||
start_date: parsed.data.start_date,
|
||||
end_date: parsed.data.end_date,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
revalidatePath(`/products/${parsed.data.productId}`)
|
||||
return { success: true, sprintId: sprint.id }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue