Scrum4Me/components/ideas/idea-timeline.tsx
Madhura68 1ba9feac1a ui: idea-timeline + pbi-link-card + download-md-button (M12 T-512)
components/ideas/idea-timeline.tsx:
- Chronological merge of IdeaLog + ClaudeQuestion (sorted desc by created_at)
- Per-entry icon by log-type (DECISION/NOTE/GRILL_RESULT/PLAN_RESULT/
  STATUS_CHANGE/JOB_EVENT) + question-status label
- MD3-tokens, vertical timeline rail (border-left + dots)
- Question entries show options + answer (border-left highlight)
- Metadata expansion via <details> for log entries

components/ideas/idea-pbi-link-card.tsx:
- PLANNED + pbi present: green status-done card with PBI link
- PLANNED + pbi removed (FK SetNull): blocked-color banner with
  "Plan opnieuw beschikbaar maken" → relinkIdeaPlanAction
- Demo blocked on relink

components/ideas/download-md-button.tsx:
- Calls downloadIdeaMdAction → builds Blob + anchor + click()
- Filename: {idea.code}-{kind}.md
- Demo MAY use it (read-only)

components/ideas/idea-detail-layout.tsx:
- Replaces inline placeholders with extracted components
- Md tabs gain Download (.md) button + Edit button row

Tests: 546/546 still green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 21:39:33 +02:00

163 lines
5.3 KiB
TypeScript

'use client'
// IdeaTimeline — chronologische merge van IdeaLog + ClaudeQuestion entries.
// Server-component zou ook kunnen, maar we mounten dit binnen de client-side
// detail-layout dus client is simpler (geen rsc-boundary doorbreken).
//
// Iconen + kleur per log-type voor snelle herkenning.
import {
ClipboardList,
FileText,
HelpCircle,
Lightbulb,
RefreshCw,
StickyNote,
Wrench,
} from 'lucide-react'
import type { IdeaLogType } from '@prisma/client'
export interface TimelineLog {
id: string
type: string
content: string
metadata: unknown
created_at: string
}
export interface TimelineQuestion {
id: string
question: string
options: string[] | null
status: 'open' | 'answered' | 'cancelled' | 'expired'
answer: string | null
created_at: string
expires_at: string
}
interface Props {
logs: TimelineLog[]
questions: TimelineQuestion[]
}
const LOG_ICON: Record<IdeaLogType, React.ReactNode> = {
DECISION: <Lightbulb className="size-4" />,
NOTE: <StickyNote className="size-4" />,
GRILL_RESULT: <FileText className="size-4" />,
PLAN_RESULT: <ClipboardList className="size-4" />,
STATUS_CHANGE: <RefreshCw className="size-4" />,
JOB_EVENT: <Wrench className="size-4" />,
}
const LOG_LABEL: Record<IdeaLogType, string> = {
DECISION: 'Beslissing',
NOTE: 'Notitie',
GRILL_RESULT: 'Grill-resultaat',
PLAN_RESULT: 'Plan-resultaat',
STATUS_CHANGE: 'Status',
JOB_EVENT: 'Job-event',
}
const QUESTION_STATUS_LABEL: Record<TimelineQuestion['status'], string> = {
open: 'Open',
answered: 'Beantwoord',
cancelled: 'Geannuleerd',
expired: 'Verlopen',
}
export function IdeaTimeline({ logs, questions }: Props) {
const merged = [
...logs.map((l) => ({
kind: 'log' as const,
created_at: l.created_at,
data: l,
})),
...questions.map((q) => ({
kind: 'question' as const,
created_at: q.created_at,
data: q,
})),
].sort((a, b) => (a.created_at < b.created_at ? 1 : -1))
if (merged.length === 0) {
return (
<p className="text-sm text-muted-foreground py-8 text-center italic">
Nog geen activiteit op dit idee.
</p>
)
}
return (
<ol className="border-l-2 border-input pl-4 space-y-3 ml-2">
{merged.map((entry, i) => {
const time = new Date(entry.created_at).toLocaleString()
if (entry.kind === 'log') {
const type = entry.data.type as IdeaLogType
return (
<li key={`l-${entry.data.id}`} className="relative">
<span className="absolute -left-[26px] top-1 flex size-5 items-center justify-center rounded-full bg-surface-container text-muted-foreground">
{LOG_ICON[type] ?? <StickyNote className="size-4" />}
</span>
<div className="rounded-md border border-input bg-surface-container p-3 space-y-1">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span className="font-medium uppercase tracking-wide">
{LOG_LABEL[type] ?? type}
</span>
<span>·</span>
<time>{time}</time>
</div>
<p className="text-sm whitespace-pre-wrap">{entry.data.content}</p>
{entry.data.metadata != null &&
typeof entry.data.metadata === 'object' &&
Object.keys(entry.data.metadata as object).length > 0 ? (
<details className="text-xs text-muted-foreground">
<summary className="cursor-pointer">metadata</summary>
<pre className="mt-1 whitespace-pre-wrap font-mono text-[10px]">
{JSON.stringify(entry.data.metadata, null, 2)}
</pre>
</details>
) : null}
</div>
</li>
)
}
const q = entry.data
return (
<li key={`q-${q.id}-${i}`} className="relative">
<span className="absolute -left-[26px] top-1 flex size-5 items-center justify-center rounded-full bg-surface-container text-status-review">
<HelpCircle className="size-4" />
</span>
<div className="rounded-md border border-input bg-surface-container p-3 space-y-2">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span className="font-medium uppercase tracking-wide">Vraag</span>
<span>·</span>
<span>{QUESTION_STATUS_LABEL[q.status]}</span>
<span>·</span>
<time>{time}</time>
</div>
<p className="text-sm">{q.question}</p>
{q.options && q.options.length > 0 ? (
<ul className="text-xs text-muted-foreground list-disc list-inside">
{q.options.map((o, ii) => (
<li key={ii}>{o}</li>
))}
</ul>
) : null}
{q.answer ? (
<p className="text-sm border-l-2 border-primary pl-2 text-foreground">
<span className="text-xs font-medium uppercase tracking-wide text-primary mr-2">
Antwoord
</span>
{q.answer}
</p>
) : null}
</div>
</li>
)
})}
</ol>
)
}