Scrum4Me/actions/active-sprint.ts
Madhura68 d7d11124e3 feat(PBI-79): sprint-switch auto-select PBI/story + user-settings persist
Bij sprint-switch wordt de sprint-content server-side opgevraagd. Wanneer
de sprint precies één PBI (en die PBI exact één story binnen de sprint)
heeft, worden PBI en story automatisch geselecteerd. Alle drie keuzes
(sprint, pbi, story) worden atomair in user-settings opgeslagen zodat ze
cross-device blijven hangen.

- lib/user-settings.ts: layout krijgt nullable activePbis +
  activeStories per product.
- lib/active-sprint.ts: setActiveSelectionInSettings schrijft de drie
  keys atomair + notify pg_notify.
- actions/active-sprint.ts: switchActiveSprintAction(productId, sprintId)
  doet de server-side auto-select-resolutie (single PBI → single story)
  en returnt { sprintId, pbiId, storyId }.
- components/shared/sprint-switcher.tsx: handleSwitchSprint roept de
  nieuwe action aan en synchroniseert de workspace-store gelijk zodat
  de UI geen flash krijgt voor de SSR-refresh.
- components/backlog/active-selection-hydrator.tsx (nieuw): client-side
  effect dat user-settings.activePbis/activeStories naar workspace-store
  spiegelt; wint van de localStorage hint-restore.
- app/(app)/products/[id]/page.tsx: ActiveSelectionHydrator gemount
  binnen BacklogHydrationWrapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:13:20 +02:00

164 lines
5.1 KiB
TypeScript

'use server'
import { revalidatePath } from 'next/cache'
import { cookies } from 'next/headers'
import { getIronSession } from 'iron-session'
import { z } from 'zod'
import { prisma } from '@/lib/prisma'
import { SessionData, sessionOptions } from '@/lib/session'
import { productAccessFilter } from '@/lib/product-access'
import {
clearActiveSprintInSettings,
setActiveSelectionInSettings,
setActiveSprintInSettings,
} from '@/lib/active-sprint'
async function getSession() {
return getIronSession<SessionData>(await cookies(), sessionOptions)
}
const setSchema = z.object({
productId: z.string().min(1),
sprintId: z.string().min(1),
})
const clearSchema = z.object({
productId: z.string().min(1),
})
export async function setActiveSprintAction(productId: string, sprintId: string) {
const session = await getSession()
if (!session.userId) return { error: 'Niet ingelogd' }
if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' }
const parsed = setSchema.safeParse({ productId, sprintId })
if (!parsed.success) return { error: 'Ongeldig product- of sprint-id' }
const sprint = await prisma.sprint.findFirst({
where: {
id: parsed.data.sprintId,
product_id: parsed.data.productId,
product: productAccessFilter(session.userId),
},
select: { id: true },
})
if (!sprint) return { error: 'Sprint niet gevonden of niet toegankelijk' }
await setActiveSprintInSettings(session.userId, parsed.data.productId, parsed.data.sprintId)
revalidatePath('/', 'layout')
return { success: true, sprintId: parsed.data.sprintId }
}
export async function clearActiveSprintAction(productId: string) {
const session = await getSession()
if (!session.userId) return { error: 'Niet ingelogd' }
if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' }
const parsed = clearSchema.safeParse({ productId })
if (!parsed.success) return { error: 'Ongeldig product-id' }
const product = await prisma.product.findFirst({
where: { id: parsed.data.productId, ...productAccessFilter(session.userId) },
select: { id: true },
})
if (!product) return { error: 'Product niet gevonden of niet toegankelijk' }
await clearActiveSprintInSettings(session.userId, parsed.data.productId)
revalidatePath('/', 'layout')
return { success: true }
}
const selectionSchema = z.object({
productId: z.string().min(1),
sprintId: z.string().min(1),
})
/**
* PBI-79: kies een sprint en auto-select zijn enige PBI/story (indien
* singleton). Resultaat wordt server-side bepaald + atomair in user-settings
* weggeschreven (sprint+pbi+story) zodat cross-device-restore klopt.
*/
export async function switchActiveSprintAction(
productId: string,
sprintId: string,
): Promise<
| {
success: true
sprintId: string
pbiId: string | null
storyId: string | null
}
| { error: string }
> {
const session = await getSession()
if (!session.userId) return { error: 'Niet ingelogd' }
if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' }
const parsed = selectionSchema.safeParse({ productId, sprintId })
if (!parsed.success) return { error: 'Ongeldig product- of sprint-id' }
const sprint = await prisma.sprint.findFirst({
where: {
id: parsed.data.sprintId,
product_id: parsed.data.productId,
product: productAccessFilter(session.userId),
},
select: { id: true },
})
if (!sprint) return { error: 'Sprint niet gevonden of niet toegankelijk' }
// Auto-select: alleen wanneer sprint exact één PBI heeft. Story-auto-select
// alleen wanneer die PBI exact één story binnen deze sprint heeft.
const sprintStories = await prisma.story.findMany({
where: {
sprint_id: parsed.data.sprintId,
product_id: parsed.data.productId,
},
select: { id: true, pbi_id: true },
})
const uniquePbiIds = Array.from(new Set(sprintStories.map((s) => s.pbi_id)))
let autoPbiId: string | null = null
let autoStoryId: string | null = null
if (uniquePbiIds.length === 1) {
autoPbiId = uniquePbiIds[0]
const storiesForPbi = sprintStories.filter((s) => s.pbi_id === autoPbiId)
if (storiesForPbi.length === 1) {
autoStoryId = storiesForPbi[0].id
}
}
await setActiveSelectionInSettings(session.userId, parsed.data.productId, {
sprintId: parsed.data.sprintId,
pbiId: autoPbiId,
storyId: autoStoryId,
})
revalidatePath('/', 'layout')
return {
success: true,
sprintId: parsed.data.sprintId,
pbiId: autoPbiId,
storyId: autoStoryId,
}
}
export async function syncActiveSprintCookieAction(productId: string, sprintId: string) {
const session = await getSession()
if (!session.userId) return
if (session.isDemo) return
const parsed = setSchema.safeParse({ productId, sprintId })
if (!parsed.success) return
const sprint = await prisma.sprint.findFirst({
where: {
id: parsed.data.sprintId,
product_id: parsed.data.productId,
product: productAccessFilter(session.userId),
},
select: { id: true },
})
if (!sprint) return
await setActiveSprintInSettings(session.userId, parsed.data.productId, parsed.data.sprintId)
}