feat: enhanced create modal, project/assignee badges, status filter, keyboard shortcuts

- CreateTaskModal: project selector, due date picker, source in 'more options'
- TaskCard: project name badge (📁) and assignee badge (👤)
- QueuePage: status filter dropdown, clear filters button, filter count indicator
- QueuePage: Ctrl+N keyboard shortcut to create task
- DashboardPage: project/assignee badges on active task cards
- Search now also matches assignee name
This commit is contained in:
2026-01-29 08:03:47 +00:00
parent 578b092a78
commit f00e0720e1
5 changed files with 368 additions and 125 deletions

View File

@@ -1,4 +1,6 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import { fetchProjects } from "../lib/api";
import type { Project } from "../lib/types";
interface CreateTaskModalProps { interface CreateTaskModalProps {
open: boolean; open: boolean;
@@ -8,6 +10,8 @@ interface CreateTaskModalProps {
description?: string; description?: string;
source?: string; source?: string;
priority?: string; priority?: string;
projectId?: string;
dueDate?: string;
}) => void; }) => void;
} }
@@ -16,6 +20,26 @@ export function CreateTaskModal({ open, onClose, onCreate }: CreateTaskModalProp
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
const [source, setSource] = useState("donovan"); const [source, setSource] = useState("donovan");
const [priority, setPriority] = useState("medium"); const [priority, setPriority] = useState("medium");
const [projectId, setProjectId] = useState("");
const [dueDate, setDueDate] = useState("");
const [projects, setProjects] = useState<Project[]>([]);
const [showAdvanced, setShowAdvanced] = useState(false);
useEffect(() => {
if (open) {
fetchProjects().then(setProjects).catch(() => {});
}
}, [open]);
// Keyboard shortcut: Escape to close
useEffect(() => {
if (!open) return;
const handleKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
window.addEventListener("keydown", handleKey);
return () => window.removeEventListener("keydown", handleKey);
}, [open, onClose]);
if (!open) return null; if (!open) return null;
@@ -27,75 +51,166 @@ export function CreateTaskModal({ open, onClose, onCreate }: CreateTaskModalProp
description: description.trim() || undefined, description: description.trim() || undefined,
source, source,
priority, priority,
projectId: projectId || undefined,
dueDate: dueDate ? new Date(dueDate).toISOString() : undefined,
}); });
// Reset form
setTitle(""); setTitle("");
setDescription(""); setDescription("");
setSource("donovan"); setSource("donovan");
setPriority("medium"); setPriority("medium");
setProjectId("");
setDueDate("");
setShowAdvanced(false);
onClose(); onClose();
}; };
return ( return (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-2xl p-6 w-full max-w-md mx-4"> <div className="bg-white rounded-xl shadow-2xl p-6 w-full max-w-lg mx-4 max-h-[90vh] overflow-y-auto">
<h2 className="text-lg font-bold mb-4">New Task</h2> <div className="flex items-center justify-between mb-4">
<form onSubmit={handleSubmit} className="space-y-3"> <h2 className="text-lg font-bold">New Task</h2>
<input <button
type="text" onClick={onClose}
placeholder="Task title..." className="text-gray-400 hover:text-gray-600 p-1 rounded-lg hover:bg-gray-100 transition"
value={title} >
onChange={(e) => setTitle(e.target.value)} <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
className="w-full border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-amber-400" <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
autoFocus </svg>
/> </button>
<textarea </div>
placeholder="Description / context (optional)" <form onSubmit={handleSubmit} className="space-y-4">
value={description} {/* Title */}
onChange={(e) => setDescription(e.target.value)} <div>
rows={3} <label className="text-xs font-medium text-gray-500 block mb-1">Title</label>
className="w-full border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-amber-400" <input
/> type="text"
placeholder="What needs to be done?"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full border border-gray-200 rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-amber-400 focus:border-amber-400"
autoFocus
/>
</div>
{/* Description */}
<div>
<label className="text-xs font-medium text-gray-500 block mb-1">Description</label>
<textarea
placeholder="Context, requirements, links... (optional)"
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={3}
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-amber-400 focus:border-amber-400 resize-y"
/>
</div>
{/* Priority & Source row */}
<div className="flex gap-3"> <div className="flex gap-3">
<div className="flex-1"> <div className="flex-1">
<label className="text-xs text-gray-500 block mb-1">Source</label> <label className="text-xs font-medium text-gray-500 block mb-1">Priority</label>
<select <div className="flex gap-1">
value={source} {(["critical", "high", "medium", "low"] as const).map((p) => (
onChange={(e) => setSource(e.target.value)} <button
className="w-full border rounded-lg px-3 py-2 text-sm" key={p}
> type="button"
<option value="donovan">Donovan</option> onClick={() => setPriority(p)}
<option value="david">David</option> className={`flex-1 text-xs py-2 rounded-lg font-medium transition border ${
<option value="hammer">Hammer</option> priority === p
<option value="heartbeat">Heartbeat</option> ? p === "critical" ? "bg-red-500 text-white border-red-500"
<option value="cron">Cron</option> : p === "high" ? "bg-orange-500 text-white border-orange-500"
<option value="other">Other</option> : p === "medium" ? "bg-blue-500 text-white border-blue-500"
</select> : "bg-gray-500 text-white border-gray-500"
</div> : "bg-white text-gray-500 border-gray-200 hover:border-gray-300 hover:bg-gray-50"
<div className="flex-1"> }`}
<label className="text-xs text-gray-500 block mb-1">Priority</label> >
<select {p === "critical" ? "🔴" : p === "high" ? "🟠" : p === "medium" ? "🔵" : "⚪"} {p[0].toUpperCase() + p.slice(1)}
value={priority} </button>
onChange={(e) => setPriority(e.target.value)} ))}
className="w-full border rounded-lg px-3 py-2 text-sm" </div>
>
<option value="critical">Critical</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
</div> </div>
</div> </div>
{/* Project selector */}
<div>
<label className="text-xs font-medium text-gray-500 block mb-1">Project</label>
<select
value={projectId}
onChange={(e) => setProjectId(e.target.value)}
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-amber-400 bg-white"
>
<option value="">No project</option>
{projects.map((p) => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
</div>
{/* Advanced toggle */}
<button
type="button"
onClick={() => setShowAdvanced(!showAdvanced)}
className="text-xs text-gray-400 hover:text-gray-600 flex items-center gap-1 transition"
>
{showAdvanced ? "▾" : "▸"} More options
</button>
{showAdvanced && (
<div className="space-y-3 pl-2 border-l-2 border-gray-100">
{/* Due Date */}
<div>
<label className="text-xs font-medium text-gray-500 block mb-1">Due Date</label>
<div className="flex items-center gap-2">
<input
type="datetime-local"
value={dueDate}
onChange={(e) => setDueDate(e.target.value)}
className="text-sm border border-gray-200 rounded-lg px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-amber-400 bg-white"
/>
{dueDate && (
<button
type="button"
onClick={() => setDueDate("")}
className="text-xs text-gray-400 hover:text-red-500 transition"
>
</button>
)}
</div>
</div>
{/* Source */}
<div>
<label className="text-xs font-medium text-gray-500 block mb-1">Source</label>
<select
value={source}
onChange={(e) => setSource(e.target.value)}
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-amber-400 bg-white"
>
<option value="donovan">Donovan</option>
<option value="david">David</option>
<option value="hammer">Hammer</option>
<option value="heartbeat">Heartbeat</option>
<option value="cron">Cron</option>
<option value="other">Other</option>
</select>
</div>
</div>
)}
{/* Submit buttons */}
<div className="flex gap-2 pt-2"> <div className="flex gap-2 pt-2">
<button <button
type="submit" type="submit"
className="flex-1 bg-amber-500 text-white rounded-lg py-2 text-sm font-medium hover:bg-amber-600 transition" disabled={!title.trim()}
className="flex-1 bg-amber-500 text-white rounded-lg py-2.5 text-sm font-semibold hover:bg-amber-600 transition disabled:opacity-50 disabled:cursor-not-allowed"
> >
Create Task Create Task
</button> </button>
<button <button
type="button" type="button"
onClick={onClose} onClick={onClose}
className="px-4 py-2 text-sm text-gray-600 hover:text-gray-900" className="px-4 py-2.5 text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition"
> >
Cancel Cancel
</button> </button>

View File

@@ -53,6 +53,7 @@ interface TaskCardProps {
isLast?: boolean; isLast?: boolean;
isActive?: boolean; isActive?: boolean;
onClick?: () => void; onClick?: () => void;
projectName?: string;
} }
export function TaskCard({ export function TaskCard({
@@ -64,6 +65,7 @@ export function TaskCard({
isLast, isLast,
isActive, isActive,
onClick, onClick,
projectName,
}: TaskCardProps) { }: TaskCardProps) {
const actions = statusActions[task.status] || []; const actions = statusActions[task.status] || [];
const noteCount = task.progressNotes?.length || 0; const noteCount = task.progressNotes?.length || 0;
@@ -110,6 +112,18 @@ export function TaskCard({
<span className={`text-[10px] sm:text-xs px-1.5 sm:px-2 py-0.5 rounded-full font-medium ${sourceColors[task.source] || sourceColors.other}`}> <span className={`text-[10px] sm:text-xs px-1.5 sm:px-2 py-0.5 rounded-full font-medium ${sourceColors[task.source] || sourceColors.other}`}>
{task.source} {task.source}
</span> </span>
{/* Project badge */}
{projectName && (
<span className="text-[10px] sm:text-xs px-1.5 sm:px-2 py-0.5 rounded-full font-medium bg-sky-100 text-sky-700">
📁 {projectName}
</span>
)}
{/* Assignee badge */}
{task.assigneeName && (
<span className="text-[10px] sm:text-xs px-1.5 sm:px-2 py-0.5 rounded-full font-medium bg-emerald-100 text-emerald-700">
👤 {task.assigneeName}
</span>
)}
<span className="text-[10px] sm:text-xs text-gray-400"> <span className="text-[10px] sm:text-xs text-gray-400">
{timeAgo(task.createdAt)} {timeAgo(task.createdAt)}
</span> </span>

View File

@@ -38,7 +38,7 @@ export async function reorderTasks(ids: string[], token?: string): Promise<void>
} }
export async function createTask( export async function createTask(
task: { title: string; description?: string; source?: string; priority?: string; status?: string }, task: { title: string; description?: string; source?: string; priority?: string; status?: string; projectId?: string; dueDate?: string },
token?: string token?: string
): Promise<Task> { ): Promise<Task> {
const headers: Record<string, string> = { "Content-Type": "application/json" }; const headers: Record<string, string> = { "Content-Type": "application/json" };

View File

@@ -1,7 +1,8 @@
import { useMemo } from "react"; import { useMemo, useEffect, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useTasks } from "../hooks/useTasks"; import { useTasks } from "../hooks/useTasks";
import type { Task, ProgressNote } from "../lib/types"; import { fetchProjects } from "../lib/api";
import type { Task, ProgressNote, Project } from "../lib/types";
function StatCard({ label, value, icon, color }: { label: string; value: number; icon: string; color: string }) { function StatCard({ label, value, icon, color }: { label: string; value: number; icon: string; color: string }) {
return ( return (
@@ -74,6 +75,17 @@ function RecentActivity({ tasks }: { tasks: Task[] }) {
export function DashboardPage() { export function DashboardPage() {
const { tasks, loading } = useTasks(10000); const { tasks, loading } = useTasks(10000);
const [projects, setProjects] = useState<Project[]>([]);
useEffect(() => {
fetchProjects().then(setProjects).catch(() => {});
}, []);
const projectMap = useMemo(() => {
const map: Record<string, string> = {};
for (const p of projects) map[p.id] = p.name;
return map;
}, [projects]);
const stats = useMemo(() => { const stats = useMemo(() => {
const active = tasks.filter((t) => t.status === "active").length; const active = tasks.filter((t) => t.status === "active").length;
@@ -146,6 +158,12 @@ export function DashboardPage() {
</span> </span>
<span className="text-xs font-bold text-amber-700 font-mono">HQ-{task.taskNumber}</span> <span className="text-xs font-bold text-amber-700 font-mono">HQ-{task.taskNumber}</span>
<span className="text-xs text-amber-600 capitalize px-1.5 py-0.5 bg-amber-200/50 rounded-full">{task.priority}</span> <span className="text-xs text-amber-600 capitalize px-1.5 py-0.5 bg-amber-200/50 rounded-full">{task.priority}</span>
{task.projectId && projectMap[task.projectId] && (
<span className="text-xs text-sky-600 px-1.5 py-0.5 bg-sky-100 rounded-full">📁 {projectMap[task.projectId]}</span>
)}
{task.assigneeName && (
<span className="text-xs text-emerald-600 px-1.5 py-0.5 bg-emerald-100 rounded-full">👤 {task.assigneeName}</span>
)}
{task.dueDate && (() => { {task.dueDate && (() => {
const due = new Date(task.dueDate); const due = new Date(task.dueDate);
const diffMs = due.getTime() - Date.now(); const diffMs = due.getTime() - Date.now();

View File

@@ -1,12 +1,12 @@
import { useState, useMemo } from "react"; import { useState, useMemo, useEffect } from "react";
import { useTasks } from "../hooks/useTasks"; import { useTasks } from "../hooks/useTasks";
import { useCurrentUser } from "../hooks/useCurrentUser"; import { useCurrentUser } from "../hooks/useCurrentUser";
import { TaskCard } from "../components/TaskCard"; import { TaskCard } from "../components/TaskCard";
import { TaskDetailPanel } from "../components/TaskDetailPanel"; import { TaskDetailPanel } from "../components/TaskDetailPanel";
import { CreateTaskModal } from "../components/CreateTaskModal"; import { CreateTaskModal } from "../components/CreateTaskModal";
import { useToast } from "../components/Toast"; import { useToast } from "../components/Toast";
import { updateTask, reorderTasks, createTask } from "../lib/api"; import { updateTask, reorderTasks, createTask, fetchProjects } from "../lib/api";
import type { TaskStatus } from "../lib/types"; import type { TaskStatus, Project } from "../lib/types";
export function QueuePage() { export function QueuePage() {
const { tasks, loading, error, refresh } = useTasks(5000); const { tasks, loading, error, refresh } = useTasks(5000);
@@ -16,8 +16,35 @@ export function QueuePage() {
const [selectedTask, setSelectedTask] = useState<string | null>(null); const [selectedTask, setSelectedTask] = useState<string | null>(null);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [filterPriority, setFilterPriority] = useState<string>(""); const [filterPriority, setFilterPriority] = useState<string>("");
const [filterStatus, setFilterStatus] = useState<string>("");
const [projects, setProjects] = useState<Project[]>([]);
const { toast } = useToast(); const { toast } = useToast();
// Load projects for name display
useEffect(() => {
fetchProjects().then(setProjects).catch(() => {});
}, []);
const projectMap = useMemo(() => {
const map: Record<string, string> = {};
for (const p of projects) {
map[p.id] = p.name;
}
return map;
}, [projects]);
// Keyboard shortcut: Ctrl+N to create new task
useEffect(() => {
const handleKey = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === "n" && !showCreate) {
e.preventDefault();
setShowCreate(true);
}
};
window.addEventListener("keydown", handleKey);
return () => window.removeEventListener("keydown", handleKey);
}, [showCreate]);
const filteredTasks = useMemo(() => { const filteredTasks = useMemo(() => {
let filtered = tasks; let filtered = tasks;
if (search.trim()) { if (search.trim()) {
@@ -26,14 +53,18 @@ export function QueuePage() {
(t) => (t) =>
t.title.toLowerCase().includes(q) || t.title.toLowerCase().includes(q) ||
(t.description && t.description.toLowerCase().includes(q)) || (t.description && t.description.toLowerCase().includes(q)) ||
(t.taskNumber && `hq-${t.taskNumber}`.includes(q)) (t.taskNumber && `hq-${t.taskNumber}`.includes(q)) ||
(t.assigneeName && t.assigneeName.toLowerCase().includes(q))
); );
} }
if (filterPriority) { if (filterPriority) {
filtered = filtered.filter((t) => t.priority === filterPriority); filtered = filtered.filter((t) => t.priority === filterPriority);
} }
if (filterStatus) {
filtered = filtered.filter((t) => t.status === filterStatus);
}
return filtered; return filtered;
}, [tasks, search, filterPriority]); }, [tasks, search, filterPriority, filterStatus]);
const selectedTaskData = useMemo(() => { const selectedTaskData = useMemo(() => {
if (!selectedTask) return null; if (!selectedTask) return null;
@@ -48,6 +79,9 @@ export function QueuePage() {
[filteredTasks] [filteredTasks]
); );
// When filtering by status, determine which sections to show
const showSection = (status: string) => !filterStatus || filterStatus === status;
const handleStatusChange = async (id: string, status: TaskStatus) => { const handleStatusChange = async (id: string, status: TaskStatus) => {
try { try {
await updateTask(id, { status }); await updateTask(id, { status });
@@ -79,6 +113,8 @@ export function QueuePage() {
description?: string; description?: string;
source?: string; source?: string;
priority?: string; priority?: string;
projectId?: string;
dueDate?: string;
}) => { }) => {
try { try {
await createTask(task); await createTask(task);
@@ -89,6 +125,8 @@ export function QueuePage() {
} }
}; };
const activeFilters = [filterPriority, filterStatus].filter(Boolean).length;
return ( return (
<div className="min-h-screen"> <div className="min-h-screen">
{/* Page Header */} {/* Page Header */}
@@ -97,11 +135,17 @@ export function QueuePage() {
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<div> <div>
<h1 className="text-lg sm:text-xl font-bold text-gray-900">Task Queue</h1> <h1 className="text-lg sm:text-xl font-bold text-gray-900">Task Queue</h1>
<p className="text-xs sm:text-sm text-gray-400">Manage what Hammer is working on</p> <p className="text-xs sm:text-sm text-gray-400">
Manage what Hammer is working on
{filteredTasks.length !== tasks.length && (
<span className="ml-1 text-amber-500">· {filteredTasks.length} of {tasks.length} shown</span>
)}
</p>
</div> </div>
<button <button
onClick={() => setShowCreate(true)} onClick={() => setShowCreate(true)}
className="text-sm bg-amber-500 text-white px-3 sm:px-4 py-2 rounded-lg hover:bg-amber-600 transition font-medium" className="text-sm bg-amber-500 text-white px-3 sm:px-4 py-2 rounded-lg hover:bg-amber-600 transition font-medium"
title="Ctrl+N"
> >
+ New + New
</button> </button>
@@ -140,6 +184,27 @@ export function QueuePage() {
<option value="medium">🔵 Medium</option> <option value="medium">🔵 Medium</option>
<option value="low"> Low</option> <option value="low"> Low</option>
</select> </select>
<select
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)}
className="text-sm border border-gray-200 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-amber-200 bg-white"
>
<option value="">All statuses</option>
<option value="active"> Active</option>
<option value="queued">📋 Queued</option>
<option value="blocked">🚫 Blocked</option>
<option value="completed"> Completed</option>
<option value="cancelled"> Cancelled</option>
</select>
{activeFilters > 0 && (
<button
onClick={() => { setFilterPriority(""); setFilterStatus(""); }}
className="text-xs text-amber-600 hover:text-amber-700 font-medium px-2 py-2 shrink-0"
title="Clear all filters"
>
Clear ({activeFilters})
</button>
)}
</div> </div>
</div> </div>
</header> </header>
@@ -161,31 +226,34 @@ export function QueuePage() {
)} )}
{/* Active Task */} {/* Active Task */}
<section> {showSection("active") && (
<h2 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3"> <section>
Currently Working On <h2 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
</h2> Currently Working On
{activeTasks.length === 0 ? ( </h2>
<div className="border-2 border-dashed border-gray-200 rounded-lg p-8 text-center text-gray-400"> {activeTasks.length === 0 ? (
No active task Hammer is idle <div className="border-2 border-dashed border-gray-200 rounded-lg p-8 text-center text-gray-400">
</div> No active task Hammer is idle
) : ( </div>
<div className="space-y-3"> ) : (
{activeTasks.map((task) => ( <div className="space-y-3">
<TaskCard {activeTasks.map((task) => (
key={task.id} <TaskCard
task={task} key={task.id}
onStatusChange={handleStatusChange} task={task}
isActive onStatusChange={handleStatusChange}
onClick={() => setSelectedTask(task.id)} isActive
/> onClick={() => setSelectedTask(task.id)}
))} projectName={task.projectId ? projectMap[task.projectId] : undefined}
</div> />
)} ))}
</section> </div>
)}
</section>
)}
{/* Blocked */} {/* Blocked */}
{blockedTasks.length > 0 && ( {showSection("blocked") && blockedTasks.length > 0 && (
<section> <section>
<h2 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3"> <h2 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
🚫 Blocked ({blockedTasks.length}) 🚫 Blocked ({blockedTasks.length})
@@ -197,6 +265,7 @@ export function QueuePage() {
task={task} task={task}
onStatusChange={handleStatusChange} onStatusChange={handleStatusChange}
onClick={() => setSelectedTask(task.id)} onClick={() => setSelectedTask(task.id)}
projectName={task.projectId ? projectMap[task.projectId] : undefined}
/> />
))} ))}
</div> </div>
@@ -204,53 +273,80 @@ export function QueuePage() {
)} )}
{/* Queue */} {/* Queue */}
<section> {showSection("queued") && (
<h2 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3"> <section>
📋 Queue ({queuedTasks.length}) <h2 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
</h2> 📋 Queue ({queuedTasks.length})
{queuedTasks.length === 0 ? ( </h2>
<div className="border-2 border-dashed border-gray-200 rounded-lg p-6 text-center text-gray-400"> {queuedTasks.length === 0 ? (
Queue is empty <div className="border-2 border-dashed border-gray-200 rounded-lg p-6 text-center text-gray-400">
</div> Queue is empty
) : ( </div>
<div className="space-y-2"> ) : (
{queuedTasks.map((task, i) => ( <div className="space-y-2">
<TaskCard {queuedTasks.map((task, i) => (
key={task.id} <TaskCard
task={task} key={task.id}
onStatusChange={handleStatusChange} task={task}
onMoveUp={() => handleMoveUp(i)} onStatusChange={handleStatusChange}
onMoveDown={() => handleMoveDown(i)} onMoveUp={() => handleMoveUp(i)}
isFirst={i === 0} onMoveDown={() => handleMoveDown(i)}
isLast={i === queuedTasks.length - 1} isFirst={i === 0}
onClick={() => setSelectedTask(task.id)} isLast={i === queuedTasks.length - 1}
/> onClick={() => setSelectedTask(task.id)}
))} projectName={task.projectId ? projectMap[task.projectId] : undefined}
</div> />
)} ))}
</section> </div>
)}
</section>
)}
{/* Completed */} {/* Completed */}
<section> {(showSection("completed") || showSection("cancelled")) && (
<button <section>
onClick={() => setShowCompleted(!showCompleted)} {filterStatus ? (
className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3 flex items-center gap-1 hover:text-gray-700" <>
> <h2 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
{showCompleted ? "" : ""} Completed / Cancelled ({completedTasks.length}) {filterStatus === "completed" ? "" : ""} {filterStatus.charAt(0).toUpperCase() + filterStatus.slice(1)} ({completedTasks.length})
</button> </h2>
{showCompleted && ( <div className="space-y-2 opacity-60">
<div className="space-y-2 opacity-60"> {completedTasks.map((task) => (
{completedTasks.map((task) => ( <TaskCard
<TaskCard key={task.id}
key={task.id} task={task}
task={task} onStatusChange={handleStatusChange}
onStatusChange={handleStatusChange} onClick={() => setSelectedTask(task.id)}
onClick={() => setSelectedTask(task.id)} projectName={task.projectId ? projectMap[task.projectId] : undefined}
/> />
))} ))}
</div> </div>
)} </>
</section> ) : (
<>
<button
onClick={() => setShowCompleted(!showCompleted)}
className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3 flex items-center gap-1 hover:text-gray-700"
>
{showCompleted ? "▾" : "▸"} Completed / Cancelled ({completedTasks.length})
</button>
{showCompleted && (
<div className="space-y-2 opacity-60">
{completedTasks.map((task) => (
<TaskCard
key={task.id}
task={task}
onStatusChange={handleStatusChange}
onClick={() => setSelectedTask(task.id)}
projectName={task.projectId ? projectMap[task.projectId] : undefined}
/>
))}
</div>
)}
</>
)}
</section>
)}
</div> </div>
{/* Task Detail Panel */} {/* Task Detail Panel */}