feat(ST-905): add Activeer button per product row in dashboard and product header

This commit is contained in:
Janpeter Visser 2026-04-27 19:18:00 +02:00
parent b7033c40ae
commit 754d033669
4 changed files with 78 additions and 8 deletions

View file

@ -16,10 +16,15 @@ export default async function DashboardPage({ searchParams }: Props) {
const { archived } = await searchParams
const showArchived = archived === '1'
const products = await prisma.product.findMany({
where: { archived: showArchived, ...productAccessFilter(session.userId) },
orderBy: { created_at: 'desc' },
})
const [products, user] = await Promise.all([
prisma.product.findMany({
where: { archived: showArchived, ...productAccessFilter(session.userId) },
orderBy: { created_at: 'desc' },
}),
session.userId
? prisma.user.findUnique({ where: { id: session.userId }, select: { active_product_id: true } })
: null,
])
return (
<div className="p-6 max-w-4xl mx-auto w-full">
@ -47,6 +52,7 @@ export default async function DashboardPage({ searchParams }: Props) {
products={products}
isDemo={session.isDemo ?? false}
showArchived={showArchived}
activeProductId={user?.active_product_id ?? null}
/>
</div>
)

View file

@ -7,6 +7,7 @@ import { PbiList } from '@/components/backlog/pbi-list'
import { StoryPanel } from '@/components/backlog/story-panel'
import type { Story } from '@/components/backlog/story-panel'
import { StartSprintButton } from '@/components/sprint/start-sprint-button'
import { ActivateProductButton } from '@/components/shared/activate-product-button'
import Link from 'next/link'
interface Props {
@ -21,9 +22,10 @@ export default async function ProductBacklogPage({ params }: Props) {
const product = await getAccessibleProduct(id, session.userId)
if (!product) notFound()
const activeSprint = await prisma.sprint.findFirst({
where: { product_id: id, status: 'ACTIVE' },
})
const [activeSprint, user] = await Promise.all([
prisma.sprint.findFirst({ where: { product_id: id, status: 'ACTIVE' } }),
prisma.user.findUnique({ where: { id: session.userId! }, select: { active_product_id: true } }),
])
const pbis = await prisma.pbi.findMany({
where: { product_id: id },
@ -66,6 +68,9 @@ export default async function ProductBacklogPage({ params }: Props) {
)}
</div>
<div className="flex items-center gap-3">
{user?.active_product_id !== id && (
<ActivateProductButton productId={id} isDemo={isDemo} />
)}
{activeSprint ? (
<Link href={`/products/${id}/sprint`} className="text-xs text-primary hover:underline font-medium">
Sprint actief

View file

@ -5,8 +5,10 @@ import { useRouter } from 'next/navigation'
import { useTransition } from 'react'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { CodeBadge } from '@/components/shared/code-badge'
import { restoreProductAction } from '@/actions/products'
import { setActiveProductAction } from '@/actions/active-product'
interface Product {
id: string
@ -20,9 +22,10 @@ interface ProductListProps {
products: Product[]
isDemo: boolean
showArchived?: boolean
activeProductId: string | null
}
export function ProductList({ products, isDemo, showArchived = false }: ProductListProps) {
export function ProductList({ products, isDemo, showArchived = false, activeProductId }: ProductListProps) {
const router = useRouter()
const [, startTransition] = useTransition()
@ -34,6 +37,15 @@ export function ProductList({ products, isDemo, showArchived = false }: ProductL
})
}
function handleActivate(id: string) {
if (isDemo) { toast.error('Niet beschikbaar in demo-modus'); return }
startTransition(async () => {
const result = await setActiveProductAction(id)
if (result?.error) toast.error(typeof result.error === 'string' ? result.error : 'Activeren mislukt')
else router.push(`/products/${id}`)
})
}
if (products.length === 0) {
return (
<div className="bg-surface-container-low rounded-xl border border-border p-12 text-center space-y-3">
@ -87,6 +99,18 @@ export function ProductList({ products, isDemo, showArchived = false }: ProductL
Repo
</a>
)}
{!showArchived && (
product.id === activeProductId
? <Badge className="bg-primary-container text-primary-container-foreground text-xs px-2 py-0">Actief</Badge>
: (
<button
onClick={(e) => { e.stopPropagation(); handleActivate(product.id) }}
className="text-xs text-primary hover:underline"
>
Activeer
</button>
)
)}
{showArchived && !isDemo && (
<button
onClick={(e) => { e.stopPropagation(); handleRestore(product.id) }}

View file

@ -0,0 +1,35 @@
'use client'
import { useRouter } from 'next/navigation'
import { useTransition } from 'react'
import { toast } from 'sonner'
import { setActiveProductAction } from '@/actions/active-product'
interface Props {
productId: string
isDemo: boolean
}
export function ActivateProductButton({ productId, isDemo }: Props) {
const router = useRouter()
const [isPending, startTransition] = useTransition()
function handleActivate() {
if (isDemo) { toast.error('Niet beschikbaar in demo-modus'); return }
startTransition(async () => {
const result = await setActiveProductAction(productId)
if (result?.error) toast.error(typeof result.error === 'string' ? result.error : 'Activeren mislukt')
else router.push(`/products/${productId}`)
})
}
return (
<button
onClick={handleActivate}
disabled={isPending}
className="text-xs text-primary hover:underline font-medium disabled:opacity-50"
>
Activeer
</button>
)
}