import { prisma } from '@/lib/prisma' import { productAccessFilter } from '@/lib/product-access' export type VerifyResultKey = 'ALIGNED' | 'PARTIAL' | 'EMPTY' | 'DIVERGENT' export interface TopJob { jobId: string taskId: string taskTitle: string productId: string productName: string finishedAt: Date } export interface VerifyResultStats { counts: { result: VerifyResultKey; count: number }[] topEmpty: TopJob[] topDivergent: TopJob[] } export interface TrendPoint { sprintId: string sprintGoal: string productName: string alignedRatio: number total: number } const RESULT_ORDER: VerifyResultKey[] = ['ALIGNED', 'PARTIAL', 'EMPTY', 'DIVERGENT'] export async function getVerifyResultStats( userId: string, daysBack = 30, ): Promise { const cutoff = new Date() cutoff.setDate(cutoff.getDate() - daysBack) const baseWhere = { user_id: userId, status: 'DONE' as const, verify_result: { not: null as null }, finished_at: { gt: cutoff }, task_id: { not: null }, } const [grouped, rawEmpty, rawDivergent] = await Promise.all([ prisma.claudeJob.groupBy({ by: ['verify_result'], where: baseWhere, _count: { _all: true }, }), prisma.claudeJob.findMany({ where: { ...baseWhere, verify_result: 'EMPTY' }, orderBy: { finished_at: 'desc' }, take: 5, select: { id: true, finished_at: true, task: { select: { id: true, title: true } }, product: { select: { id: true, name: true } }, }, }), prisma.claudeJob.findMany({ where: { ...baseWhere, verify_result: 'DIVERGENT' }, orderBy: { finished_at: 'desc' }, take: 5, select: { id: true, finished_at: true, task: { select: { id: true, title: true } }, product: { select: { id: true, name: true } }, }, }), ]) const countMap = new Map( grouped .filter(g => g.verify_result !== null) .map(g => [g.verify_result as VerifyResultKey, g._count._all]), ) const counts = RESULT_ORDER .filter(r => countMap.has(r)) .map(r => ({ result: r, count: countMap.get(r)! })) function toTopJob(j: { id: string; finished_at: Date | null; task: { id: string; title: string } | null; product: { id: string; name: string } }): TopJob | null { if (!j.task) return null return { jobId: j.id, taskId: j.task.id, taskTitle: j.task.title, productId: j.product.id, productName: j.product.name, finishedAt: j.finished_at!, } } return { counts, topEmpty: rawEmpty.map(toTopJob).filter((j): j is TopJob => j !== null), topDivergent: rawDivergent.map(toTopJob).filter((j): j is TopJob => j !== null), } } export async function getAlignmentTrend( userId: string, sprintsBack = 5, ): Promise { const sprints = await prisma.sprint.findMany({ where: { status: 'COMPLETED', product: productAccessFilter(userId), }, orderBy: { completed_at: 'desc' }, take: sprintsBack, select: { id: true, sprint_goal: true, completed_at: true, product: { select: { name: true } }, }, }) const points = await Promise.all( sprints.map(async sprint => { const jobs = await prisma.claudeJob.findMany({ where: { user_id: userId, status: 'DONE', verify_result: { not: null }, task: { story: { sprint_id: sprint.id } }, }, select: { verify_result: true }, }) const aligned = jobs.filter(j => j.verify_result === 'ALIGNED').length return { sprintId: sprint.id, sprintGoal: sprint.sprint_goal, productName: sprint.product.name, alignedRatio: jobs.length > 0 ? Math.round((aligned / jobs.length) * 100) : 0, total: jobs.length, } }), ) // chronologisch oplopend (we fetched desc, so reverse) return points.reverse() }