// CIL[BEGIN chunk_id="WorkItemDetailPage_module_header_1784761401" type="module_header" identity="module_header" parent="none"] // CIL[DOC-MACRO purpose="Create and edit Workbench work items"] // CIL[DOC-RESPONSIBILITY responsibility="Fetch, create, and update workbench_work_item records and their comment threads"] // CIL[DOC-CONTEXT module="workbench.pages.work.WorkItemDetailPage" layer="frontend-page" related_files="[\"custom/apps/workbench/pages/work/WorkBoardPage.tsx\",\"custom/apps/workbench/components/work/WorkItemForm.tsx\",\"custom/apps/workbench/components/work/WorkItemThread.tsx\"]"] // CIL[DOC-CONTRACT inputs="route param id or 'new'" outputs="JSX detail page" errors="load/save errors rendered inline" side_effects="calls /api/admin-data and /api/custom_workbench-add-comment, navigates after create"] // CIL[DOC-MICRO logic_summary="Loads the record and thread, renders a work-item form, and posts comments via a custom endpoint" notable_decisions="[]"] // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"] // CIL[DOC-STABILITY stability="experimental"] // CIL[DOC-AUDIENCE audience="developer"] // CIL[DOC-NOTE type="layout" content="The two-column layout is constrained with max-w-6xl so it does not stretch uncomfortably wide on large screens; PrioritizationPage and RecordDetailPage use the same constraint pattern"] import { useEffect, useRef, useState, type ReactNode } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { useAppPath } from '@core/hooks/useAppPath' import { useWorkbenchRole } from '../../hooks/useWorkbenchRole' import { apiFetch } from '@core/lib/api' import { Card, CardContent, CardHeader, CardTitle } from '@core/components/ui/card' import { Button } from '@core/components/ui/button' import { Input } from '@core/components/ui/input' import { Bot, CircleOff, CircleParking, FileText, GitPullRequest, Layers, Lightbulb, Pencil, ShieldCheck, ShieldX, ShieldQuestion, TestTube, UserCheck, UserX, X } from 'lucide-react' import { WorkItemForm, type WorkItemFormHandle } from '../../components/work/WorkItemForm' import { WorkItemThread } from '../../components/work/WorkItemThread' import { OriginChain } from '../../components/work/OriginChain' import { WorkItemLinks } from '../../components/work/WorkItemLinks' import { startDiscovery, parkIdea, rejectIdea } from '../../lib/workbench-data' // CIL[END chunk_id="WorkItemDetailPage_module_header_1784761401"] // CIL[BEGIN chunk_id="WorkItemDetailPage_actionbutton_1784761401" type="function" identity="ActionButton" parent="none"] // CIL[DOC-MACRO description="Implementation of ActionButton" macro="Renders a small icon+label action button for the work item sidebar"] // CIL[DOC-CONTRACT inputs="props: ActionButtonProps" outputs="Button JSX" side_effects="none"] // CIL[DOC-STABILITY stability="experimental"] type ActionButtonProps = { icon: any; label: ReactNode; onClick?: () => void; disabled?: boolean } function ActionButton(props: ActionButtonProps) { const { icon: Icon, label, onClick, disabled } = props return ( ) } // CIL[END chunk_id="WorkItemDetailPage_actionbutton_1784761401"] // CIL[BEGIN chunk_id="WorkItemDetailPage_workItemDetailPage_1784761401" type="function" identity="WorkItemDetailPage" parent="none"] // CIL[DOC-MACRO description="Implementation of WorkItemDetailPage" macro="Renders and manages WorkItemDetailPage UI state"] // CIL[DOC-CONTRACT inputs="none" outputs="JSX detail page" side_effects="none"] // CIL[DOC-STABILITY stability="experimental"] type WorkItemDetailPageProps = { context?: 'work' | 'intake' } export default function WorkItemDetailPage({ context = 'work' }: WorkItemDetailPageProps = {}) { const { id } = useParams<{ id: string }>() const navigate = useNavigate() const appPath = useAppPath() const { role } = useWorkbenchRole() const isCreating = id === 'new' const itemLabel = context === 'intake' ? 'Idea' : 'Work item' const getItemPath = (itemId: string) => appPath(context === 'intake' ? `/intake/ideas/${itemId}` : `/work/${role}/items/${itemId}`) const defaultItem = context === 'intake' ? { data: { work_type: 'idea', team: 'product', intake_disposition: 'pending' } } : undefined const [item, setItem] = useState(null) const isIdea = (isCreating && context === 'intake') || item?.data?.work_type === 'idea' const [title, setTitle] = useState('') const [editingTitle, setEditingTitle] = useState(isCreating) const [comments, setComments] = useState([]) const [loading, setLoading] = useState(true) const [loadingComments, setLoadingComments] = useState(false) const [error, setError] = useState(null) const [saving, setSaving] = useState(false) const [aiMode, setAiMode] = useState(false) const [analyzing, setAnalyzing] = useState(false) const [testPlanLink, setTestPlanLink] = useState(null) const [docsTicketLink, setDocsTicketLink] = useState(null) const [creatingTestPlan, setCreatingTestPlan] = useState(false) const [creatingDocsTicket, setCreatingDocsTicket] = useState(false) const [claiming, setClaiming] = useState(false) const [approving, setApproving] = useState(false) const formRef = useRef(null) const loadItem = () => { if (!id || isCreating) return setLoading(true) apiFetch(`/api/admin-data?entity=items&id=${id}`) .then(r => r.json()) .then(data => { const rec = data?.data ?? data if (!rec) throw new Error('Work item not found') setItem(rec) setTitle(rec.title || '') setEditingTitle(false) setError(null) }) .catch(err => setError(err.message || 'Failed to load work item')) .finally(() => setLoading(false)) } const loadComments = (targetId?: string) => { const itemId = targetId || id if (!itemId) return setLoadingComments(true) apiFetch(`/api/admin-data?entity=threads&target_id=${itemId}&target_type=items&type_slug=eng_ticket_thread&limit=1`) .then(r => r.json()) .then(async json => { const threads = Array.isArray(json?.data) ? json.data : Array.isArray(json) ? json : [] if (threads.length === 0) { setComments([]) return } const threadId = threads[0].id const mr = await apiFetch(`/api/admin-data?entity=messages&thread_id=${threadId}&type_slug=eng_ticket_comment&limit=200&sort_field=created_at&sort_direction=asc`) const mjson = await mr.json() setComments(Array.isArray(mjson?.data) ? mjson.data : Array.isArray(mjson) ? mjson : []) }) .catch(() => setComments([])) .finally(() => setLoadingComments(false)) } const loadLinks = async (targetId?: string) => { const itemId = targetId || id if (!itemId) return const [tpRes, dtRes] = await Promise.all([ apiFetch(`/api/admin-data?entity=links&source_id=${itemId}&source_type=items&link_type=workbench_verified_by&target_type=items&limit=1`), apiFetch(`/api/admin-data?entity=links&source_id=${itemId}&source_type=items&link_type=workbench_documented_by&target_type=items&limit=1`), ]) const tpJson = await tpRes.json() const dtJson = await dtRes.json() const tpRow = Array.isArray(tpJson?.data) ? tpJson.data[0] : Array.isArray(tpJson) ? tpJson[0] : null const dtRow = Array.isArray(dtJson?.data) ? dtJson.data[0] : Array.isArray(dtJson) ? dtJson[0] : null const [tpItem, dtItem] = await Promise.all([ tpRow?.target_id ? apiFetch(`/api/admin-data?entity=items&id=${tpRow.target_id}`).then(r => r.json()) : null, dtRow?.target_id ? apiFetch(`/api/admin-data?entity=items&id=${dtRow.target_id}`).then(r => r.json()) : null, ]) setTestPlanLink(tpRow ? { ...tpRow, target: tpItem?.data ?? tpItem } : null) setDocsTicketLink(dtRow ? { ...dtRow, target: dtItem?.data ?? dtItem } : null) } useEffect(() => { if (isCreating) { setLoading(false) setError(null) setComments([]) setTitle('') setEditingTitle(true) return } loadItem() loadComments() loadLinks() }, [id, isCreating]) const handleSave = async (payload: any, opts?: { navigate?: boolean }): Promise => { if (!isCreating && !id) return setSaving(true) try { if (isCreating) { const r = await apiFetch('/api/admin-data?entity=items', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ entity: 'items', type_slug: 'workbench_work_item', ...payload, }), }) if (!r.ok) throw new Error('Failed to create work item') const rec = await r.json() const newId = rec?.id || rec?.data?.id if (!newId) throw new Error('Work item was created but no id was returned') if (opts?.navigate !== false) { navigate(getItemPath(newId), { replace: true }) } return newId } else { const r = await apiFetch(`/api/admin-data?entity=items&id=${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }) if (!r.ok) throw new Error('Failed to update work item') loadItem() return id } } catch (err: any) { setError(err.message || (isCreating ? 'Failed to create work item' : 'Failed to update work item')) return } finally { setSaving(false) } } const handlePost = async (content: string) => { let itemId = id if (isCreating) { const payload = formRef.current?.getPayload() if (!payload) throw new Error('Failed to build work item payload') const newId = await handleSave(payload, { navigate: false }) if (!newId) throw new Error('Failed to create work item') itemId = newId } if (!itemId) throw new Error('Cannot post message') const threadsRes = await apiFetch(`/api/admin-data?entity=threads&target_id=${itemId}&target_type=items&limit=1`) if (!threadsRes.ok) throw new Error('Failed to load thread') const threadsJson = await threadsRes.json() const threads = Array.isArray(threadsJson?.data) ? threadsJson.data : Array.isArray(threadsJson) ? threadsJson : [] let threadId = threads[0]?.id if (!threadId) { const createThreadRes = await apiFetch('/api/admin-data?entity=threads', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ entity: 'threads', type_slug: 'eng_ticket_thread', target_type: 'items', target_id: itemId, title: 'Workbench timeline', }), }) if (!createThreadRes.ok) throw new Error('Failed to create thread') const newThread = await createThreadRes.json() threadId = newThread?.id ?? newThread?.data?.id if (!threadId) throw new Error('Thread was created but no id was returned') } const nextSequence = comments.reduce((max, c) => Math.max(max, c.sequence || 0), 0) + 1 const createMsgRes = await apiFetch('/api/admin-data?entity=messages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ entity: 'messages', type_slug: 'eng_ticket_comment', thread_id: threadId, content: content.trim(), direction: 'outbound', sequence: nextSequence, }), }) if (!createMsgRes.ok) throw new Error('Failed to post message') loadComments(itemId) if (isCreating && itemId) { navigate(getItemPath(itemId), { replace: true }) } } const handleUpdateDescription = async (description: string) => { if (!id || isCreating || !item) return const r = await apiFetch(`/api/admin-data?entity=items&id=${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ description }), }) if (!r.ok) throw new Error('Failed to update description') loadItem() } const handleTitleBlur = async (value: string) => { setEditingTitle(false) if (isCreating || !id || !item) return const trimmed = value.trim() if (trimmed === (item.title || '')) return const r = await apiFetch(`/api/admin-data?entity=items&id=${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: trimmed }), }) if (!r.ok) throw new Error('Failed to update title') loadItem() } const handleUnassign = async () => { if (!id || isCreating || !item) return try { const r = await apiFetch(`/api/admin-data?entity=items&id=${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ data: { ...(item.data || {}), assignee_id: null }, }), }) if (!r.ok) throw new Error('Failed to unassign') loadItem() } catch (err: any) { setError(err.message || 'Failed to unassign') } } const handleEscalate = async () => { if (!id || isCreating || !item) return setSaving(true) try { const r = await apiFetch(`/api/admin-data?entity=items&id=${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ data: { ...(item.data || {}), work_type: 'incident_followup', severity: 'sev1', incident_status: 'declared', }, }), }) if (!r.ok) throw new Error('Failed to escalate') if (typeof window !== 'undefined') window.location.reload() } catch (err: any) { setSaving(false) setError(err.message || 'Failed to escalate to incident') } } const handleClaimRelease = async (action: 'claim' | 'release') => { if (!id || isCreating || !item) return setClaiming(true) try { const r = await apiFetch('/.netlify/functions/custom_workbench-claim-release', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ item_id: id, action }), }) if (!r.ok) throw new Error('Claim/release request failed') await loadItem() } catch (err: any) { setError(err.message || 'Failed to update claim') } finally { setClaiming(false) } } const handleApproveProduction = async (action: 'request' | 'approve' | 'reject') => { if (!id || isCreating || !item) return setApproving(true) try { const r = await apiFetch('/.netlify/functions/custom_workbench-approve-production', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ item_id: id, action }), }) if (!r.ok) throw new Error('Production approval request failed') await loadItem() } catch (err: any) { setError(err.message || 'Failed to update production approval') } finally { setApproving(false) } } const handleCreateTestPlan = async () => { if (!id || isCreating || creatingTestPlan) return setCreatingTestPlan(true) try { const r = await apiFetch('/.netlify/functions/custom_workbench-create-test-plan', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ item_id: id }), }) if (!r.ok) throw new Error('Create test plan request failed') const json = await r.json() if (json?.test_plan_id) { navigate(appPath(`/work/${role}/test-plans/${json.test_plan_id}`)) } await loadLinks(id) } catch (err: any) { setError(err.message || 'Failed to create test plan') } finally { setCreatingTestPlan(false) } } const handleCreateDocsTicket = async () => { if (!id || isCreating || creatingDocsTicket) return setCreatingDocsTicket(true) try { const r = await apiFetch('/.netlify/functions/custom_workbench-create-docs-ticket', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ item_id: id }), }) if (!r.ok) throw new Error('Create docs ticket request failed') const json = await r.json() if (json?.docs_ticket_id) { navigate(appPath(`/work/${role}/docs-tickets/${json.docs_ticket_id}`)) } await loadLinks(id) } catch (err: any) { setError(err.message || 'Failed to create docs ticket') } finally { setCreatingDocsTicket(false) } } const handleAnalyze = async () => { let itemId = id if (isCreating) { const payload = formRef.current?.getPayload() if (!payload) throw new Error('Failed to build work item payload') const newId = await handleSave(payload, { navigate: false }) if (!newId) throw new Error('Failed to create work item') itemId = newId } if (!itemId) throw new Error('Cannot analyze work item') setAiMode(true) setAnalyzing(true) try { const r = await apiFetch('/.netlify/functions/custom_workbench-intake', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ item_id: itemId }), }) if (!r.ok) throw new Error('Analyze request failed') await loadComments(itemId) if (isCreating && itemId) { navigate(getItemPath(itemId), { replace: true }) } } catch (err: any) { setError(err.message || 'Failed to run analyze') } finally { setAnalyzing(false) } } const handlePostAi = async (content: string) => { let itemId = id if (isCreating) { const payload = formRef.current?.getPayload() if (!payload) throw new Error('Failed to build work item payload') const newId = await handleSave(payload, { navigate: false }) if (!newId) throw new Error('Failed to create work item') itemId = newId } if (!itemId) throw new Error('Cannot send AI message') try { const r = await apiFetch('/.netlify/functions/custom_workbench-intake', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ item_id: itemId, user_message: content }), }) if (!r.ok) throw new Error('AI message request failed') await loadComments(itemId) if (isCreating && itemId) { navigate(getItemPath(itemId), { replace: true }) } } catch (err: any) { setError(err.message || 'Failed to send AI message') } } if (loading) return
Loading {itemLabel.toLowerCase()}…
if (!isCreating && (error || !item)) return
{error || `${itemLabel} not found`}
const titleInput = ( setTitle(e.target.value)} onBlur={e => handleTitleBlur(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); handleTitleBlur(e.currentTarget.value) } }} placeholder={`${itemLabel} title`} autoFocus={!isCreating} 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" /> ) const approvalStatus = item?.data?.production_approval_status const approvalBadge = approvalStatus && approvalStatus !== 'pending' ? ( {approvalStatus === 'approved' ? 'Approved for Production' : approvalStatus === 'rejected' ? 'Production Rejected' : 'Approval Requested'} ) : null const titleDisplay = (

{title || 'Untitled'}

setEditingTitle(true)} />
{item?.assignee_id && ( Claimed by {item.assignee_id} )} {approvalBadge}
) return (
{isCreating || editingTitle ? titleInput : titleDisplay} setAiMode(v => !v)} onPost={handlePost} onPostAi={handlePostAi} onUpdateDescription={handleUpdateDescription} />
{!isCreating && ( Actions {isIdea ? ( <> startDiscovery(item).then(d => navigate(appPath(`/intake/discoveries/${d.id}`)))} /> parkIdea(item).then(() => navigate(appPath('/intake/ideas')))} /> rejectIdea(item).then(() => navigate(appPath('/intake/ideas')))} /> ) : ( <> navigate(appPath(`/work/${role}/test-plans/${testPlanLink.target_id}`)) : handleCreateTestPlan} disabled={creatingTestPlan} /> navigate(appPath(`/work/${role}/docs-tickets/${docsTicketLink.target_id}`)) : handleCreateDocsTicket} disabled={creatingDocsTicket} /> {item.assignee_id ? ( handleClaimRelease('release')} disabled={claiming} /> ) : ( handleClaimRelease('claim')} disabled={claiming} /> )} {}} /> {(item.data?.production_approval_status === 'approved' || item.data?.production_approval_status === 'rejected') ? null : ( <> {item.data?.production_approval_status === 'requested' ? ( <> handleApproveProduction('approve')} disabled={approving} /> handleApproveProduction('reject')} disabled={approving} /> ) : ( handleApproveProduction('request')} disabled={approving} /> )} )} )} )} {!isCreating && item && ( )} {!isCreating && id && ( )} {isCreating ? `New ${itemLabel}` : itemLabel}
) } // CIL[END chunk_id="WorkItemDetailPage_workItemDetailPage_1784761401"]