Scrum4Me/lib/active-sprint.ts
Madhura68 2af6f24598 feat(PBI-79/ST-1333): active-sprint null-contract + clearActiveSprintAction
- lib/user-settings.ts: activeSprints values nullable in Zod-schema.
  Key-aanwezigheid heeft nu betekenis (key+null = bewust geen sprint;
  key ontbreekt = fallback-cascade).
- lib/active-sprint.ts: nieuwe readStoredActiveSprintState helper +
  resolveActiveSprint respecteert expliciet 'cleared' state zonder fallback.
  clearActiveSprintInSettings schrijft null i.p.v. de key te verwijderen.
- actions/active-sprint.ts: nieuwe clearActiveSprintAction met auth +
  membership-check.
- components/shared/sprint-switcher.tsx: '— Geen actieve sprint —'-optie
  in dropdown, disabled wanneer er geen actieve sprint is.
- Tests: nieuwe active-sprint.test.ts (resolver-paden + clear),
  active-sprint-action.test.ts (action-laag), uitbreiding user-settings.test.ts.

Plan: docs/plans/PBI-79-backlog-sprint-workflow.md

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

126 lines
3.3 KiB
TypeScript

import type { Prisma, SprintStatus } from '@prisma/client'
import { prisma } from '@/lib/prisma'
import {
mergeSettings,
parseUserSettings,
type UserSettings,
} from '@/lib/user-settings'
export type ActiveSprint = {
id: string
code: string
status: SprintStatus
}
async function readSettings(userId: string): Promise<UserSettings> {
const user = await prisma.user.findUnique({
where: { id: userId },
select: { settings: true },
})
return parseUserSettings(user?.settings)
}
async function writeSettings(userId: string, next: UserSettings): Promise<void> {
await prisma.user.update({
where: { id: userId },
data: { settings: next as unknown as Prisma.InputJsonValue },
})
}
async function notifyUserSettings(
userId: string,
patch: Partial<UserSettings>,
): Promise<void> {
await prisma.$executeRaw`
SELECT pg_notify('scrum4me_changes', ${JSON.stringify({
kind: 'user_settings',
userId,
patch,
})}::text)
`
}
type StoredActiveSprintState =
| { kind: 'unset' }
| { kind: 'cleared' }
| { kind: 'set'; sprintId: string }
export function readStoredActiveSprintState(
settings: UserSettings,
productId: string,
): StoredActiveSprintState {
const map = settings.layout?.activeSprints
if (!map || !(productId in map)) return { kind: 'unset' }
const value = map[productId]
if (value === null) return { kind: 'cleared' }
return { kind: 'set', sprintId: value }
}
export async function setActiveSprintInSettings(
userId: string,
productId: string,
sprintId: string,
): Promise<void> {
const current = await readSettings(userId)
const patch: Partial<UserSettings> = {
layout: {
activeSprints: {
...(current.layout?.activeSprints ?? {}),
[productId]: sprintId,
},
},
}
await writeSettings(userId, mergeSettings(current, patch))
await notifyUserSettings(userId, patch)
}
export async function clearActiveSprintInSettings(
userId: string,
productId: string,
): Promise<void> {
const current = await readSettings(userId)
const nextActiveSprints: Record<string, string | null> = {
...(current.layout?.activeSprints ?? {}),
[productId]: null,
}
const next: UserSettings = {
...current,
layout: { ...current.layout, activeSprints: nextActiveSprints },
}
await writeSettings(userId, next)
await notifyUserSettings(userId, {
layout: { activeSprints: nextActiveSprints },
})
}
export async function resolveActiveSprint(
productId: string,
userId: string,
): Promise<ActiveSprint | null> {
const settings = await readSettings(userId)
const state = readStoredActiveSprintState(settings, productId)
if (state.kind === 'cleared') return null
if (state.kind === 'set') {
const sprint = await prisma.sprint.findFirst({
where: { id: state.sprintId, product_id: productId },
select: { id: true, code: true, status: true },
})
if (sprint) return sprint
}
const open = await prisma.sprint.findFirst({
where: { product_id: productId, status: 'OPEN' },
orderBy: { created_at: 'desc' },
select: { id: true, code: true, status: true },
})
if (open) return open
const closed = await prisma.sprint.findFirst({
where: { product_id: productId, status: 'CLOSED' },
orderBy: { created_at: 'desc' },
select: { id: true, code: true, status: true },
})
return closed ?? null
}