- lib/user-settings.ts: nieuw workflow.pendingSprintDraft veld met compacte intent-shape (pbiIntent + per-PBI storyOverrides). - actions/sprint-draft.ts: setPendingSprintDraftAction + clearPendingSprintDraftAction met product-membership-check + Zod-validatie. - stores/user-settings/store.ts: setPendingSprintDraft / clearPendingSprintDraft optimistic acties + fine-grained mutators upsertPbiIntent / upsertStoryOverride. Sprint-draft actions worden dynamisch geïmporteerd zodat jsdom-tests zonder DATABASE_URL niet falen. - Tests: nieuwe sprint-draft.test.ts (action-laag), uitbreiding user-settings store-tests (5 nieuwe cases) en schema-tests (4 cases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
121 lines
3.8 KiB
TypeScript
121 lines
3.8 KiB
TypeScript
'use server'
|
|
|
|
import { revalidatePath } from 'next/cache'
|
|
import { cookies } from 'next/headers'
|
|
import { getIronSession } from 'iron-session'
|
|
import { z } from 'zod'
|
|
import type { Prisma } from '@prisma/client'
|
|
import { prisma } from '@/lib/prisma'
|
|
import { SessionData, sessionOptions } from '@/lib/session'
|
|
import { productAccessFilter } from '@/lib/product-access'
|
|
import {
|
|
mergeSettings,
|
|
parseUserSettings,
|
|
type PendingSprintDraft,
|
|
type UserSettings,
|
|
} from '@/lib/user-settings'
|
|
|
|
async function getSession() {
|
|
return getIronSession<SessionData>(await cookies(), sessionOptions)
|
|
}
|
|
|
|
const StoryOverridesSchema = z.object({
|
|
add: z.array(z.string()),
|
|
remove: z.array(z.string()),
|
|
}).strict()
|
|
|
|
const DraftSchema = z.object({
|
|
goal: z.string().min(1),
|
|
startAt: z.string().datetime().optional(),
|
|
endAt: z.string().datetime().optional(),
|
|
pbiIntent: z.record(z.string(), z.enum(['all', 'none'])).default({}),
|
|
storyOverrides: z.record(z.string(), StoryOverridesSchema).default({}),
|
|
}).strict()
|
|
|
|
const SetSchema = z.object({
|
|
productId: z.string().min(1),
|
|
draft: DraftSchema,
|
|
})
|
|
|
|
const ClearSchema = z.object({
|
|
productId: z.string().min(1),
|
|
})
|
|
|
|
async function ensureProductAccess(userId: string, productId: string) {
|
|
return prisma.product.findFirst({
|
|
where: { id: productId, ...productAccessFilter(userId) },
|
|
select: { id: true },
|
|
})
|
|
}
|
|
|
|
async function readUserSettings(userId: string): Promise<UserSettings> {
|
|
const user = await prisma.user.findUnique({
|
|
where: { id: userId },
|
|
select: { settings: true },
|
|
})
|
|
return parseUserSettings(user?.settings)
|
|
}
|
|
|
|
async function writeUserSettings(userId: string, next: UserSettings) {
|
|
await prisma.user.update({
|
|
where: { id: userId },
|
|
data: { settings: next as unknown as Prisma.InputJsonValue },
|
|
})
|
|
}
|
|
|
|
export async function setPendingSprintDraftAction(
|
|
productId: string,
|
|
draft: PendingSprintDraft,
|
|
) {
|
|
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, draft })
|
|
if (!parsed.success) {
|
|
return { error: 'Ongeldige draft', issues: parsed.error.issues }
|
|
}
|
|
|
|
const product = await ensureProductAccess(session.userId, parsed.data.productId)
|
|
if (!product) return { error: 'Product niet gevonden of niet toegankelijk' }
|
|
|
|
const current = await readUserSettings(session.userId)
|
|
const patch: Partial<UserSettings> = {
|
|
workflow: {
|
|
pendingSprintDraft: {
|
|
...(current.workflow?.pendingSprintDraft ?? {}),
|
|
[parsed.data.productId]: parsed.data.draft,
|
|
},
|
|
},
|
|
}
|
|
await writeUserSettings(session.userId, mergeSettings(current, patch))
|
|
revalidatePath('/', 'layout')
|
|
return { success: true }
|
|
}
|
|
|
|
export async function clearPendingSprintDraftAction(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 ensureProductAccess(session.userId, parsed.data.productId)
|
|
if (!product) return { error: 'Product niet gevonden of niet toegankelijk' }
|
|
|
|
const current = await readUserSettings(session.userId)
|
|
const existingMap = current.workflow?.pendingSprintDraft
|
|
if (!existingMap || !(parsed.data.productId in existingMap)) {
|
|
return { success: true }
|
|
}
|
|
const nextMap = { ...existingMap }
|
|
delete nextMap[parsed.data.productId]
|
|
const next: UserSettings = {
|
|
...current,
|
|
workflow: { ...current.workflow, pendingSprintDraft: nextMap },
|
|
}
|
|
await writeUserSettings(session.userId, next)
|
|
revalidatePath('/', 'layout')
|
|
return { success: true }
|
|
}
|