'use client' // IdeaList — top-level lijstpagina voor /ideas. // - Strikt user_id-only data (server haalt al; client filtert binnen die set). // - Filters: zoeken op titel, product-dropdown, status-multiselect. // - Klik op rij navigeert naar /ideas/[id]. Acties (Grill / Make Plan / // Materialiseer) staan in components/ideas/idea-row-actions.tsx (T-508). // - DemoTooltip rondom muteer-acties; bulk-archive blijft achter feature-flag // in T-508 en latere stories. import { useMemo, useState, useTransition } from 'react' import { useRouter } from 'next/navigation' import { Plus, ArrowUp, ArrowDown, ArrowUpDown } from 'lucide-react' import { toast } from 'sonner' import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { DemoTooltip } from '@/components/shared/demo-tooltip' import { getIdeaStatusBadge } from '@/lib/idea-status-colors' import type { IdeaStatusApi } from '@/lib/idea-status' import type { IdeaDto } from '@/lib/idea-dto' import { debugProps } from '@/lib/debug' import { createIdeaAction, archiveIdeaAction } from '@/actions/ideas' import { IdeaRowActions } from '@/components/ideas/idea-row-actions' // Reverse mapping voor het renderen van de status-badge — DTO bevat lowercase // API-strings, het badge-helper verwacht DB-enum. const API_TO_DB: Record[0]> = { draft: 'DRAFT', grilling: 'GRILLING', grill_failed: 'GRILL_FAILED', grilled: 'GRILLED', planning: 'PLANNING', plan_failed: 'PLAN_FAILED', plan_ready: 'PLAN_READY', reviewing_plan: 'REVIEWING_PLAN', plan_review_failed: 'PLAN_REVIEW_FAILED', plan_reviewed: 'PLAN_REVIEWED', planned: 'PLANNED', } interface ProductOption { id: string name: string repo_url: string | null } type SortKey = 'code' | 'title' | 'product' | 'status' interface IdeaListProps { ideas: IdeaDto[] products: ProductOption[] isDemo: boolean } const STATUS_FILTERS: { value: IdeaStatusApi; label: string }[] = [ { value: 'draft', label: 'Concept' }, { value: 'grilling', label: 'Grillen' }, { value: 'grilled', label: 'Gegrilld' }, { value: 'planning', label: 'Plannen' }, { value: 'plan_ready', label: 'Plan klaar' }, { value: 'reviewing_plan', label: 'Plan beoordelen' }, { value: 'planned', label: 'Gepland' }, { value: 'grill_failed', label: 'Grill mislukt' }, { value: 'plan_failed', label: 'Plan mislukt' }, { value: 'plan_review_failed', label: 'Beoordeling mislukt' }, { value: 'plan_reviewed', label: 'Plan beoordeeld' }, ] const STATUS_SORT_ORDER: Record = { draft: 0, grilling: 1, grilled: 2, planning: 3, plan_ready: 4, reviewing_plan: 5, plan_reviewed: 6, planned: 7, grill_failed: 8, plan_failed: 9, plan_review_failed: 10, } function SortHeader({ col, label, sortKey, sortDir, onSort, }: { col: SortKey label: string sortKey: SortKey sortDir: 'asc' | 'desc' onSort: (col: SortKey) => void }) { const active = sortKey === col const Icon = active ? sortDir === 'asc' ? ArrowUp : ArrowDown : ArrowUpDown return ( ) } export function IdeaList({ ideas, products, isDemo }: IdeaListProps) { const router = useRouter() const [isPending, startTransition] = useTransition() // Filter state const [search, setSearch] = useState('') const [productFilter, setProductFilter] = useState('all') const [statusFilter, setStatusFilter] = useState>(new Set()) // Sort state const [sortKey, setSortKey] = useState('code') const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc') // Create-form state const [showCreate, setShowCreate] = useState(false) const [newTitle, setNewTitle] = useState('') const [newDescription, setNewDescription] = useState('') const [newProductId, setNewProductId] = useState('') // Quick-idea form state const [showQuick, setShowQuick] = useState(false) const [quickTitle, setQuickTitle] = useState('') const [quickDescription, setQuickDescription] = useState('') const filtered = useMemo(() => { const q = search.trim().toLowerCase() const result = ideas.filter((idea) => { if (q && !idea.title.toLowerCase().includes(q)) return false if (productFilter !== 'all') { if (productFilter === 'none') { if (idea.product_id !== null) return false } else { const matchesPrimary = idea.product_id === productFilter const matchesSecondary = idea.secondary_products?.some((sp) => sp.product_id === productFilter) ?? false if (!matchesPrimary && !matchesSecondary) return false } } if (statusFilter.size > 0 && !statusFilter.has(idea.status)) return false return true }) const dir = sortDir === 'asc' ? 1 : -1 return [...result].sort((a, b) => { switch (sortKey) { case 'code': return dir * a.code.localeCompare(b.code) case 'title': return dir * a.title.localeCompare(b.title) case 'product': return dir * (a.product?.name ?? '').localeCompare(b.product?.name ?? '') case 'status': return dir * a.status.localeCompare(b.status) } }) }, [ideas, search, productFilter, statusFilter, sortKey, sortDir]) const sorted = useMemo(() => { return [...filtered].sort((a, b) => { let cmp = 0 if (sortKey === 'code') { cmp = (a.code ?? '').localeCompare(b.code ?? '', 'nl', { numeric: true }) } else if (sortKey === 'title') { cmp = a.title.localeCompare(b.title, 'nl') } else if (sortKey === 'product') { const aN = a.product?.name ?? '' const bN = b.product?.name ?? '' if (!aN && bN) return sortDir === 'asc' ? 1 : -1 if (aN && !bN) return sortDir === 'asc' ? -1 : 1 cmp = aN.localeCompare(bN, 'nl') } else { cmp = (STATUS_SORT_ORDER[a.status] ?? 99) - (STATUS_SORT_ORDER[b.status] ?? 99) } return sortDir === 'asc' ? cmp : -cmp }) }, [filtered, sortKey, sortDir]) function handleSort(col: SortKey) { if (sortKey === col) { setSortDir((d) => (d === 'asc' ? 'desc' : 'asc')) } else { setSortKey(col) setSortDir('asc') } } function toggleStatus(s: IdeaStatusApi) { setStatusFilter((prev) => { const next = new Set(prev) if (next.has(s)) next.delete(s) else next.add(s) return next }) } function handleCreate() { if (isDemo) return const title = newTitle.trim() if (!title) { toast.error('Titel is verplicht') return } startTransition(async () => { const r = await createIdeaAction({ title, description: newDescription.trim() || null, product_id: newProductId || null, }) if ('error' in r) { toast.error(r.error) return } toast.success(`Idee aangemaakt (${r.data?.code})`) setNewTitle('') setNewDescription('') setNewProductId('') setShowCreate(false) router.refresh() }) } function handleQuickCreate() { if (isDemo) return const title = quickTitle.trim() if (!title) { toast.error('Titel is verplicht') return } startTransition(async () => { const r = await createIdeaAction({ title, description: quickDescription.trim() || null, product_id: null }) if ('error' in r) { toast.error(r.error) return } toast.success(`Idee aangemaakt (${r.data?.code})`) setQuickTitle('') setQuickDescription('') setShowQuick(false) router.refresh() }) } function handleArchive(id: string) { if (isDemo) return startTransition(async () => { const r = await archiveIdeaAction(id) if ('error' in r) { toast.error(r.error) return } toast.success('Idee gearchiveerd') router.refresh() }) } return (
{/* Top-bar: search + nieuw-knop */}
setSearch(e.target.value)} placeholder="Zoek op titel..." className="max-w-sm" />
{/* Status-chips als multi-select filter */}
{STATUS_FILTERS.map((s) => { const active = statusFilter.has(s.value) return ( ) })}
{/* Snel idee form — geen product-dropdown */} {showQuick && (
setQuickTitle(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleQuickCreate()} />