* feat(ST-1109.2): add PbiStatus enum and status field to Pbi model - New PbiStatus enum (READY/BLOCKED/DONE) for PBI lifecycle tracking - Pbi.status PbiStatus @default(READY) - Index on (product_id, status) for filter queries - Migration: 20260429150643_add_pbi_status - ERD regenerated via prisma generate Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ST-1109.3): add PBI status API mappers - pbiStatusToApi / pbiStatusFromApi following same pattern as task/story - PbiStatusApi type derived from PBI_DB_TO_API - PBI_STATUS_API_VALUES export for downstream Zod schemas - Lowercase API surface (ready/blocked/done), DB stays UPPER_SNAKE Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ST-1109.4): support status in PBI create/update actions - Optional status field in Zod schemas (lowercase API: ready/blocked/done) - pbiStatusFromApi() maps to DB enum before persistence - Status omitted on create => Prisma @default(READY) takes effect - Update preserves existing status when not provided - Demo-check unchanged Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ST-1109.5): auto-mark PBI as DONE when all its stories are DONE on sprint close Extends completeSprintAction's $transaction with PBI status cascade: - Pre-transaction: identify PBIs touched by this close (via stories.pbi_id), fetch each with all its stories - Skip PBIs already DONE; skip PBIs with 0 stories - Mark PBI DONE only when every story (post-decision) is DONE — stories outside the sprint are evaluated against their current DB status - Promote-only: never demotes a PBI that becomes "incomplete" again Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ST-1109.6): add Popover primitive (base-ui wrapper) - Mirrors the Tooltip pattern: render-prop composition, data-slot attrs - Exports Popover (Root), PopoverTrigger, PopoverContent (Portal+Positioner+Popup) - MD3 popover/popover-foreground tokens, animated open/close states - Will be used to consolidate the backlog filter UI in ST-1109.8 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ST-1109.7): add status select to PBI dialog - New components/shared/pbi-status-select.tsx mirrors PrioritySelect: PBI_STATUS_LABELS (NL), PBI_STATUS_COLORS, PbiStatusSelect component - Reuses existing --status-todo/blocked/done MD3 tokens - PbiDialog: status state with sync-on-open; default 'ready' for create, pbi.status for edit; hidden input submits lowercase API value - Priority + Status sit side-by-side in 2-col grid - PbiDialogPbi.status is optional; pbi-list.tsx will populate in ST-1109.8 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ST-1109.8): show PBI status badge and consolidate filters into popover - Pbi.status (lowercase API) flows from page.tsx via pbiStatusToApi - Status badge rendered in BacklogCard's badge slot using PBI_STATUS_COLORS - Two old Select dropdowns replaced by single Popover with three pill-button sections (Sorteren, Prioriteit, Status) and a "Wis filters" footer - Filter trigger shows active count "(n)" badge in label - Active priority/status filters still surface as dismissable chips next to the trigger for at-a-glance feedback - onEdit passes the full Pbi (incl. status) so the dialog opens with the correct current status — closes the data flow loop opened in ST-1109.7 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(ST-1109.9): cover PBI status mappers and sprint-close cascade - __tests__/lib/task-status.test.ts: 11 cases incl. round-trip + invalid input for task/story/pbi mappers; verifies PBI_STATUS_API_VALUES shape - __tests__/actions/sprints-cascade.test.ts: 8 cases for completeSprintAction: promote on all-DONE, no promote on partial OPEN, respect out-of-sprint story status, skip already-DONE PBIs, multi-PBI cascade, 0-story guard, demo-user block - Full vitest run: 170/170 green across 21 files Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(ST-1109.10): document PbiStatus enum, sprint-close cascade, and filter UI - docs/scrum4me-architecture.md: pbis-table updated with status column + index; PbiStatus enum + Pbi model in the Prisma schema sample; cascade-on-sprint-close rule documented inline - docs/scrum4me-styling.md: short note pointing to PBI_STATUS_LABELS / PBI_STATUS_COLORS in components/shared/pbi-status-select.tsx so future components don't ad-hoc-copy the color map - docs/plans/ST-1109-pbi-status.md: in-repo mirror of the approved plan (per feedback_plan_location memory) with cascade pseudo-code and end-to-end verification checklist Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ST-1109.11): persist backlog filters in localStorage Filters reset op reload was verwarrend. Nu net als sortMode: - scrum4me:pbi_filter_priority — 'all' | '1' | '2' | '3' | '4' - scrum4me:pbi_filter_status — 'all' | 'ready' | 'blocked' | 'done' useState-init met SSR-guard; ongeldige waarden vallen terug op 'all'. Wis filters reset alle drie de keys correct (sortMode -> 'priority', beide filters -> 'all'), waardoor de localStorage-staat consistent wordt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
308 lines
9 KiB
Text
308 lines
9 KiB
Text
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
generator erd {
|
|
provider = "prisma-erd-generator"
|
|
output = "../docs/erd.svg"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
}
|
|
|
|
enum Role {
|
|
PRODUCT_OWNER
|
|
SCRUM_MASTER
|
|
DEVELOPER
|
|
}
|
|
|
|
enum StoryStatus {
|
|
OPEN
|
|
IN_SPRINT
|
|
DONE
|
|
}
|
|
|
|
enum PbiStatus {
|
|
READY
|
|
BLOCKED
|
|
DONE
|
|
}
|
|
|
|
enum TaskStatus {
|
|
TO_DO
|
|
IN_PROGRESS
|
|
REVIEW
|
|
DONE
|
|
}
|
|
|
|
enum LogType {
|
|
IMPLEMENTATION_PLAN
|
|
TEST_RESULT
|
|
COMMIT
|
|
}
|
|
|
|
enum TestStatus {
|
|
PASSED
|
|
FAILED
|
|
}
|
|
|
|
enum SprintStatus {
|
|
ACTIVE
|
|
COMPLETED
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
username String @unique
|
|
email String? @unique
|
|
password_hash String
|
|
is_demo Boolean @default(false)
|
|
bio String? @db.VarChar(160)
|
|
bio_detail String? @db.VarChar(2000)
|
|
avatar_data Bytes?
|
|
active_product_id String?
|
|
active_product Product? @relation("UserActiveProduct", fields: [active_product_id], references: [id], onDelete: SetNull)
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime @updatedAt
|
|
roles UserRole[]
|
|
api_tokens ApiToken[]
|
|
products Product[]
|
|
todos Todo[]
|
|
product_members ProductMember[]
|
|
assigned_stories Story[] @relation("StoryAssignee")
|
|
login_pairings LoginPairing[]
|
|
asked_questions ClaudeQuestion[] @relation("ClaudeQuestionAsker")
|
|
answered_questions ClaudeQuestion[] @relation("ClaudeQuestionAnswerer")
|
|
|
|
@@index([active_product_id])
|
|
@@map("users")
|
|
}
|
|
|
|
model UserRole {
|
|
id String @id @default(cuid())
|
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
user_id String
|
|
role Role
|
|
|
|
@@unique([user_id, role])
|
|
@@map("user_roles")
|
|
}
|
|
|
|
model ApiToken {
|
|
id String @id @default(cuid())
|
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
user_id String
|
|
token_hash String @unique
|
|
label String?
|
|
created_at DateTime @default(now())
|
|
revoked_at DateTime?
|
|
|
|
@@index([token_hash])
|
|
@@map("api_tokens")
|
|
}
|
|
|
|
model Product {
|
|
id String @id @default(cuid())
|
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
user_id String
|
|
name String
|
|
code String? @db.VarChar(30)
|
|
description String?
|
|
repo_url String?
|
|
definition_of_done String
|
|
archived Boolean @default(false)
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime @updatedAt
|
|
pbis Pbi[]
|
|
sprints Sprint[]
|
|
stories Story[]
|
|
todos Todo[]
|
|
members ProductMember[]
|
|
active_for_users User[] @relation("UserActiveProduct")
|
|
claude_questions ClaudeQuestion[]
|
|
|
|
@@unique([user_id, name])
|
|
@@unique([user_id, code])
|
|
@@index([user_id, archived])
|
|
@@map("products")
|
|
}
|
|
|
|
model Pbi {
|
|
id String @id @default(cuid())
|
|
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
|
product_id String
|
|
code String? @db.VarChar(30)
|
|
title String
|
|
description String?
|
|
priority Int
|
|
sort_order Float
|
|
status PbiStatus @default(READY)
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime @updatedAt
|
|
stories Story[]
|
|
|
|
@@unique([product_id, code])
|
|
@@index([product_id, priority, sort_order])
|
|
@@index([product_id, status])
|
|
@@map("pbis")
|
|
}
|
|
|
|
model Story {
|
|
id String @id @default(cuid())
|
|
pbi Pbi @relation(fields: [pbi_id], references: [id], onDelete: Cascade)
|
|
pbi_id String
|
|
product Product @relation(fields: [product_id], references: [id])
|
|
product_id String
|
|
sprint Sprint? @relation(fields: [sprint_id], references: [id])
|
|
sprint_id String?
|
|
assignee User? @relation("StoryAssignee", fields: [assignee_id], references: [id], onDelete: SetNull)
|
|
assignee_id String?
|
|
code String? @db.VarChar(30)
|
|
title String
|
|
description String?
|
|
acceptance_criteria String?
|
|
priority Int
|
|
sort_order Float
|
|
status StoryStatus @default(OPEN)
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime @updatedAt
|
|
logs StoryLog[]
|
|
tasks Task[]
|
|
claude_questions ClaudeQuestion[]
|
|
|
|
@@unique([product_id, code])
|
|
@@index([pbi_id, priority, sort_order])
|
|
@@index([sprint_id, sort_order])
|
|
@@index([product_id, status])
|
|
@@index([sprint_id, assignee_id])
|
|
@@map("stories")
|
|
}
|
|
|
|
model StoryLog {
|
|
id String @id @default(cuid())
|
|
story Story @relation(fields: [story_id], references: [id], onDelete: Cascade)
|
|
story_id String
|
|
type LogType
|
|
content String
|
|
status TestStatus?
|
|
commit_hash String?
|
|
commit_message String?
|
|
metadata Json?
|
|
created_at DateTime @default(now())
|
|
|
|
@@index([story_id, created_at])
|
|
@@map("story_logs")
|
|
}
|
|
|
|
model Sprint {
|
|
id String @id @default(cuid())
|
|
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
|
product_id String
|
|
sprint_goal String
|
|
status SprintStatus @default(ACTIVE)
|
|
created_at DateTime @default(now())
|
|
completed_at DateTime?
|
|
stories Story[]
|
|
tasks Task[]
|
|
|
|
@@index([product_id, status])
|
|
@@map("sprints")
|
|
}
|
|
|
|
model Task {
|
|
id String @id @default(cuid())
|
|
story Story @relation(fields: [story_id], references: [id], onDelete: Cascade)
|
|
story_id String
|
|
sprint Sprint? @relation(fields: [sprint_id], references: [id])
|
|
sprint_id String?
|
|
title String
|
|
description String?
|
|
implementation_plan String?
|
|
priority Int
|
|
sort_order Float
|
|
status TaskStatus @default(TO_DO)
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime @updatedAt
|
|
claude_questions ClaudeQuestion[]
|
|
|
|
@@index([story_id, priority, sort_order])
|
|
@@index([sprint_id, status])
|
|
@@map("tasks")
|
|
}
|
|
|
|
model ProductMember {
|
|
id String @id @default(cuid())
|
|
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
|
product_id String
|
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
user_id String
|
|
created_at DateTime @default(now())
|
|
|
|
@@unique([product_id, user_id])
|
|
@@index([user_id])
|
|
@@map("product_members")
|
|
}
|
|
|
|
model Todo {
|
|
id String @id @default(cuid())
|
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
user_id String
|
|
product Product? @relation(fields: [product_id], references: [id], onDelete: SetNull)
|
|
product_id String?
|
|
title String
|
|
description String? @db.VarChar(2000)
|
|
done Boolean @default(false)
|
|
archived Boolean @default(false)
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime @updatedAt
|
|
|
|
@@index([user_id, done, archived])
|
|
@@index([user_id, product_id])
|
|
@@map("todos")
|
|
}
|
|
|
|
model LoginPairing {
|
|
id String @id @default(cuid())
|
|
secret_hash String
|
|
desktop_token_hash String
|
|
status String
|
|
user_id String?
|
|
user User? @relation(fields: [user_id], references: [id], onDelete: SetNull)
|
|
desktop_ua String? @db.VarChar(255)
|
|
desktop_ip String? @db.VarChar(45)
|
|
created_at DateTime @default(now())
|
|
expires_at DateTime
|
|
approved_at DateTime?
|
|
consumed_at DateTime?
|
|
|
|
@@index([expires_at])
|
|
@@index([status, expires_at])
|
|
@@map("login_pairings")
|
|
}
|
|
|
|
model ClaudeQuestion {
|
|
id String @id @default(cuid())
|
|
story Story @relation(fields: [story_id], references: [id], onDelete: Cascade)
|
|
story_id String
|
|
task Task? @relation(fields: [task_id], references: [id], onDelete: SetNull)
|
|
task_id String?
|
|
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
|
product_id String // gedenormaliseerd uit story.product_id voor SSE-filter
|
|
asker User @relation("ClaudeQuestionAsker", fields: [asked_by], references: [id])
|
|
asked_by String // user_id van token-houder (= Claude-token)
|
|
question String @db.Text
|
|
options Json? // string[] voor multi-choice; null voor free-text
|
|
status String // 'open' | 'answered' | 'cancelled' | 'expired'
|
|
answer String? @db.Text
|
|
answerer User? @relation("ClaudeQuestionAnswerer", fields: [answered_by], references: [id])
|
|
answered_by String?
|
|
answered_at DateTime?
|
|
created_at DateTime @default(now())
|
|
expires_at DateTime // ingesteld door MCP-tool, default now() + 24h
|
|
|
|
@@index([story_id, status])
|
|
@@index([product_id, status])
|
|
@@index([status, expires_at])
|
|
@@map("claude_questions")
|
|
}
|