feat: Todo altijd gekoppeld aan product backlog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b541379964
commit
cb7eb36fbb
9 changed files with 128 additions and 46 deletions
|
|
@ -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'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'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)} />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue