// ============ 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;