Skip to main content
Back to examples
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 + " &rarr; \"" + 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 (&le; 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>