import { useEffect, useState } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { useAppPath } from '@core/hooks/useAppPath' import { apiFetchJson, getItem, createItem, updateItem, approveDiscovery } from '../../lib/workbench-data' import { Button } from '@core/components/ui/button' import { Input } from '@core/components/ui/input' import { Card, CardContent, CardHeader, CardTitle } from '@core/components/ui/card' import { Textarea } from '@core/components/ui/textarea' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@core/components/ui/select' import { WorkItemThread } from '../../components/work/WorkItemThread' import { ArrowLeft } from 'lucide-react' import { PRIORITY_OPTIONS } from '../../components/work/options' const DISCOVERY_STATUS_OPTIONS = [ { value: 'discovery', label: 'Discovery' }, { value: 'decision', label: 'Decision' }, { value: 'refinement', label: 'Refinement' }, { value: 'approved', label: 'Approved' }, { value: 'rejected', label: 'Rejected' }, { value: 'parked', label: 'Parked' }, ] type FormData = { priority: string problem: string intended_outcome: string constraints: string assumptions: string risks: string decision_summary: string } const emptyForm: FormData = { priority: '', problem: '', intended_outcome: '', constraints: '', assumptions: '', risks: '', decision_summary: '', } function getInitialForm(item?: any): FormData { const d = item?.data || {} return { priority: d.priority || '', problem: d.problem || '', intended_outcome: d.intended_outcome || '', constraints: d.constraints || '', assumptions: d.assumptions || '', risks: d.risks || '', decision_summary: d.decision_summary || '', } } export default function DiscoveryDetailPage() { const { id } = useParams<{ id: string }>() const navigate = useNavigate() const appPath = useAppPath() const isCreating = id === 'new' const [item, setItem] = useState(null) const [title, setTitle] = useState('') const [status, setStatus] = useState('discovery') const [form, setForm] = useState(emptyForm) const [comments, setComments] = useState([]) const [loading, setLoading] = useState(true) const [loadingComments, setLoadingComments] = useState(false) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const loadItem = () => { if (!id || isCreating) { setLoading(false) return } setLoading(true) getItem(id) .then(rec => { setItem(rec) setTitle(rec.title || '') setStatus(rec.status || 'discovery') setForm(getInitialForm(rec)) setError(null) }) .catch(err => setError(err.message || 'Failed to load discovery')) .finally(() => setLoading(false)) } const loadComments = () => { const itemId = id if (!itemId || isCreating) return setLoadingComments(true) apiFetchJson(`/api/admin-data?entity=threads&target_id=${itemId}&target_type=items&type_slug=discovery_thread&limit=1`) .then(async (json: any) => { const threads = Array.isArray(json?.data) ? json.data : Array.isArray(json) ? json : [] if (threads.length === 0) { setComments([]) return } const threadId = threads[0].id const mjson = await apiFetchJson(`/api/admin-data?entity=messages&thread_id=${threadId}&type_slug=discovery_message&limit=200&sort_field=created_at&sort_direction=asc`) setComments(Array.isArray(mjson?.data) ? mjson.data : []) }) .catch(() => setComments([])) .finally(() => setLoadingComments(false)) } useEffect(() => { if (isCreating) { setLoading(false) setTitle('') setStatus('discovery') setForm(emptyForm) setComments([]) setError(null) return } loadItem() loadComments() }, [id, isCreating]) const handleTitleBlur = async (value: string) => { if (isCreating || !id || !item) return const trimmed = value.trim() if (trimmed === (item.title || '')) return try { await updateItem(id, { title: trimmed }) loadItem() } catch (err: any) { setError(err.message || 'Failed to update title') } } const handleUpdateDescription = async (description: string) => { if (isCreating || !id || !item) return try { await updateItem(id, { description }) loadItem() } catch (err: any) { setError(err.message || 'Failed to update description') } } const buildPayload = () => ({ status, data: { ...(item?.data || {}), ...form, priority: form.priority || null, }, }) const handleSave = async (e: React.FormEvent) => { e.preventDefault() setSaving(true) try { const payload = buildPayload() if (isCreating) { const created = await createItem('workbench_discovery', { title: title.trim() || 'Untitled discovery', description: '', ...payload }) const newId = created.id if (!newId) throw new Error('Discovery was created but no id was returned') navigate(appPath(`/intake/discoveries/${newId}`), { replace: true }) } else { await updateItem(id, payload) loadItem() } } catch (err: any) { setError(err.message || 'Failed to save discovery') } finally { setSaving(false) } } const handlePost = async (content: string) => { if (isCreating || !id) return const tjson = await apiFetchJson(`/api/admin-data?entity=threads&target_id=${id}&target_type=items&type_slug=discovery_thread&limit=1`) const threads = Array.isArray(tjson?.data) ? tjson.data : Array.isArray(tjson) ? tjson : [] let threadId = threads[0]?.id if (!threadId) { const created = await apiFetchJson('/api/admin-data?entity=threads', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ entity: 'threads', type_slug: 'discovery_thread', target_id: id, target_type: 'items' }), }) threadId = created?.id || created?.data?.id } if (!threadId) throw new Error('Failed to create thread') const mjson = await apiFetchJson(`/api/admin-data?entity=messages&thread_id=${threadId}&type_slug=discovery_message&limit=200&sort_field=created_at&sort_direction=asc`) const existing = Array.isArray(mjson?.data) ? mjson.data : [] const nextSequence = existing.length await apiFetchJson('/api/admin-data?entity=messages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ entity: 'messages', type_slug: 'discovery_message', thread_id: threadId, content: content.trim(), direction: 'outbound', sequence: nextSequence, }), }) loadComments() } const handleApprove = async () => { if (!item) return try { const initiative = await approveDiscovery(item) navigate(appPath(`/planning/initiatives/${initiative.id}`)) } catch (err: any) { setError(err.message || 'Failed to approve discovery') } } const handlePark = async () => { if (!id) return await updateItem(id, { status: 'parked' }) navigate(appPath('/intake/discovery-board')) } const handleReject = async () => { if (!id) return await updateItem(id, { status: 'rejected' }) navigate(appPath('/intake/discovery-board')) } const handleChange = (field: keyof FormData, value: string) => { setForm(prev => ({ ...prev, [field]: value })) } const isTerminal = !isCreating && ['approved', 'rejected', 'parked'].includes(item?.status) if (loading) return
Loading discovery…
if (!isCreating && error && !item) return
{error || 'Discovery not found'}
return (
{error &&
{error}
}
setTitle(e.target.value)} onBlur={e => handleTitleBlur(e.target.value)} placeholder="Discovery title" className="text-2xl font-bold tracking-tight border-0 bg-transparent px-0 h-auto py-1 focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:text-muted-foreground/50" /> {}} onUpdateDescription={handleUpdateDescription} itemLabel="Discovery" emptyMessage="Save the discovery to start the conversation." placeholder="Add a discovery note…" />
{!isCreating && !isTerminal && ( Actions )} {isCreating ? 'New Discovery' : 'Discovery'}
{[ { key: 'problem', label: 'Problem / Opportunity' }, { key: 'intended_outcome', label: 'Intended Outcome' }, { key: 'constraints', label: 'Constraints' }, { key: 'assumptions', label: 'Assumptions' }, { key: 'risks', label: 'Risks' }, { key: 'decision_summary', label: 'Decision Summary' }, ].map(({ key, label }) => (