feat: Todo altijd gekoppeld aan product backlog

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-25 12:35:40 +02:00
parent b541379964
commit cb7eb36fbb
9 changed files with 128 additions and 46 deletions

View file

@ -19,6 +19,8 @@ interface Todo {
title: string
done: boolean
created_at: string
product_id: string | null
product_name: string | null
}
interface Pbi {
@ -38,7 +40,7 @@ interface TodoListProps {
isDemo: boolean
}
function QuickInput({ isDemo }: { isDemo: boolean }) {
function QuickInput({ products, isDemo }: { products: Product[]; isDemo: boolean }) {
const [, formAction] = useActionState(createTodoAction, undefined)
const ref = useRef<HTMLFormElement>(null)
@ -49,15 +51,27 @@ function QuickInput({ isDemo }: { isDemo: boolean }) {
onSubmit={() => setTimeout(() => ref.current?.reset(), 0)}
className="flex gap-2 mb-6"
>
<select
name="productId"
required
disabled={isDemo || products.length === 0}
className="border border-border rounded-lg px-3 py-1.5 text-sm bg-input-background shrink-0"
>
{products.length === 0 ? (
<option value="">Geen producten</option>
) : (
products.map(p => <option key={p.id} value={p.id}>{p.name}</option>)
)}
</select>
<Input
name="title"
placeholder={isDemo ? 'Alleen-lezen in demo' : 'Nieuwe todo… (Enter om op te slaan)'}
disabled={isDemo}
disabled={isDemo || products.length === 0}
className="flex-1"
autoComplete="off"
/>
<DemoTooltip show={isDemo}>
<QuickSubmitButton isDemo={isDemo} />
<QuickSubmitButton isDemo={isDemo || products.length === 0} />
</DemoTooltip>
</form>
)
@ -79,7 +93,10 @@ function PromotePbiDialog({
onClose,
}: { todo: Todo; products: Product[]; onClose: () => void }) {
const handleKey = useCallback((e: KeyboardEvent) => { if (e.key === 'Escape') onClose() }, [onClose])
useEffect(() => { document.addEventListener('keydown', handleKey); return () => document.removeEventListener('keydown', handleKey) }, [handleKey])
useEffect(() => {
document.addEventListener('keydown', handleKey)
return () => document.removeEventListener('keydown', handleKey)
}, [handleKey])
const [state, formAction] = useActionState(
async (_prev: unknown, fd: FormData) => {
@ -106,7 +123,12 @@ function PromotePbiDialog({
{products.length === 0 ? (
<p className="text-sm text-muted-foreground">Maak eerst een product aan.</p>
) : (
<select name="productId" required className="w-full border border-border rounded-lg px-3 py-1.5 text-sm bg-input-background">
<select
name="productId"
required
defaultValue={todo.product_id ?? products[0]?.id}
className="w-full border border-border rounded-lg px-3 py-1.5 text-sm bg-input-background"
>
{products.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
)}
@ -115,7 +137,7 @@ function PromotePbiDialog({
<label className="text-sm font-medium">Prioriteit</label>
<select name="priority" required className="w-full border border-border rounded-lg px-3 py-1.5 text-sm bg-input-background">
<option value="1">Kritiek</option>
<option value="2" selected>Hoog</option>
<option value="2">Hoog</option>
<option value="3">Gemiddeld</option>
<option value="4">Laag</option>
</select>
@ -138,9 +160,12 @@ function PromoteStoryDialog({
onClose,
}: { todo: Todo; products: Product[]; onClose: () => void }) {
const handleKey = useCallback((e: KeyboardEvent) => { if (e.key === 'Escape') onClose() }, [onClose])
useEffect(() => { document.addEventListener('keydown', handleKey); return () => document.removeEventListener('keydown', handleKey) }, [handleKey])
useEffect(() => {
document.addEventListener('keydown', handleKey)
return () => document.removeEventListener('keydown', handleKey)
}, [handleKey])
const [selectedProductId, setSelectedProductId] = useState(products[0]?.id ?? '')
const [selectedProductId, setSelectedProductId] = useState(todo.product_id ?? products[0]?.id ?? '')
const selectedProduct = products.find(p => p.id === selectedProductId)
const [state, formAction] = useActionState(
@ -192,7 +217,7 @@ function PromoteStoryDialog({
<label className="text-sm font-medium">Prioriteit</label>
<select name="priority" required className="w-full border border-border rounded-lg px-3 py-1.5 text-sm bg-input-background">
<option value="1">Kritiek</option>
<option value="2" selected>Hoog</option>
<option value="2">Hoog</option>
<option value="3">Gemiddeld</option>
<option value="4">Laag</option>
</select>
@ -233,13 +258,17 @@ export function TodoList({ todos, products, isDemo }: TodoListProps) {
return (
<div className="space-y-4">
<QuickInput isDemo={isDemo} />
<QuickInput products={products} isDemo={isDemo} />
{todos.length === 0 ? (
{products.length === 0 && (
<p className="text-sm text-muted-foreground">Maak eerst een product aan om todo&apos;s toe te voegen.</p>
)}
{todos.length === 0 && products.length > 0 ? (
<div className="bg-surface-container-low border border-border rounded-xl p-12 text-center">
<p className="text-muted-foreground text-sm">Geen todo&apos;s. Voeg er een toe hierboven.</p>
</div>
) : (
) : todos.length > 0 ? (
<>
<div className="bg-surface-container-low border border-border rounded-xl divide-y divide-border">
{open.map(todo => (
@ -252,6 +281,11 @@ export function TodoList({ todos, products, isDemo }: TodoListProps) {
className="w-4 h-4 rounded accent-primary cursor-pointer"
/>
<span className="flex-1 text-sm">{todo.title}</span>
{todo.product_name && (
<span className="text-xs text-muted-foreground bg-surface-container px-1.5 py-0.5 rounded shrink-0">
{todo.product_name}
</span>
)}
{!isDemo && (
<div className="opacity-0 group-hover:opacity-100 flex gap-2 shrink-0">
<button onClick={() => setPromotePbi(todo)} className="text-xs text-muted-foreground hover:text-foreground"> PBI</button>
@ -270,6 +304,11 @@ export function TodoList({ todos, products, isDemo }: TodoListProps) {
className="w-4 h-4 rounded accent-primary cursor-pointer"
/>
<span className="flex-1 text-sm line-through text-muted-foreground">{todo.title}</span>
{todo.product_name && (
<span className="text-xs text-muted-foreground bg-surface-container px-1.5 py-0.5 rounded shrink-0">
{todo.product_name}
</span>
)}
</div>
))}
</div>
@ -282,7 +321,7 @@ export function TodoList({ todos, products, isDemo }: TodoListProps) {
</div>
)}
</>
)}
) : null}
{promotePbi && (
<PromotePbiDialog todo={promotePbi} products={products} onClose={() => setPromotePbi(null)} />