import { useState, useEffect, useCallback } from "react"; import { useParams, Link, useNavigate } from "react-router-dom"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import type { Task, TaskStatus, Project } from "../lib/types"; import { updateTask, fetchProjects, addProgressNote, addSubtask, toggleSubtask, deleteSubtask, deleteTask } from "../lib/api"; import { useToast } from "../components/Toast"; import { TaskComments } from "../components/TaskComments"; const priorityColors: Record = { critical: "bg-red-500 text-white", high: "bg-orange-500 text-white", medium: "bg-blue-500 text-white", low: "bg-gray-400 text-white", }; const statusColors: Record = { active: "bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-300 border-amber-300 dark:border-amber-700", queued: "bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300 border-blue-300 dark:border-blue-700", blocked: "bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300 border-red-300 dark:border-red-700", completed: "bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 border-green-300 dark:border-green-700", cancelled: "bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-300 dark:border-gray-600", }; const statusIcons: Record = { active: "โšก", queued: "๐Ÿ“‹", blocked: "๐Ÿšซ", completed: "โœ…", cancelled: "โŒ", }; const statusActions: Record = { active: [ { label: "โธ Pause", next: "queued", color: "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-700 hover:bg-blue-100 dark:hover:bg-blue-900/40" }, { label: "๐Ÿšซ Block", next: "blocked", color: "bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 border-red-200 dark:border-red-700 hover:bg-red-100 dark:hover:bg-red-900/40" }, { label: "โœ… Complete", next: "completed", color: "bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 border-green-200 dark:border-green-700 hover:bg-green-100 dark:hover:bg-green-900/40" }, ], queued: [ { label: "โ–ถ Activate", next: "active", color: "bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-300 border-amber-200 dark:border-amber-700 hover:bg-amber-100 dark:hover:bg-amber-900/40" }, { label: "๐Ÿšซ Block", next: "blocked", color: "bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 border-red-200 dark:border-red-700 hover:bg-red-100 dark:hover:bg-red-900/40" }, ], blocked: [ { label: "โ–ถ Activate", next: "active", color: "bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-300 border-amber-200 dark:border-amber-700 hover:bg-amber-100 dark:hover:bg-amber-900/40" }, { label: "๐Ÿ“‹ Queue", next: "queued", color: "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-700 hover:bg-blue-100 dark:hover:bg-blue-900/40" }, ], completed: [{ label: "๐Ÿ”„ Requeue", next: "queued", color: "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-700 hover:bg-blue-100 dark:hover:bg-blue-900/40" }], cancelled: [{ label: "๐Ÿ”„ Requeue", next: "queued", color: "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-700 hover:bg-blue-100 dark:hover:bg-blue-900/40" }], }; function formatDate(dateStr: string): string { return new Date(dateStr).toLocaleString(undefined, { month: "short", day: "numeric", year: "numeric", hour: "2-digit", minute: "2-digit", }); } function timeAgo(dateStr: string): string { const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000); if (seconds < 60) return "just now"; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); return `${days}d ago`; } // Markdown prose classes for descriptions and notes const proseClasses = "prose prose-sm prose-gray dark:prose-invert max-w-none [&_pre]:bg-gray-800 dark:[&_pre]:bg-gray-900 [&_pre]:text-gray-100 [&_pre]:rounded-lg [&_pre]:p-3 [&_pre]:overflow-x-auto [&_pre]:text-xs [&_code]:bg-gray-200 dark:[&_code]:bg-gray-700 [&_code]:px-1 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-xs [&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_p]:mb-2 [&_p:last-child]:mb-0 [&_ul]:mb-2 [&_ol]:mb-2 [&_li]:mb-0.5 [&_h1]:text-base [&_h2]:text-sm [&_h3]:text-sm [&_a]:text-amber-600 dark:[&_a]:text-amber-400 [&_a]:underline [&_blockquote]:border-l-2 [&_blockquote]:border-gray-300 dark:[&_blockquote]:border-gray-600 [&_blockquote]:pl-3 [&_blockquote]:text-gray-500 dark:[&_blockquote]:text-gray-400"; export function TaskPage() { const { taskRef } = useParams<{ taskRef: string }>(); const [task, setTask] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [projects, setProjects] = useState([]); const [noteText, setNoteText] = useState(""); const [addingNote, setAddingNote] = useState(false); const [newSubtaskTitle, setNewSubtaskTitle] = useState(""); const [addingSubtask, setAddingSubtask] = useState(false); const [saving, setSaving] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [deleting, setDeleting] = useState(false); // Description editing const [editingDescription, setEditingDescription] = useState(false); const [descriptionDraft, setDescriptionDraft] = useState(""); const [savingDescription, setSavingDescription] = useState(false); // Title editing const [editingTitle, setEditingTitle] = useState(false); const [titleDraft, setTitleDraft] = useState(""); const [savingTitle, setSavingTitle] = useState(false); const { toast } = useToast(); const navigate = useNavigate(); const fetchTask = useCallback(async () => { if (!taskRef) return; try { const res = await fetch(`/api/tasks/${taskRef}`, { credentials: "include" }); if (!res.ok) throw new Error(res.status === 404 ? "Task not found" : "Failed to load task"); const data = await res.json(); setTask(data); setError(null); } catch (e: any) { setError(e.message); } finally { setLoading(false); } }, [taskRef]); useEffect(() => { fetchTask(); fetchProjects().then(setProjects).catch(() => {}); }, [fetchTask]); // Auto-refresh every 15s useEffect(() => { const interval = setInterval(fetchTask, 15000); return () => clearInterval(interval); }, [fetchTask]); const handleStatusChange = async (status: TaskStatus) => { if (!task) return; setSaving(true); try { await updateTask(task.id, { status }); fetchTask(); toast(`Task moved to ${status}`, "success"); } catch (e) { console.error("Failed to update status:", e); toast("Failed to update status", "error"); } finally { setSaving(false); } }; const handleSaveDescription = async () => { if (!task) return; setSavingDescription(true); try { await updateTask(task.id, { description: descriptionDraft }); fetchTask(); setEditingDescription(false); toast("Description updated", "success"); } catch (e) { console.error("Failed to save description:", e); toast("Failed to save description", "error"); } finally { setSavingDescription(false); } }; const handleSaveTitle = async () => { if (!task || !titleDraft.trim()) return; setSavingTitle(true); try { await updateTask(task.id, { title: titleDraft.trim() }); fetchTask(); setEditingTitle(false); toast("Title updated", "success"); } catch (e) { console.error("Failed to save title:", e); toast("Failed to save title", "error"); } finally { setSavingTitle(false); } }; const handleDelete = async () => { if (!task) return; setDeleting(true); try { await deleteTask(task.id); toast("Task deleted", "success"); navigate("/queue"); } catch (e) { console.error("Failed to delete:", e); toast("Failed to delete task", "error"); } finally { setDeleting(false); setShowDeleteConfirm(false); } }; if (loading) { return (
Loading task...
); } if (error || !task) { return (
๐Ÿ˜•

{error || "Task not found"}

โ† Back to Queue
); } const isActive = task.status === "active"; const actions = statusActions[task.status] || []; const project = projects.find((p) => p.id === task.projectId); const subtaskProgress = task.subtasks?.length > 0 ? Math.round((task.subtasks.filter(s => s.completed).length / task.subtasks.length) * 100) : 0; return (
{/* Header */}
โ† Queue / HQ-{task.taskNumber}
{isActive && ( )} {statusIcons[task.status]} {task.status.toUpperCase()} {task.priority} {project && ( ๐Ÿ“ {project.name} )} {task.recurrence && ( ๐Ÿ”„ {task.recurrence.frequency === "biweekly" ? "Bi-weekly" : task.recurrence.frequency.charAt(0).toUpperCase() + task.recurrence.frequency.slice(1)} )} {task.tags?.map(tag => ( ๐Ÿท๏ธ {tag} ))}
{editingTitle ? (
setTitleDraft(e.target.value)} className="text-xl font-bold text-gray-900 dark:text-gray-100 bg-transparent border-b-2 border-amber-400 dark:border-amber-500 outline-none flex-1 py-0.5" onKeyDown={(e) => { if (e.key === "Enter") handleSaveTitle(); if (e.key === "Escape") setEditingTitle(false); }} disabled={savingTitle} />
) : (

{ setTitleDraft(task.title); setEditingTitle(true); }} title="Click to edit title" > {task.title} โœ๏ธ

)}
{/* Status Actions */}
{actions.map((action) => ( ))}
{/* Due date badge */} {task.dueDate && (() => { const due = new Date(task.dueDate); const now = new Date(); const diffMs = due.getTime() - now.getTime(); const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24)); const isOverdue = diffMs < 0; const isDueSoon = diffDays <= 2 && !isOverdue; return (
๐Ÿ“… Due: {formatDate(task.dueDate)} ({isOverdue ? `${Math.abs(diffDays)}d overdue` : diffDays === 0 ? "today" : diffDays === 1 ? "tomorrow" : `${diffDays}d left`})
); })()}
{/* Main content */}
{/* Description */}

Description

{!editingDescription && ( )}
{editingDescription ? (