From e94959c5bc85ca4df4e5a58cb25f6a8cc8d1598d Mon Sep 17 00:00:00 2001 From: janpeter visser Date: Sat, 25 Apr 2026 13:40:47 +0200 Subject: [PATCH] feat: PB-overzicht in instellingen + documentatie bijgewerkt Settings: - "Mijn teams" vervangen door gecombineerde "Product Backlogs" sectie - Toont eigen producten (badge Eigenaar) en team-lidmaatschappen (badge Developer) - Productnaam is klikbaar naar de product backlog - "Verlaten"-knop met bevestiging alleen voor Developer-lidmaatschappen - Lege staat met link naar product aanmaken Docs: - architecture.md: users tabel aangevuld met bio/bio_detail/avatar_data; Prisma schema excerpt bijgewerkt; projectstructuur bijgewerkt (profile route, ProfileEditor) - functional-spec.md: F-02b gebruikersprofiel en F-02c PB-overzicht toegevoegd; datamodel users rij bijgewerkt; settings route bijgewerkt - backlog.md: ST-507 profiel en ST-508 PB-overzicht toegevoegd als afgerond Co-Authored-By: Claude Sonnet 4.6 --- app/(app)/settings/page.tsx | 76 +++++++++++++++++++++++++------- docs/scrum4me-architecture.md | 42 +++++++++++------- docs/scrum4me-backlog.md | 8 ++++ docs/scrum4me-functional-spec.md | 55 ++++++++++++++++++++++- 4 files changed, 145 insertions(+), 36 deletions(-) diff --git a/app/(app)/settings/page.tsx b/app/(app)/settings/page.tsx index 68af447..0c57363 100644 --- a/app/(app)/settings/page.tsx +++ b/app/(app)/settings/page.tsx @@ -10,12 +10,17 @@ import Link from 'next/link' export default async function SettingsPage() { const session = await getIronSession(await cookies(), sessionOptions) - const [user, userRoles, memberships] = await Promise.all([ + const [user, userRoles, ownedProducts, memberships] = await Promise.all([ prisma.user.findUnique({ where: { id: session.userId }, select: { username: true, bio: true, bio_detail: true, avatar_data: true, updated_at: true }, }), prisma.userRole.findMany({ where: { user_id: session.userId } }), + prisma.product.findMany({ + where: { user_id: session.userId, archived: false }, + select: { id: true, name: true }, + orderBy: { name: 'asc' }, + }), prisma.productMember.findMany({ where: { user_id: session.userId }, include: { product: { select: { id: true, name: true, user: { select: { username: true } } } } }, @@ -24,6 +29,20 @@ export default async function SettingsPage() { ]) const currentRoles = userRoles.map(r => r.role as string) + type PbEntry = + | { kind: 'owner'; id: string; name: string } + | { kind: 'member'; id: string; name: string; ownerUsername: string } + + const productBacklogs: PbEntry[] = [ + ...ownedProducts.map(p => ({ kind: 'owner' as const, id: p.id, name: p.name })), + ...memberships.map(m => ({ + kind: 'member' as const, + id: m.product.id, + name: m.product.name, + ownerUsername: m.product.user.username, + })), + ] + return (

Instellingen

@@ -57,27 +76,50 @@ export default async function SettingsPage() { - {memberships.length > 0 && ( -
-
-

Mijn teams

-

- Products waarbij je als Developer bent toegevoegd. -

-
+
+
+

Product Backlogs

+

+ Alle product backlogs waarbij je betrokken bent. +

+
+ {productBacklogs.length === 0 ? ( +

+ Je bent nog niet gekoppeld aan een product backlog.{' '} + Maak een product aan. +

+ ) : (
    - {memberships.map(m => ( -
  • -
    -

    {m.product.name}

    -

    Eigenaar: {m.product.user.username}

    + {productBacklogs.map(pb => ( +
  • +
    +
    + + {pb.name} + + + {pb.kind === 'owner' ? 'Eigenaar' : 'Developer'} + +
    + {pb.kind === 'member' && ( +

    Eigenaar: {pb.ownerUsername}

    + )}
    - {!session.isDemo && } + {pb.kind === 'member' && !session.isDemo && ( + + )}
  • ))}
-
- )} + )} +
diff --git a/docs/scrum4me-architecture.md b/docs/scrum4me-architecture.md index 03f1f06..ff3941e 100644 --- a/docs/scrum4me-architecture.md +++ b/docs/scrum4me-architecture.md @@ -58,8 +58,11 @@ Scrum4Me is een desktop-first Next.js 16 webapplicatie die server-side wordt ger | username | String | unique, not null, min 3 | Inlognaam | | password_hash | String | not null | bcrypt hash (cost factor 12) | | is_demo | Boolean | default false | Demo-gebruiker heeft read-only rechten | +| bio | String | nullable, max 160 | Korte profielomschrijving | +| bio_detail | String | nullable, max 2000 | Uitgebreide profielbeschrijving | +| avatar_data | Bytes | nullable | Profielfoto als WebP bytea (max 700×700) | | created_at | DateTime | default now() | | -| updated_at | DateTime | auto-update | | +| updated_at | DateTime | auto-update | Gebruikt als cache-buster voor avatar-URL | **Indexes:** `username` (unique lookup bij inloggen) @@ -286,6 +289,9 @@ model User { username String @unique password_hash String is_demo Boolean @default(false) + bio String? @db.VarChar(160) + bio_detail String? @db.VarChar(2000) + avatar_data Bytes? created_at DateTime @default(now()) updated_at DateTime @updatedAt roles UserRole[] @@ -501,29 +507,31 @@ scrum4me/ │ │ │ │ └── planning/page.tsx # Sprint Planning (gesplitst scherm) │ │ ├── todos/page.tsx │ │ └── settings/ -│ │ ├── page.tsx +│ │ ├── page.tsx # Profiel, account, PB-overzicht, rollen, tokens │ │ └── tokens/page.tsx -│ └── api/ # REST API voor Claude Code -│ ├── products/ -│ │ └── [id]/ -│ │ └── next-story/route.ts -│ ├── sprints/ -│ │ └── [id]/ -│ │ └── tasks/route.ts -│ ├── stories/ -│ │ └── [id]/ -│ │ ├── log/route.ts -│ │ └── tasks/reorder/route.ts -│ ├── tasks/ -│ │ └── [id]/route.ts -│ └── todos/route.ts +│ ├── api/ # REST API voor Claude Code +│ │ ├── products/ +│ │ │ └── [id]/ +│ │ │ └── next-story/route.ts +│ │ ├── profile/ +│ │ │ └── avatar/route.ts # POST upload + GET serve profielfoto +│ │ ├── sprints/ +│ │ │ └── [id]/ +│ │ │ └── tasks/route.ts +│ │ ├── stories/ +│ │ │ └── [id]/ +│ │ │ ├── log/route.ts +│ │ │ └── tasks/reorder/route.ts +│ │ ├── tasks/ +│ │ │ └── [id]/route.ts +│ │ └── todos/route.ts ├── components/ │ ├── ui/ # shadcn/ui primitieven │ ├── split-pane/ # Gesplitst scherm component │ ├── backlog/ # PBI- en story-componenten │ ├── sprint/ # Sprint-componenten │ ├── products/ # ProductForm, TeamManager, ArchiveProductButton -│ ├── settings/ # RoleManager, LeaveProductButton +│ ├── settings/ # RoleManager, ProfileEditor, LeaveProductButton │ └── dnd/ # dnd-kit wrappers ├── lib/ │ ├── prisma.ts # Prisma Client singleton diff --git a/docs/scrum4me-backlog.md b/docs/scrum4me-backlog.md index cecd8a3..6f57482 100644 --- a/docs/scrum4me-backlog.md +++ b/docs/scrum4me-backlog.md @@ -273,6 +273,14 @@ De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan - `/settings` pagina met roltoewijzing (checkbox per rol: Product Owner, Scrum Master, Developer); minimaal één rol verplicht; `updateRoles` Server Action; geselecteerde rollen zichtbaar in profielbalk - Done when: rollen bijwerken persisterend; profielbalk toont gekozen rollen; uitvinken van alle rollen geeft validatiefout +- [x] **ST-507** Gebruikersprofiel (buiten originele backlog toegevoegd) + - Profielfoto-upload (JPEG/PNG/WebP, max 12 MB), server-side resizing naar max 700×700 WebP met Sharp, opgeslagen als bytea in Neon; bio (max 160) en bio_detail (max 2000) als aparte velden; `POST /api/profile/avatar` + `GET /api/profile/avatar` + `updateProfileAction` + - Done when: foto geüpload en zichtbaar in instellingen; bio opgeslagen; ongeldige bestanden geweigerd vóór verwerking + +- [x] **ST-508** Product Backlog-overzicht in instellingen (buiten originele backlog toegevoegd) + - Gecombineerde lijst op `/settings` van eigen producten (badge "Eigenaar") en team-lidmaatschappen (badge "Developer" + eigenaarsnaam); klikbaar naar product; "Verlaten"-knop met bevestiging voor lidmaatschappen; lege staat met CTA + - Done when: eigenaar-producten en team-producten zichtbaar in één lijst; verlaten werkt en verwijdert rij + --- ### M6: Polish & Launch-ready diff --git a/docs/scrum4me-functional-spec.md b/docs/scrum4me-functional-spec.md index 7605ae2..c3d2812 100644 --- a/docs/scrum4me-functional-spec.md +++ b/docs/scrum4me-functional-spec.md @@ -75,6 +75,57 @@ Een gebruiker kan bij registratie of in instellingen één of meerdere Scrum-rol --- +### F-02b: Gebruikersprofiel + +**Prioriteit:** v1 +**Persona:** Lars, Dina, Remi + +**Omschrijving:** +Een gebruiker kan een profielfoto, korte omschrijving (bio) en uitgebreide beschrijving toevoegen via de instellingenpagina. De profielfoto wordt server-side verwerkt met Sharp en opgeslagen als WebP bytea in PostgreSQL. Het profiel is niet publiek zichtbaar in v1 maar vormt de basis voor v2-teamweergaven. + +**Acceptatiecriteria:** +- [ ] Gebruiker kan een foto uploaden (JPEG, PNG of WebP, max 12 MB) +- [ ] Validatie op MIME-type en bestandsgrootte vóór verwerking — ongeldige bestanden worden geweigerd met een foutmelding +- [ ] Server converteert afbeelding naar WebP, maximaal 700×700 px (fit inside, niet uitrekken) +- [ ] Profielfoto wordt weergegeven in de instellingenpagina na upload +- [ ] Gebruiker kan een korte omschrijving invoeren (max 160 tekens) +- [ ] Gebruiker kan een uitgebreide beschrijving invoeren (max 2000 tekens) +- [ ] Opslaan van bio-velden is los van de foto-upload (apart formulier) +- [ ] Demo-gebruiker ziet de profiel-sectie niet (uitgeschakeld) + +**Implementatie:** +- `POST /api/profile/avatar` — upload + Sharp-verwerking + opslag als bytea +- `GET /api/profile/avatar?v=` — serveert avatar met `Cache-Control: private, max-age=3600`; timestamp in de URL zorgt voor cache-invalidatie na upload +- `updateProfileAction` Server Action — slaat bio en bio_detail op + +**Data:** +- `users.bio` — VarChar(160), nullable +- `users.bio_detail` — VarChar(2000), nullable +- `users.avatar_data` — Bytes (bytea), nullable; altijd WebP na verwerking +- `users.updated_at` — wordt bijgewerkt bij elke wijziging; gebruikt als versienummer in de avatar-URL + +--- + +### F-02c: Product Backlog-overzicht in instellingen + +**Prioriteit:** v1 +**Persona:** Lars, Dina, Remi + +**Omschrijving:** +De instellingenpagina toont een gecombineerde lijst van alle product backlogs waarbij de gebruiker betrokken is: producten waarvan hij/zij eigenaar is, en producten waarbij hij/zij als Developer is toegevoegd. Vanuit deze lijst kan een team-lidmaatschap worden beëindigd. + +**Acceptatiecriteria:** +- [ ] Alle actieve (niet-gearchiveerde) producten van de ingelogde gebruiker zijn zichtbaar met badge "Eigenaar" +- [ ] Alle producten waarbij de gebruiker als Developer is toegevoegd zijn zichtbaar met badge "Developer" en de naam van de eigenaar +- [ ] Klikken op een productnaam navigeert naar de product backlog +- [ ] Bij een Developer-lidmaatschap is een "Verlaten"-knop zichtbaar met bevestigingsstap +- [ ] Na verlaten verdwijnt het product uit de lijst +- [ ] Eigenaar-producten hebben geen verlaat-actie +- [ ] Lege staat toont een link naar "Product aanmaken" +- [ ] Demo-gebruiker ziet de lijst maar heeft geen verlaat-knop + +--- + ### F-03: Productbeheer **Prioriteit:** v1 — Kritiek @@ -386,7 +437,7 @@ De app is deployable op Vercel + Neon PostgreSQL en lokaal draaibaar met een Neo /products/:id/sprint (Sprint Backlog — gesplitst scherm) /products/:id/sprint/planning (Sprint Planning — gesplitst scherm) /todos (todo-lijst) -/settings (profiel, rollen, mijn teams, API-tokens) +/settings (profiel, account, product backlogs, rollen, API-tokens) /settings/tokens (API-tokenbeheer) ``` @@ -396,7 +447,7 @@ De app is deployable op Vercel + Neon PostgreSQL en lokaal draaibaar met een Neo | Entiteit | Sleutelvelden | Relaties / opmerkingen | |---|---|---| -| `users` | id, username, password_hash, is_demo, created_at | Basis authenticatie-entiteit | +| `users` | id, username, password_hash, is_demo, bio?, bio_detail?, avatar_data?, created_at | Profielvelden optioneel; avatar opgeslagen als WebP bytea | | `user_roles` | id, user_id, role (enum) | Meervoudige rollen per gebruiker | | `api_tokens` | id, user_id, token_hash, label, revoked_at | Max. 10 actief per gebruiker | | `products` | id, user_id, name, description, repo_url, definition_of_done, archived | Hoogste niveau in de hiërarchie |