'use client' // IdeaRowActions — Grill Me / Make Plan / Materialiseer / Archive / Open. // Disabled-rules per M12 T-508: // // Grill Me: niet in GRILLING|PLANNING; vereist product-met-repo + // connectedWorkers > 0 // Make Plan: alleen in GRILLED|PLAN_FAILED|PLAN_READY (re-plan); idem // voorwaarden // Materialiseer: alleen in PLAN_READY (geen worker nodig — synchrone parser) // PLANNED: alle drie disabled, "Bekijk PBI" link // *_FAILED: "Probeer opnieuw" knop (= start-job opnieuw) // // Demo-tooltip om elke muteer-knop. connectedWorkers wordt gelezen uit // useSoloStore (M12 grill-keuze 16 — geen lift voor v1). import { useRef, useTransition } from 'react' import { useRouter } from 'next/navigation' import { Archive, ArrowRight, ExternalLink, Flame, Layers, RotateCw, Sparkles, Upload, } from 'lucide-react' import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip' import { DemoTooltip } from '@/components/shared/demo-tooltip' import { useSoloStore } from '@/stores/solo-store' import { debugProps } from '@/lib/debug' import { startGrillJobAction, startMakePlanJobAction, materializeIdeaPlanAction, uploadPlanMdAction, } from '@/actions/ideas' import type { IdeaDto } from '@/lib/idea-dto' interface IdeaRowActionsProps { idea: IdeaDto isDemo: boolean onArchive: () => void } export function IdeaRowActions({ idea, isDemo, onArchive }: IdeaRowActionsProps) { const router = useRouter() const connectedWorkers = useSoloStore((s) => s.connectedWorkers) const [pending, startTransition] = useTransition() const hasProductWithRepo = idea.product != null && idea.product.repo_url !== null const workerOk = connectedWorkers > 0 const status = idea.status // ---- Grill Me ---- const grillBlockedReason = (() => { if (status === 'grilling' || status === 'planning') return 'Job loopt al' if (!hasProductWithRepo) return 'Idee heeft een product met repo nodig' if (!workerOk) return 'Geen Claude-worker actief' return null })() const grillEnabled = !grillBlockedReason && !isDemo && !pending // ---- Make Plan ---- const makePlanAllowedStates = ['grilled', 'plan_failed', 'plan_ready'] const makePlanBlockedReason = (() => { if (!makePlanAllowedStates.includes(status)) { if (status === 'draft' || status === 'grill_failed') return 'Eerst grillen' if (status === 'grilling' || status === 'planning') return 'Job loopt al' if (status === 'planned') return 'Idee is gepland — open de PBI' return null } if (!hasProductWithRepo) return 'Idee heeft een product met repo nodig' if (!workerOk) return 'Geen Claude-worker actief' return null })() const makePlanEnabled = !makePlanBlockedReason && !isDemo && !pending // ---- Materialiseer ---- const materializeBlockedReason = (() => { if (status !== 'plan_ready') return 'Plan is niet klaar' return null })() const materializeEnabled = !materializeBlockedReason && !isDemo && !pending // ---- Upload plan ---- // Synchrone server-action (parse + DB), geen worker nodig. Mag vanuit // DRAFT (skip-grill), GRILLED, PLAN_FAILED of PLAN_READY (overschrijft het // bestaande plan zonder confirmation — consistent met updatePlanMdAction). const uploadPlanAllowedStates = ['draft', 'grilled', 'plan_failed', 'plan_ready'] const uploadPlanBlockedReason = (() => { if (uploadPlanAllowedStates.includes(status)) return null if (status === 'grilling' || status === 'planning') return 'Job loopt al' if (status === 'planned') return 'Idee is al gepland' return null })() const uploadPlanEnabled = !uploadPlanBlockedReason && !isDemo && !pending const fileInputRef = useRef(null) function handleUploadPlanClick() { fileInputRef.current?.click() } function handlePlanFileChange(e: React.ChangeEvent) { const file = e.target.files?.[0] // Reset zodat dezelfde file na een fout opnieuw gekozen kan worden. e.target.value = '' if (!file) return startTransition(async () => { let text: string try { text = await file.text() } catch { toast.error('Kon bestand niet lezen') return } const r = await uploadPlanMdAction(idea.id, text) if ('error' in r) { toast.error(r.error) return } toast.success('Plan geüpload — idee staat nu op PLAN_READY') router.refresh() }) } // ---- Failed-states tonen "Probeer opnieuw" ---- const isFailedState = status === 'grill_failed' || status === 'plan_failed' function runStart(action: typeof startGrillJobAction | typeof startMakePlanJobAction) { startTransition(async () => { const r = await action(idea.id) if ('error' in r) { toast.error(r.error) return } toast.success('Job in de wachtrij — een worker pakt hem op.') router.refresh() }) } function handleMaterialize() { if (!confirm('Plan materialiseren? Dit maakt PBI + stories + taken aan.')) return startTransition(async () => { let r = await materializeIdeaPlanAction(idea.id) if ('error' in r && r.code === 409 && r.error.startsWith('PBI_HAS_ACTIVE_TASKS:')) { const pbiCode = r.error.split(':')[1] const alongside = confirm( `De bestaande PBI (${pbiCode}) heeft uitgevoerde taken.\n` + `OK = nieuwe PBI naast bestaande aanmaken.\n` + `Annuleren = stoppen.` ) if (!alongside) return r = await materializeIdeaPlanAction(idea.id, { allowAlongside: true }) } if ('error' in r) { toast.error(r.error) return } toast.success(`Gematerialiseerd als ${r.data?.pbi_code}`) if (idea.product_id) { router.push(`/products/${idea.product_id}`) } else { router.refresh() } }) } return (
{/* Bekijk PBI — alleen zichtbaar in PLANNED */} {status === 'planned' && idea.pbi && idea.product_id && ( )} {/* Grill Me */} } enabled={grillEnabled} blockedReason={grillBlockedReason} isDemo={isDemo} onClick={() => runStart(startGrillJobAction)} /> {/* Make Plan */} } enabled={makePlanEnabled} blockedReason={makePlanBlockedReason} isDemo={isDemo} onClick={() => runStart(startMakePlanJobAction)} /> {/* Upload plan — synchrone short-circuit van de Make-Plan AI-flow */} } enabled={uploadPlanEnabled} blockedReason={uploadPlanBlockedReason} isDemo={isDemo} onClick={handleUploadPlanClick} /> {/* Materialiseer */} } enabled={materializeEnabled} blockedReason={materializeBlockedReason} isDemo={isDemo} onClick={handleMaterialize} variant="default" /> {/* Failed-states: kleine retry-shortcut */} {isFailedState && ( )} {/* Open detail */} {/* Archive */}
) } interface ActionButtonProps { label: string icon: React.ReactNode enabled: boolean blockedReason: string | null isDemo: boolean onClick: () => void variant?: 'default' | 'outline' } function ActionButton({ label, icon, enabled, blockedReason, isDemo, onClick, variant = 'outline', }: ActionButtonProps) { // Bij demo: DemoTooltip toont reden. Bij niet-demo + reden: gewone tooltip. if (isDemo) { return ( ) } if (!enabled && blockedReason) { return ( }> {blockedReason} ) } return ( ) }