"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"; import { User, Calendar, Clock, Target, AlertTriangle, GitBranch, FolderGit2, Wrench, Briefcase, Hash, GitPullRequest, ExternalLink, } from "lucide-react"; import { toast } from "sonner"; import { getAgentDisplayName, resolveToSlug } from "@/lib/agent-utils"; import { branchUrl } from "@/lib/repo-url"; import { formatAbsoluteTimestamp } from "@/lib/utils"; import { CopyButton } from "@/components/ui/copy-button"; import { HelpTip } from "@/components/ui/help-tip"; import { TaskTypeBadge } from "../task-type-badge"; import { DocsStatusBadge } from "../docs-status-badge"; import Link from "next/link"; // Priority colors const priorityColors: Record = { 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 = { 0: "P0 - Highest", 1: "P1 - High", 2: "P2 - Medium", 3: "P3 - Low", }; const complexityLabels: Record = { [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 ?? ""); const branchHref = branchUrl(project?.git_url, task.branch_name); // Editing states - use local state only while editing const [editingAssigned, setEditingAssigned] = useState(false); const [localAssignedValue, setLocalAssignedValue] = useState(""); const assignedInputRef = useRef(null); const [editingTargetDate, setEditingTargetDate] = useState(false); const [localTargetDateValue, setLocalTargetDateValue] = useState(""); const targetDateInputRef = useRef(null); // Display prop value when not editing, local value when editing const assignedValue = editingAssigned ? localAssignedValue : (task.assigned_to ?? ""); const setAssignedValue = (value: string) => setLocalAssignedValue(value); const targetDateValue = editingTargetDate ? localTargetDateValue : formatDateForInput(task.target_date); 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 () => { const newValue = targetDateValue ? new Date(targetDateValue).toISOString() : null; 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 (
{/* Priority - Editable Select */}
Priority
{/* Complexity - Editable Select */}
Complexity
{/* Assigned To - Editable Input */}
Assigned To
{editingAssigned ? ( setAssignedValue(e.target.value)} onKeyDown={handleAssignedKeyDown} onBlur={handleAssignedSave} placeholder="Agent ID" className="h-8 text-sm" disabled={updateTask.isPending} /> ) : ( {getAgentDisplayName(task.assigned_to)} )}
{/* Created By - Read-only */}
Created By
{getAgentDisplayName(task.created_by)}
{/* Sequence - Read-only */} {task.sequence != null && (
Sequence
#{task.sequence}
)} {/* Created At - Read-only */}
Created
{formatRelativeTime(task.created_at)}
{/* Started At - Read-only */}
Started
{formatRelativeTime(task.started_at)}
{/* Target Date - Editable */}
Target Date
{editingTargetDate ? ( setTargetDateValue(e.target.value)} onKeyDown={handleTargetDateKeyDown} onBlur={handleTargetDateSave} className="h-8 text-sm" disabled={updateTask.isPending} /> ) : ( {task.target_date ? formatDate(task.target_date) : "Not set"} )}
{/* Completed At - Read-only */}
Completed
{formatRelativeTime(task.completed_at)}
{/* Task Type - Read-only */}
Task Type
{/* Nature - Read-only */}
Nature
{task.nature === TaskNature.TECHNICAL ? "Technical" : "Non-Technical"}
{/* Branch Name - read-only, but clickable (opens the branch on GitHub) and copyable. Same look as before; the link/copy are additive. */} {task.branch_name && (
Branch
{branchHref ? ( {task.branch_name} ) : ( {task.branch_name} )}
)} {/* Pull Request - Read-only Link (only show when PR exists) */} {task.pr_number && (
Pull Request
{task.pr_url ? ( PR #{task.pr_number} ) : ( #{task.pr_number} )}
)} {/* Project - Read-only Link */}
Project
{task.project_id && project ? ( {project.name} ) : ( - )}
{/* Docs/PR Status - Read-only (only show for relevant statuses) */} {(task.docs_complete !== undefined || task.pr_created !== undefined) && (
Completion Status
)}
); }