diff --git a/__tests__/app/m-solo-page.test.ts b/__tests__/app/m-solo-page.test.ts new file mode 100644 index 0000000..a464eb2 --- /dev/null +++ b/__tests__/app/m-solo-page.test.ts @@ -0,0 +1,35 @@ +// ST-1138: regressie-vangnet voor mobile solo-page (server component). +import { describe, it, expect } from 'vitest' +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' + +const PAGE = resolve(process.cwd(), 'app/(mobile)/m/products/[id]/solo/page.tsx') +const TASK_DETAIL = resolve(process.cwd(), 'components/solo/task-detail-dialog.tsx') + +describe('mobile solo page (ST-1138)', () => { + const src = readFileSync(PAGE, 'utf-8') + + it('hergebruikt SoloBoard zonder content-aanpassingen', () => { + expect(src).toContain('SoloBoard') + expect(src).toContain("from '@/components/solo/solo-board'") + }) + + it('auth via gedeelde requireSession()', () => { + expect(src).toContain("from '@/lib/auth-guard'") + expect(src).toContain('requireSession()') + }) + + it('geeft NoActiveSprint terug als geen actieve sprint (zelfde gedrag als desktop)', () => { + expect(src).toContain('NoActiveSprint') + }) +}) + +describe('TaskDetailDialog erft mobile-fullscreen (ST-1138 T-332 verify-only)', () => { + // Beslissing A: TaskDetailDialog gebruikt entityDialogContentClasses; mobile-classes + // komen automatisch door uit T-317. Dit test bewijst de wiring blijft staan. + const src = readFileSync(TASK_DETAIL, 'utf-8') + + it('rendert DialogContent met entityDialogContentClasses (geen eigen className-override)', () => { + expect(src).toContain('className={entityDialogContentClasses}') + }) +}) diff --git a/app/(mobile)/m/products/[id]/solo/page.tsx b/app/(mobile)/m/products/[id]/solo/page.tsx new file mode 100644 index 0000000..ce8aa19 --- /dev/null +++ b/app/(mobile)/m/products/[id]/solo/page.tsx @@ -0,0 +1,120 @@ +// PBI-11 / ST-1138: Mobile Solo Paneel — wraps de bestaande SoloBoard zonder +// content-aanpassingen. 3-koloms-kanban blijft (overflow-x scrollt zijwaarts). +// TaskDetailDialog krijgt full-screen-mobile via gedeelde +// entityDialogContentClasses (beslissing A in docs/plans/PBI-11-mobile-shell.md; +// ingebouwd via ST-1133/T-317). + +import { notFound } from 'next/navigation' +import { getAccessibleProduct } from '@/lib/product-access' +import { prisma } from '@/lib/prisma' +import { requireSession } from '@/lib/auth-guard' +import { SoloBoard } from '@/components/solo/solo-board' +import { NoActiveSprint } from '@/components/solo/no-active-sprint' +import type { SoloTask } from '@/components/solo/solo-board' +import type { UnassignedStory } from '@/components/solo/unassigned-stories-sheet' + +interface Props { + params: Promise<{ id: string }> +} + +export default async function MobileSoloProductPage({ params }: Props) { + const { id } = await params + const session = await requireSession() + + const product = await getAccessibleProduct(id, session.userId) + if (!product) notFound() + + const sprint = await prisma.sprint.findFirst({ + where: { product_id: id, status: 'ACTIVE' }, + }) + + if (!sprint) { + return ( +
+ +
+ ) + } + + const [rawTasks, rawUnassigned] = await Promise.all([ + prisma.task.findMany({ + where: { + story: { + sprint_id: sprint.id, + assignee_id: session.userId, + }, + }, + include: { + story: { + select: { + id: true, + code: true, + title: true, + tasks: { select: { id: true }, orderBy: { sort_order: 'asc' } }, + }, + }, + }, + orderBy: [ + { story: { pbi: { priority: 'asc' } } }, + { story: { pbi: { sort_order: 'asc' } } }, + { story: { sort_order: 'asc' } }, + { priority: 'asc' }, + { sort_order: 'asc' }, + ], + }), + prisma.story.findMany({ + where: { sprint_id: sprint.id, assignee_id: null }, + select: { + id: true, + code: true, + title: true, + tasks: { + select: { id: true, title: true, description: true, priority: true, status: true }, + orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }], + }, + }, + orderBy: { sort_order: 'asc' }, + }), + ]) + + const tasks: SoloTask[] = rawTasks.map(t => ({ + id: t.id, + title: t.title, + description: t.description, + implementation_plan: t.implementation_plan, + priority: t.priority, + sort_order: t.sort_order, + status: t.status as SoloTask['status'], + verify_only: t.verify_only, + verify_required: t.verify_required as SoloTask['verify_required'], + story_id: t.story.id, + story_code: t.story.code, + story_title: t.story.title, + task_code: t.code, + })) + + const unassignedStories: UnassignedStory[] = rawUnassigned.map(s => ({ + id: s.id, + code: s.code, + title: s.title, + tasks: s.tasks.map(t => ({ + id: t.id, + title: t.title, + description: t.description, + priority: t.priority, + status: t.status, + })), + })) + + return ( + + ) +}