From 51c8a86be4b9ead7605db218aad336bc695477a0 Mon Sep 17 00:00:00 2001 From: Scrum4Me Agent <30029041+madhura68@users.noreply.github.com> Date: Wed, 6 May 2026 03:20:53 +0200 Subject: [PATCH] =?UTF-8?q?feat(ST-d9sl8egw):=20/insights/tokens=20pagina?= =?UTF-8?q?=20=E2=80=94=20sprint-selector,=20historiek-tabel,=20dag-grafie?= =?UTF-8?q?k=20&=20PBI-aggregaat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Server Component met data-fetching via getSprintTokenHistory/getDayTokenData/ getPbiTokenAggregates. SprintTokenHistoryTable, TokenDayChart (Recharts LineChart), PbiTokenTable en TokenSelectors (client, URL-params). Link toegevoegd in insights/page.tsx. Co-Authored-By: Claude Sonnet 4.6 --- app/(app)/insights/page.tsx | 7 +- .../tokens/components/pbi-token-table.tsx | 37 ++++++++ .../components/sprint-token-history-table.tsx | 40 +++++++++ .../tokens/components/token-day-chart.tsx | 32 +++++++ .../tokens/components/token-selectors.tsx | 81 ++++++++++++++++++ app/(app)/insights/tokens/page.tsx | 84 +++++++++++++++++++ 6 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 app/(app)/insights/tokens/components/pbi-token-table.tsx create mode 100644 app/(app)/insights/tokens/components/sprint-token-history-table.tsx create mode 100644 app/(app)/insights/tokens/components/token-day-chart.tsx create mode 100644 app/(app)/insights/tokens/components/token-selectors.tsx create mode 100644 app/(app)/insights/tokens/page.tsx diff --git a/app/(app)/insights/page.tsx b/app/(app)/insights/page.tsx index 77164d5..9a1427e 100644 --- a/app/(app)/insights/page.tsx +++ b/app/(app)/insights/page.tsx @@ -91,7 +91,12 @@ export default async function InsightsPage({ searchParams }: InsightsPageProps) return (
-

Insights

+
+

Insights

+ + Historisch token-overzicht → + +
{/* Sprint Health */}
diff --git a/app/(app)/insights/tokens/components/pbi-token-table.tsx b/app/(app)/insights/tokens/components/pbi-token-table.tsx new file mode 100644 index 0000000..0d94ef2 --- /dev/null +++ b/app/(app)/insights/tokens/components/pbi-token-table.tsx @@ -0,0 +1,37 @@ +import type { PbiTokenRow } from '@/lib/insights/token-history' + +interface Props { + rows: PbiTokenRow[] +} + +export function PbiTokenTable({ rows }: Props) { + if (rows.length === 0) { + return

Geen PBI-data voor deze sprint.

+ } + + return ( +
+ + + + + + + + + + {rows.map(r => ( + + + + + + ))} + +
PBITokensKosten (USD)
+ {r.pbiCode} + {r.pbiTitle} + {r.totalTokens.toLocaleString()}${r.totalCostUsd.toFixed(4)}
+
+ ) +} diff --git a/app/(app)/insights/tokens/components/sprint-token-history-table.tsx b/app/(app)/insights/tokens/components/sprint-token-history-table.tsx new file mode 100644 index 0000000..c6d56de --- /dev/null +++ b/app/(app)/insights/tokens/components/sprint-token-history-table.tsx @@ -0,0 +1,40 @@ +import type { SprintTokenRow } from '@/lib/insights/token-history' + +interface Props { + rows: SprintTokenRow[] + selectedSprintId: string +} + +export function SprintTokenHistoryTable({ rows, selectedSprintId }: Props) { + if (rows.length === 0) { + return

Geen sprint-data met token-registratie.

+ } + + return ( +
+ + + + + + + + + + + {rows.map(r => ( + + + + + + + ))} + +
SprintTokensKosten (USD)Jobs
{r.sprintGoal}{r.totalTokens.toLocaleString()}${r.totalCostUsd.toFixed(4)}{r.jobCount}
+
+ ) +} diff --git a/app/(app)/insights/tokens/components/token-day-chart.tsx b/app/(app)/insights/tokens/components/token-day-chart.tsx new file mode 100644 index 0000000..8a95d1f --- /dev/null +++ b/app/(app)/insights/tokens/components/token-day-chart.tsx @@ -0,0 +1,32 @@ +'use client' + +import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts' +import type { DayTokenRow } from '@/lib/insights/token-history' + +interface Props { + data: DayTokenRow[] +} + +export function TokenDayChart({ data }: Props) { + if (data.length === 0) { + return

Geen dagdata voor deze sprint.

+ } + + return ( + + + (v as string).slice(5)} /> + `$${(v as number).toFixed(3)}`} /> + [`$${(v as number).toFixed(4)}`, 'Kosten (USD)']} /> + + + + ) +} diff --git a/app/(app)/insights/tokens/components/token-selectors.tsx b/app/(app)/insights/tokens/components/token-selectors.tsx new file mode 100644 index 0000000..9ef8ca0 --- /dev/null +++ b/app/(app)/insights/tokens/components/token-selectors.tsx @@ -0,0 +1,81 @@ +'use client' + +import { useRouter, usePathname, useSearchParams } from 'next/navigation' +import { useTransition } from 'react' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' + +interface Sprint { + id: string + sprint_goal: string +} + +interface Props { + productList: { id: string; name: string }[] + sprintList: Sprint[] + currentProductId?: string + currentSprintId?: string +} + +export function TokenSelectors({ productList, sprintList, currentProductId, currentSprintId }: Props) { + const router = useRouter() + const pathname = usePathname() + const searchParams = useSearchParams() + const [isPending, startTransition] = useTransition() + + function setParam(key: string, value: string | null) { + startTransition(() => { + const params = new URLSearchParams(searchParams.toString()) + if (value === null || value === '') { + params.delete(key) + } else { + params.set(key, value) + } + router.replace(`${pathname}?${params.toString()}`) + }) + } + + return ( +
+ {productList.length > 0 && ( + + )} + + {sprintList.length > 0 && ( + + )} +
+ ) +} diff --git a/app/(app)/insights/tokens/page.tsx b/app/(app)/insights/tokens/page.tsx new file mode 100644 index 0000000..1fd5664 --- /dev/null +++ b/app/(app)/insights/tokens/page.tsx @@ -0,0 +1,84 @@ +import { cookies } from 'next/headers' +import { getIronSession } from 'iron-session' +import { SessionData, sessionOptions } from '@/lib/session' +import { prisma } from '@/lib/prisma' +import { productAccessFilter } from '@/lib/product-access' +import { getSprintTokenHistory, getDayTokenData, getPbiTokenAggregates } from '@/lib/insights/token-history' +import { SprintTokenHistoryTable } from './components/sprint-token-history-table' +import { TokenDayChart } from './components/token-day-chart' +import { PbiTokenTable } from './components/pbi-token-table' +import { TokenSelectors } from './components/token-selectors' + +interface TokensPageProps { + searchParams: Promise<{ product?: string; sprint?: string }> +} + +export default async function TokensPage({ searchParams }: TokensPageProps) { + const session = await getIronSession(await cookies(), sessionOptions) + const userId = session.userId! + const { product: filterProductId, sprint: filterSprintId } = await searchParams + + const [sprintHistory, productList, activeSprint, allSprints] = await Promise.all([ + getSprintTokenHistory(userId, filterProductId), + prisma.product.findMany({ + where: productAccessFilter(userId), + select: { id: true, name: true }, + orderBy: { name: 'asc' }, + }), + prisma.sprint.findFirst({ + where: { status: 'ACTIVE', product: productAccessFilter(userId) }, + select: { id: true, sprint_goal: true }, + }), + prisma.sprint.findMany({ + where: { product: productAccessFilter(userId) }, + select: { id: true, sprint_goal: true }, + orderBy: { created_at: 'desc' }, + take: 20, + }), + ]) + + const selectedSprintId = filterSprintId ?? activeSprint?.id ?? '' + + const [dayData, pbiData] = selectedSprintId + ? await Promise.all([ + getDayTokenData(userId, selectedSprintId), + getPbiTokenAggregates(userId, selectedSprintId), + ]) + : [[], []] + + return ( +
+
+

Token-gebruik & kosten

+ + ← Terug naar Insights + +
+ + + + {/* Sprint historiek */} +
+

Sprint-historiek

+ +
+ + {/* Dagelijkse kosten */} +
+

Dagelijkse kosten

+ +
+ + {/* PBI-aggregaat */} +
+

Per PBI

+ +
+
+ ) +}