feat: assignee picker in task detail panel

- Quick-assign buttons for Hammer/Donovan/David
- Assignee tracked in save/cancel/dirty detection
- Shows custom assignee names if set by API
This commit is contained in:
2026-01-29 08:05:17 +00:00
parent f00e0720e1
commit 24c9539c00

View File

@@ -258,6 +258,7 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
const [draftSource, setDraftSource] = useState(task.source); const [draftSource, setDraftSource] = useState(task.source);
const [draftProjectId, setDraftProjectId] = useState(task.projectId || ""); const [draftProjectId, setDraftProjectId] = useState(task.projectId || "");
const [draftDueDate, setDraftDueDate] = useState(task.dueDate ? new Date(task.dueDate).toISOString().slice(0, 16) : ""); const [draftDueDate, setDraftDueDate] = useState(task.dueDate ? new Date(task.dueDate).toISOString().slice(0, 16) : "");
const [draftAssigneeName, setDraftAssigneeName] = useState(task.assigneeName || "");
const [projects, setProjects] = useState<Project[]>([]); const [projects, setProjects] = useState<Project[]>([]);
const [newSubtaskTitle, setNewSubtaskTitle] = useState(""); const [newSubtaskTitle, setNewSubtaskTitle] = useState("");
const [addingSubtask, setAddingSubtask] = useState(false); const [addingSubtask, setAddingSubtask] = useState(false);
@@ -284,7 +285,8 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
setDraftSource(task.source); setDraftSource(task.source);
setDraftProjectId(task.projectId || ""); setDraftProjectId(task.projectId || "");
setDraftDueDate(task.dueDate ? new Date(task.dueDate).toISOString().slice(0, 16) : ""); setDraftDueDate(task.dueDate ? new Date(task.dueDate).toISOString().slice(0, 16) : "");
}, [task.id, task.title, task.description, task.priority, task.source, task.projectId, task.dueDate]); setDraftAssigneeName(task.assigneeName || "");
}, [task.id, task.title, task.description, task.priority, task.source, task.projectId, task.dueDate, task.assigneeName]);
// Detect if any field has been modified // Detect if any field has been modified
const currentDueDate = task.dueDate ? new Date(task.dueDate).toISOString().slice(0, 16) : ""; const currentDueDate = task.dueDate ? new Date(task.dueDate).toISOString().slice(0, 16) : "";
@@ -294,7 +296,8 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
draftPriority !== task.priority || draftPriority !== task.priority ||
draftSource !== task.source || draftSource !== task.source ||
draftProjectId !== (task.projectId || "") || draftProjectId !== (task.projectId || "") ||
draftDueDate !== currentDueDate; draftDueDate !== currentDueDate ||
draftAssigneeName !== (task.assigneeName || "");
const handleCancel = () => { const handleCancel = () => {
setDraftTitle(task.title); setDraftTitle(task.title);
@@ -303,6 +306,7 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
setDraftSource(task.source); setDraftSource(task.source);
setDraftProjectId(task.projectId || ""); setDraftProjectId(task.projectId || "");
setDraftDueDate(task.dueDate ? new Date(task.dueDate).toISOString().slice(0, 16) : ""); setDraftDueDate(task.dueDate ? new Date(task.dueDate).toISOString().slice(0, 16) : "");
setDraftAssigneeName(task.assigneeName || "");
}; };
const handleSave = async () => { const handleSave = async () => {
@@ -316,6 +320,7 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
if (draftSource !== task.source) updates.source = draftSource; if (draftSource !== task.source) updates.source = draftSource;
if (draftProjectId !== (task.projectId || "")) updates.projectId = draftProjectId || null; if (draftProjectId !== (task.projectId || "")) updates.projectId = draftProjectId || null;
if (draftDueDate !== currentDueDate) updates.dueDate = draftDueDate ? new Date(draftDueDate).toISOString() : null; if (draftDueDate !== currentDueDate) updates.dueDate = draftDueDate ? new Date(draftDueDate).toISOString() : null;
if (draftAssigneeName !== (task.assigneeName || "")) updates.assigneeName = draftAssigneeName || null;
await updateTask(task.id, updates, token); await updateTask(task.id, updates, token);
onTaskUpdated(); onTaskUpdated();
toast("Changes saved", "success"); toast("Changes saved", "success");
@@ -502,6 +507,45 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
</div> </div>
)} )}
{/* Assignee */}
<div className="px-4 sm:px-6 py-4 border-b border-gray-100">
<h3 className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Assignee</h3>
{hasToken ? (
<div className="flex items-center gap-2">
<div className="flex gap-1.5 flex-wrap">
{["Hammer", "Donovan", "David"].map((name) => (
<button
key={name}
onClick={() => setDraftAssigneeName(draftAssigneeName === name ? "" : name)}
className={`text-xs px-2.5 py-1.5 rounded-lg font-medium transition border ${
draftAssigneeName === name
? "bg-emerald-500 text-white border-emerald-500"
: "bg-white text-gray-600 border-gray-200 hover:border-gray-300 hover:bg-gray-50"
}`}
>
👤 {name}
</button>
))}
</div>
{draftAssigneeName && !["Hammer", "Donovan", "David"].includes(draftAssigneeName) && (
<span className="text-xs px-2 py-1 bg-emerald-100 text-emerald-700 rounded-full font-medium">
👤 {draftAssigneeName}
</span>
)}
</div>
) : (
<span className="text-sm text-gray-700">
{task.assigneeName ? (
<span className="text-xs px-2 py-1 bg-emerald-100 text-emerald-700 rounded-full font-medium">
👤 {task.assigneeName}
</span>
) : (
<span className="text-gray-400 italic">Unassigned</span>
)}
</span>
)}
</div>
{/* Due Date */} {/* Due Date */}
<div className="px-4 sm:px-6 py-4 border-b border-gray-100"> <div className="px-4 sm:px-6 py-4 border-b border-gray-100">
<h3 className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Due Date</h3> <h3 className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Due Date</h3>