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:
parent
2f41f8917a
commit
7595474fcc
1 changed files with 66 additions and 0 deletions
|
|
@ -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 ‘<strong>{todo.title}</strong>’. 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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue