Foundation: route, recharts, sprint-dates migration, chart-colors helper (#46)
* feat(ST-1201): add Sprint start_date/end_date + claude_jobs index migration - Sprint model: optionele start_date en end_date (DATE) voor burndown x-as - CREATE INDEX claude_jobs(status, finished_at) voor agent-throughput-queries - Bestaande sprints houden NULL; burndown skipt die Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ST-1202): add lib/chart-colors.ts + vitest coverage MD3-token-to-CSS-var mappings for STATUS, PRIORITY, VERIFY, JOB_STATUS and SERIES_COLORS; all 5 tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ST-1203): add Insights link to NavBar Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ST-1204): move Insights NavBar link between Solo and Todo's Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ST-1205): add sprint start_date/end_date UI + server actions - createSprintAction + updateSprintDatesAction: Zod date validation with end_date >= start_date cross-check - start-sprint-button: date inputs in create dialog - sprint-header: date display button + edit dialog with updateSprintDatesAction - sprint page: select start_date/end_date for SprintHeader prop - Demo blokkade via bestaande isDemo checks - 6 tests groen (validation + demo guard) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
55a1ee035c
commit
ce94fb48c3
10 changed files with 333 additions and 7 deletions
|
|
@ -16,6 +16,14 @@ function hasDuplicateIds(ids: string[]) {
|
|||
return new Set(ids).size !== ids.length
|
||||
}
|
||||
|
||||
const dateField = z.string().optional().nullable().transform(v => (v && v.trim() !== '' ? new Date(v) : null))
|
||||
|
||||
function validateDateOrder(data: { start_date: Date | null; end_date: Date | null }, ctx: z.RefinementCtx) {
|
||||
if (data.start_date && data.end_date && data.end_date < data.start_date) {
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['end_date'], message: 'Einddatum moet na startdatum liggen' })
|
||||
}
|
||||
}
|
||||
|
||||
export async function createSprintAction(_prevState: unknown, formData: FormData) {
|
||||
const session = await getSession()
|
||||
if (!session.userId) return { error: 'Niet ingelogd' }
|
||||
|
|
@ -24,9 +32,13 @@ export async function createSprintAction(_prevState: unknown, formData: FormData
|
|||
const parsed = z.object({
|
||||
productId: z.string(),
|
||||
sprint_goal: z.string().min(1, 'Sprint Goal is verplicht').max(500),
|
||||
}).safeParse({
|
||||
start_date: dateField,
|
||||
end_date: dateField,
|
||||
}).superRefine(validateDateOrder).safeParse({
|
||||
productId: formData.get('productId'),
|
||||
sprint_goal: formData.get('sprint_goal'),
|
||||
start_date: formData.get('start_date'),
|
||||
end_date: formData.get('end_date'),
|
||||
})
|
||||
if (!parsed.success) return { error: parsed.error.flatten().fieldErrors }
|
||||
|
||||
|
|
@ -43,6 +55,8 @@ export async function createSprintAction(_prevState: unknown, formData: FormData
|
|||
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,
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -50,6 +64,35 @@ export async function createSprintAction(_prevState: unknown, formData: FormData
|
|||
return { success: true, sprintId: sprint.id }
|
||||
}
|
||||
|
||||
export async function updateSprintDatesAction(_prevState: unknown, formData: FormData) {
|
||||
const session = await getSession()
|
||||
if (!session.userId) return { error: 'Niet ingelogd' }
|
||||
if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' }
|
||||
|
||||
const parsed = z.object({
|
||||
id: z.string(),
|
||||
start_date: dateField,
|
||||
end_date: dateField,
|
||||
}).superRefine(validateDateOrder).safeParse({
|
||||
id: formData.get('id'),
|
||||
start_date: formData.get('start_date'),
|
||||
end_date: formData.get('end_date'),
|
||||
})
|
||||
if (!parsed.success) return { error: parsed.error.flatten().fieldErrors }
|
||||
|
||||
const sprint = await prisma.sprint.findFirst({
|
||||
where: { id: parsed.data.id, product: productAccessFilter(session.userId) },
|
||||
})
|
||||
if (!sprint) return { error: 'Sprint niet gevonden' }
|
||||
|
||||
await prisma.sprint.update({
|
||||
where: { id: parsed.data.id },
|
||||
data: { start_date: parsed.data.start_date, end_date: parsed.data.end_date },
|
||||
})
|
||||
revalidatePath(`/products/${sprint.product_id}/sprint`)
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function updateSprintGoalAction(_prevState: unknown, formData: FormData) {
|
||||
const session = await getSession()
|
||||
if (!session.userId) return { error: 'Niet ingelogd' }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue