ui: "→ Idee" promote button in TodoCard + PromoteIdeaDialog (M12 T-514)

components/todos/todo-list.tsx:
- TodoCard: new "→ Idee" button next to "→ PBI" + "→ Story" (only shown
  for non-demo)
- PromoteIdeaDialog: confirmation modal — no extra inputs needed since
  promoteTodoToIdeaAction takes only todoId; title/description carry
  over from the todo, status starts as DRAFT
- onPromoteIdea callback wired through TodoCard props
- On success: navigates to /ideas/{new-id} so user lands on the fresh
  idea-detail page

Tests: 546/546 still green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-04 21:42:59 +02:00
parent 2f41f8917a
commit 7595474fcc

View file

@ -3,6 +3,7 @@
import { useState, useTransition, useMemo, useEffect, useRef, useCallback } from 'react'
import { useActionState } from 'react'
import { useFormStatus } from 'react-dom'
import { useRouter } from 'next/navigation'
import {
useReactTable,
getCoreRowModel,
@ -26,6 +27,7 @@ import {
archiveSelectedTodosAction,
promoteTodoToPbiAction,
promoteTodoToStoryAction,
promoteTodoToIdeaAction,
} from '@/actions/todos'
interface Todo {
@ -233,6 +235,60 @@ function PromoteStoryDialog({
)
}
// --- Promote to Idea dialog (M12 T-514) ---
// Geen extra inputs nodig — title/description komen uit de todo, en
// promoteTodoToIdeaAction archiveert de todo automatisch.
function PromoteIdeaDialog({
todo,
onClose,
}: { todo: Todo; onClose: () => void }) {
const router = useRouter()
const handleKey = useCallback((e: KeyboardEvent) => { if (e.key === 'Escape') onClose() }, [onClose])
useEffect(() => {
document.addEventListener('keydown', handleKey)
return () => document.removeEventListener('keydown', handleKey)
}, [handleKey])
const [pending, startTransition] = useTransition()
function handleConfirm() {
startTransition(async () => {
const r = await promoteTodoToIdeaAction(todo.id)
if ('error' in r) {
toast.error(r.error)
return
}
toast.success(`Idee aangemaakt (${r.idea_code})`)
onClose()
router.push(`/ideas/${r.idea_id}`)
})
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-popover border border-border rounded-xl p-6 w-full max-w-md shadow-xl space-y-4">
<h2 className="font-medium text-foreground">Promoveer naar Idee</h2>
<p className="text-sm text-muted-foreground">
Maak een nieuw idee van &lsquo;<strong>{todo.title}</strong>&rsquo;. De Todo wordt
gearchiveerd; je kunt hem later terugvinden in de archief-filter.
</p>
<p className="text-xs text-muted-foreground">
Het idee start als <code>DRAFT</code>. Je kunt het daarna grillen, plannen, en
materialiseren tot een PBI.
</p>
<div className="flex gap-2 justify-end pt-2">
<Button type="button" variant="ghost" onClick={onClose} disabled={pending}>
Annuleren
</Button>
<Button onClick={handleConfirm} disabled={pending}>
{pending ? 'Bezig…' : 'Promoveren'}
</Button>
</div>
</div>
</div>
)
}
// --- Detail card ---
function TodoCard({
mode,
@ -243,6 +299,7 @@ function TodoCard({
onSuccess,
onPromotePbi,
onPromoteStory,
onPromoteIdea,
}: {
mode: 'idle' | 'create' | 'edit'
activeTodo: Todo | null
@ -252,6 +309,7 @@ function TodoCard({
onSuccess: () => void
onPromotePbi: (todo: Todo) => void
onPromoteStory: (todo: Todo) => void
onPromoteIdea: (todo: Todo) => void
}) {
const [createState, createFormAction] = useActionState(createTodoAction, undefined)
const [editState, editFormAction] = useActionState(updateTodoAction, undefined)
@ -366,6 +424,9 @@ function TodoCard({
<div className="flex items-center gap-2">
{!isDemo && (
<>
<Button type="button" variant="outline" size="sm" onClick={() => onPromoteIdea(activeTodo)}>
Idee
</Button>
<Button type="button" variant="outline" size="sm" onClick={() => onPromotePbi(activeTodo)}>
PBI
</Button>
@ -393,6 +454,7 @@ export function TodoList({ todos, products, isDemo }: TodoListProps) {
const [mode, setMode] = useState<'idle' | 'create'>('idle')
const [promotePbi, setPromotePbi] = useState<Todo | null>(null)
const [promoteStory, setPromoteStory] = useState<Todo | null>(null)
const [promoteIdea, setPromoteIdea] = useState<Todo | null>(null)
const filtered = useMemo(() => {
if (selectedProductId === 'all') return todos
@ -608,6 +670,7 @@ export function TodoList({ todos, products, isDemo }: TodoListProps) {
onSuccess={handleCancel}
onPromotePbi={setPromotePbi}
onPromoteStory={setPromoteStory}
onPromoteIdea={setPromoteIdea}
/>
{promotePbi && (
@ -616,6 +679,9 @@ export function TodoList({ todos, products, isDemo }: TodoListProps) {
{promoteStory && (
<PromoteStoryDialog todo={promoteStory} products={products} onClose={() => setPromoteStory(null)} />
)}
{promoteIdea && (
<PromoteIdeaDialog todo={promoteIdea} onClose={() => setPromoteIdea(null)} />
)}
</div>
)
}