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

@ -14,11 +14,20 @@ async function getSession() {
export async function createTodoAction(_prevState: unknown, formData: FormData) { export async function createTodoAction(_prevState: unknown, formData: FormData) {
const session = await getSession() const session = await getSession()
if (!session.userId) return { error: 'Niet ingelogd' } if (!session.userId) return { error: 'Niet ingelogd' }
if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' }
const title = (formData.get('title') as string)?.trim() const title = (formData.get('title') as string)?.trim()
if (!title) return { error: 'Titel is verplicht' } const productId = (formData.get('productId') as string)?.trim()
await prisma.todo.create({ data: { user_id: session.userId, title } }) if (!title) return { error: 'Titel is verplicht' }
if (!productId) return { error: 'Product is verplicht' }
const product = await prisma.product.findFirst({
where: { id: productId, user_id: session.userId, archived: false },
})
if (!product) return { error: 'Product niet gevonden' }
await prisma.todo.create({ data: { user_id: session.userId, product_id: productId, title } })
revalidatePath('/todos') revalidatePath('/todos')
return { success: true } return { success: true }
} }

View file

@ -10,6 +10,7 @@ export default async function TodosPage() {
const todos = await prisma.todo.findMany({ const todos = await prisma.todo.findMany({
where: { user_id: session.userId, archived: false }, where: { user_id: session.userId, archived: false },
orderBy: { created_at: 'asc' }, orderBy: { created_at: 'asc' },
include: { product: { select: { name: true } } },
}) })
const products = await prisma.product.findMany({ const products = await prisma.product.findMany({
@ -24,7 +25,14 @@ export default async function TodosPage() {
<div className="p-6 max-w-2xl mx-auto w-full"> <div className="p-6 max-w-2xl mx-auto w-full">
<h1 className="text-xl font-medium text-foreground mb-6">Todo&apos;s</h1> <h1 className="text-xl font-medium text-foreground mb-6">Todo&apos;s</h1>
<TodoList <TodoList
todos={todos.map(t => ({ id: t.id, title: t.title, done: t.done, created_at: t.created_at.toISOString() }))} todos={todos.map(t => ({
id: t.id,
title: t.title,
done: t.done,
created_at: t.created_at.toISOString(),
product_id: t.product_id ?? null,
product_name: t.product?.name ?? null,
}))}
products={products.map(p => ({ products={products.map(p => ({
id: p.id, id: p.id,
name: p.name, name: p.name,

View file

@ -4,6 +4,7 @@ import { z } from 'zod'
const bodySchema = z.object({ const bodySchema = z.object({
title: z.string().min(1, 'Titel is verplicht').max(500), title: z.string().min(1, 'Titel is verplicht').max(500),
product_id: z.string().min(1, 'Product is verplicht'),
}) })
export async function POST(request: Request) { export async function POST(request: Request) {
@ -21,9 +22,17 @@ export async function POST(request: Request) {
return Response.json({ error: parsed.error.flatten() }, { status: 400 }) return Response.json({ error: parsed.error.flatten() }, { status: 400 })
} }
const product = await prisma.product.findFirst({
where: { id: parsed.data.product_id, user_id: auth.userId, archived: false },
})
if (!product) {
return Response.json({ error: 'Product niet gevonden' }, { status: 404 })
}
const todo = await prisma.todo.create({ const todo = await prisma.todo.create({
data: { data: {
user_id: auth.userId, user_id: auth.userId,
product_id: parsed.data.product_id,
title: parsed.data.title, title: parsed.data.title,
}, },
}) })

View file

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

View file

@ -208,13 +208,14 @@ Scrum4Me is een desktop-first Next.js 15 webapplicatie die server-side wordt ger
|---|---|---|---| |---|---|---|---|
| id | String (cuid) | PK | | | id | String (cuid) | PK | |
| user_id | String | FK → users, not null | | | user_id | String | FK → users, not null | |
| product_id | String | FK → products, nullable | Verplicht in UI en API; nullable voor backward compatibility |
| title | String | not null | | | title | String | not null | |
| done | Boolean | default false | | | done | Boolean | default false | |
| archived | Boolean | default false | | | archived | Boolean | default false | |
| created_at | DateTime | default now() | | | created_at | DateTime | default now() | |
| updated_at | DateTime | auto-update | | | updated_at | DateTime | auto-update | |
**Indexes:** `(user_id, done, archived)` — standaard weergave filtert op actieve todo's **Indexes:** `(user_id, done, archived)` — standaard weergave filtert op actieve todo's; `(user_id, product_id)` — filteren per product
--- ---
@ -405,6 +406,8 @@ model Todo {
id String @id @default(cuid()) id String @id @default(cuid())
user User @relation(fields: [user_id], references: [id], onDelete: Cascade) user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
user_id String user_id String
product Product? @relation(fields: [product_id], references: [id], onDelete: SetNull)
product_id String?
title String title String
done Boolean @default(false) done Boolean @default(false)
archived Boolean @default(false) archived Boolean @default(false)
@ -412,6 +415,7 @@ model Todo {
updated_at DateTime @updatedAt updated_at DateTime @updatedAt
@@index([user_id, done, archived]) @@index([user_id, done, archived])
@@index([user_id, product_id])
} }
``` ```

View file

@ -233,13 +233,13 @@ De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan
- Route Handler; body: `{ type, content, status?, commit_hash?, commit_message? }`; Zod-validatie per type; schrijf naar `story_logs`; retourneer `{ id, created_at }` - Route Handler; body: `{ type, content, status?, commit_hash?, commit_message? }`; Zod-validatie per type; schrijf naar `story_logs`; retourneer `{ id, created_at }`
- Done when: drie typen werken (IMPLEMENTATION_PLAN, TEST_RESULT, COMMIT); log-entry zichtbaar in story-detail UI na aanmaken via API; ontbrekend verplicht veld geeft 400 - Done when: drie typen werken (IMPLEMENTATION_PLAN, TEST_RESULT, COMMIT); log-entry zichtbaar in story-detail UI na aanmaken via API; ontbrekend verplicht veld geeft 400
- [ ] **ST-408** `PATCH /api/tasks/:id` — taakstatus bijwerken - [ ] **ST-408** `PATCH /api/tasks/:id` — taakstatus en implementatieplan bijwerken
- Route Handler; body: `{ status: "TO_DO" | "IN_PROGRESS" | "DONE" }`; valideer dat taak aan requester's product behoort; update status; retourneer `{ id, status }` - Route Handler; body: `{ status?: "TO_DO" | "IN_PROGRESS" | "DONE", implementation_plan?: string }`; minimaal één veld verplicht; valideer dat taak aan requester's product behoort; retourneer `{ id, status, implementation_plan }`
- Done when: status update via API zichtbaar in Sprint Planning UI; ongeldige status geeft 400; andere gebruikers taak geeft 403 - Done when: status update via API zichtbaar in Sprint Planning UI; implementation_plan opgeslagen en opvraagbaar; lege body geeft 400; andere gebruikers taak geeft 403
- [ ] **ST-409** `POST /api/todos` — todo aanmaken - [ ] **ST-409** `POST /api/todos` — todo aanmaken
- Route Handler; body: `{ title: string }`; schrijf naar `todos` voor de geverifieerde gebruiker; retourneer `{ id, title, created_at }` - Route Handler; body: `{ title: string, product_id: string }`; valideer dat product bij de geverifieerde gebruiker hoort; schrijf naar `todos`; retourneer `{ id, title, created_at }`
- Done when: todo aangemaakt via API verschijnt in todo-lijst UI; lege titel geeft 400 - Done when: todo aangemaakt via API met product_id verschijnt in todo-lijst UI gekoppeld aan het juiste product; lege titel of ontbrekend product_id geeft 400; onbekend product geeft 404
- [ ] **ST-410** Story-activiteitenlog UI - [ ] **ST-410** Story-activiteitenlog UI
- Activiteitenlog sectie in story-detail slide-over; haal `story_logs` op via Server Component; render chronologisch; visuele stijl per type (IMPLEMENTATION_PLAN = blauw, TEST_RESULT passed = groen, failed = rood, COMMIT = paars); commit-hash klikbaar als `repo_url` ingesteld; lege staat - Activiteitenlog sectie in story-detail slide-over; haal `story_logs` op via Server Component; render chronologisch; visuele stijl per type (IMPLEMENTATION_PLAN = blauw, TEST_RESULT passed = groen, failed = rood, COMMIT = paars); commit-hash klikbaar als `repo_url` ingesteld; lege staat
@ -250,8 +250,8 @@ De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan
### M5: Todo-lijst ### M5: Todo-lijst
- [ ] **ST-501** Todo-lijst pagina - [ ] **ST-501** Todo-lijst pagina
- `/todos` pagina; haal actieve (niet-gearchiveerde) todos op; snel-invoerveld bovenaan (Enter om op te slaan); `createTodo` Server Action; lege staat met instructie - `/todos` pagina; haal actieve (niet-gearchiveerde) todos op inclusief productnaam; snel-invoerveld bovenaan met product-dropdown (verplicht) en titel (Enter om op te slaan); `createTodo` Server Action; lege staat met instructie; productnaam-badge per todo-item
- Done when: todo aanmaken via Enter persisteert en verschijnt in lijst; lege staat zichtbaar bij geen todos - Done when: todo aanmaken via Enter persisteert en verschijnt in lijst met productnaam; aanmaken zonder product geblokkeerd; lege staat zichtbaar bij geen todos
- [ ] **ST-502** Todo afvinken - [ ] **ST-502** Todo afvinken
- Checkbox per todo; `toggleTodo` Server Action; afgevinkte todos visueel doorgestreept; afgevinkte todos blijven zichtbaar onderaan de lijst - Checkbox per todo; `toggleTodo` Server Action; afgevinkte todos visueel doorgestreept; afgevinkte todos blijven zichtbaar onderaan de lijst

View file

@ -220,16 +220,16 @@ Elke story heeft een activiteitenlog die alle door Claude Code vastgelegde stapp
**Persona:** Lars (snelle vastlegging), Dina (losse klantnotities) **Persona:** Lars (snelle vastlegging), Dina (losse klantnotities)
**Omschrijving:** **Omschrijving:**
Een snelle todo-lijst voor losse taken die (nog) niet aan een product of Sprint gekoppeld zijn. Todo-items kunnen worden afgevinkt en gepromoveerd naar een PBI of story in een bestaand product. Een snelle todo-lijst voor taken die aan een specifiek product zijn gekoppeld. Todo-items kunnen worden afgevinkt en gepromoveerd naar een PBI of story in dat product. Zowel de UI als de REST API vereisen een `product_id` bij aanmaken — zodat Claude Code altijd werkt binnen de context van de actieve product backlog.
**Acceptatiecriteria:** **Acceptatiecriteria:**
- [ ] Todo aanmaken via snel-invoerveld (Enter om op te slaan); alleen titel verplicht - [ ] Todo aanmaken via snel-invoerveld (Enter om op te slaan); product (dropdown) en titel verplicht
- [ ] Todo aanmaken ook mogelijk via REST API: `POST /api/todos` (body: `{ "title": string }`) — zodat Claude Code losse bevindingen kan vastleggen zonder de planningsflow te onderbreken - [ ] Todo aanmaken ook mogelijk via REST API: `POST /api/todos` (body: `{ "title": string, "product_id": string }`) — zodat Claude Code bevindingen kan vastleggen binnen de actieve product backlog
- [ ] Todo-lijst is zichtbaar als apart scherm of persistent zijpaneel - [ ] Todo-lijst is zichtbaar als apart scherm of persistent zijpaneel
- [ ] Todo afvinken markeert het als afgerond (visueel doorgestreept) - [ ] Todo afvinken markeert het als afgerond (visueel doorgestreept)
- [ ] Afgevinkte todo's blijven zichtbaar; kunnen worden gearchiveerd via "Archiveer afgeronde items" - [ ] Afgevinkte todo's blijven zichtbaar; kunnen worden gearchiveerd via "Archiveer afgeronde items"
- [ ] Todo promoveren naar PBI: dialoog vraagt om product en prioriteit; todo verdwijnt na promotie - [ ] Todo promoveren naar PBI: dialoog pre-selecteert het gekoppelde product (bewerkbaar), vraagt prioriteit; todo verdwijnt na promotie
- [ ] Todo promoveren naar story: dialoog vraagt om product, PBI en prioriteit; todo verdwijnt na promotie - [ ] Todo promoveren naar story: dialoog pre-selecteert het gekoppelde product (bewerkbaar), vraagt PBI en prioriteit; todo verdwijnt na promotie
- [ ] Titel van het todo-item is vooringevuld in de promotiedialoog (bewerkbaar) - [ ] Titel van het todo-item is vooringevuld in de promotiedialoog (bewerkbaar)
- [ ] Promotie is niet ongedaan te maken; dialoog waarschuwt hiervoor - [ ] Promotie is niet ongedaan te maken; dialoog waarschuwt hiervoor
@ -238,7 +238,7 @@ Een snelle todo-lijst voor losse taken die (nog) niet aan een product of Sprint
- Promoveren naar story zonder PBI's in het product → dialoog toont melding "Maak eerst een PBI aan" - Promoveren naar story zonder PBI's in het product → dialoog toont melding "Maak eerst een PBI aan"
**Data:** **Data:**
- Opgeslagen: `todos` (id, user_id, title, done, archived, created_at, updated_at) - Opgeslagen: `todos` (id, user_id, product_id, title, done, archived, created_at, updated_at)
--- ---
@ -316,8 +316,8 @@ Een REST API waarmee Claude Code stories en taken kan ophalen, de taakvolgorde k
- [ ] `GET /api/sprints/:id/tasks?limit=10` — eerste N taken in huidige volgorde - [ ] `GET /api/sprints/:id/tasks?limit=10` — eerste N taken in huidige volgorde
- [ ] `PATCH /api/stories/:id/tasks/reorder` — accepteert geordende lijst van taak-id's - [ ] `PATCH /api/stories/:id/tasks/reorder` — accepteert geordende lijst van taak-id's
- [ ] `POST /api/stories/:id/log` — vastleggen van implementatieplan, testresultaat of commit - [ ] `POST /api/stories/:id/log` — vastleggen van implementatieplan, testresultaat of commit
- [ ] `PATCH /api/tasks/:id` — status bijwerken (TO_DO → IN_PROGRESS → DONE) - [ ] `PATCH /api/tasks/:id` — status bijwerken (TO_DO → IN_PROGRESS → DONE) en/of `implementation_plan` opslaan
- [ ] `POST /api/todos` — todo aanmaken vanuit Claude Code (body: `{ "title": string }`) - [ ] `POST /api/todos` — todo aanmaken vanuit Claude Code (body: `{ "title": string, "product_id": string }`)
**Authenticatie:** **Authenticatie:**
- [ ] Alle endpoints vereisen `Authorization: Bearer <token>` header - [ ] Alle endpoints vereisen `Authorization: Bearer <token>` header
@ -404,8 +404,8 @@ De app is deployable op Vercel + Neon PostgreSQL en lokaal draaibaar met een Neo
| `stories` | id, pbi_id, product_id, title, description, acceptance_criteria, priority, sort_order, status, sprint_id? | Status: OPEN / IN_SPRINT / DONE | | `stories` | id, pbi_id, product_id, title, description, acceptance_criteria, priority, sort_order, status, sprint_id? | Status: OPEN / IN_SPRINT / DONE |
| `story_logs` | id, story_id, type, content, status?, commit_hash?, commit_message?, created_at | Aangemaakt via API; read-only in UI | | `story_logs` | id, story_id, type, content, status?, commit_hash?, commit_message?, created_at | Aangemaakt via API; read-only in UI |
| `sprints` | id, product_id, sprint_goal, status (ACTIVE / COMPLETED), created_at, completed_at? | Max. 1 actieve Sprint per product | | `sprints` | id, product_id, sprint_goal, status (ACTIVE / COMPLETED), created_at, completed_at? | Max. 1 actieve Sprint per product |
| `tasks` | id, story_id, sprint_id, title, description, priority, sort_order, status | Status: TO_DO / IN_PROGRESS / DONE | | `tasks` | id, story_id, sprint_id, title, description, implementation_plan?, priority, sort_order, status | Status: TO_DO / IN_PROGRESS / DONE; implementation_plan door MCP |
| `todos` | id, user_id, title, done, archived, created_at | Ontkoppeld van producthiërarchie | | `todos` | id, user_id, product_id, title, done, archived, created_at | Gekoppeld aan product backlog; verplicht in UI en API |
--- ---

View file

@ -0,0 +1,8 @@
-- AlterTable
ALTER TABLE "todos" ADD COLUMN "product_id" TEXT;
-- CreateIndex
CREATE INDEX "todos_user_id_product_id_idx" ON "todos"("user_id", "product_id");
-- AddForeignKey
ALTER TABLE "todos" ADD CONSTRAINT "todos_product_id_fkey" FOREIGN KEY ("product_id") REFERENCES "products"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View file

@ -92,6 +92,7 @@ model Product {
pbis Pbi[] pbis Pbi[]
sprints Sprint[] sprints Sprint[]
stories Story[] stories Story[]
todos Todo[]
@@unique([user_id, name]) @@unique([user_id, name])
@@index([user_id, archived]) @@index([user_id, archived])
@ -170,18 +171,19 @@ model Sprint {
} }
model Task { model Task {
id String @id @default(cuid()) id String @id @default(cuid())
story Story @relation(fields: [story_id], references: [id], onDelete: Cascade) story Story @relation(fields: [story_id], references: [id], onDelete: Cascade)
story_id String story_id String
sprint Sprint? @relation(fields: [sprint_id], references: [id]) sprint Sprint? @relation(fields: [sprint_id], references: [id])
sprint_id String? sprint_id String?
title String title String
description String? description String?
priority Int implementation_plan String?
sort_order Float priority Int
status TaskStatus @default(TO_DO) sort_order Float
created_at DateTime @default(now()) status TaskStatus @default(TO_DO)
updated_at DateTime @updatedAt created_at DateTime @default(now())
updated_at DateTime @updatedAt
@@index([story_id, priority, sort_order]) @@index([story_id, priority, sort_order])
@@index([sprint_id, status]) @@index([sprint_id, status])
@ -192,6 +194,8 @@ model Todo {
id String @id @default(cuid()) id String @id @default(cuid())
user User @relation(fields: [user_id], references: [id], onDelete: Cascade) user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
user_id String user_id String
product Product? @relation(fields: [product_id], references: [id], onDelete: SetNull)
product_id String?
title String title String
done Boolean @default(false) done Boolean @default(false)
archived Boolean @default(false) archived Boolean @default(false)
@ -199,5 +203,6 @@ model Todo {
updated_at DateTime @updatedAt updated_at DateTime @updatedAt
@@index([user_id, done, archived]) @@index([user_id, done, archived])
@@index([user_id, product_id])
@@map("todos") @@map("todos")
} }