From 04fa9bf4a99df069f08b55347e8ac95620db7ecc Mon Sep 17 00:00:00 2001 From: Scrum4Me Agent <30029041+madhura68@users.noreply.github.com> Date: Wed, 6 May 2026 03:32:39 +0200 Subject: [PATCH 1/2] feat(ST-dgognlsz): SoloTaskCard 4-regels layout met Tooltips 4-regels layout: taaknaam+task_code badge (tooltip: naam+beschrijving), beschrijving+pbi_code badge (tooltip: pbi_title+pbi_description), story+job-badge. SoloTaskCardOverlay identieke 4-regels structuur zonder tooltips. PBI-velden toegevoegd aan SoloTask-interface + Prisma-queries + test-fixtures. Co-Authored-By: Claude Sonnet 4.6 --- .../solo/solo-board-batch-enqueue.test.tsx | 3 + .../solo/task-detail-dialog.test.tsx | 3 + __tests__/stores/solo-store-realtime.test.ts | 3 + app/(app)/products/[id]/solo/page.tsx | 4 + app/(mobile)/m/products/[id]/solo/page.tsx | 4 + components/solo/solo-board.tsx | 3 + components/solo/solo-task-card.tsx | 74 +++++++++++++++++-- 7 files changed, 88 insertions(+), 6 deletions(-) diff --git a/__tests__/components/solo/solo-board-batch-enqueue.test.tsx b/__tests__/components/solo/solo-board-batch-enqueue.test.tsx index 392bf6e..d47242d 100644 --- a/__tests__/components/solo/solo-board-batch-enqueue.test.tsx +++ b/__tests__/components/solo/solo-board-batch-enqueue.test.tsx @@ -94,6 +94,9 @@ const TODO_TASK = { story_code: 'ST-1', story_title: 'Story 1', task_code: 'ST-1.1', + pbi_code: null, + pbi_title: null, + pbi_description: null, } const DEFAULT_PROPS = { diff --git a/__tests__/components/solo/task-detail-dialog.test.tsx b/__tests__/components/solo/task-detail-dialog.test.tsx index 3b767fc..6c56a22 100644 --- a/__tests__/components/solo/task-detail-dialog.test.tsx +++ b/__tests__/components/solo/task-detail-dialog.test.tsx @@ -65,6 +65,9 @@ const baseTask: SoloTask = { story_code: 'ST-100', story_title: 'Test Story', task_code: 'ST-100.1', + pbi_code: null, + pbi_title: null, + pbi_description: null, } const DEFAULT_PROPS = { diff --git a/__tests__/stores/solo-store-realtime.test.ts b/__tests__/stores/solo-store-realtime.test.ts index f61a7f8..2047a77 100644 --- a/__tests__/stores/solo-store-realtime.test.ts +++ b/__tests__/stores/solo-store-realtime.test.ts @@ -17,6 +17,9 @@ const baseTask = (id: string, overrides: Partial = {}): SoloTask => ({ story_code: 'ST-100', story_title: 'Original Story', task_code: 'ST-100.1', + pbi_code: null, + pbi_title: null, + pbi_description: null, ...overrides, }) diff --git a/app/(app)/products/[id]/solo/page.tsx b/app/(app)/products/[id]/solo/page.tsx index 868e579..6c03d5b 100644 --- a/app/(app)/products/[id]/solo/page.tsx +++ b/app/(app)/products/[id]/solo/page.tsx @@ -46,6 +46,7 @@ export default async function SoloProductPage({ params }: Props) { code: true, title: true, tasks: { select: { id: true }, orderBy: { sort_order: 'asc' } }, + pbi: { select: { code: true, title: true, description: true } }, }, }, }, @@ -86,6 +87,9 @@ export default async function SoloProductPage({ params }: Props) { story_code: t.story.code, story_title: t.story.title, task_code: t.code, + pbi_code: t.story.pbi?.code ?? null, + pbi_title: t.story.pbi?.title ?? null, + pbi_description: t.story.pbi?.description ?? null, })) const unassignedStories: UnassignedStory[] = rawUnassigned.map(s => ({ diff --git a/app/(mobile)/m/products/[id]/solo/page.tsx b/app/(mobile)/m/products/[id]/solo/page.tsx index ce8aa19..132e980 100644 --- a/app/(mobile)/m/products/[id]/solo/page.tsx +++ b/app/(mobile)/m/products/[id]/solo/page.tsx @@ -51,6 +51,7 @@ export default async function MobileSoloProductPage({ params }: Props) { code: true, title: true, tasks: { select: { id: true }, orderBy: { sort_order: 'asc' } }, + pbi: { select: { code: true, title: true, description: true } }, }, }, }, @@ -91,6 +92,9 @@ export default async function MobileSoloProductPage({ params }: Props) { story_code: t.story.code, story_title: t.story.title, task_code: t.code, + pbi_code: t.story.pbi?.code ?? null, + pbi_title: t.story.pbi?.title ?? null, + pbi_description: t.story.pbi?.description ?? null, })) const unassignedStories: UnassignedStory[] = rawUnassigned.map(s => ({ diff --git a/components/solo/solo-board.tsx b/components/solo/solo-board.tsx index 0dd6fa2..24b5cdf 100644 --- a/components/solo/solo-board.tsx +++ b/components/solo/solo-board.tsx @@ -32,6 +32,9 @@ export interface SoloTask { story_code: string | null story_title: string task_code: string | null + pbi_code: string | null + pbi_title: string | null + pbi_description: string | null } export interface SoloBoardProps { diff --git a/components/solo/solo-task-card.tsx b/components/solo/solo-task-card.tsx index 6ba263f..5629dca 100644 --- a/components/solo/solo-task-card.tsx +++ b/components/solo/solo-task-card.tsx @@ -8,6 +8,7 @@ import { cn } from '@/lib/utils' import { CodeBadge } from '@/components/shared/code-badge' import { JOB_STATUS_LABELS, JOB_STATUS_COLORS, JOB_STATUS_ACTIVE } from '@/components/shared/job-status' import { useSoloStore } from '@/stores/solo-store' +import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip' import type { SoloTask } from './solo-board' const PRIORITY_BORDER: Record = { @@ -30,10 +31,6 @@ export function SoloTaskCard({ task, isDemo, onClick }: SoloTaskCardProps) { disabled: isDemo, }) - // view-transition-name laat de browser deze card snapshotten zodat hij - // soepel van kolom naar kolom animeert wanneer de status realtime wijzigt - // (ST-805 animatie A). Tijdens drag uit zetten — dnd-kit beheert de - // transform dan zelf en dubbele transitions willen we niet. const style: React.CSSProperties | undefined = transform ? { transform: CSS.Translate.toString(transform) } : { viewTransitionName: `solo-task-${task.id}` } @@ -51,12 +48,66 @@ export function SoloTaskCard({ task, isDemo, onClick }: SoloTaskCardProps) { )} {...(!isDemo ? { ...attributes, ...listeners } : {})} > + {/* Regel 1: taaknaam + task_code */}

{task.title}

- {task.task_code && } + {task.task_code && ( + + + }> + + + +

{task.title}

+ {task.description && ( +

{task.description.slice(0, 100)}

+ )} +
+
+
+ )}
+ + {/* Regels 2–3: beschrijving + pbi_code */} +
+ {task.description ? ( + + + + }> + {task.description} + + {task.description.length > 80 && ( + + {task.description} + + )} + + + ) : ( +
+ )} + {task.pbi_code && ( + + + }> + + + +

{task.pbi_title}

+ {task.pbi_description && ( +

{task.pbi_description.slice(0, 100)}

+ )} +
+
+
+ )} +
+ + {/* Regel 4: story-info + job-badge */}
-

+

{task.story_code && {task.story_code}} {task.story_title}

@@ -95,10 +146,21 @@ export function SoloTaskCardOverlay({ task }: { task: SoloTask }) { PRIORITY_BORDER[task.priority], )} > + {/* Regel 1 */}

{task.title}

{task.task_code && }
+ {/* Regels 2–3 */} +
+ {task.description ? ( +

{task.description}

+ ) : ( +
+ )} + {task.pbi_code && } +
+ {/* Regel 4 */}

{task.story_code && {task.story_code}} {task.story_title} From 27820ce32e363b0ff04bae415250c83bd5f4e640 Mon Sep 17 00:00:00 2001 From: Scrum4Me Agent <30029041+madhura68@users.noreply.github.com> Date: Wed, 6 May 2026 03:39:35 +0200 Subject: [PATCH 2/2] test(ST-dgognlsz): vitest-tests voor SoloTaskCard pbi-velden en SoloTaskCardOverlay Co-Authored-By: Claude Sonnet 4.6 --- .../components/solo/solo-task-card.test.tsx | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 __tests__/components/solo/solo-task-card.test.tsx diff --git a/__tests__/components/solo/solo-task-card.test.tsx b/__tests__/components/solo/solo-task-card.test.tsx new file mode 100644 index 0000000..9a0d6e0 --- /dev/null +++ b/__tests__/components/solo/solo-task-card.test.tsx @@ -0,0 +1,99 @@ +// @vitest-environment jsdom +import '@testing-library/jest-dom' +import { describe, it, expect, vi } from 'vitest' +import { render, screen } from '@testing-library/react' +import type { SoloTask } from '@/components/solo/solo-board' + +vi.mock('@/components/ui/tooltip', () => ({ + TooltipProvider: ({ children }: { children: React.ReactNode }) => <>{children}, + Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}, + TooltipTrigger: ({ render: r, children }: { render?: React.ReactElement; children?: React.ReactNode }) => + r ? <>{r} : <>{children}, + TooltipContent: ({ children }: { children: React.ReactNode }) => + {children}, +})) + +vi.mock('@dnd-kit/core', () => ({ + useDraggable: () => ({ + attributes: {}, + listeners: {}, + setNodeRef: vi.fn(), + transform: null, + isDragging: false, + }), +})) + +vi.mock('@/stores/solo-store', () => ({ + useSoloStore: () => null, +})) + +vi.mock('@/components/shared/code-badge', () => ({ + CodeBadge: ({ code }: { code: string }) => {code}, +})) + +import { SoloTaskCard, SoloTaskCardOverlay } from '@/components/solo/solo-task-card' + +function makeSoloTask(overrides: Partial = {}): SoloTask { + return { + id: 'task-1', + title: 'Taak titel', + description: 'Omschrijving van de taak die langer is dan tachtig tekens voor de tooltip test', + implementation_plan: null, + priority: 2, + sort_order: 0, + status: 'TO_DO', + verify_only: false, + verify_required: 'ALIGNED', + story_id: 'story-1', + story_code: 'ST-1', + story_title: 'Story titel', + task_code: 'T-1', + pbi_code: 'PBI-1', + pbi_title: 'PBI titel', + pbi_description: 'PBI omschrijving', + ...overrides, + } +} + +describe('SoloTaskCard', () => { + it('toont taaknaam, story_code en story_title', () => { + render() + // title appears in card + tooltip-content mock, so use getAllByText + expect(screen.getAllByText('Taak titel').length).toBeGreaterThan(0) + expect(screen.getByText('ST-1')).toBeInTheDocument() + expect(screen.getByText('Story titel')).toBeInTheDocument() + }) + + it('verbergt pbi_code badge als pbi_code null is', () => { + render() + const badges = screen.queryAllByTestId('code-badge') + const pbiAttempt = badges.find(b => b.textContent === 'PBI-1') + expect(pbiAttempt).toBeUndefined() + }) + + it('verbergt description-paragraaf als description null is', () => { + render() + expect(screen.queryByText('Omschrijving')).not.toBeInTheDocument() + }) + + it('rendert zonder crash met alle velden ingevuld', () => { + expect(() => + render() + ).not.toThrow() + }) +}) + +describe('SoloTaskCardOverlay', () => { + it('toont taaknaam en codes zonder tooltip-wrappers', () => { + render() + expect(screen.getAllByText('Taak titel').length).toBeGreaterThan(0) + expect(screen.getByText('ST-1')).toBeInTheDocument() + expect(screen.getByText('Story titel')).toBeInTheDocument() + }) + + it('rendert zonder crash als pbi_code null is', () => { + expect(() => + render() + ).not.toThrow() + }) +})