2026-04-20 15:10:54 +02:00
"use client" ;
import { useState , useRef , useEffect } from "react" ;
import { Task , Complexity , TaskNature } from "@/types" ;
import { useUpdateTask } from "@/hooks/use-tasks" ;
import { useProject } from "@/hooks/use-projects" ;
import { Card , CardContent } from "@/components/ui/card" ;
import { Badge } from "@/components/ui/badge" ;
import { Input } from "@/components/ui/input" ;
import {
Select ,
SelectContent ,
SelectItem ,
SelectTrigger ,
SelectValue ,
} from "@/components/ui/select" ;
2026-06-21 07:15:58 +02:00
import {
User ,
Calendar ,
Clock ,
Target ,
AlertTriangle ,
GitBranch ,
FolderGit2 ,
Wrench ,
Briefcase ,
Hash ,
GitPullRequest ,
ExternalLink ,
} from "lucide-react" ;
2026-04-20 15:10:54 +02:00
import { toast } from "sonner" ;
import { getAgentDisplayName , resolveToSlug } from "@/lib/agent-utils" ;
2026-06-21 07:15:58 +02:00
import { branchUrl } from "@/lib/repo-url" ;
2026-07-15 18:04:00 +02:00
import { formatAbsoluteTimestamp } from "@/lib/utils" ;
2026-06-21 07:15:58 +02:00
import { CopyButton } from "@/components/ui/copy-button" ;
2026-07-15 16:30:26 +02:00
import { HelpTip } from "@/components/ui/help-tip" ;
2026-04-20 15:10:54 +02:00
import { TaskTypeBadge } from "../task-type-badge" ;
import { DocsStatusBadge } from "../docs-status-badge" ;
import Link from "next/link" ;
// Priority colors
const priorityColors : Record < number , string > = {
0 : "bg-red-200 text-red-700 dark:bg-red-900 dark:text-red-200" ,
1 : "bg-orange-200 text-orange-700 dark:bg-orange-900 dark:text-orange-200" ,
2 : "bg-blue-200 text-blue-700 dark:bg-blue-900 dark:text-blue-200" ,
3 : "bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200" ,
};
const priorityLabels : Record < number , string > = {
0 : "P0 - Highest" ,
1 : "P1 - High" ,
2 : "P2 - Medium" ,
3 : "P3 - Low" ,
};
const complexityLabels : Record < string , string > = {
[ Complexity . LOW ] : "Low" ,
[ Complexity . MEDIUM ] : "Medium" ,
[ Complexity . HIGH ] : "High" ,
};
interface TaskMetadataProps {
task : Task ;
}
function formatDate ( date : string | null ) : string {
if ( ! date ) return "-" ;
return new Date ( date ). toLocaleDateString ( "en-US" , {
month : "short" ,
day : "numeric" ,
year : "numeric" ,
hour : "2-digit" ,
minute : "2-digit" ,
});
}
function formatRelativeTime ( date : string | null ) : string {
if ( ! date ) return "-" ;
const now = new Date ();
const d = new Date ( date );
const diffMs = now . getTime () - d . getTime ();
const diffHours = Math . floor ( diffMs / ( 1000 * 60 * 60 ));
const diffDays = Math . floor ( diffHours / 24 );
if ( diffHours < 1 ) return "Just now" ;
if ( diffHours < 24 ) return ` ${ diffHours } h ago` ;
if ( diffDays < 7 ) return ` ${ diffDays } d ago` ;
return formatDate ( date );
}
function formatDateForInput ( date : string | null ) : string {
if ( ! date ) return "" ;
const d = new Date ( date );
return d . toISOString (). slice ( 0 , 16 ); // Format: YYYY-MM-DDTHH:mm
}
export function TaskMetadata ({ task } : TaskMetadataProps ) {
const updateTask = useUpdateTask ();
const { data : project } = useProject ( task . project_id ?? "" );
2026-06-21 07:15:58 +02:00
const branchHref = branchUrl ( project ? . git_url , task . branch_name );
2026-04-20 15:10:54 +02:00
// Editing states - use local state only while editing
const [ editingAssigned , setEditingAssigned ] = useState ( false );
const [ localAssignedValue , setLocalAssignedValue ] = useState ( "" );
const assignedInputRef = useRef < HTMLInputElement >( null );
const [ editingTargetDate , setEditingTargetDate ] = useState ( false );
const [ localTargetDateValue , setLocalTargetDateValue ] = useState ( "" );
const targetDateInputRef = useRef < HTMLInputElement >( null );
// Display prop value when not editing, local value when editing
2026-06-21 07:15:58 +02:00
const assignedValue = editingAssigned
? localAssignedValue
: ( task . assigned_to ?? "" );
2026-04-20 15:10:54 +02:00
const setAssignedValue = ( value : string ) => setLocalAssignedValue ( value );
2026-06-21 07:15:58 +02:00
const targetDateValue = editingTargetDate
? localTargetDateValue
: formatDateForInput ( task . target_date );
2026-04-20 15:10:54 +02:00
const setTargetDateValue = ( value : string ) => setLocalTargetDateValue ( value );
// Start editing - copy current prop value to local state (resolved to slug)
const startEditingAssigned = () => {
setLocalAssignedValue ( resolveToSlug ( task . assigned_to ));
setEditingAssigned ( true );
};
const startEditingTargetDate = () => {
setLocalTargetDateValue ( formatDateForInput ( task . target_date ));
setEditingTargetDate ( true );
};
// Focus inputs when editing starts
useEffect (() => {
if ( editingAssigned && assignedInputRef . current ) {
assignedInputRef . current . focus ();
assignedInputRef . current . select ();
}
}, [ editingAssigned ]);
useEffect (() => {
if ( editingTargetDate && targetDateInputRef . current ) {
targetDateInputRef . current . focus ();
}
}, [ editingTargetDate ]);
const handlePriorityChange = async ( value : string ) => {
try {
await updateTask . mutateAsync ({
taskId : task.id ,
updates : { priority : parseInt ( value , 10 ) },
});
} catch {
toast . error ( "Failed to update priority" );
}
};
const handleComplexityChange = async ( value : string ) => {
try {
await updateTask . mutateAsync ({
taskId : task.id ,
updates : { estimated_complexity : value as Complexity },
});
} catch {
toast . error ( "Failed to update complexity" );
}
};
const handleAssignedSave = async () => {
const newValue = assignedValue . trim () || null ;
if ( newValue === task . assigned_to ) {
setEditingAssigned ( false );
return ;
}
try {
await updateTask . mutateAsync ({
taskId : task.id ,
updates : { assigned_to : newValue },
});
setEditingAssigned ( false );
} catch {
toast . error ( "Failed to update assignment" );
setAssignedValue ( resolveToSlug ( task . assigned_to ));
}
};
const handleAssignedKeyDown = ( e : React.KeyboardEvent ) => {
if ( e . key === "Enter" ) {
handleAssignedSave ();
} else if ( e . key === "Escape" ) {
setAssignedValue ( resolveToSlug ( task . assigned_to ));
setEditingAssigned ( false );
}
};
const handleTargetDateSave = async () => {
2026-06-21 07:15:58 +02:00
const newValue = targetDateValue
? new Date ( targetDateValue ). toISOString ()
: null ;
2026-04-20 15:10:54 +02:00
if ( newValue === task . target_date ) {
setEditingTargetDate ( false );
return ;
}
try {
await updateTask . mutateAsync ({
taskId : task.id ,
updates : { target_date : newValue },
});
setEditingTargetDate ( false );
} catch {
toast . error ( "Failed to update target date" );
setTargetDateValue ( formatDateForInput ( task . target_date ));
}
};
const handleTargetDateKeyDown = ( e : React.KeyboardEvent ) => {
if ( e . key === "Enter" ) {
handleTargetDateSave ();
} else if ( e . key === "Escape" ) {
setTargetDateValue ( formatDateForInput ( task . target_date ));
setEditingTargetDate ( false );
}
};
return (
< div className = "grid grid-cols-2 md:grid-cols-4 gap-4 mt-6" >
{ /* Priority - Editable Select */ }
< Card >
< CardContent className = "pt-4" >
< div className = "flex items-center gap-2 text-sm text-muted-foreground mb-1" >
< AlertTriangle className = "h-4 w-4" />
Priority
</ div >
2026-07-15 18:04:00 +02:00
< HelpTip label = "How urgently this should be worked — P0 is highest, P3 is lowest" >
< Select
value = { task . priority . toString ()}
onValueChange = { handlePriorityChange }
disabled = { updateTask . isPending }
2026-06-21 07:15:58 +02:00
>
2026-07-15 18:04:00 +02:00
< SelectTrigger
className = { `w-full h-8 text-sm border-0 ${ priorityColors [ task . priority ] ?? priorityColors [ 2 ] } ` }
>
< SelectValue />
</ SelectTrigger >
< SelectContent >
{ Object . entries ( priorityLabels ). map (([ value , label ]) => (
< SelectItem key = { value } value = { value }>
< span
className = { `px-2 py-0.5 rounded ${ priorityColors [ parseInt ( value )] } ` }
>
{ label }
</ span >
</ SelectItem >
))}
</ SelectContent >
</ Select >
</ HelpTip >
2026-04-20 15:10:54 +02:00
</ CardContent >
</ Card >
{ /* Complexity - Editable Select */ }
< Card >
< CardContent className = "pt-4" >
< div className = "flex items-center gap-2 text-sm text-muted-foreground mb-1" >
< Target className = "h-4 w-4" />
Complexity
</ div >
2026-07-15 18:04:00 +02:00
< HelpTip label = "A manual estimate of implementation effort; not derived from the plan or ACs" >
< Select
value = { task . estimated_complexity }
onValueChange = { handleComplexityChange }
disabled = { updateTask . isPending }
>
< SelectTrigger className = "w-full h-8 text-sm border-0 bg-transparent hover:bg-muted/50" >
< SelectValue />
</ SelectTrigger >
< SelectContent >
{ Object . values ( Complexity ). map (( complexity ) => (
< SelectItem key = { complexity } value = { complexity }>
{ complexityLabels [ complexity ]}
</ SelectItem >
))}
</ SelectContent >
</ Select >
</ HelpTip >
2026-04-20 15:10:54 +02:00
</ CardContent >
</ Card >
{ /* Assigned To - Editable Input */ }
< Card >
< CardContent className = "pt-4" >
< div className = "flex items-center gap-2 text-sm text-muted-foreground mb-1" >
< User className = "h-4 w-4" />
Assigned To
</ div >
{ editingAssigned ? (
< Input
ref = { assignedInputRef }
value = { assignedValue }
onChange = {( e ) => setAssignedValue ( e . target . value )}
onKeyDown = { handleAssignedKeyDown }
onBlur = { handleAssignedSave }
placeholder = "Agent ID"
className = "h-8 text-sm"
disabled = { updateTask . isPending }
/>
) : (
2026-07-15 18:04:00 +02:00
< HelpTip label = "Click to reassign — enter an agent slug (e.g. be-dev-1), or clear to unassign" >
< span
className = "font-medium cursor-pointer hover:bg-muted/50 px-2 py-1 -mx-2 rounded transition-colors inline-block"
onClick = { startEditingAssigned }
>
{ getAgentDisplayName ( task . assigned_to )}
</ span >
</ HelpTip >
2026-04-20 15:10:54 +02:00
)}
</ CardContent >
</ Card >
{ /* Created By - Read-only */ }
< Card >
< CardContent className = "pt-4" >
< div className = "flex items-center gap-2 text-sm text-muted-foreground mb-1" >
< User className = "h-4 w-4" />
Created By
</ div >
2026-07-15 18:04:00 +02:00
< HelpTip label = "The agent or human who originally created this task record" >
< span className = "font-medium" >
{ getAgentDisplayName ( task . created_by )}
</ span >
</ HelpTip >
2026-04-20 15:10:54 +02:00
</ CardContent >
</ Card >
{ /* Sequence - Read-only */ }
{ task . sequence != null && (
< Card >
< CardContent className = "pt-4" >
< div className = "flex items-center gap-2 text-sm text-muted-foreground mb-1" >
< Hash className = "h-4 w-4" />
Sequence
</ div >
2026-07-15 16:30:26 +02:00
< HelpTip label = "Delegation order among same-parent siblings — a sibling with a strictly lower sequence must reach a terminal state before this task can be claimed. Ties run in parallel." >
< span className = "font-medium" > # { task . sequence }</ span >
</ HelpTip >
2026-04-20 15:10:54 +02:00
</ CardContent >
</ Card >
)}
{ /* Created At - Read-only */ }
< Card >
< CardContent className = "pt-4" >
< div className = "flex items-center gap-2 text-sm text-muted-foreground mb-1" >
< Calendar className = "h-4 w-4" />
Created
</ div >
2026-07-15 18:04:00 +02:00
< HelpTip
label = {
task . created_at
? `Exact timestamp: ${ formatAbsoluteTimestamp ( task . created_at ) } `
: ""
}
>
< span className = "font-medium" >
{ formatRelativeTime ( task . created_at )}
</ span >
</ HelpTip >
2026-04-20 15:10:54 +02:00
</ CardContent >
</ Card >
{ /* Started At - Read-only */ }
< Card >
< CardContent className = "pt-4" >
< div className = "flex items-center gap-2 text-sm text-muted-foreground mb-1" >
< Clock className = "h-4 w-4" />
Started
</ div >
2026-07-15 18:04:00 +02:00
< HelpTip
label = {
task . started_at
? `Exact timestamp: ${ formatAbsoluteTimestamp ( task . started_at ) } `
: ""
}
>
< span className = "font-medium" >
{ formatRelativeTime ( task . started_at )}
</ span >
</ HelpTip >
2026-04-20 15:10:54 +02:00
</ CardContent >
</ Card >
{ /* Target Date - Editable */ }
< Card >
< CardContent className = "pt-4" >
< div className = "flex items-center gap-2 text-sm text-muted-foreground mb-1" >
< Target className = "h-4 w-4" />
Target Date
</ div >
{ editingTargetDate ? (
< Input
ref = { targetDateInputRef }
type = "datetime-local"
value = { targetDateValue }
onChange = {( e ) => setTargetDateValue ( e . target . value )}
onKeyDown = { handleTargetDateKeyDown }
onBlur = { handleTargetDateSave }
className = "h-8 text-sm"
disabled = { updateTask . isPending }
/>
) : (
2026-07-15 18:04:00 +02:00
< HelpTip label = "Click to set/change the target completion date" >
< span
className = "font-medium cursor-pointer hover:bg-muted/50 px-2 py-1 -mx-2 rounded transition-colors inline-block"
onClick = { startEditingTargetDate }
>
{ task . target_date ? formatDate ( task . target_date ) : "Not set" }
</ span >
</ HelpTip >
2026-04-20 15:10:54 +02:00
)}
</ CardContent >
</ Card >
{ /* Completed At - Read-only */ }
< Card >
< CardContent className = "pt-4" >
< div className = "flex items-center gap-2 text-sm text-muted-foreground mb-1" >
< Clock className = "h-4 w-4" />
Completed
</ div >
2026-07-15 18:04:00 +02:00
< HelpTip
label = {
task . completed_at
? `Exact timestamp: ${ formatAbsoluteTimestamp ( task . completed_at ) } `
: ""
}
>
< span className = "font-medium" >
{ formatRelativeTime ( task . completed_at )}
</ span >
</ HelpTip >
2026-04-20 15:10:54 +02:00
</ CardContent >
</ Card >
{ /* Task Type - Read-only */ }
< Card >
< CardContent className = "pt-4" >
< div className = "flex items-center gap-2 text-sm text-muted-foreground mb-1" >
< Wrench className = "h-4 w-4" />
Task Type
</ div >
< TaskTypeBadge type = { task . task_type } />
</ CardContent >
</ Card >
{ /* Nature - Read-only */ }
< Card >
< CardContent className = "pt-4" >
< div className = "flex items-center gap-2 text-sm text-muted-foreground mb-1" >
< Briefcase className = "h-4 w-4" />
Nature
</ div >
2026-07-15 16:30:26 +02:00
< HelpTip
label = {
2026-04-20 15:10:54 +02:00
task . nature === TaskNature . TECHNICAL
2026-07-15 16:30:26 +02:00
? "Code/dev work — routes through the normal PM chain."
: "Strategic work (product/marketing) — routes to the Board for review."
2026-04-20 15:10:54 +02:00
}
>
2026-07-15 16:30:26 +02:00
< Badge
variant = "outline"
className = {
task . nature === TaskNature . TECHNICAL
? "bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300"
: "bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300"
}
>
{ task . nature === TaskNature . TECHNICAL
? "Technical"
: "Non-Technical" }
</ Badge >
</ HelpTip >
2026-04-20 15:10:54 +02:00
</ CardContent >
</ Card >
2026-06-21 07:15:58 +02:00
{ /* Branch Name - read-only, but clickable (opens the branch on GitHub)
and copyable. Same look as before; the link/copy are additive. */ }
2026-04-20 15:10:54 +02:00
{ task . branch_name && (
2026-06-21 09:41:09 +02:00
< Card className = "col-span-2" >
2026-04-20 15:10:54 +02:00
< CardContent className = "pt-4" >
< div className = "flex items-center gap-2 text-sm text-muted-foreground mb-1" >
< GitBranch className = "h-4 w-4" />
Branch
</ div >
2026-06-21 07:15:58 +02:00
< div className = "flex items-center gap-1" >
{ branchHref ? (
2026-07-15 18:04:00 +02:00
< HelpTip label = "Open this branch on GitHub in a new tab" >
< a
href = { branchHref }
target = "_blank"
rel = "noopener noreferrer"
className = "inline-flex transition-opacity hover:opacity-80"
>
< Badge variant = "outline" className = "font-mono text-sm" >
{ task . branch_name }
</ Badge >
</ a >
</ HelpTip >
) : (
< HelpTip label = "The git branch for this task; no project git_url configured to link out to it" >
2026-06-21 09:41:09 +02:00
< Badge variant = "outline" className = "font-mono text-sm" >
2026-06-21 07:15:58 +02:00
{ task . branch_name }
</ Badge >
2026-07-15 18:04:00 +02:00
</ HelpTip >
2026-06-21 07:15:58 +02:00
)}
< CopyButton value = { task . branch_name } />
</ div >
2026-04-20 15:10:54 +02:00
</ CardContent >
</ Card >
)}
{ /* Pull Request - Read-only Link (only show when PR exists) */ }
{ task . pr_number && (
< Card >
< CardContent className = "pt-4" >
< div className = "flex items-center gap-2 text-sm text-muted-foreground mb-1" >
< GitPullRequest className = "h-4 w-4" />
Pull Request
</ div >
{ task . pr_url ? (
2026-07-15 18:04:00 +02:00
< HelpTip label = "Opens this task's pull request on GitHub in a new tab" >
< a
href = { task . pr_url }
target = "_blank"
rel = "noopener noreferrer"
className = "inline-flex items-center gap-1 text-blue-600 hover:underline dark:text-blue-400"
>
PR # { task . pr_number }
< ExternalLink className = "h-3 w-3" />
</ a >
</ HelpTip >
2026-04-20 15:10:54 +02:00
) : (
2026-07-15 18:04:00 +02:00
< HelpTip label = "PR number on record; no pr_url stored to link out to it" >
< span className = "font-medium" > # { task . pr_number }</ span >
</ HelpTip >
2026-04-20 15:10:54 +02:00
)}
</ CardContent >
</ Card >
)}
{ /* Project - Read-only Link */ }
< Card >
< CardContent className = "pt-4" >
< div className = "flex items-center gap-2 text-sm text-muted-foreground mb-1" >
< FolderGit2 className = "h-4 w-4" />
Project
</ div >
{ task . project_id && project ? (
2026-07-15 18:04:00 +02:00
< HelpTip label = "Opens the Projects list (not a deep link to this specific project)" >
< Link
prefetch = { false }
href = { `/projects` }
className = "font-medium text-blue-600 hover:underline dark:text-blue-400"
>
{ project . name }
</ Link >
</ HelpTip >
2026-04-20 15:10:54 +02:00
) : (
2026-07-15 18:04:00 +02:00
< HelpTip label = "This task has no associated project" >
< span className = "font-medium text-muted-foreground" > - </ span >
</ HelpTip >
2026-04-20 15:10:54 +02:00
)}
</ CardContent >
</ Card >
{ /* Docs/PR Status - Read-only (only show for relevant statuses) */ }
{( task . docs_complete !== undefined || task . pr_created !== undefined ) && (
< Card className = "col-span-2" >
< CardContent className = "pt-4" >
< div className = "flex items-center gap-2 text-sm text-muted-foreground mb-2" >
Completion Status
</ div >
< DocsStatusBadge
docsComplete = { task . docs_complete }
prCreated = { task . pr_created }
variant = "full"
/>
</ CardContent >
</ Card >
)}
</ div >
);
}