Popular features/Team planner
Loading live demo…from dhtmlxcode.com
Popular features
Team planner
Drag people from a workload panel onto tasks, and watch a card turn red the moment that person is overbooked.
gantt.plugins({ tooltip: true });
gantt.config.date_format = "%Y-%m-%d";
gantt.config.resources = true; // parse the "resources" collection + assignments
gantt.config.resource_store = "resource";
gantt.config.resource_property = "owner";
gantt.config.work_time = true;
gantt.config.open_tree_initially = true;
gantt.config.row_height = 40;
gantt.config.bar_height = 24;
gantt.config.scale_height = 50;
gantt.config.min_column_width = 44;
gantt.config.grid_width = 360;
gantt.config.scales = [
{ unit: "week", step: 1, format: function (d) { return "Week of " + gantt.date.date_to_str("%d %M")(d); } },
{ unit: "day", step: 1, format: "%d" }
];
let CAPACITY = 8; // hours/day one person can take
let DROP_HOURS = 4; // hours/day added by a drag & drop assignment
let TEAM = [
{ id: 1, text: "Ana Kim", role: "Frontend", color: "#6366f1" },
{ id: 2, text: "Ben Ruiz", role: "Backend", color: "#0ea5e9" },
{ id: 3, text: "Cleo Mara", role: "QA", color: "#10b981" },
{ id: 4, text: "Dev Shah", role: "Design", color: "#f59e0b" },
{ id: 5, text: "Eva Lin", role: "DevOps", color: "#ec4899" }
];
let byRes = {};
TEAM.forEach(function (p) { byRes[p.id] = p; });
/* One bar color per person, generated from the same palette as the cards. */
let barStyles = document.createElement("style");
barStyles.textContent = TEAM.map(function (p) {
return ".gantt_task_line.owner_" + p.id + " { background: " + p.color + "; }";
}).join("\n");
document.head.appendChild(barStyles);
function isAssignable(task) {
return task.type !== gantt.config.types.project && task.type !== gantt.config.types.milestone;
}
/* ---------------------------------------------------------------------------
* TEMPLATES
* ------------------------------------------------------------------------ */
gantt.templates.timeline_cell_class = function (task, date) {
return gantt.isWorkTime({ date, task }) ? "" : "week_end";
};
gantt.templates.task_class = function (start, end, task) {
if (!isAssignable(task)) return "";
let owner = task.owner || [];
return owner.length ? "owner_" + owner[0].resource_id : "bar_unassigned";
};
gantt.templates.tooltip_text = function (start, end, task) {
if (!isAssignable(task)) return "<b>" + task.text + "</b>";
let who = (task.owner || []).map(function (a) {
return byRes[a.resource_id].text + ": " + a.value + "h/day";
}).join("<br>");
return "<b>" + task.text + "</b><br>" + (who || "Unassigned: drop a person here");
};
gantt.config.columns = [
{ name: "text", label: "Task", tree: true, width: "*", resize: true },
{ name: "owner", label: "Team", align: "center", width: 104, resize: true, template: function (task) {
if (!isAssignable(task)) return "";
let owner = task.owner || [];
if (!owner.length) return "<span class='drop-hint'>drop here</span>";
return owner.map(function (a) {
let p = byRes[a.resource_id];
return "<span class='chip' data-res='" + p.id + "' style='background:" + p.color + "' title='" +
p.text + ", " + a.value + "h/day; click to remove'>" + p.text.charAt(0) + "</span>";
}).join("");
}},
{ name: "duration", label: "Days", align: "center", width: 52, resize: true }
];
/* ---------------------------------------------------------------------------
* WORKLOAD: peak booked hours on a person's busiest working day
* ------------------------------------------------------------------------ */
function peakLoad(resourceId) {
let days = {};
gantt.eachTask(function (task) {
(task.owner || []).forEach(function (a) {
if (String(a.resource_id) !== String(resourceId)) return;
for (let d = task.start_date; d < task.end_date; d = gantt.date.add(d, 1, "day")) {
if (gantt.isWorkTime({ date: d, task })) days[+d] = (days[+d] || 0) + Number(a.value);
}
});
});
let peak = 0;
for (let k in days) peak = Math.max(peak, days[k]);
return peak;
}
function renderTeam() {
document.getElementById("team_list").innerHTML = TEAM.map(function (p) {
let peak = peakLoad(p.id);
let over = peak > CAPACITY;
let width = Math.min(peak / CAPACITY, 1) * 100;
return "<div class='person" + (over ? " person--over" : "") + "' draggable='true' data-res='" + p.id + "'>" +
"<span class='person__avatar' style='background:" + p.color + "'>" + p.text.charAt(0) + "</span>" +
"<span class='person__name'>" + p.text + "<i>" + p.role + "</i></span>" +
"<span class='person__peak'>" + (peak ? peak + "h/day" : "free") + (over ? " !" : "") + "</span>" +
"<span class='person__bar'><i style='width:" + width + "%'></i></span>" +
"</div>";
}).join("");
let unassigned = 0;
gantt.eachTask(function (task) {
if (isAssignable(task) && !(task.owner || []).length) unassigned++;
});
DHX.ui.setText("footer_status", unassigned ? unassigned + " task" + (unassigned > 1 ? "s" : "") + " unassigned" : "All tasks staffed");
}
let teamRenderPending = false;
function requestTeamRender() {
if (dragRes != null) {
teamRenderPending = true;
return;
}
renderTeam();
}
["onAfterTaskUpdate", "onAfterTaskAdd", "onAfterTaskDelete", "onParse"].forEach(function (event) {
gantt.attachEvent(event, requestTeamRender);
});
/* ---------------------------------------------------------------------------
* ASSIGN / UNASSIGN
* ------------------------------------------------------------------------ */
function assign(taskId, resourceId) {
let task = gantt.getTask(taskId);
let person = byRes[resourceId];
task.owner = task.owner || [];
let already = task.owner.some(function (a) { return String(a.resource_id) === String(resourceId); });
if (already) {
gantt.message({ type: "warning", text: person.text + " is already on \"" + task.text + "\"" });
return;
}
task.owner.push({ resource_id: resourceId, value: DROP_HOURS });
gantt.updateTask(taskId);
gantt.message(person.text + " → \"" + task.text + "\" (" + DROP_HOURS + "h/day)");
}
gantt.attachEvent("onTaskClick", function (id, e) {
let chip = e.target.closest(".chip[data-res]");
if (!chip) return true;
let task = gantt.getTask(id);
let resourceId = chip.getAttribute("data-res");
task.owner = (task.owner || []).filter(function (a) { return String(a.resource_id) !== String(resourceId); });
gantt.updateTask(id);
return false;
});
/* ---------------------------------------------------------------------------
* DRAG & DROP: team cards are HTML5 drag sources, gantt rows are targets
* ------------------------------------------------------------------------ */
let dragRes = null, dropId = null, draggedCard = null;
function markDrop(id) {
if (dropId == id) return;
gantt.$container.querySelectorAll(".drop_target").forEach(function (el) { el.classList.remove("drop_target"); });
dropId = id;
if (id != null) {
gantt.$container.querySelectorAll("[data-task-id='" + id + "']").forEach(function (el) { el.classList.add("drop_target"); });
}
}
let teamList = document.getElementById("team_list");
teamList.addEventListener("dragstart", function (e) {
let card = e.target.closest(".person");
if (!card) return;
dragRes = card.getAttribute("data-res");
draggedCard = card;
e.dataTransfer.setData("text/plain", dragRes);
e.dataTransfer.effectAllowed = "copy";
/* The browser must capture the normal card before it becomes a placeholder. */
setTimeout(function () {
if (draggedCard === card) {
card.classList.add("person--placeholder");
}
}, 0);
});
teamList.addEventListener("dragend", function () {
if (draggedCard && draggedCard.isConnected) {
draggedCard.classList.remove("person--placeholder");
}
dragRes = null;
draggedCard = null;
markDrop(null);
if (teamRenderPending) {
teamRenderPending = false;
renderTeam();
}
});
let ganttEl = document.getElementById("gantt_here");
ganttEl.addEventListener("dragover", function (e) {
if (!dragRes) return;
let id = gantt.locate(e);
let ok = id != null && gantt.isTaskExists(id) && isAssignable(gantt.getTask(id));
markDrop(ok ? id : null);
if (ok) { e.preventDefault(); e.dataTransfer.dropEffect = "copy"; }
});
ganttEl.addEventListener("drop", function (e) {
e.preventDefault();
if (dropId != null && dragRes) assign(dropId, dragRes);
markDrop(null);
});
ganttEl.addEventListener("dragleave", function (e) {
if (!ganttEl.contains(e.relatedTarget)) markDrop(null);
});
/* Hide both the visible tooltip and a tooltip that may still be waiting for
its configured show delay when the pointer leaves the Gantt container. */
function hideGanttTooltip() {
let tooltips = gantt.ext && gantt.ext.tooltips;
if (!tooltips) return;
if (typeof tooltips.delayHide === "function") {
tooltips.delayHide();
}
if (tooltips.tooltip && typeof tooltips.tooltip.hide === "function") {
tooltips.tooltip.hide();
}
}
let pointerInsideGantt = false;
document.addEventListener("mousemove", function (e) {
let inside = ganttEl.contains(e.target);
if (pointerInsideGantt && !inside) {
hideGanttTooltip();
}
pointerInsideGantt = inside;
}, true);
ganttEl.addEventListener("mouseleave", function () {
pointerInsideGantt = false;
hideGanttTooltip();
});
window.addEventListener("blur", hideGanttTooltip);
/* ---------------------------------------------------------------------------
* INIT + DATA: Ben carries Permissions AND Staging in week 2 (10h/day)
* ------------------------------------------------------------------------ */
gantt.init("gantt_here");
gantt.parse({
tasks: [
{ id: 100, text: "Team workspaces: July release", type: "project", open: true },
{ id: 11, text: "UX spec & flows", parent: 100, start_date: "2026-07-06", duration: 4, progress: 0.6, owner: [{ resource_id: 4, value: 6 }] },
{ id: 12, text: "API contract", parent: 100, start_date: "2026-07-06", duration: 3, progress: 0.5, owner: [{ resource_id: 2, value: 6 }] },
{ id: 13, text: "Test plan", parent: 100, start_date: "2026-07-08", duration: 3, progress: 0.2, owner: [{ resource_id: 3, value: 4 }] },
{ id: 14, text: "CI pipeline upgrade", parent: 100, start_date: "2026-07-07", duration: 4, progress: 0.3, owner: [{ resource_id: 5, value: 6 }] },
{ id: 21, text: "Workspace UI shell", parent: 100, start_date: "2026-07-13", duration: 5, progress: 0.1, owner: [{ resource_id: 1, value: 6 }] },
{ id: 22, text: "Permissions service", parent: 100, start_date: "2026-07-13", duration: 5, progress: 0, owner: [{ resource_id: 2, value: 6 }] },
{ id: 23, text: "Design review & polish",parent: 100, start_date: "2026-07-15", duration: 3, progress: 0, owner: [] },
{ id: 24, text: "Staging environment", parent: 100, start_date: "2026-07-16", duration: 2, progress: 0, owner: [{ resource_id: 2, value: 4 }] },
{ id: 31, text: "API integration", parent: 100, start_date: "2026-07-20", duration: 4, progress: 0, owner: [] },
{ id: 32, text: "E2E test suite", parent: 100, start_date: "2026-07-21", duration: 4, progress: 0, owner: [{ resource_id: 3, value: 6 }] },
{ id: 33, text: "Load & soak tests", parent: 100, start_date: "2026-07-22", duration: 3, progress: 0, owner: [] },
{ id: 34, text: "Release notes & docs", parent: 100, start_date: "2026-07-23", duration: 2, progress: 0, owner: [] },
{ id: 35, text: "Ship v3.0", parent: 100, start_date: "2026-07-27", type: "milestone" },
{ id: 41, text: "Post-release bug triage", parent: 100, start_date: "2026-07-27", duration: 4, progress: 0, owner: [{ resource_id: 1, value: 6 }] },
{ id: 42, text: "Regression test pass", parent: 100, start_date: "2026-07-28", duration: 3, progress: 0, owner: [{ resource_id: 3, value: 6 }] },
{ id: 43, text: "Perf tuning", parent: 100, start_date: "2026-07-29", duration: 3, progress: 0, owner: [] },
{ id: 44, text: "Monitoring & alerts", parent: 100, start_date: "2026-07-29", duration: 4, progress: 0, owner: [{ resource_id: 5, value: 6 }] },
{ id: 51, text: "UI polish follow-up", parent: 100, start_date: "2026-08-03", duration: 4, progress: 0, owner: [{ resource_id: 4, value: 6 }] },
{ id: 52, text: "Docs update for v3.1", parent: 100, start_date: "2026-08-04", duration: 3, progress: 0, owner: [] },
{ id: 53, text: "Hotfix support", parent: 100, start_date: "2026-08-05", duration: 2, progress: 0, owner: [{ resource_id: 2, value: 6 }] },
{ id: 54, text: "Customer feedback pass", parent: 100, start_date: "2026-08-06", duration: 3, progress: 0, owner: [{ resource_id: 3, value: 6 }] },
{ id: 55, text: "Ship v3.1", parent: 100, start_date: "2026-08-11", type: "milestone" }
],
links: [
{ id: 1, source: 11, target: 21, type: "0" },
{ id: 2, source: 12, target: 22, type: "0" },
{ id: 3, source: 21, target: 31, type: "0" },
{ id: 4, source: 22, target: 31, type: "0" },
{ id: 5, source: 31, target: 32, type: "0" },
{ id: 6, source: 32, target: 35, type: "0" },
{ id: 7, source: 35, target: 41, type: "0" },
{ id: 8, source: 41, target: 51, type: "0" },
{ id: 9, source: 44, target: 51, type: "0" },
{ id: 10, source: 51, target: 54, type: "0" },
{ id: 11, source: 54, target: 55, type: "0" }
],
resources: TEAM.map(function (p) {
return { id: p.id, text: p.text, parent: null, unit: "hours/day" };
})
});
gantt.showDate(new Date(2026, 6, 6));<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>Team planner: drag people onto tasks | dhtmlxGantt</title>
<script src="../../codebase/dhtmlxgantt.js?v=10.0.0"></script>
<link rel="stylesheet" href="../../codebase/dhtmlxgantt.css?v=10.0.0">
<link rel="stylesheet" href="../common/demo-controls/dhx_controls.css">
<script src="../common/demo-controls/dhx_controls.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
<style>
body { display: flex; flex-direction: column; }
.planner { flex: 1 1 auto; min-height: 0; display: flex; }
#gantt_here { flex: 1 1 auto; min-width: 0; }
.team { width: 236px; flex: none; display: flex; flex-direction: column; overflow-y: auto; border-right: 1px solid var(--dhx-line); background: var(--dhx-bg); }
.team__head { padding: 14px 16px 8px; font: 600 13px var(--dhx-font); color: var(--dhx-ink); }
.team__head small { display: block; margin-top: 2px; font: 500 11px var(--dhx-font); color: var(--dhx-muted); }
.person {
position: relative;
display: grid;
grid-template-columns: 36px 1fr auto;
grid-template-rows: auto auto;
column-gap: 10px;
row-gap: 6px;
align-items: center;
margin: 5px 10px;
padding: 9px 11px;
background: var(--dhx-surface);
border: 1px solid var(--dhx-line);
border-radius: 10px;
cursor: grab;
user-select: none;
}
.person:active { cursor: grabbing; }
.person--placeholder {
border-style: dashed;
border-color: #a5b4fc;
background: #eef2ff;
cursor: grabbing;
}
.person--placeholder > * {
visibility: hidden;
}
.person--placeholder::after {
content: "Dragging...";
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: #6366f1;
font: 600 11px var(--dhx-font);
}
.person__avatar { grid-row: 1 / span 2; width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #fff; font: 700 14px var(--dhx-font); }
.person__name { font: 600 12px var(--dhx-font); color: var(--dhx-ink); line-height: 1.25; }
.person__name i { display: block; font: 500 11px var(--dhx-font); font-style: normal; color: var(--dhx-muted); }
.person__peak { font: 600 11px var(--dhx-font); color: #3aa757; }
.person__bar { grid-column: 2 / span 2; height: 5px; border-radius: 3px; background: #e2e8f0; overflow: hidden; }
.person__bar i { display: block; height: 100%; border-radius: 3px; background: #3aa757; }
.person--over { border-color: var(--dhx-danger-line); background: var(--dhx-danger-bg); }
.person--over .person__peak { color: #e5484d; }
.person--over .person__bar i { background: #e5484d; }
.gantt_task_line { border: none; border-radius: 6px; box-shadow: 0 1px 2px rgba(16,24,40,.16); }
.gantt_task_line .gantt_task_progress { background: rgba(0,0,0,.18); border-radius: 6px 0 0 6px; }
.gantt_task_cell.week_end { background: rgba(15,23,42,.04); }
.gantt_task_line.bar_unassigned { background: #dbe2ea; border: 1px dashed #94a3b8; box-shadow: none; }
.gantt_task_line.bar_unassigned .gantt_task_content { color: #475569; }
:root[data-gantt-theme="dark"] .person__bar { background: #333c48; }
:root[data-gantt-theme="dark"] .gantt_task_line.bar_unassigned { background: #333c48; border-color: #566374; }
:root[data-gantt-theme="dark"] .gantt_task_line.bar_unassigned .gantt_task_content { color: #c3ccd8; }
.gantt_row.drop_target, .gantt_task_row.drop_target { background: var(--dhx-info-bg); }
.gantt_task_line.drop_target { box-shadow: 0 0 0 2px #6366f1; }
.chip { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 50%; margin: 0 2px; color: #fff; font: 700 11px var(--dhx-font); cursor: pointer; }
.chip:hover { opacity: .75; }
.drop-hint { font: 500 11px var(--dhx-font); color: var(--dhx-muted); }
.legend { display: inline-flex; gap: 14px; }
.legend span { display: inline-flex; align-items: center; gap: 6px; font: 500 12px var(--dhx-font); color: var(--dhx-ink-2); }
.legend i { width: 12px; height: 12px; border-radius: 4px; }
.hint { font: 500 12px var(--dhx-font); color: var(--dhx-muted); }
.hint b { color: var(--dhx-ink-2); font-weight: 600; }
</style>
</head>
<body>
<div class="dhx_header">
<div class="dhx_brand dhx_brand--static">
<span class="dhx_brand__btn">
<span class="dhx_brand__logo" style="background:linear-gradient(140deg,#6366f1,#8b5cf6)">TP</span>
<span class="dhx_brand__titles">
<span class="dhx_brand__title-row"><span class="dhx_brand__title">Team Planner</span></span>
<span class="dhx_brand__meta"><span class="dhx_brand__subtitle">Drag people onto tasks: workload updates live</span></span>
</span>
</span>
</div>
<div class="dhx_spacer"></div>
<div class="legend">
<span><i style="background:#3aa757"></i> Within capacity (≤ 8h/day)</span>
<span><i style="background:#e5484d"></i> Overbooked</span>
<span><i style="background:#dbe2ea;border:1px dashed #94a3b8"></i> Unassigned task</span>
</div>
</div>
<div class="planner">
<aside class="team">
<div class="team__head">Team<small>Drag a person onto a task</small></div>
<div id="team_list"></div>
</aside>
<div id="gantt_here"></div>
</div>
<div class="dhx_footer">
<span class="hint">Drop assigns <b>4h/day</b>. Click a chip in the <b>Team</b> column to remove an assignment. Ben is overbooked in week 2: hand <b>Staging environment</b> to someone free.</span>
<div class="dhx_footer__right"><span id="footer_status"></span></div>
</div>
</body>
</html>