From dbf30a2fcb9db81bce77264c9b6ec7d2e80653c8 Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Wed, 6 May 2026 00:20:12 +0200 Subject: [PATCH] feat(T-562): IdeaSyncTab component met StoryLog-hergebruik MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Toont per Story onder de gekoppelde PBI: status-badge, taak-rij met job-status (incl. SKIPPED), branch, pushed_at, pr_url, en bestaande -component voor activity-log. PBI-header met PR-link en gemerged-badge. Realtime: subscribed op /api/realtime/notifications. Bij story_log- event waar story_id matcht, of claude_job_status voor dit idea → router.refresh() (server-render levert nieuwe data). MD3-tokens overal: bg-status-todo/in-progress/done, bg-surface- container, bg-muted/60. Geen bg-blue-500. Co-Authored-By: Claude Opus 4.7 (1M context) --- components/ideas/idea-sync-tab.tsx | 233 +++++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 components/ideas/idea-sync-tab.tsx diff --git a/components/ideas/idea-sync-tab.tsx b/components/ideas/idea-sync-tab.tsx new file mode 100644 index 0000000..b05e46b --- /dev/null +++ b/components/ideas/idea-sync-tab.tsx @@ -0,0 +1,233 @@ +'use client' + +// Sync-tab op /ideas/[id] (PBI-36 ST-1219). Toont per Story onder de +// gekoppelde PBI: status, job-rij (ClaudeJobs incl. branch/pushed_at/pr_url), +// en de bestaande activity-log via . Realtime refresh via +// notifications-SSE: bij elk story_log of relevant claude_job-event triggeren +// we router.refresh() (server-render verzorgt nieuwe data). + +import { useEffect } from 'react' +import { useRouter } from 'next/navigation' +import { Badge } from '@/components/ui/badge' +import { StoryLog } from '@/components/shared/story-log' +import { JOB_STATUS_LABELS, JOB_STATUS_COLORS } from '@/components/shared/job-status' +import type { ClaudeJobStatusApi } from '@/lib/job-status' +import type { IdeaSyncData } from '@/app/(app)/ideas/[id]/sync-tab-server' + +interface Props { + data: IdeaSyncData +} + +const TASK_STATUS_LABEL: Record = { + TO_DO: 'TO-DO', + IN_PROGRESS: 'Bezig', + REVIEW: 'Review', + DONE: 'Klaar', +} + +const TASK_STATUS_COLOR: Record = { + TO_DO: 'bg-status-todo/15 text-status-todo border-status-todo/30', + IN_PROGRESS: 'bg-status-in-progress/15 text-status-in-progress border-status-in-progress/30', + REVIEW: 'bg-warning/15 text-warning border-warning/30', + DONE: 'bg-status-done/15 text-status-done border-status-done/30', +} + +function formatRelative(iso: string | Date | null): string { + if (!iso) return '—' + const d = typeof iso === 'string' ? new Date(iso) : iso + const diffMs = Date.now() - d.getTime() + const min = Math.round(diffMs / 60_000) + if (min < 1) return 'zojuist' + if (min < 60) return `${min} min geleden` + const h = Math.round(min / 60) + if (h < 24) return `${h} u geleden` + return d.toLocaleDateString('nl-NL', { day: 'numeric', month: 'short' }) +} + +function jobStatusKey(dbStatus: string): ClaudeJobStatusApi { + return dbStatus.toLowerCase() as ClaudeJobStatusApi +} + +export function IdeaSyncTab({ data }: Props) { + const router = useRouter() + const pbi = data.pbi + const storyIdsKey = pbi ? pbi.stories.map((s) => s.id).join(',') : '' + + // Realtime refresh op story_log inserts en idea-job updates. + // Listen op de bestaande user-scoped notifications stream (SSE-route filtert + // al op accessibleIdeaIds + accessibleProductIds). + useEffect(() => { + if (!storyIdsKey) return + const storyIds = new Set(storyIdsKey.split(',')) + const es = new EventSource('/api/realtime/notifications') + + es.addEventListener('message', (ev) => { + try { + const payload = JSON.parse(ev.data) + if (payload.entity === 'story_log' && storyIds.has(payload.story_id)) { + router.refresh() + return + } + if (payload.type === 'claude_job_status' && payload.idea_id === data.id) { + router.refresh() + } + } catch { + // niet-JSON of niet-relevant — negeren + } + }) + + es.addEventListener('error', () => { + // EventSource probeert zelf opnieuw te verbinden; geen actie nodig. + }) + + return () => { + es.close() + } + }, [data.id, storyIdsKey, router]) + + if (!pbi) return null + + return ( +
+ {/* Header: PBI-link + PR-status */} +
+ + {pbi.code} + + {pbi.title} +
+ {pbi.pr_url && ( + + PR open + + )} + {pbi.pr_merged_at && ( + + Gemerged {formatRelative(pbi.pr_merged_at)} + + )} +
+
+ + {/* Stories */} + {pbi.stories.length === 0 && ( +

+ Deze PBI heeft nog geen stories. +

+ )} + {pbi.stories.map((story) => ( +
+ + + {story.code} + + {story.title} + + {TASK_STATUS_LABEL[story.status] ?? story.status} + + + +
+ {/* Tasks + jobs */} + {story.tasks.length === 0 ? ( +

Geen taken.

+ ) : ( +
    + {story.tasks.map((task) => { + const latestJob = task.claude_jobs[0] + return ( +
  • + + {task.code} + + {task.title} + + {TASK_STATUS_LABEL[task.status] ?? task.status} + + {latestJob ? ( + + {JOB_STATUS_LABELS[jobStatusKey(latestJob.status)] ?? + latestJob.status} + + ) : ( + + Geen job + + )} + {latestJob?.branch && ( + + {latestJob.branch} + + )} + {latestJob?.pushed_at && ( + + gepusht {formatRelative(latestJob.pushed_at)} + + )} + {latestJob?.pr_url && ( + + PR + + )} +
  • + ) + })} +
+ )} + + {/* Activity log (StoryLog hergebruik) */} +
+

+ Activiteit +

+ ({ + id: l.id, + type: l.type, + content: l.content, + status: l.status, + commit_hash: l.commit_hash, + commit_message: l.commit_message, + created_at: + typeof l.created_at === 'string' + ? l.created_at + : l.created_at.toISOString(), + }))} + repoUrl={data.product?.repo_url ?? null} + /> +
+
+
+ ))} +
+ ) +}