feat(ST-d9sl8egw): token-components — CartesianGrid, useRouter click, client-side sort + index.ts
TokenDayChart: CartesianGrid, height 280, DD/MM formatter, var(--color-primary). SprintTokenHistoryTable: 'use client' + useRouter click naar ?sprint=<id>. PbiTokenTable: 'use client' + useState/useMemo kostensort. index.ts barrel export toegevoegd. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
51c8a86be4
commit
b2bde6934e
4 changed files with 63 additions and 31 deletions
3
app/(app)/insights/tokens/components/index.ts
Normal file
3
app/(app)/insights/tokens/components/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { TokenDayChart } from './token-day-chart'
|
||||
export { SprintTokenHistoryTable } from './sprint-token-history-table'
|
||||
export { PbiTokenTable } from './pbi-token-table'
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
'use client'
|
||||
|
||||
import { useState, useMemo } from 'react'
|
||||
import type { PbiTokenRow } from '@/lib/insights/token-history'
|
||||
|
||||
interface Props {
|
||||
|
|
@ -5,6 +8,13 @@ interface Props {
|
|||
}
|
||||
|
||||
export function PbiTokenTable({ rows }: Props) {
|
||||
const [sortDesc, setSortDesc] = useState(true)
|
||||
|
||||
const sorted = useMemo(
|
||||
() => [...rows].sort((a, b) => sortDesc ? b.totalCostUsd - a.totalCostUsd : a.totalCostUsd - b.totalCostUsd),
|
||||
[rows, sortDesc],
|
||||
)
|
||||
|
||||
if (rows.length === 0) {
|
||||
return <p className="text-muted-foreground text-sm">Geen PBI-data voor deze sprint.</p>
|
||||
}
|
||||
|
|
@ -14,22 +24,31 @@ export function PbiTokenTable({ rows }: Props) {
|
|||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border bg-muted text-muted-foreground text-xs uppercase tracking-wide">
|
||||
<th className="py-2 pr-4 text-left font-medium">PBI</th>
|
||||
<th className="py-2 pr-4 text-left font-medium">PBI-code</th>
|
||||
<th className="py-2 pr-4 text-left font-medium">Titel</th>
|
||||
<th className="py-2 pr-4 text-right font-medium">Tokens</th>
|
||||
<th className="py-2 text-right font-medium">Kosten (USD)</th>
|
||||
<th
|
||||
className="py-2 text-right font-medium cursor-pointer select-none text-primary"
|
||||
onClick={() => setSortDesc(d => !d)}
|
||||
>
|
||||
Kosten (USD) {sortDesc ? '▾' : '▴'}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map(r => (
|
||||
<tr key={r.pbiId} className="border-b border-border last:border-0">
|
||||
<td className="py-2 pr-4 text-foreground">
|
||||
<span className="text-muted-foreground mr-2">{r.pbiCode}</span>
|
||||
{r.pbiTitle}
|
||||
</td>
|
||||
<td className="py-2 pr-4 text-right tabular-nums">{r.totalTokens.toLocaleString()}</td>
|
||||
<td className="py-2 text-right tabular-nums">${r.totalCostUsd.toFixed(4)}</td>
|
||||
</tr>
|
||||
))}
|
||||
{sorted.map(r => {
|
||||
const title = r.pbiTitle.length > 60 ? r.pbiTitle.slice(0, 57) + '…' : r.pbiTitle
|
||||
return (
|
||||
<tr key={r.pbiId} className="border-b border-border last:border-0">
|
||||
<td className="py-2 pr-4 text-muted-foreground whitespace-nowrap">{r.pbiCode}</td>
|
||||
<td className="py-2 pr-4 text-foreground">{title}</td>
|
||||
<td className="py-2 pr-4 text-right tabular-nums">{r.totalTokens.toLocaleString()}</td>
|
||||
<td className="py-2 text-right tabular-nums">
|
||||
{r.totalCostUsd > 0 ? `$${r.totalCostUsd.toFixed(4)}` : '—'}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
'use client'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import type { SprintTokenRow } from '@/lib/insights/token-history'
|
||||
|
||||
interface Props {
|
||||
|
|
@ -6,6 +9,8 @@ interface Props {
|
|||
}
|
||||
|
||||
export function SprintTokenHistoryTable({ rows, selectedSprintId }: Props) {
|
||||
const router = useRouter()
|
||||
|
||||
if (rows.length === 0) {
|
||||
return <p className="text-muted-foreground text-sm">Geen sprint-data met token-registratie.</p>
|
||||
}
|
||||
|
|
@ -15,24 +20,28 @@ export function SprintTokenHistoryTable({ rows, selectedSprintId }: Props) {
|
|||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border bg-muted text-muted-foreground text-xs uppercase tracking-wide">
|
||||
<th className="py-2 pr-4 text-left font-medium">Sprint</th>
|
||||
<th className="py-2 pr-4 text-left font-medium">Sprint goal</th>
|
||||
<th className="py-2 pr-4 text-right font-medium">Tokens</th>
|
||||
<th className="py-2 pr-4 text-right font-medium">Kosten (USD)</th>
|
||||
<th className="py-2 text-right font-medium">Jobs</th>
|
||||
<th className="py-2 text-right font-medium">Aantal jobs</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map(r => (
|
||||
<tr
|
||||
key={r.sprintId}
|
||||
className={`border-b border-border last:border-0 ${r.sprintId === selectedSprintId ? 'bg-muted/50' : ''}`}
|
||||
>
|
||||
<td className="py-2 pr-4 text-foreground max-w-72 truncate">{r.sprintGoal}</td>
|
||||
<td className="py-2 pr-4 text-right tabular-nums">{r.totalTokens.toLocaleString()}</td>
|
||||
<td className="py-2 pr-4 text-right tabular-nums">${r.totalCostUsd.toFixed(4)}</td>
|
||||
<td className="py-2 text-right tabular-nums">{r.jobCount}</td>
|
||||
</tr>
|
||||
))}
|
||||
{rows.map(r => {
|
||||
const goal = r.sprintGoal.length > 40 ? r.sprintGoal.slice(0, 37) + '…' : r.sprintGoal
|
||||
return (
|
||||
<tr
|
||||
key={r.sprintId}
|
||||
className={`border-b border-border last:border-0 cursor-pointer hover:bg-muted/40 ${r.sprintId === selectedSprintId ? 'bg-muted/50' : ''}`}
|
||||
onClick={() => router.push(`/insights/tokens?sprint=${r.sprintId}`)}
|
||||
>
|
||||
<td className="py-2 pr-4 text-foreground">{goal}</td>
|
||||
<td className="py-2 pr-4 text-right tabular-nums">{r.totalTokens.toLocaleString()}</td>
|
||||
<td className="py-2 pr-4 text-right tabular-nums">${r.totalCostUsd.toFixed(4)}</td>
|
||||
<td className="py-2 text-right tabular-nums">{r.jobCount}</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client'
|
||||
|
||||
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'
|
||||
import { LineChart, Line, XAxis, YAxis, Tooltip, CartesianGrid, ResponsiveContainer } from 'recharts'
|
||||
import type { DayTokenRow } from '@/lib/insights/token-history'
|
||||
|
||||
interface Props {
|
||||
|
|
@ -9,19 +9,20 @@ interface Props {
|
|||
|
||||
export function TokenDayChart({ data }: Props) {
|
||||
if (data.length === 0) {
|
||||
return <p className="text-muted-foreground text-sm py-4 text-center">Geen dagdata voor deze sprint.</p>
|
||||
return <p className="text-muted-foreground">Geen dag-data beschikbaar</p>
|
||||
}
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<ResponsiveContainer width="100%" height={280}>
|
||||
<LineChart data={data}>
|
||||
<XAxis dataKey="day" tick={{ fontSize: 11 }} tickFormatter={v => (v as string).slice(5)} />
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
|
||||
<XAxis dataKey="day" tick={{ fontSize: 11 }} tickFormatter={v => (v as string).slice(5).replace('-', '/')} />
|
||||
<YAxis tick={{ fontSize: 11 }} tickFormatter={v => `$${(v as number).toFixed(3)}`} />
|
||||
<Tooltip formatter={(v: unknown) => [`$${(v as number).toFixed(4)}`, 'Kosten (USD)']} />
|
||||
<Tooltip formatter={(v: unknown) => [`$${Number(v).toFixed(4)}`, 'Kosten']} />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="totalCostUsd"
|
||||
stroke="var(--primary)"
|
||||
stroke="var(--color-primary)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
name="Kosten (USD)"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue