// ============ TaskQuest — Kanban board, tiles, drag & drop ============ const { useState, useEffect, useRef } = React; const { ROLES: R4, PROJECTS: P4, COLUMNS: COLS4 } = window.TQ; function needsModal(from, to) { if (from === to) return null; if (to === "unhappy") return "date"; if (to === "onhold") return "hold"; if (to === "done") return "done"; return null; // free move (e.g. back to happy) } function TaskTile({ task, role, showAssignee, showProject, layout, delivName, onClick, onDragStart, onDragEnd, dragging }) { const { daysUntil, relDue, fmtDate } = window.TQ; const overdue = task.status !== "done" && daysUntil(task.due) < 0; const proj = P4[task.project]; const draggable = task.status !== "done"; const hasBar = layout === "bar"; const barColor = COLS4.find(c => c.key === task.status).color; return (
onClick(task)} onDragStart={(e) => { e.dataTransfer.effectAllowed = "move"; onDragStart(task); }} onDragEnd={onDragEnd}> {hasBar && } {showProject && (
{proj.name}
)} {delivName && (
{delivName}
)}
{task.name}
{task.id} {showAssignee && } {overdue ? relDue(task.due) : fmtDate(task.due)}
{(overdue || (task.request && task.request.status === "pending") || (task.completion && task.completion.status === "pending") || (task.status === "done" && task.completion)) && (
{overdue && Overdue} {task.request && task.request.status === "pending" && Approval pending} {task.completion && task.completion.status === "pending" && Needs sign-off} {task.status === "done" && task.completion && task.completion.status === "approved" && ( task.completion.onTime ? On time · +10 : Late · +0 )}
)}
); } function Column({ col, tasks, role, showAssignee, showProject, layout, delivMap, onTileClick, drag, onDropTask }) { const [over, setOver] = useState(false); const dragTask = drag.task; const modalKind = dragTask ? needsModal(dragTask.status, col.key) : null; const sameCol = dragTask && dragTask.status === col.key; const canDrop = dragTask && !sameCol; return (
{ if (canDrop) { e.preventDefault(); setOver(true); } }} onDragLeave={() => setOver(false)} onDrop={(e) => { e.preventDefault(); setOver(false); if (canDrop) onDropTask(dragTask, col.key); }}>
{col.name} {tasks.length}
{tasks.length === 0 &&
{over && canDrop ? "Drop here" : "No tasks"}
} {tasks.map(t => ( ))}
); } function KanbanBoard({ role, project, tasks, layout, deliverables = [], filterAssignee = "", onTileClick, onMove }) { const r = R4[role]; const showAssignee = r.type === "pm" || r.type === "dm"; const scope = r.type === "dm" ? project : project; const showProject = scope === "all"; const [drag, setDrag] = useState({ task: null }); const delivMap = {}; deliverables.forEach(d => { delivMap[d.id] = d.name; }); // visible tasks let visible = tasks; if (r.type === "consultant") visible = visible.filter(t => t.assignee === role); else if (scope !== "all") visible = visible.filter(t => t.project === scope); if (filterAssignee) visible = visible.filter(t => t.assignee === filterAssignee); const dragApi = { task: drag.task, start: (t) => setDrag({ task: t }), end: () => setDrag({ task: null }), }; function handleDrop(task, toCol) { setDrag({ task: null }); onMove(task, toCol); } const byCol = {}; COLS4.forEach(c => { byCol[c.key] = visible.filter(t => t.status === c.key); }); return (
{COLS4.map(col => ( ))}
); } window.KanbanBoard = KanbanBoard; window.needsModal = needsModal;